github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/state/cloud.go (about) 1 // Copyright 2016 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package state 5 6 import ( 7 "fmt" 8 9 "github.com/juju/collections/set" 10 "github.com/juju/errors" 11 "github.com/juju/mgo/v3" 12 "github.com/juju/mgo/v3/bson" 13 "github.com/juju/mgo/v3/txn" 14 "github.com/juju/names/v5" 15 jujutxn "github.com/juju/txn/v3" 16 17 "github.com/juju/juju/cloud" 18 "github.com/juju/juju/controller" 19 "github.com/juju/juju/core/permission" 20 "github.com/juju/juju/environs/bootstrap" 21 environscloudspec "github.com/juju/juju/environs/cloudspec" 22 "github.com/juju/juju/mongo/utils" 23 ) 24 25 // cloudGlobalKey will return the key for a given cloud. 26 func cloudGlobalKey(cloudName string) string { 27 return fmt.Sprintf("cloud#%s", cloudName) 28 } 29 30 // cloudDoc records information about the cloud that the controller operates in. 31 type cloudDoc struct { 32 DocID string `bson:"_id"` 33 Name string `bson:"name"` 34 Type string `bson:"type"` 35 AuthTypes []string `bson:"auth-types"` 36 Endpoint string `bson:"endpoint"` 37 IdentityEndpoint string `bson:"identity-endpoint,omitempty"` 38 StorageEndpoint string `bson:"storage-endpoint,omitempty"` 39 Regions map[string]cloudRegionSubdoc `bson:"regions,omitempty"` 40 CACertificates []string `bson:"ca-certificates,omitempty"` 41 SkipTLSVerify bool `bson:"skip-tls-verify,omitempty"` 42 } 43 44 // cloudRegionSubdoc records information about cloud regions. 45 type cloudRegionSubdoc struct { 46 Endpoint string `bson:"endpoint,omitempty"` 47 IdentityEndpoint string `bson:"identity-endpoint,omitempty"` 48 StorageEndpoint string `bson:"storage-endpoint,omitempty"` 49 } 50 51 // createCloudOp returns a txn.Op that will initialize 52 // the cloud definition for the controller. 53 func createCloudOp(cloud cloud.Cloud) txn.Op { 54 authTypes := make([]string, len(cloud.AuthTypes)) 55 for i, authType := range cloud.AuthTypes { 56 authTypes[i] = string(authType) 57 } 58 regions := make(map[string]cloudRegionSubdoc) 59 for _, region := range cloud.Regions { 60 regions[utils.EscapeKey(region.Name)] = cloudRegionSubdoc{ 61 region.Endpoint, 62 region.IdentityEndpoint, 63 region.StorageEndpoint, 64 } 65 } 66 return txn.Op{ 67 C: cloudsC, 68 Id: cloud.Name, 69 Assert: txn.DocMissing, 70 Insert: &cloudDoc{ 71 Name: cloud.Name, 72 Type: cloud.Type, 73 AuthTypes: authTypes, 74 Endpoint: cloud.Endpoint, 75 IdentityEndpoint: cloud.IdentityEndpoint, 76 StorageEndpoint: cloud.StorageEndpoint, 77 Regions: regions, 78 CACertificates: cloud.CACertificates, 79 SkipTLSVerify: cloud.SkipTLSVerify, 80 }, 81 } 82 } 83 84 // updateCloudOp returns a txn.Op that will update 85 // an existing cloud definition for the controller. 86 func updateCloudOps(cloud cloud.Cloud) txn.Op { 87 authTypes := make([]string, len(cloud.AuthTypes)) 88 for i, authType := range cloud.AuthTypes { 89 authTypes[i] = string(authType) 90 } 91 regions := make(map[string]cloudRegionSubdoc) 92 for _, region := range cloud.Regions { 93 regions[utils.EscapeKey(region.Name)] = cloudRegionSubdoc{ 94 region.Endpoint, 95 region.IdentityEndpoint, 96 region.StorageEndpoint, 97 } 98 } 99 updatedCloud := &cloudDoc{ 100 DocID: cloud.Name, 101 Name: cloud.Name, 102 Type: cloud.Type, 103 AuthTypes: authTypes, 104 Endpoint: cloud.Endpoint, 105 IdentityEndpoint: cloud.IdentityEndpoint, 106 StorageEndpoint: cloud.StorageEndpoint, 107 Regions: regions, 108 CACertificates: cloud.CACertificates, 109 SkipTLSVerify: cloud.SkipTLSVerify, 110 } 111 return txn.Op{ 112 C: cloudsC, 113 Id: cloud.Name, 114 Assert: txn.DocExists, 115 Update: bson.M{"$set": updatedCloud}, 116 } 117 } 118 119 // cloudModelRefCountKey returns a key for refcounting models 120 // for the specified cloud. Each time a model for the cloud is created, 121 // the refcount is incremented, and the opposite happens on removal. 122 func cloudModelRefCountKey(cloudName string) string { 123 return fmt.Sprintf("cloudModel#%s", cloudName) 124 } 125 126 // incApplicationOffersRefOp returns a txn.Op that increments the reference 127 // count for a cloud model. 128 func incCloudModelRefOp(mb modelBackend, cloudName string) (txn.Op, error) { 129 refcounts, closer := mb.db().GetCollection(globalRefcountsC) 130 defer closer() 131 cloudModelRefCountKey := cloudModelRefCountKey(cloudName) 132 incRefOp, err := nsRefcounts.CreateOrIncRefOp(refcounts, cloudModelRefCountKey, 1) 133 return incRefOp, errors.Trace(err) 134 } 135 136 // countCloudModelRefOp returns the number of models for a cloud, 137 // along with a txn.Op that ensures that that does not change. 138 func countCloudModelRefOp(mb modelBackend, cloudName string) (txn.Op, int, error) { 139 refcounts, closer := mb.db().GetCollection(globalRefcountsC) 140 defer closer() 141 key := cloudModelRefCountKey(cloudName) 142 return nsRefcounts.CurrentOp(refcounts, key) 143 } 144 145 // decCloudModelRefOp returns a txn.Op that decrements the reference 146 // count for a cloud model. 147 func decCloudModelRefOp(mb modelBackend, cloudName string) (txn.Op, error) { 148 refcounts, closer := mb.db().GetCollection(globalRefcountsC) 149 defer closer() 150 cloudModelRefCountKey := cloudModelRefCountKey(cloudName) 151 decRefOp, _, err := nsRefcounts.DyingDecRefOp(refcounts, cloudModelRefCountKey) 152 if err != nil { 153 return txn.Op{}, errors.Trace(err) 154 } 155 return decRefOp, nil 156 } 157 158 func (d cloudDoc) toCloud(controllerCloudName string) cloud.Cloud { 159 authTypes := make([]cloud.AuthType, len(d.AuthTypes)) 160 for i, authType := range d.AuthTypes { 161 authTypes[i] = cloud.AuthType(authType) 162 } 163 regionNames := make(set.Strings) 164 for name := range d.Regions { 165 regionNames.Add(name) 166 } 167 regions := make([]cloud.Region, len(d.Regions)) 168 for i, name := range regionNames.SortedValues() { 169 region := d.Regions[name] 170 regions[i] = cloud.Region{ 171 utils.UnescapeKey(name), 172 region.Endpoint, 173 region.IdentityEndpoint, 174 region.StorageEndpoint, 175 } 176 } 177 return cloud.Cloud{ 178 Name: d.Name, 179 Type: d.Type, 180 AuthTypes: authTypes, 181 Endpoint: d.Endpoint, 182 IdentityEndpoint: d.IdentityEndpoint, 183 StorageEndpoint: d.StorageEndpoint, 184 Regions: regions, 185 CACertificates: d.CACertificates, 186 SkipTLSVerify: d.SkipTLSVerify, 187 IsControllerCloud: d.Name == controllerCloudName, 188 } 189 } 190 191 // Clouds returns the definitions for all clouds in the controller. 192 func (st *State) Clouds() (map[names.CloudTag]cloud.Cloud, error) { 193 ci, err := st.ControllerInfo() 194 if err != nil { 195 return nil, errors.Trace(err) 196 } 197 198 coll, cleanup := st.db().GetCollection(cloudsC) 199 defer cleanup() 200 201 var doc cloudDoc 202 clouds := make(map[names.CloudTag]cloud.Cloud) 203 iter := coll.Find(nil).Iter() 204 for iter.Next(&doc) { 205 clouds[names.NewCloudTag(doc.Name)] = doc.toCloud(ci.CloudName) 206 } 207 if err := iter.Close(); err != nil { 208 return nil, errors.Annotate(err, "getting clouds") 209 } 210 return clouds, nil 211 } 212 213 // Cloud returns the controller's cloud definition. 214 func (st *State) Cloud(name string) (cloud.Cloud, error) { 215 ci, err := st.ControllerInfo() 216 if err != nil { 217 return cloud.Cloud{}, errors.Trace(err) 218 } 219 220 coll, cleanup := st.db().GetCollection(cloudsC) 221 defer cleanup() 222 223 var doc cloudDoc 224 err = coll.FindId(name).One(&doc) 225 if err == mgo.ErrNotFound { 226 return cloud.Cloud{}, errors.NotFoundf("cloud %q", name) 227 } 228 if err != nil { 229 return cloud.Cloud{}, errors.Annotatef(err, "cannot get cloud %q", name) 230 } 231 return doc.toCloud(ci.CloudName), nil 232 } 233 234 // AddCloud creates a cloud with the given name and details. 235 // Note that the Config is deliberately ignored - it's only 236 // relevant when bootstrapping. 237 func (st *State) AddCloud(c cloud.Cloud, owner string) error { 238 if err := validateCloud(c); err != nil { 239 return errors.Annotate(err, "invalid cloud") 240 } 241 ops := []txn.Op{createCloudOp(c)} 242 if err := st.db().RunTransaction(ops); err != nil { 243 if err == txn.ErrAborted { 244 err = errors.AlreadyExistsf("cloud %q", c.Name) 245 } 246 return err 247 } 248 // Ensure the owner has access to the cloud. 249 ownerTag := names.NewUserTag(owner) 250 err := st.CreateCloudAccess(c.Name, ownerTag, permission.AdminAccess) 251 if err != nil { 252 return errors.Annotatef(err, "granting %s permission to the cloud owner", permission.AdminAccess) 253 } 254 return st.updateConfigDefaults(c.Name, c.Config, c.RegionConfig) 255 } 256 257 // UpdateCloud updates an existing cloud with the given name and details. 258 // Note that the Config is deliberately ignored - it's only 259 // relevant when bootstrapping. 260 func (st *State) UpdateCloud(c cloud.Cloud) error { 261 if err := validateCloud(c); err != nil { 262 return errors.Trace(err) 263 } 264 265 if err := st.db().RunTransaction([]txn.Op{updateCloudOps(c)}); err != nil { 266 if err == txn.ErrAborted { 267 err = errors.NotFoundf("cloud %q", c.Name) 268 } 269 return errors.Trace(err) 270 } 271 return st.updateConfigDefaults(c.Name, c.Config, c.RegionConfig) 272 } 273 274 func (st *State) updateConfigDefaults(cloudName string, config cloud.Attrs, regionConfig cloud.RegionConfig) error { 275 cfg := make(map[string]interface{}) 276 for k, v := range config { 277 if bootstrap.IsBootstrapAttribute(k) || controller.ControllerOnlyAttribute(k) { 278 continue 279 } 280 cfg[k] = v 281 } 282 regionSpec, err := environscloudspec.NewCloudRegionSpec(cloudName, "") 283 if err != nil { 284 return errors.Trace(err) 285 } 286 if err := st.UpdateModelConfigDefaultValues(cfg, nil, regionSpec); err != nil { 287 return errors.Trace(err) 288 } 289 for r, regionConfig := range regionConfig { 290 regionSpec, err := environscloudspec.NewCloudRegionSpec(cloudName, r) 291 if err != nil { 292 return errors.Trace(err) 293 } 294 if err := st.UpdateModelConfigDefaultValues(regionConfig, nil, regionSpec); err != nil { 295 return errors.Trace(err) 296 } 297 } 298 return nil 299 } 300 301 // validateCloud checks that the supplied cloud is valid. 302 func validateCloud(cloud cloud.Cloud) error { 303 if cloud.Name == "" { 304 return errors.NotValidf("empty Name") 305 } 306 if cloud.Type == "" { 307 return errors.NotValidf("empty Type") 308 } 309 if len(cloud.AuthTypes) == 0 { 310 return errors.NotValidf("empty auth-types") 311 } 312 // TODO(axw) we should ensure that the cloud auth-types is a subset 313 // of the auth-types supported by the provider. To do that, we'll 314 // need a new "policy". 315 316 if cloud.SkipTLSVerify && len(cloud.CACertificates) > 0 && cloud.CACertificates[0] != "" { 317 return errors.NotValidf("cloud with both skip-TLS-verify=true and CA certificates") 318 } 319 return nil 320 } 321 322 // regionSettingsGlobalKey concatenates the cloud a hash and the region string. 323 func regionSettingsGlobalKey(cloud, region string) string { 324 return cloud + "#" + region 325 } 326 327 // RemoveCloud removes a cloud and any credentials for that cloud. 328 // If the cloud is in use, ie has models deployed to it, the operation fails. 329 func (st *State) RemoveCloud(name string) error { 330 buildTxn := func(attempt int) ([]txn.Op, error) { 331 if _, err := st.Cloud(name); err != nil { 332 // Fail with not found error on first attempt if cloud doesn't exist. 333 // On subsequent attempts, if cloud not found then 334 // it was deleted by someone else and that's a no-op. 335 if attempt > 1 && errors.IsNotFound(err) { 336 return nil, jujutxn.ErrNoOperations 337 } 338 return nil, errors.Trace(err) 339 } 340 return st.removeCloudOps(name) 341 } 342 return st.db().Run(buildTxn) 343 } 344 345 // removeCloudOp returns a list of txn.Ops that will remove 346 // the specified cloud and any associated credentials. 347 func (st *State) removeCloudOps(name string) ([]txn.Op, error) { 348 countOp, n, err := countCloudModelRefOp(st, name) 349 if err != nil { 350 return nil, errors.Trace(err) 351 } 352 if n != 0 { 353 return nil, errors.Errorf("cloud is used by %d model%s", n, plural(n)) 354 } 355 356 ops := []txn.Op{{ 357 C: cloudsC, 358 Id: name, 359 Remove: true, 360 }, countOp} 361 362 credPattern := bson.M{ 363 "_id": bson.M{"$regex": "^" + name + "#"}, 364 } 365 credOps, err := st.removeInCollectionOps(cloudCredentialsC, credPattern) 366 if err != nil { 367 return nil, errors.Trace(err) 368 } 369 ops = append(ops, credOps...) 370 371 permPattern := bson.M{ 372 "_id": bson.M{"$regex": "^" + cloudGlobalKey(name) + "#"}, 373 } 374 permOps, err := st.removeInCollectionOps(permissionsC, permPattern) 375 if err != nil { 376 return nil, errors.Trace(err) 377 } 378 ops = append(ops, permOps...) 379 380 settingsPattern := bson.M{ 381 "_id": bson.M{"$regex": "^(" + cloudGlobalKey(name) + "|" + name + "#.*)"}, 382 } 383 settingsOps, err := st.removeInCollectionOps(globalSettingsC, settingsPattern) 384 if err != nil { 385 return nil, errors.Trace(err) 386 } 387 ops = append(ops, settingsOps...) 388 return ops, nil 389 }