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, &params.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  }