github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/api/client/cloud/cloud.go (about)

     1  // Copyright 2016 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package cloud
     5  
     6  import (
     7  	"strings"
     8  
     9  	"github.com/juju/errors"
    10  	"github.com/juju/loggo"
    11  	"github.com/juju/names/v5"
    12  
    13  	"github.com/juju/juju/api/base"
    14  	jujucloud "github.com/juju/juju/cloud"
    15  	"github.com/juju/juju/core/permission"
    16  	"github.com/juju/juju/rpc/params"
    17  )
    18  
    19  var logger = loggo.GetLogger("juju.api.cloud")
    20  
    21  // CloudInfo holds cloud details and who can access the cloud.
    22  type CloudInfo struct {
    23  	jujucloud.Cloud
    24  	Users map[string]CloudUserInfo
    25  }
    26  
    27  // CloudUserInfo holds details of who can access a cloud.
    28  type CloudUserInfo struct {
    29  	DisplayName string
    30  	Access      string
    31  }
    32  
    33  // Client provides methods that the Juju client command uses to interact
    34  // with models stored in the Juju Server.
    35  type Client struct {
    36  	base.ClientFacade
    37  	facade base.FacadeCaller
    38  }
    39  
    40  // NewClient creates a new `Client` based on an existing authenticated API
    41  // connection.
    42  func NewClient(st base.APICallCloser) *Client {
    43  	frontend, backend := base.NewClientFacade(st, "Cloud")
    44  	return &Client{ClientFacade: frontend, facade: backend}
    45  }
    46  
    47  // Clouds returns the details of all clouds supported by the controller.
    48  func (c *Client) Clouds() (map[names.CloudTag]jujucloud.Cloud, error) {
    49  	var result params.CloudsResult
    50  	if err := c.facade.FacadeCall("Clouds", nil, &result); err != nil {
    51  		return nil, errors.Trace(err)
    52  	}
    53  	clouds := make(map[names.CloudTag]jujucloud.Cloud)
    54  	for tagString, cloud := range result.Clouds {
    55  		tag, err := names.ParseCloudTag(tagString)
    56  		if err != nil {
    57  			return nil, errors.Trace(err)
    58  		}
    59  		clouds[tag] = cloudFromParams(tag.Id(), cloud)
    60  	}
    61  	return clouds, nil
    62  }
    63  
    64  // Cloud returns the details of the cloud with the given tag.
    65  func (c *Client) Cloud(tag names.CloudTag) (jujucloud.Cloud, error) {
    66  	var results params.CloudResults
    67  	args := params.Entities{Entities: []params.Entity{{Tag: tag.String()}}}
    68  	if err := c.facade.FacadeCall("Cloud", args, &results); err != nil {
    69  		return jujucloud.Cloud{}, errors.Trace(err)
    70  	}
    71  	if len(results.Results) != 1 {
    72  		return jujucloud.Cloud{}, errors.Errorf("expected 1 result, got %d", len(results.Results))
    73  	}
    74  	if err := results.Results[0].Error; err != nil {
    75  		if params.IsCodeNotFound(err) {
    76  			return jujucloud.Cloud{}, errors.NotFoundf("cloud %s", tag.Id())
    77  		}
    78  		return jujucloud.Cloud{}, err
    79  	}
    80  	return cloudFromParams(tag.Id(), *results.Results[0].Cloud), nil
    81  }
    82  
    83  // CloudInfo returns details and user access for the cloud with the given tag.
    84  func (c *Client) CloudInfo(tags []names.CloudTag) ([]CloudInfo, error) {
    85  	var results params.CloudInfoResults
    86  	args := params.Entities{
    87  		Entities: make([]params.Entity, len(tags)),
    88  	}
    89  	for i, tag := range tags {
    90  		args.Entities[i] = params.Entity{Tag: tag.String()}
    91  	}
    92  	if err := c.facade.FacadeCall("CloudInfo", args, &results); err != nil {
    93  		return nil, errors.Trace(err)
    94  	}
    95  	if len(results.Results) != len(tags) {
    96  		return nil, errors.Errorf("expected %d result, got %d", len(tags), len(results.Results))
    97  	}
    98  	infos := make([]CloudInfo, len(tags))
    99  	for i, result := range results.Results {
   100  		if err := result.Error; err != nil {
   101  			if params.IsCodeNotFound(err) {
   102  				return nil, errors.NotFoundf("cloud %s", tags[i].Id())
   103  			}
   104  			return nil, errors.Trace(err)
   105  		}
   106  		info := CloudInfo{
   107  			Cloud: cloudDetailsFromParams(tags[i].Id(), result.Result.CloudDetails),
   108  			Users: make(map[string]CloudUserInfo),
   109  		}
   110  		for _, user := range result.Result.Users {
   111  			info.Users[user.UserName] = CloudUserInfo{
   112  				DisplayName: user.DisplayName,
   113  				Access:      user.Access,
   114  			}
   115  		}
   116  		infos[i] = info
   117  	}
   118  	return infos, nil
   119  }
   120  
   121  // UserCredentials returns the tags for cloud credentials available to a user for
   122  // use with a specific cloud.
   123  func (c *Client) UserCredentials(user names.UserTag, cloud names.CloudTag) ([]names.CloudCredentialTag, error) {
   124  	var results params.StringsResults
   125  	args := params.UserClouds{UserClouds: []params.UserCloud{
   126  		{UserTag: user.String(), CloudTag: cloud.String()},
   127  	}}
   128  	if err := c.facade.FacadeCall("UserCredentials", args, &results); err != nil {
   129  		return nil, errors.Trace(err)
   130  	}
   131  	if len(results.Results) != 1 {
   132  		return nil, errors.Errorf("expected 1 result, got %d", len(results.Results))
   133  	}
   134  	if results.Results[0].Error != nil {
   135  		return nil, results.Results[0].Error
   136  	}
   137  	tags := make([]names.CloudCredentialTag, len(results.Results[0].Result))
   138  	for i, s := range results.Results[0].Result {
   139  		tag, err := names.ParseCloudCredentialTag(s)
   140  		if err != nil {
   141  			return nil, errors.Trace(err)
   142  		}
   143  		tags[i] = tag
   144  	}
   145  	return tags, nil
   146  }
   147  
   148  // UpdateCloudsCredentials updates clouds credentials content on the controller.
   149  // Passed in credentials are keyed on the credential tag.
   150  // This operation can be forced to ignore validation checks.
   151  func (c *Client) UpdateCloudsCredentials(cloudCredentials map[string]jujucloud.Credential, force bool) ([]params.UpdateCredentialResult, error) {
   152  	return c.internalUpdateCloudsCredentials(params.UpdateCredentialArgs{Force: force}, cloudCredentials)
   153  }
   154  
   155  // AddCloudsCredentials adds/uploads clouds credentials content to the controller.
   156  // Passed in credentials are keyed on the credential tag.
   157  func (c *Client) AddCloudsCredentials(cloudCredentials map[string]jujucloud.Credential) ([]params.UpdateCredentialResult, error) {
   158  	return c.internalUpdateCloudsCredentials(params.UpdateCredentialArgs{}, cloudCredentials)
   159  }
   160  
   161  func (c *Client) internalUpdateCloudsCredentials(in params.UpdateCredentialArgs, cloudCredentials map[string]jujucloud.Credential) ([]params.UpdateCredentialResult, error) {
   162  	for tag, credential := range cloudCredentials {
   163  		in.Credentials = append(in.Credentials, params.TaggedCredential{
   164  			Tag: tag,
   165  			Credential: params.CloudCredential{
   166  				AuthType:   string(credential.AuthType()),
   167  				Attributes: credential.Attributes(),
   168  			},
   169  		})
   170  	}
   171  	count := len(cloudCredentials)
   172  
   173  	countErr := func(got int) error {
   174  		plural := "s"
   175  		if count == 1 {
   176  			plural = ""
   177  		}
   178  		return errors.Errorf("expected %d result%v got %d when updating credentials", count, plural, got)
   179  	}
   180  	var out params.UpdateCredentialResults
   181  	if err := c.facade.FacadeCall("UpdateCredentialsCheckModels", in, &out); err != nil {
   182  		return nil, errors.Trace(err)
   183  	}
   184  	if len(out.Results) != count {
   185  		return nil, countErr(len(out.Results))
   186  	}
   187  	// Older facades incorrectly set an error if models are invalid.
   188  	// The model result structs themselves contain the errors.
   189  	for i, r := range out.Results {
   190  		if r.Error == nil {
   191  			continue
   192  		}
   193  		if r.Error.Message == "some models are no longer visible" {
   194  			r.Error = nil
   195  		}
   196  		out.Results[i] = r
   197  	}
   198  	return out.Results, nil
   199  }
   200  
   201  // UpdateCredentialsCheckModels updates a cloud credential content
   202  // stored on the controller. This call validates that the new content works
   203  // for all models that are using this credential.
   204  func (c *Client) UpdateCredentialsCheckModels(tag names.CloudCredentialTag, credential jujucloud.Credential) ([]params.UpdateCredentialModelResult, error) {
   205  	out, err := c.UpdateCloudsCredentials(map[string]jujucloud.Credential{tag.String(): credential}, false)
   206  	if err != nil {
   207  		return nil, errors.Trace(err)
   208  	}
   209  	if out[0].Error != nil {
   210  		// Unlike many other places, we want to return something valid here to provide more details.
   211  		return out[0].Models, errors.Trace(out[0].Error)
   212  	}
   213  	return out[0].Models, nil
   214  }
   215  
   216  // RevokeCredential revokes/deletes a cloud credential.
   217  func (c *Client) RevokeCredential(tag names.CloudCredentialTag, force bool) error {
   218  	var results params.ErrorResults
   219  
   220  	args := params.RevokeCredentialArgs{
   221  		Credentials: []params.RevokeCredentialArg{
   222  			{Tag: tag.String(), Force: force},
   223  		},
   224  	}
   225  	if err := c.facade.FacadeCall("RevokeCredentialsCheckModels", args, &results); err != nil {
   226  		return errors.Trace(err)
   227  	}
   228  	return results.OneError()
   229  }
   230  
   231  // Credentials returns a slice of credential values for the specified tags.
   232  // Secrets are excluded from the credential attributes.
   233  func (c *Client) Credentials(tags ...names.CloudCredentialTag) ([]params.CloudCredentialResult, error) {
   234  	if len(tags) == 0 {
   235  		return []params.CloudCredentialResult{}, nil
   236  	}
   237  	var results params.CloudCredentialResults
   238  	args := params.Entities{
   239  		Entities: make([]params.Entity, len(tags)),
   240  	}
   241  	for i, tag := range tags {
   242  		args.Entities[i].Tag = tag.String()
   243  	}
   244  	if err := c.facade.FacadeCall("Credential", args, &results); err != nil {
   245  		return nil, errors.Trace(err)
   246  	}
   247  	return results.Results, nil
   248  }
   249  
   250  // AddCredential adds a credential to the controller with a given tag.
   251  // This can be a credential for a cloud that is not the same cloud as the controller's host.
   252  func (c *Client) AddCredential(tag string, credential jujucloud.Credential) error {
   253  	var results params.ErrorResults
   254  	cloudCredential := params.CloudCredential{
   255  		AuthType:   string(credential.AuthType()),
   256  		Attributes: credential.Attributes(),
   257  	}
   258  	args := params.TaggedCredentials{
   259  		Credentials: []params.TaggedCredential{{
   260  			Tag:        tag,
   261  			Credential: cloudCredential,
   262  		},
   263  		}}
   264  	if err := c.facade.FacadeCall("AddCredentials", args, &results); err != nil {
   265  		return errors.Trace(err)
   266  	}
   267  	return results.OneError()
   268  }
   269  
   270  // AddCloud adds a new cloud to current controller.
   271  func (c *Client) AddCloud(cloud jujucloud.Cloud, force bool) error {
   272  	args := params.AddCloudArgs{Name: cloud.Name, Cloud: cloudToParams(cloud)}
   273  	if force {
   274  		args.Force = &force
   275  	}
   276  	err := c.facade.FacadeCall("AddCloud", args, nil)
   277  	if err != nil {
   278  		return errors.Trace(err)
   279  	}
   280  	return nil
   281  }
   282  
   283  // UpdateCloud updates an existing cloud on a current controller.
   284  func (c *Client) UpdateCloud(cloud jujucloud.Cloud) error {
   285  	args := params.UpdateCloudArgs{
   286  		Clouds: []params.AddCloudArgs{{
   287  			Name:  cloud.Name,
   288  			Cloud: cloudToParams(cloud),
   289  		}},
   290  	}
   291  	var results params.ErrorResults
   292  	if err := c.facade.FacadeCall("UpdateCloud", args, &results); err != nil {
   293  		return errors.Trace(err)
   294  	}
   295  	return results.OneError()
   296  }
   297  
   298  // RemoveCloud removes a cloud from the current controller.
   299  func (c *Client) RemoveCloud(cloud string) error {
   300  	args := params.Entities{Entities: []params.Entity{{Tag: names.NewCloudTag(cloud).String()}}}
   301  	var result params.ErrorResults
   302  	err := c.facade.FacadeCall("RemoveClouds", args, &result)
   303  	if err != nil {
   304  		return errors.Trace(err)
   305  	}
   306  
   307  	if err = result.OneError(); err == nil {
   308  		return nil
   309  	}
   310  	if pErr, ok := errors.Cause(err).(*params.Error); ok {
   311  		switch pErr.Code {
   312  		case params.CodeNotFound:
   313  			// Remove the "not found" in the error message to prevent
   314  			// stuttering.
   315  			msg := strings.Replace(err.Error(), " not found", "", -1)
   316  			return errors.NotFoundf(msg)
   317  		}
   318  	}
   319  	return errors.Trace(err)
   320  }
   321  
   322  // CredentialContents returns contents of the credential values for the specified
   323  // cloud and credential name. Secrets will be included if requested.
   324  func (c *Client) CredentialContents(cloud, credential string, withSecrets bool) ([]params.CredentialContentResult, error) {
   325  	oneCredential := params.CloudCredentialArg{}
   326  	if cloud == "" && credential == "" {
   327  		// this is valid and means we want all.
   328  	} else if cloud == "" {
   329  		return nil, errors.New("cloud name must be supplied")
   330  	} else if credential == "" {
   331  		return nil, errors.New("credential name must be supplied")
   332  	} else {
   333  		oneCredential.CloudName = cloud
   334  		oneCredential.CredentialName = credential
   335  	}
   336  	var out params.CredentialContentResults
   337  	in := params.CloudCredentialArgs{
   338  		IncludeSecrets: withSecrets,
   339  	}
   340  	if !oneCredential.IsEmpty() {
   341  		in.Credentials = []params.CloudCredentialArg{oneCredential}
   342  	}
   343  	err := c.facade.FacadeCall("CredentialContents", in, &out)
   344  	if err != nil {
   345  		return nil, errors.Trace(err)
   346  	}
   347  	if !oneCredential.IsEmpty() && len(out.Results) != 1 {
   348  		return nil, errors.Errorf("expected 1 result for credential %q on cloud %q, got %d", cloud, credential, len(out.Results))
   349  	}
   350  	return out.Results, nil
   351  }
   352  
   353  // GrantCloud grants a user access to a cloud.
   354  func (c *Client) GrantCloud(user, access string, clouds ...string) error {
   355  	return c.modifyCloudUser(params.GrantCloudAccess, user, access, clouds)
   356  }
   357  
   358  // RevokeCloud revokes a user's access to a cloud.
   359  func (c *Client) RevokeCloud(user, access string, clouds ...string) error {
   360  	return c.modifyCloudUser(params.RevokeCloudAccess, user, access, clouds)
   361  }
   362  
   363  func (c *Client) modifyCloudUser(action params.CloudAction, user, access string, clouds []string) error {
   364  	var args params.ModifyCloudAccessRequest
   365  
   366  	if !names.IsValidUser(user) {
   367  		return errors.Errorf("invalid username: %q", user)
   368  	}
   369  	userTag := names.NewUserTag(user)
   370  
   371  	cloudAccess := permission.Access(access)
   372  	if err := permission.ValidateCloudAccess(cloudAccess); err != nil {
   373  		return errors.Trace(err)
   374  	}
   375  	for _, cloud := range clouds {
   376  		if !names.IsValidCloud(cloud) {
   377  			return errors.NotValidf("cloud %q", cloud)
   378  		}
   379  		cloudTag := names.NewCloudTag(cloud)
   380  		args.Changes = append(args.Changes, params.ModifyCloudAccess{
   381  			UserTag:  userTag.String(),
   382  			Action:   action,
   383  			Access:   access,
   384  			CloudTag: cloudTag.String(),
   385  		})
   386  	}
   387  
   388  	var result params.ErrorResults
   389  	err := c.facade.FacadeCall("ModifyCloudAccess", args, &result)
   390  	if err != nil {
   391  		return errors.Trace(err)
   392  	}
   393  	if len(result.Results) != len(args.Changes) {
   394  		return errors.Errorf("expected %d results, got %d", len(args.Changes), len(result.Results))
   395  	}
   396  
   397  	for i, r := range result.Results {
   398  		if r.Error != nil && r.Error.Code == params.CodeAlreadyExists {
   399  			logger.Warningf("cloud %q is already shared with %q", clouds[i], userTag.Id())
   400  			result.Results[i].Error = nil
   401  		}
   402  	}
   403  	return result.Combine()
   404  }
   405  
   406  func cloudFromParams(cloudName string, p params.Cloud) jujucloud.Cloud {
   407  	authTypes := make([]jujucloud.AuthType, len(p.AuthTypes))
   408  	for i, authType := range p.AuthTypes {
   409  		authTypes[i] = jujucloud.AuthType(authType)
   410  	}
   411  	regions := make([]jujucloud.Region, len(p.Regions))
   412  	for i, region := range p.Regions {
   413  		regions[i] = jujucloud.Region{
   414  			Name:             region.Name,
   415  			Endpoint:         region.Endpoint,
   416  			IdentityEndpoint: region.IdentityEndpoint,
   417  			StorageEndpoint:  region.StorageEndpoint,
   418  		}
   419  	}
   420  	var regionConfig map[string]jujucloud.Attrs
   421  	for r, attr := range p.RegionConfig {
   422  		if regionConfig == nil {
   423  			regionConfig = make(map[string]jujucloud.Attrs)
   424  		}
   425  		regionConfig[r] = attr
   426  	}
   427  	return jujucloud.Cloud{
   428  		Name:              cloudName,
   429  		Type:              p.Type,
   430  		AuthTypes:         authTypes,
   431  		Endpoint:          p.Endpoint,
   432  		IdentityEndpoint:  p.IdentityEndpoint,
   433  		StorageEndpoint:   p.StorageEndpoint,
   434  		Regions:           regions,
   435  		CACertificates:    p.CACertificates,
   436  		SkipTLSVerify:     p.SkipTLSVerify,
   437  		Config:            p.Config,
   438  		RegionConfig:      regionConfig,
   439  		IsControllerCloud: p.IsControllerCloud,
   440  	}
   441  }
   442  
   443  func cloudToParams(cloud jujucloud.Cloud) params.Cloud {
   444  	authTypes := make([]string, len(cloud.AuthTypes))
   445  	for i, authType := range cloud.AuthTypes {
   446  		authTypes[i] = string(authType)
   447  	}
   448  	regions := make([]params.CloudRegion, len(cloud.Regions))
   449  	for i, region := range cloud.Regions {
   450  		regions[i] = params.CloudRegion{
   451  			Name:             region.Name,
   452  			Endpoint:         region.Endpoint,
   453  			IdentityEndpoint: region.IdentityEndpoint,
   454  			StorageEndpoint:  region.StorageEndpoint,
   455  		}
   456  	}
   457  	var regionConfig map[string]map[string]interface{}
   458  	for r, attr := range cloud.RegionConfig {
   459  		if regionConfig == nil {
   460  			regionConfig = make(map[string]map[string]interface{})
   461  		}
   462  		regionConfig[r] = attr
   463  	}
   464  	return params.Cloud{
   465  		Type:              cloud.Type,
   466  		HostCloudRegion:   cloud.HostCloudRegion,
   467  		AuthTypes:         authTypes,
   468  		Endpoint:          cloud.Endpoint,
   469  		IdentityEndpoint:  cloud.IdentityEndpoint,
   470  		StorageEndpoint:   cloud.StorageEndpoint,
   471  		Regions:           regions,
   472  		CACertificates:    cloud.CACertificates,
   473  		SkipTLSVerify:     cloud.SkipTLSVerify,
   474  		Config:            cloud.Config,
   475  		RegionConfig:      regionConfig,
   476  		IsControllerCloud: cloud.IsControllerCloud,
   477  	}
   478  }
   479  
   480  func cloudDetailsFromParams(cloudName string, p params.CloudDetails) jujucloud.Cloud {
   481  	authTypes := make([]jujucloud.AuthType, len(p.AuthTypes))
   482  	for i, authType := range p.AuthTypes {
   483  		authTypes[i] = jujucloud.AuthType(authType)
   484  	}
   485  	regions := make([]jujucloud.Region, len(p.Regions))
   486  	for i, region := range p.Regions {
   487  		regions[i] = jujucloud.Region{
   488  			Name:             region.Name,
   489  			Endpoint:         region.Endpoint,
   490  			IdentityEndpoint: region.IdentityEndpoint,
   491  			StorageEndpoint:  region.StorageEndpoint,
   492  		}
   493  	}
   494  	return jujucloud.Cloud{
   495  		Name:             cloudName,
   496  		Type:             p.Type,
   497  		AuthTypes:        authTypes,
   498  		Endpoint:         p.Endpoint,
   499  		IdentityEndpoint: p.IdentityEndpoint,
   500  		StorageEndpoint:  p.StorageEndpoint,
   501  		Regions:          regions,
   502  	}
   503  }