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 }