github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/apiserver/facades/client/modelgeneration/modelgeneration.go (about) 1 // Copyright 2019 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package modelgeneration 5 6 import ( 7 "fmt" 8 9 "github.com/juju/collections/set" 10 "github.com/juju/errors" 11 "github.com/juju/names/v5" 12 13 "github.com/juju/juju/apiserver/authentication" 14 apiservererrors "github.com/juju/juju/apiserver/errors" 15 "github.com/juju/juju/apiserver/facade" 16 "github.com/juju/juju/core/model" 17 "github.com/juju/juju/core/permission" 18 "github.com/juju/juju/rpc/params" 19 ) 20 21 // API is the concrete implementation of the API endpoint. 22 type API struct { 23 authorizer facade.Authorizer 24 apiUser names.UserTag 25 st State 26 model Model 27 modelCache ModelCache 28 } 29 30 // NewModelGenerationAPI creates a new API endpoint for dealing with model generations. 31 func NewModelGenerationAPI( 32 st State, 33 authorizer facade.Authorizer, 34 m Model, 35 mc ModelCache, 36 ) (*API, error) { 37 if !authorizer.AuthClient() { 38 return nil, apiservererrors.ErrPerm 39 } 40 // Since we know this is a user tag (because AuthClient is true), 41 // we just do the type assertion to the UserTag. 42 apiUser, _ := authorizer.GetAuthTag().(names.UserTag) 43 44 return &API{ 45 authorizer: authorizer, 46 apiUser: apiUser, 47 st: st, 48 model: m, 49 modelCache: mc, 50 }, nil 51 } 52 53 func (api *API) hasAdminAccess() error { 54 // We used to cache the result on the api object if the user was a superuser. 55 // We don't do that anymore as permission caching could become invalid 56 // for long lived connections. 57 err := api.authorizer.HasPermission(permission.SuperuserAccess, api.st.ControllerTag()) 58 if err != nil && !errors.Is(err, authentication.ErrorEntityMissingPermission) { 59 return err 60 } 61 62 if err == nil { 63 return nil 64 } 65 66 return api.authorizer.HasPermission(permission.AdminAccess, api.model.ModelTag()) 67 } 68 69 // AddBranch adds a new branch with the input name to the model. 70 func (api *API) AddBranch(arg params.BranchArg) (params.ErrorResult, error) { 71 result := params.ErrorResult{} 72 if err := api.hasAdminAccess(); err != nil { 73 return result, err 74 } 75 76 if err := model.ValidateBranchName(arg.BranchName); err != nil { 77 result.Error = apiservererrors.ServerError(err) 78 } else { 79 result.Error = apiservererrors.ServerError(api.model.AddBranch(arg.BranchName, api.apiUser.Name())) 80 } 81 return result, nil 82 } 83 84 // TrackBranch marks the input units and/or applications as tracking the input 85 // branch, causing them to realise changes made under that branch. 86 func (api *API) TrackBranch(arg params.BranchTrackArg) (params.ErrorResults, error) { 87 if err := api.hasAdminAccess(); err != nil { 88 return params.ErrorResults{}, err 89 } 90 91 // Ensure we guard against the numUnits being greater than 0 and the number 92 // units/applications greater than 1. This is because we don't know how to 93 // topographically distribute between all the applications and units, 94 // especially if an error occurs whilst assigning the units. 95 if arg.NumUnits > 0 && len(arg.Entities) > 1 { 96 return params.ErrorResults{}, errors.Errorf("number of units and unit IDs can not be specified at the same time") 97 } 98 99 branch, err := api.model.Branch(arg.BranchName) 100 if err != nil { 101 return params.ErrorResults{}, errors.Trace(err) 102 } 103 104 result := params.ErrorResults{ 105 Results: make([]params.ErrorResult, len(arg.Entities)), 106 } 107 for i, entity := range arg.Entities { 108 tag, err := names.ParseTag(entity.Tag) 109 if err != nil { 110 result.Results[i].Error = apiservererrors.ServerError(err) 111 continue 112 } 113 switch tag.Kind() { 114 case names.ApplicationTagKind: 115 result.Results[i].Error = apiservererrors.ServerError(branch.AssignUnits(tag.Id(), arg.NumUnits)) 116 case names.UnitTagKind: 117 result.Results[i].Error = apiservererrors.ServerError(branch.AssignUnit(tag.Id())) 118 default: 119 result.Results[i].Error = apiservererrors.ServerError( 120 errors.Errorf("expected names.UnitTag or names.ApplicationTag, got %T", tag)) 121 } 122 } 123 return result, nil 124 } 125 126 // CommitBranch commits the input branch, making its changes applicable to 127 // the whole model and marking it complete. 128 func (api *API) CommitBranch(arg params.BranchArg) (params.IntResult, error) { 129 result := params.IntResult{} 130 131 if err := api.hasAdminAccess(); err != nil { 132 return result, err 133 } 134 135 branch, err := api.model.Branch(arg.BranchName) 136 if err != nil { 137 return intResultsError(err) 138 } 139 140 if genId, err := branch.Commit(api.apiUser.Name()); err != nil { 141 result.Error = apiservererrors.ServerError(err) 142 } else { 143 result.Result = genId 144 } 145 return result, nil 146 } 147 148 // AbortBranch aborts the input branch, marking it complete. However no 149 // changes are made applicable to the whole model. No units may be assigned 150 // to the branch when aborting. 151 func (api *API) AbortBranch(arg params.BranchArg) (params.ErrorResult, error) { 152 result := params.ErrorResult{} 153 154 if err := api.hasAdminAccess(); err != nil { 155 return result, err 156 } 157 158 branch, err := api.model.Branch(arg.BranchName) 159 if err != nil { 160 result.Error = apiservererrors.ServerError(err) 161 return result, nil 162 } 163 164 if err := branch.Abort(api.apiUser.Name()); err != nil { 165 result.Error = apiservererrors.ServerError(err) 166 } 167 return result, nil 168 } 169 170 // BranchInfo will return details of branch identified by the input argument, 171 // including units on the branch and the configuration disjoint with the 172 // master generation. 173 // An error is returned if no in-flight branch matching in input is found. 174 func (api *API) BranchInfo( 175 args params.BranchInfoArgs) (params.BranchResults, error) { 176 result := params.BranchResults{} 177 178 if err := api.hasAdminAccess(); err != nil { 179 return result, err 180 } 181 182 // From clients, we expect a single branch name or none, 183 // but we accommodate any number - they all must exist to avoid an error. 184 // If no branch is supplied, get them all. 185 var err error 186 var branches []Generation 187 if len(args.BranchNames) > 0 { 188 branches = make([]Generation, len(args.BranchNames)) 189 for i, name := range args.BranchNames { 190 if branches[i], err = api.model.Branch(name); err != nil { 191 return branchResultsError(err) 192 } 193 } 194 } else { 195 if branches, err = api.model.Branches(); err != nil { 196 return branchResultsError(err) 197 } 198 } 199 200 results := make([]params.Generation, len(branches)) 201 for i, b := range branches { 202 if results[i], err = api.oneBranchInfo(b, args.Detailed); err != nil { 203 return branchResultsError(err) 204 } 205 } 206 result.Generations = results 207 return result, nil 208 } 209 210 // ShowCommit will return details a commit given by its generationId 211 // An error is returned if either no branch can be found corresponding to the generation id. 212 // Or the generation id given is below 1. 213 func (api *API) ShowCommit(arg params.GenerationId) (params.GenerationResult, error) { 214 result := params.GenerationResult{} 215 216 if err := api.hasAdminAccess(); err != nil { 217 return result, err 218 } 219 220 if arg.GenerationId < 1 { 221 err := errors.Errorf("supplied generation id has to be higher than 0") 222 return generationResultError(err) 223 } 224 225 branch, err := api.model.Generation(arg.GenerationId) 226 if err != nil { 227 result.Error = apiservererrors.ServerError(err) 228 return result, nil 229 } 230 231 generationCommit, err := api.getGenerationCommit(branch) 232 if err != nil { 233 return generationResultError(err) 234 } 235 236 result.Generation = generationCommit 237 238 return result, nil 239 } 240 241 // ListCommits will return the commits, hence only branches with generation_id higher than 0 242 func (api *API) ListCommits() (params.BranchResults, error) { 243 var result params.BranchResults 244 245 if err := api.hasAdminAccess(); err != nil { 246 return result, err 247 } 248 249 var err error 250 var branches []Generation 251 if branches, err = api.model.Generations(); err != nil { 252 return branchResultsError(err) 253 } 254 255 results := make([]params.Generation, len(branches)) 256 for i, b := range branches { 257 gen := params.Generation{ 258 BranchName: b.BranchName(), 259 Completed: b.Completed(), 260 CompletedBy: b.CompletedBy(), 261 GenerationId: b.GenerationId(), 262 } 263 results[i] = gen 264 } 265 266 result.Generations = results 267 return result, nil 268 } 269 270 func (api *API) oneBranchInfo(branch Generation, detailed bool) (params.Generation, error) { 271 deltas := branch.Config() 272 273 var apps []params.GenerationApplication 274 for appName, tracking := range branch.AssignedUnits() { 275 app, err := api.st.Application(appName) 276 if err != nil { 277 return params.Generation{}, errors.Trace(err) 278 } 279 allUnits, err := app.UnitNames() 280 if err != nil { 281 return params.Generation{}, errors.Trace(err) 282 } 283 284 branchApp := params.GenerationApplication{ 285 ApplicationName: appName, 286 UnitProgress: fmt.Sprintf("%d/%d", len(tracking), len(allUnits)), 287 } 288 289 // Determine the effective charm configuration changes. 290 defaults, err := app.DefaultCharmConfig() 291 if err != nil { 292 return params.Generation{}, errors.Trace(err) 293 } 294 branchApp.ConfigChanges = deltas[appName].EffectiveChanges(defaults) 295 296 // TODO (manadart 2019-04-12): Charm URL. 297 298 // TODO (manadart 2019-04-12): Resources. 299 300 // Only include unit names if detailed info was requested. 301 if detailed { 302 trackingSet := set.NewStrings(tracking...) 303 branchApp.UnitsTracking = trackingSet.SortedValues() 304 branchApp.UnitsPending = set.NewStrings(allUnits...).Difference(trackingSet).SortedValues() 305 } 306 307 apps = append(apps, branchApp) 308 } 309 310 return params.Generation{ 311 BranchName: branch.BranchName(), 312 Created: branch.Created(), 313 CreatedBy: branch.CreatedBy(), 314 Applications: apps, 315 }, nil 316 } 317 318 func (api *API) getGenerationCommit(branch Generation) (params.Generation, error) { 319 generation, err := api.oneBranchInfo(branch, true) 320 if err != nil { 321 return params.Generation{}, errors.Trace(err) 322 } 323 return params.Generation{ 324 BranchName: branch.BranchName(), 325 Completed: branch.Completed(), 326 CompletedBy: branch.CompletedBy(), 327 GenerationId: branch.GenerationId(), 328 Created: branch.Created(), 329 CreatedBy: branch.CreatedBy(), 330 Applications: generation.Applications, 331 }, nil 332 } 333 334 // HasActiveBranch returns a true result if the input model has an "in-flight" 335 // branch matching the input name. 336 func (api *API) HasActiveBranch(arg params.BranchArg) (params.BoolResult, error) { 337 result := params.BoolResult{} 338 if err := api.hasAdminAccess(); err != nil { 339 return result, err 340 } 341 342 if _, err := api.modelCache.Branch(arg.BranchName); err != nil { 343 if errors.IsNotFound(err) { 344 result.Result = false 345 } else { 346 result.Error = apiservererrors.ServerError(err) 347 } 348 } else { 349 result.Result = true 350 } 351 return result, nil 352 } 353 354 func branchResultsError(err error) (params.BranchResults, error) { 355 return params.BranchResults{Error: apiservererrors.ServerError(err)}, nil 356 } 357 358 func generationResultError(err error) (params.GenerationResult, error) { 359 return params.GenerationResult{Error: apiservererrors.ServerError(err)}, nil 360 } 361 362 func intResultsError(err error) (params.IntResult, error) { 363 return params.IntResult{Error: apiservererrors.ServerError(err)}, nil 364 }