github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/apiserver/facades/client/application/deployrepository.go (about) 1 // Copyright 2023 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package application 5 6 import ( 7 "fmt" 8 "strconv" 9 "sync" 10 11 "github.com/juju/charm/v12" 12 "github.com/juju/charm/v12/resource" 13 jujuclock "github.com/juju/clock" 14 "github.com/juju/collections/set" 15 "github.com/juju/errors" 16 "github.com/juju/names/v5" 17 "github.com/kr/pretty" 18 19 "github.com/juju/juju/apiserver/facade" 20 "github.com/juju/juju/apiserver/facades/client/charms/services" 21 "github.com/juju/juju/controller" 22 "github.com/juju/juju/core/arch" 23 corebase "github.com/juju/juju/core/base" 24 corecharm "github.com/juju/juju/core/charm" 25 "github.com/juju/juju/core/config" 26 "github.com/juju/juju/core/constraints" 27 "github.com/juju/juju/core/instance" 28 "github.com/juju/juju/core/network" 29 "github.com/juju/juju/environs/bootstrap" 30 environsconfig "github.com/juju/juju/environs/config" 31 "github.com/juju/juju/rpc/params" 32 "github.com/juju/juju/state" 33 "github.com/juju/juju/storage" 34 "github.com/juju/juju/storage/poolmanager" 35 jujuversion "github.com/juju/juju/version" 36 ) 37 38 var deployRepoLogger = logger.Child("deployfromrepository") 39 40 // DeployFromRepositoryValidator defines an deploy config validator. 41 type DeployFromRepositoryValidator interface { 42 ValidateArg(params.DeployFromRepositoryArg) (deployTemplate, []error) 43 } 44 45 // DeployFromRepository defines an interface for deploying a charm 46 // from a repository. 47 type DeployFromRepository interface { 48 DeployFromRepository(arg params.DeployFromRepositoryArg) (params.DeployFromRepositoryInfo, []*params.PendingResourceUpload, []error) 49 } 50 51 // DeployFromRepositoryState defines a common set of functions for retrieving state 52 // objects. 53 type DeployFromRepositoryState interface { 54 AddApplication(state.AddApplicationArgs) (Application, error) 55 AddPendingResource(string, resource.Resource) (string, error) 56 RemovePendingResources(applicationID string, pendingIDs map[string]string) error 57 AddCharmMetadata(info state.CharmInfo) (Charm, error) 58 Charm(string) (Charm, error) 59 ControllerConfig() (controller.Config, error) 60 Machine(string) (Machine, error) 61 ModelConstraints() (constraints.Value, error) 62 63 services.StateBackend 64 65 network.SpaceLookup 66 DefaultEndpointBindingSpace() (string, error) 67 Space(id string) (*state.Space, error) 68 } 69 70 // DeployFromRepositoryAPI provides the deploy from repository 71 // API facade for any given version. It is expected that any API 72 // parameter changes should be performed before entering the API. 73 type DeployFromRepositoryAPI struct { 74 state DeployFromRepositoryState 75 validator DeployFromRepositoryValidator 76 stateCharm func(Charm) *state.Charm 77 } 78 79 // NewDeployFromRepositoryAPI creates a new DeployFromRepositoryAPI. 80 func NewDeployFromRepositoryAPI(state DeployFromRepositoryState, validator DeployFromRepositoryValidator) DeployFromRepository { 81 return &DeployFromRepositoryAPI{ 82 state: state, 83 validator: validator, 84 stateCharm: CharmToStateCharm, 85 } 86 } 87 88 func (api *DeployFromRepositoryAPI) DeployFromRepository(arg params.DeployFromRepositoryArg) (params.DeployFromRepositoryInfo, []*params.PendingResourceUpload, []error) { 89 deployRepoLogger.Tracef("deployOneFromRepository(%s)", pretty.Sprint(arg)) 90 // Validate the args. 91 dt, addPendingResourceErrs := api.validator.ValidateArg(arg) 92 93 if len(addPendingResourceErrs) > 0 { 94 return params.DeployFromRepositoryInfo{}, nil, addPendingResourceErrs 95 } 96 97 info := params.DeployFromRepositoryInfo{ 98 Architecture: dt.origin.Platform.Architecture, 99 Base: params.Base{ 100 Name: dt.origin.Platform.OS, 101 Channel: dt.origin.Platform.Channel, 102 }, 103 Channel: dt.origin.Channel.String(), 104 EffectiveChannel: nil, 105 Name: dt.applicationName, 106 Revision: dt.charmURL.Revision, 107 } 108 if dt.dryRun { 109 return info, nil, nil 110 } 111 // Queue async charm download. 112 // AddCharmMetadata returns no error if the charm 113 // has already been queue'd or downloaded. 114 ch, err := api.state.AddCharmMetadata(state.CharmInfo{ 115 Charm: dt.charm, 116 ID: dt.charmURL.String(), 117 }) 118 if err != nil { 119 return params.DeployFromRepositoryInfo{}, nil, []error{errors.Trace(err)} 120 } 121 122 stOrigin, err := StateCharmOrigin(dt.origin) 123 if err != nil { 124 return params.DeployFromRepositoryInfo{}, nil, []error{errors.Trace(err)} 125 } 126 127 // Last step, add pending resources. 128 pendingIDs, addPendingResourceErrs := api.addPendingResources(dt.applicationName, dt.resolvedResources) 129 130 _, addApplicationErr := api.state.AddApplication(state.AddApplicationArgs{ 131 ApplicationConfig: dt.applicationConfig, 132 AttachStorage: dt.attachStorage, 133 Charm: api.stateCharm(ch), 134 CharmConfig: dt.charmSettings, 135 CharmOrigin: stOrigin, 136 Constraints: dt.constraints, 137 Devices: stateDeviceConstraints(arg.Devices), 138 EndpointBindings: dt.endpoints, 139 Name: dt.applicationName, 140 NumUnits: dt.numUnits, 141 Placement: dt.placement, 142 Resources: pendingIDs, 143 Storage: stateStorageConstraints(dt.storage), 144 }) 145 146 if addApplicationErr != nil { 147 // Check the pending resources that are added before the AddApplication is called 148 if pendingIDs != nil && len(pendingIDs) != 0 { 149 // Remove if there's any pending resources before raising addApplicationErr 150 removeResourcesErr := api.state.RemovePendingResources(dt.applicationName, pendingIDs) 151 if removeResourcesErr != nil { 152 deployRepoLogger.Errorf("unable to remove pending resources for %q", dt.applicationName) 153 } 154 } 155 return params.DeployFromRepositoryInfo{}, nil, []error{errors.Trace(addApplicationErr)} 156 } 157 158 return info, dt.pendingResourceUploads, addPendingResourceErrs 159 } 160 161 // PendingResourceUpload is only returned for local resources 162 // which will require the client to upload the resource once 163 // the DeployFromRepository returns. Errors are not terminal, 164 // and will be collected and returned altogether. 165 func (v *deployFromRepositoryValidator) resolveResources( 166 curl *charm.URL, 167 origin corecharm.Origin, 168 deployResArg map[string]string, 169 resMeta map[string]resource.Meta, 170 ) ([]resource.Resource, []*params.PendingResourceUpload, error) { 171 var pendingUploadIDs []*params.PendingResourceUpload 172 var resources []resource.Resource 173 174 for name, meta := range resMeta { 175 r := resource.Resource{ 176 Meta: meta, 177 Origin: resource.OriginStore, 178 Revision: -1, 179 } 180 deployValue, ok := deployResArg[name] 181 if ok { 182 // resource flag is used on the cli, either a resource revision, or a filename 183 if providedRev, err := strconv.Atoi(deployValue); err == nil { 184 // a resource revision is provided 185 r.Revision = providedRev 186 resources = append(resources, r) 187 continue 188 } 189 // a file is coming from the client 190 r.Origin = resource.OriginUpload 191 192 // add a PendingResourceUpload for this resource to be uploaded by the client 193 pendingUploadIDs = append(pendingUploadIDs, ¶ms.PendingResourceUpload{ 194 Name: meta.Name, 195 Type: meta.Type.String(), 196 Filename: deployValue, 197 }) 198 } 199 resources = append(resources, r) 200 } 201 202 repo, err := v.getCharmRepository(origin.Source) 203 if err != nil { 204 return nil, nil, errors.Trace(err) 205 } 206 resolvedResources, resolveErr := repo.ResolveResources(resources, corecharm.CharmID{URL: curl, Origin: origin}) 207 208 return resolvedResources, pendingUploadIDs, resolveErr 209 } 210 211 // addPendingResource adds a pending resource doc for all resources to be 212 // added when deploying the charm. All resources will be 213 // processed. Errors are not terminal. It also returns the name to pendingIDs 214 // map that's needed by the AddApplication. 215 func (api *DeployFromRepositoryAPI) addPendingResources(appName string, resources []resource.Resource) (map[string]string, []error) { 216 var errs []error 217 pendingIDs := make(map[string]string) 218 219 for _, r := range resources { 220 pID, err := api.state.AddPendingResource(appName, r) 221 if err != nil { 222 deployRepoLogger.Errorf("Unable to add pending resource %v for application %v: %v", r.Name, appName, err) 223 errs = append(errs, err) 224 continue 225 } 226 pendingIDs[r.Name] = pID 227 } 228 229 return pendingIDs, errs 230 } 231 232 type deployTemplate struct { 233 applicationConfig *config.Config 234 applicationName string 235 attachStorage []names.StorageTag 236 charm charm.Charm 237 charmSettings charm.Settings 238 charmURL *charm.URL 239 constraints constraints.Value 240 endpoints map[string]string 241 dryRun bool 242 force bool 243 numUnits int 244 origin corecharm.Origin 245 placement []*instance.Placement 246 resources map[string]string 247 storage map[string]storage.Constraints 248 pendingResourceUploads []*params.PendingResourceUpload 249 resolvedResources []resource.Resource 250 } 251 252 type validatorConfig struct { 253 charmhubHTTPClient facade.HTTPClient 254 caasBroker CaasBrokerInterface 255 model Model 256 registry storage.ProviderRegistry 257 state DeployFromRepositoryState 258 storagePoolManager poolmanager.PoolManager 259 } 260 261 func makeDeployFromRepositoryValidator(cfg validatorConfig) DeployFromRepositoryValidator { 262 v := &deployFromRepositoryValidator{ 263 charmhubHTTPClient: cfg.charmhubHTTPClient, 264 model: cfg.model, 265 state: cfg.state, 266 newRepoFactory: func(cfg services.CharmRepoFactoryConfig) corecharm.RepositoryFactory { 267 return services.NewCharmRepoFactory(cfg) 268 }, 269 newStateBindings: func(st state.EndpointBinding, givenMap map[string]string) (Bindings, error) { 270 return state.NewBindings(st, givenMap) 271 }, 272 } 273 if cfg.model.Type() == state.ModelTypeCAAS { 274 return &caasDeployFromRepositoryValidator{ 275 caasBroker: cfg.caasBroker, 276 registry: cfg.registry, 277 storagePoolManager: cfg.storagePoolManager, 278 validator: v, 279 caasPrecheckFunc: func(dt deployTemplate) error { 280 attachStorage := make([]string, len(dt.attachStorage)) 281 for i, tag := range dt.attachStorage { 282 attachStorage[i] = tag.Id() 283 } 284 cdp := caasDeployParams{ 285 applicationName: dt.applicationName, 286 attachStorage: attachStorage, 287 charm: dt.charm, 288 config: nil, 289 placement: dt.placement, 290 storage: dt.storage, 291 } 292 return cdp.precheck(v.model, cfg.storagePoolManager, cfg.registry, cfg.caasBroker) 293 }, 294 } 295 } 296 return &iaasDeployFromRepositoryValidator{ 297 validator: v, 298 } 299 } 300 301 type deployFromRepositoryValidator struct { 302 model Model 303 state DeployFromRepositoryState 304 305 mu sync.Mutex 306 repoFactory corecharm.RepositoryFactory 307 // For testing using mocks. 308 newRepoFactory func(services.CharmRepoFactoryConfig) corecharm.RepositoryFactory 309 charmhubHTTPClient facade.HTTPClient 310 311 // For testing using mocks. 312 newStateBindings func(st state.EndpointBinding, givenMap map[string]string) (Bindings, error) 313 } 314 315 // Validating arguments to deploy a charm. 316 // General (see deployFromRepositoryValidator) 317 // - Resolve the charm and ensure it exists in a repository 318 // - Ensure supplied resources exist 319 // - Find repository resources to be used. 320 // - Check machine placement against current deployment - does not include 321 // the caas check below. 322 // - Find a charm to match the provided name and architecture at a minimum, 323 // and base, revision, and channel if provided. 324 // - Does the charm already exist in juju? If so use it, rather than 325 // attempting downloading. 326 // - Check endpoint bindings against existing 327 // - Subordinates may not have constraints nor numunits specified 328 // - Supplied charm config must validate against config defined in the charm. 329 // - Check charm assumptions against the controller config, defined in core 330 // assumes featureset. 331 // - Check minimum juju version against current as defined in charm. 332 // - NumUnits must be 1 if AttachedStorage used 333 // - CharmOrigin validation, see common.ValidateCharmOrigin 334 // - Manual deploy of juju-controller charm not allowed. 335 // 336 // IAAS specific (see iaasDeployFromRepositoryValidator) 337 // CAAS specific (see caasDeployFromRepositoryValidator) 338 // 339 // validateDeployFromRepositoryArgs does validation of all provided 340 // arguments. Returned is a deployTemplate which contains validated 341 // data necessary to deploy the application. 342 // Where possible, errors will be grouped and returned as a list. 343 func (v *deployFromRepositoryValidator) validate(arg params.DeployFromRepositoryArg) (deployTemplate, []error) { 344 errs := make([]error, 0) 345 346 if err := checkMachinePlacement(v.state, v.model.UUID(), arg.ApplicationName, arg.Placement); err != nil { 347 errs = append(errs, err) 348 } 349 350 // get the charm data to validate against, either a previously deployed 351 // charm or the essential metadata from a charm to be async downloaded. 352 charmURL, resolvedOrigin, resolvedCharm, getCharmErr := v.getCharm(arg) 353 if getCharmErr != nil { 354 errs = append(errs, getCharmErr) 355 // return any errors here, there is no need to continue with 356 // validation if we cannot find the charm. 357 return deployTemplate{}, errs 358 } 359 360 // Various checks of the resolved charm against the arg provided. 361 dt, rcErrs := v.resolvedCharmValidation(resolvedCharm, arg) 362 if len(rcErrs) > 0 { 363 errs = append(errs, rcErrs...) 364 } 365 366 dt.charmURL = charmURL 367 dt.dryRun = arg.DryRun 368 dt.force = arg.Force 369 dt.origin = resolvedOrigin 370 dt.placement = arg.Placement 371 dt.storage = arg.Storage 372 if len(arg.EndpointBindings) > 0 { 373 bindings, err := v.newStateBindings(v.state, arg.EndpointBindings) 374 if err != nil { 375 errs = append(errs, err) 376 } else { 377 dt.endpoints = bindings.Map() 378 } 379 } 380 // resolve and validate resources 381 resources, pendingResourceUploads, resolveResErr := v.resolveResources(dt.charmURL, dt.origin, dt.resources, resolvedCharm.Meta().Resources) 382 if resolveResErr != nil { 383 errs = append(errs, resolveResErr) 384 } 385 386 dt.pendingResourceUploads = pendingResourceUploads 387 dt.resolvedResources = resources 388 389 if deployRepoLogger.IsTraceEnabled() { 390 deployRepoLogger.Tracef("validateDeployFromRepositoryArgs returning: %s", pretty.Sprint(dt)) 391 } 392 return dt, errs 393 } 394 395 func validateAndParseAttachStorage(input []string, numUnits int) ([]names.StorageTag, []error) { 396 // Parse storage tags in AttachStorage. 397 if len(input) > 0 && numUnits != 1 { 398 return nil, []error{errors.Errorf("AttachStorage is non-empty, but NumUnits is %d", numUnits)} 399 } 400 if len(input) == 0 { 401 return nil, nil 402 } 403 attachStorage := make([]names.StorageTag, len(input)) 404 errs := make([]error, 0) 405 for i, stor := range input { 406 if names.IsValidStorage(stor) { 407 attachStorage[i] = names.NewStorageTag(stor) 408 } else { 409 errs = append(errs, errors.NotValidf("storage name %q", stor)) 410 } 411 } 412 return attachStorage, errs 413 } 414 415 func (v *deployFromRepositoryValidator) resolvedCharmValidation(resolvedCharm charm.Charm, arg params.DeployFromRepositoryArg) (deployTemplate, []error) { 416 errs := make([]error, 0) 417 418 var cons constraints.Value 419 var numUnits int 420 if resolvedCharm.Meta().Subordinate { 421 if arg.NumUnits != nil && *arg.NumUnits != 0 && constraints.IsEmpty(&arg.Cons) { 422 numUnits = 0 423 } 424 if !constraints.IsEmpty(&arg.Cons) { 425 errs = append(errs, fmt.Errorf("subordinate application must be deployed without constraints")) 426 } 427 } else { 428 cons = arg.Cons 429 430 if arg.NumUnits != nil { 431 numUnits = *arg.NumUnits 432 } else { 433 // The juju client defaults num units to 1. Ensure that a 434 // charm deployed by any client has at least one if no 435 // number provided. 436 numUnits = 1 437 } 438 } 439 440 // appNameForConfig is the application name used in a config file. 441 // It is based on user knowledge and either the charm or application 442 // name from the cli. 443 appNameForConfig := arg.CharmName 444 if arg.ApplicationName != "" { 445 appNameForConfig = arg.ApplicationName 446 } 447 appConfig, settings, err := v.appCharmSettings(appNameForConfig, arg.Trust, resolvedCharm.Config(), arg.ConfigYAML) 448 if err != nil { 449 errs = append(errs, err) 450 } 451 452 if err := jujuversion.CheckJujuMinVersion(resolvedCharm.Meta().MinJujuVersion, jujuversion.Current); err != nil { 453 errs = append(errs, err) 454 } 455 456 // The appName is subtly different from the application config name. 457 // The charm name in the metadata can be different from the charm 458 // name used to deploy a charm. 459 appName := resolvedCharm.Meta().Name 460 if arg.ApplicationName != "" { 461 appName = arg.ApplicationName 462 } 463 464 // Enforce "assumes" requirements if the feature flag is enabled. 465 if err := assertCharmAssumptions(resolvedCharm.Meta().Assumes, v.model, v.state.ControllerConfig); err != nil { 466 if !errors.Is(err, errors.NotSupported) || !arg.Force { 467 errs = append(errs, err) 468 } 469 deployRepoLogger.Warningf("proceeding with deployment of application even though the charm feature requirements could not be met as --force was specified") 470 } 471 472 dt := deployTemplate{ 473 applicationConfig: appConfig, 474 applicationName: appName, 475 charm: resolvedCharm, 476 charmSettings: settings, 477 constraints: cons, 478 numUnits: numUnits, 479 resources: arg.Resources, 480 } 481 482 return dt, errs 483 } 484 485 type caasDeployFromRepositoryValidator struct { 486 validator *deployFromRepositoryValidator 487 488 caasBroker CaasBrokerInterface 489 registry storage.ProviderRegistry 490 storagePoolManager poolmanager.PoolManager 491 492 // Needed for testing. caasDeployTemplate precheck functionality tested 493 // elsewhere 494 caasPrecheckFunc func(deployTemplate) error 495 } 496 497 // CAAS specific validation of arguments to deploy a charm 498 // - Storage is not allowed 499 // - Only 1 value placement allowed 500 // - Block storage is not allowed 501 // - Check the ServiceTypeConfigKey value is valid and find a translation 502 // of types 503 // - Check kubernetes model config values against the kubernetes cluster 504 // in use 505 // - Check the charm's min version against the caasVersion 506 func (v caasDeployFromRepositoryValidator) ValidateArg(arg params.DeployFromRepositoryArg) (deployTemplate, []error) { 507 dt, errs := v.validator.validate(arg) 508 if len(errs) > 0 { 509 return dt, errs 510 } 511 if corecharm.IsKubernetes(dt.charm) && charm.MetaFormat(dt.charm) == charm.FormatV1 { 512 deployRepoLogger.Debugf("DEPRECATED: %q is a podspec charm, which will be removed in a future release", arg.CharmName) 513 } 514 // TODO 515 // Convert dt.applicationConfig from Config to a map[string]string. 516 // Config across the wire as a map[string]string no longer exists for 517 // deploy. How to get the caas provider config here? 518 if err := v.caasPrecheckFunc(dt); err != nil { 519 errs = append(errs, err) 520 } 521 return dt, errs 522 } 523 524 type iaasDeployFromRepositoryValidator struct { 525 validator *deployFromRepositoryValidator 526 } 527 528 // ValidateArg validates DeployFromRepositoryArg from an iaas perspective. 529 // First checking the common validation, then any validation specific to 530 // iaas charms. 531 func (v iaasDeployFromRepositoryValidator) ValidateArg(arg params.DeployFromRepositoryArg) (deployTemplate, []error) { 532 dt, errs := v.validator.validate(arg) 533 if len(errs) > 0 { 534 return dt, errs 535 } 536 attachStorage, attachStorageErrs := validateAndParseAttachStorage(arg.AttachStorage, dt.numUnits) 537 if len(attachStorageErrs) > 0 { 538 errs = append(errs, attachStorageErrs...) 539 } 540 dt.attachStorage = attachStorage 541 return dt, errs 542 } 543 544 func (v *deployFromRepositoryValidator) createOrigin(arg params.DeployFromRepositoryArg) (*charm.URL, corecharm.Origin, bool, error) { 545 path, err := charm.EnsureSchema(arg.CharmName, charm.CharmHub) 546 if err != nil { 547 return nil, corecharm.Origin{}, false, err 548 } 549 curl, err := charm.ParseURL(path) 550 if err != nil { 551 return nil, corecharm.Origin{}, false, err 552 } 553 if arg.Revision != nil { 554 curl = curl.WithRevision(*arg.Revision) 555 } 556 if !charm.CharmHub.Matches(curl.Schema) { 557 return nil, corecharm.Origin{}, false, errors.Errorf("unknown schema for charm URL %q", curl.String()) 558 } 559 channelStr := corecharm.DefaultChannelString 560 if arg.Channel != nil && *arg.Channel != "" { 561 channelStr = *arg.Channel 562 } 563 channel, err := charm.ParseChannelNormalize(channelStr) 564 if err != nil { 565 return nil, corecharm.Origin{}, false, err 566 } 567 568 plat, usedModelDefaultBase, err := v.deducePlatform(arg) 569 if err != nil { 570 return nil, corecharm.Origin{}, false, err 571 } 572 573 origin := corecharm.Origin{ 574 Channel: &channel, 575 Platform: plat, 576 Revision: arg.Revision, 577 Source: corecharm.CharmHub, 578 } 579 return curl, origin, usedModelDefaultBase, nil 580 } 581 582 // deducePlatform returns a platform for initial resolveCharm call. 583 // At minimum, it must contain an architecture. 584 // Platform is determined by the args: architecture constraint and 585 // provided base. 586 // - Check placement to determine known machine platform. If diffs from 587 // other provided data return error. 588 // - If no base provided, use model default base. 589 // - If no model default base, will be determined later. 590 // - If no architecture provided, use model default. Fallback 591 // to DefaultArchitecture. 592 func (v *deployFromRepositoryValidator) deducePlatform(arg params.DeployFromRepositoryArg) (corecharm.Platform, bool, error) { 593 argArch := arg.Cons.Arch 594 argBase := arg.Base 595 var usedModelDefaultBase bool 596 var usedModelDefaultArch bool 597 598 // Try argBase with provided argArch and argBase first. 599 platform := corecharm.Platform{} 600 if argArch != nil { 601 platform.Architecture = *argArch 602 } 603 // Fallback to model defaults if set. DefaultArchitecture otherwise. 604 if platform.Architecture == "" { 605 mConst, err := v.state.ModelConstraints() 606 if err != nil { 607 return corecharm.Platform{}, usedModelDefaultBase, err 608 } 609 if mConst.Arch != nil { 610 platform.Architecture = *mConst.Arch 611 } else { 612 platform.Architecture = arch.DefaultArchitecture 613 usedModelDefaultArch = true 614 } 615 } 616 if argBase != nil { 617 base, err := corebase.ParseBase(argBase.Name, argBase.Channel) 618 if err != nil { 619 return corecharm.Platform{}, usedModelDefaultBase, err 620 } 621 platform.OS = base.OS 622 // platform channels don't model the concept of a risk 623 // so ensure that only the track is included 624 platform.Channel = base.Channel.Track 625 } 626 627 // Initial validation of platform from known data. 628 _, err := corecharm.ParsePlatform(platform.String()) 629 if err != nil && !errors.Is(err, errors.BadRequest) { 630 return corecharm.Platform{}, usedModelDefaultBase, err 631 } 632 633 // Match against platforms from placement 634 placementPlatform, placementsMatch, err := v.platformFromPlacement(arg.Placement) 635 if err != nil && !errors.Is(err, errors.NotFound) { 636 return corecharm.Platform{}, usedModelDefaultBase, err 637 } 638 if err == nil && !placementsMatch { 639 return corecharm.Platform{}, usedModelDefaultBase, errors.BadRequestf("bases of existing placement machines do not match") 640 } 641 642 // No platform args, and one platform from placement, use that. 643 if placementsMatch && usedModelDefaultArch && argBase == nil { 644 return placementPlatform, usedModelDefaultBase, nil 645 } 646 if platform.Channel == "" { 647 mCfg, err := v.model.Config() 648 if err != nil { 649 return corecharm.Platform{}, usedModelDefaultBase, err 650 } 651 if db, ok := mCfg.DefaultBase(); ok { 652 defaultBase, err := corebase.ParseBaseFromString(db) 653 if err != nil { 654 return corecharm.Platform{}, usedModelDefaultBase, err 655 } 656 platform.OS = defaultBase.OS 657 // platform channels don't model the concept of a risk 658 // so ensure that only the track is included 659 platform.Channel = defaultBase.Channel.Track 660 usedModelDefaultBase = true 661 } 662 } 663 return platform, usedModelDefaultBase, nil 664 } 665 666 func (v *deployFromRepositoryValidator) platformFromPlacement(placements []*instance.Placement) (corecharm.Platform, bool, error) { 667 if len(placements) == 0 { 668 return corecharm.Platform{}, false, errors.NotFoundf("placements") 669 } 670 machines := make([]Machine, 0) 671 // Find which machines in placement actually exist. 672 for _, placement := range placements { 673 m, err := v.state.Machine(placement.Directive) 674 if errors.Is(err, errors.NotFound) { 675 continue 676 } 677 if err != nil { 678 return corecharm.Platform{}, false, err 679 } 680 machines = append(machines, m) 681 } 682 if len(machines) == 0 { 683 return corecharm.Platform{}, false, errors.NotFoundf("machines in placements") 684 } 685 686 // Gather platforms for existing machines 687 var platform corecharm.Platform 688 platStrings := set.NewStrings() 689 for _, machine := range machines { 690 b := machine.Base() 691 a, err := machine.HardwareCharacteristics() 692 if err != nil { 693 return corecharm.Platform{}, false, err 694 } 695 platString := fmt.Sprintf("%s/%s/%s", *a.Arch, b.OS, b.Channel) 696 p, err := corecharm.ParsePlatformNormalize(platString) 697 if err != nil { 698 return corecharm.Platform{}, false, err 699 } 700 platform = p 701 platStrings.Add(p.String()) 702 } 703 704 return platform, platStrings.Size() == 1, nil 705 } 706 707 func (v *deployFromRepositoryValidator) resolveCharm(curl *charm.URL, requestedOrigin corecharm.Origin, force, usedModelDefaultBase bool, cons constraints.Value) (corecharm.ResolvedDataForDeploy, error) { 708 repo, err := v.getCharmRepository(requestedOrigin.Source) 709 if err != nil { 710 return corecharm.ResolvedDataForDeploy{}, errors.Trace(err) 711 } 712 713 // TODO (hml) 2023-05-16 714 // Use resource data found in resolvedData as part of ResolveResource. 715 // Will require a new method on the repo. 716 resolvedData, resolveErr := repo.ResolveForDeploy(corecharm.CharmID{URL: curl, Origin: requestedOrigin}) 717 if charm.IsUnsupportedSeriesError(resolveErr) { 718 if !force { 719 msg := fmt.Sprintf("%v. Use --force to deploy the charm anyway.", resolveErr) 720 if usedModelDefaultBase { 721 msg += " Used the default-base." 722 } 723 return corecharm.ResolvedDataForDeploy{}, errors.Errorf(msg) 724 } 725 } else if resolveErr != nil { 726 return corecharm.ResolvedDataForDeploy{}, errors.Trace(resolveErr) 727 } 728 resolvedOrigin := &resolvedData.EssentialMetadata.ResolvedOrigin 729 730 modelCons, err := v.state.ModelConstraints() 731 if err != nil { 732 return corecharm.ResolvedDataForDeploy{}, errors.Trace(err) 733 } 734 735 // The charmhub API can return "all" for architecture as it's not a real 736 // arch we don't know how to correctly model it. "all " doesn't mean use the 737 // default arch, it means use any arch which isn't quite the same. So if we 738 // do get "all" we should see if there is a clean way to resolve it. 739 if resolvedOrigin.Platform.Architecture == "all" { 740 resolvedOrigin.Platform.Architecture = constraints.ArchOrDefault(modelCons, nil) 741 } 742 743 var requestedBase corebase.Base 744 if requestedOrigin.Platform.OS != "" { 745 // The requested base has either been specified directly as a 746 // base argument, or via model config DefaultBase, to be 747 // part of the requestedOrigin. 748 var err error 749 requestedBase, err = corebase.ParseBase(requestedOrigin.Platform.OS, requestedOrigin.Platform.Channel) 750 if err != nil { 751 return corecharm.ResolvedDataForDeploy{}, errors.Trace(err) 752 } 753 } 754 755 modelCfg, err := v.model.Config() 756 if err != nil { 757 return corecharm.ResolvedDataForDeploy{}, errors.Trace(err) 758 } 759 supportedBases, err := corebase.ParseManifestBases(resolvedData.EssentialMetadata.Manifest.Bases) 760 if err != nil { 761 return corecharm.ResolvedDataForDeploy{}, errors.Trace(err) 762 } 763 workloadBases, err := corebase.WorkloadBases(jujuclock.WallClock.Now(), requestedBase, modelCfg.ImageStream()) 764 if err != nil { 765 return corecharm.ResolvedDataForDeploy{}, errors.Trace(err) 766 } 767 bsCfg := corecharm.SelectorConfig{ 768 Config: modelCfg, 769 Force: force, 770 Logger: deployRepoLogger, 771 RequestedBase: requestedBase, 772 SupportedCharmBases: supportedBases, 773 WorkloadBases: workloadBases, 774 UsingImageID: cons.HasImageID() || modelCons.HasImageID(), 775 } 776 selector, err := corecharm.ConfigureBaseSelector(bsCfg) 777 if err != nil { 778 return corecharm.ResolvedDataForDeploy{}, errors.Trace(err) 779 } 780 // Get the base to use. 781 base, err := selector.CharmBase() 782 if corecharm.IsUnsupportedBaseError(err) { 783 msg := fmt.Sprintf("%v. Use --force to deploy the charm anyway.", err) 784 if usedModelDefaultBase { 785 msg += " Used the default-base." 786 } 787 return corecharm.ResolvedDataForDeploy{}, errors.Errorf(msg) 788 } else if err != nil { 789 return corecharm.ResolvedDataForDeploy{}, errors.Trace(err) 790 } 791 deployRepoLogger.Tracef("Using base %q from %v to deploy %v", base, supportedBases, curl) 792 793 resolvedOrigin.Platform.OS = base.OS 794 // Avoid using Channel.String() here instead of Channel.Track for the Platform.Channel, 795 // because String() will return "track/risk" if the channel's risk is non-empty 796 resolvedOrigin.Platform.Channel = base.Channel.Track 797 798 return resolvedData, nil 799 } 800 801 // getCharm returns the charm being deployed. Either it already has been 802 // used once, and we get the data from state. Or we get the essential metadata. 803 func (v *deployFromRepositoryValidator) getCharm(arg params.DeployFromRepositoryArg) (*charm.URL, corecharm.Origin, charm.Charm, error) { 804 initialCurl, requestedOrigin, usedModelDefaultBase, err := v.createOrigin(arg) 805 if err != nil { 806 return nil, corecharm.Origin{}, nil, errors.Trace(err) 807 } 808 deployRepoLogger.Tracef("from createOrigin: %s, %s", initialCurl, pretty.Sprint(requestedOrigin)) 809 810 // Fetch the essential metadata that we require to deploy the charm 811 // without downloading the full archive. The remaining metadata will 812 // be populated once the charm gets downloaded. 813 resolvedData, err := v.resolveCharm(initialCurl, requestedOrigin, arg.Force, usedModelDefaultBase, arg.Cons) 814 if err != nil { 815 return nil, corecharm.Origin{}, nil, err 816 } 817 resolvedOrigin := resolvedData.EssentialMetadata.ResolvedOrigin 818 deployRepoLogger.Tracef("from resolveCharm: %s, %s", resolvedData.URL, pretty.Sprint(resolvedOrigin)) 819 if resolvedOrigin.Type != "charm" { 820 return nil, corecharm.Origin{}, nil, errors.BadRequestf("%q is not a charm", arg.CharmName) 821 } 822 823 resolvedCharm := corecharm.NewCharmInfoAdapter(resolvedData.EssentialMetadata) 824 if resolvedCharm.Meta().Name == bootstrap.ControllerCharmName { 825 return nil, corecharm.Origin{}, nil, errors.NotSupportedf("manual deploy of the controller charm") 826 } 827 828 // Check if a charm doc already exists for this charm URL. If so, the 829 // charm has already been queued for download so this is a no-op. We 830 // still need to resolve and return back a suitable origin as charmhub 831 // may refer to the same blob using the same revision in different 832 // channels. 833 deployedCharm, err := v.state.Charm(resolvedData.URL.String()) 834 if err != nil && !errors.Is(err, errors.NotFound) { 835 return nil, corecharm.Origin{}, nil, errors.Trace(err) 836 } else if err == nil { 837 return resolvedData.URL, resolvedOrigin, deployedCharm, nil 838 } 839 840 // This charm needs to be downloaded, remove the ID and Hash to 841 // allow it to happen. 842 resolvedOrigin.ID = "" 843 resolvedOrigin.Hash = "" 844 return resolvedData.URL, resolvedOrigin, resolvedCharm, nil 845 } 846 847 func (v *deployFromRepositoryValidator) appCharmSettings(appName string, trust bool, chCfg *charm.Config, configYAML string) (*config.Config, charm.Settings, error) { 848 if !trust && configYAML == "" { 849 return nil, nil, nil 850 } 851 // Cheat with trust. Trust is passed to DeployFromRepository as a flag, however 852 // it's handled internally to juju as an application config. As DFR only 853 // has charm config via yaml, stick trust into the config via map to enable 854 // reuse of current parseCharmSettings as used with the old deploy and 855 // setConfig. 856 // At deploy time, there's no need to include "trust=false" as missing is the same thing. 857 var cfg map[string]string 858 if trust { 859 cfg = map[string]string{"trust": "true"} 860 } 861 appConfig, _, charmSettings, _, err := parseCharmSettings(v.model.Type(), chCfg, appName, cfg, configYAML, environsconfig.NoDefaults) 862 return appConfig, charmSettings, err 863 } 864 865 func (v *deployFromRepositoryValidator) getCharmRepository(src corecharm.Source) (corecharm.Repository, error) { 866 // The following is only required for testing, as we generate api new http 867 // client here for production. 868 v.mu.Lock() 869 if v.repoFactory != nil { 870 defer v.mu.Unlock() 871 return v.repoFactory.GetCharmRepository(src) 872 } 873 v.mu.Unlock() 874 875 repoFactory := v.newRepoFactory(services.CharmRepoFactoryConfig{ 876 Logger: deployRepoLogger, 877 CharmhubHTTPClient: v.charmhubHTTPClient, 878 StateBackend: v.state, 879 ModelBackend: v.model, 880 }) 881 882 return repoFactory.GetCharmRepository(src) 883 }