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  }