github.com/juju/juju@v0.0.0-20240327075706-a90865de2538/core/charm/baseselector.go (about)

     1  // Copyright 2023 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package charm
     5  
     6  import (
     7  	"fmt"
     8  	"strings"
     9  
    10  	"github.com/juju/collections/set"
    11  	"github.com/juju/errors"
    12  
    13  	"github.com/juju/juju/core/base"
    14  	"github.com/juju/juju/version"
    15  )
    16  
    17  const (
    18  	msgUserRequestedBase = "with the user specified base %q"
    19  	msgLatestLTSBase     = "with the latest LTS base %q"
    20  )
    21  
    22  // SelectorLogger defines the logging methods needed
    23  type SelectorLogger interface {
    24  	Infof(string, ...interface{})
    25  	Tracef(string, ...interface{})
    26  }
    27  
    28  // BaseSelector is a helper type that determines what base the charm should
    29  // be deployed to.
    30  type BaseSelector struct {
    31  	requestedBase       base.Base
    32  	defaultBase         base.Base
    33  	explicitDefaultBase bool
    34  	force               bool
    35  	logger              SelectorLogger
    36  	// supportedBases is the union of SupportedCharmBases and
    37  	// SupportedJujuBases.
    38  	supportedBases     []base.Base
    39  	jujuSupportedBases set.Strings
    40  	usingImageID       bool
    41  }
    42  
    43  type SelectorConfig struct {
    44  	Config              SelectorModelConfig
    45  	Force               bool
    46  	Logger              SelectorLogger
    47  	RequestedBase       base.Base
    48  	SupportedCharmBases []base.Base
    49  	WorkloadBases       []base.Base
    50  	// usingImageID is true when the user is using the image-id constraint
    51  	// when deploying the charm. This is needed to validate that in that
    52  	// case the user is also explicitly providing a base.
    53  	UsingImageID bool
    54  }
    55  
    56  type SelectorModelConfig interface {
    57  	// DefaultBase returns the configured default base
    58  	// for the environment, and whether the default base was
    59  	// explicitly configured on the environment.
    60  	DefaultBase() (string, bool)
    61  }
    62  
    63  // ConfigureBaseSelector returns a configured and validated BaseSelector
    64  func ConfigureBaseSelector(cfg SelectorConfig) (BaseSelector, error) {
    65  	// TODO (hml) 2023-05-16
    66  	// Is there more we can do here and reduce the prep work
    67  	// necessary for the callers?
    68  	defaultBase, explicit := cfg.Config.DefaultBase()
    69  	var (
    70  		parsedDefaultBase base.Base
    71  		err               error
    72  	)
    73  	if explicit {
    74  		parsedDefaultBase, err = base.ParseBaseFromString(defaultBase)
    75  		if err != nil {
    76  			return BaseSelector{}, errors.Trace(err)
    77  		}
    78  	}
    79  	bs := BaseSelector{
    80  		requestedBase:       cfg.RequestedBase,
    81  		defaultBase:         parsedDefaultBase,
    82  		explicitDefaultBase: explicit,
    83  		force:               cfg.Force,
    84  		logger:              cfg.Logger,
    85  		usingImageID:        cfg.UsingImageID,
    86  		jujuSupportedBases:  set.NewStrings(),
    87  	}
    88  	bs.supportedBases, err = bs.validate(cfg.SupportedCharmBases, cfg.WorkloadBases)
    89  	if err != nil {
    90  		return BaseSelector{}, errors.Trace(err)
    91  	}
    92  	return bs, nil
    93  }
    94  
    95  // TODO(nvinuesa): The force flag is only valid if the requestedBase is specified
    96  // or to force the deploy of a LXD profile that doesn't pass validation, this
    97  // should be added to these validation checks.
    98  func (s BaseSelector) validate(supportedCharmBases, supportedJujuBases []base.Base) ([]base.Base, error) {
    99  	// If the image-id constraint is provided then base must be explicitly
   100  	// provided either by flag either by model-config default base.
   101  	if s.logger == nil {
   102  		return nil, errors.NotValidf("empty Logger")
   103  	}
   104  	if s.usingImageID && s.requestedBase.Empty() && !s.explicitDefaultBase {
   105  		return nil, errors.Forbiddenf("base must be explicitly provided when image-id constraint is used")
   106  	}
   107  	if len(supportedCharmBases) == 0 {
   108  		return nil, errors.NotValidf("charm does not define any bases,")
   109  	}
   110  	if len(supportedJujuBases) == 0 {
   111  		return nil, errors.NotValidf("no juju supported bases")
   112  	}
   113  	// Verify that the charm supported bases include at least one juju
   114  	// supported base.
   115  	var supportedBases []base.Base
   116  	for _, charmBase := range supportedCharmBases {
   117  		for _, jujuCharmBase := range supportedJujuBases {
   118  			s.jujuSupportedBases.Add(jujuCharmBase.String())
   119  			if jujuCharmBase.IsCompatible(charmBase) {
   120  				supportedBases = append(supportedBases, charmBase)
   121  				s.logger.Infof(msgUserRequestedBase, charmBase)
   122  			}
   123  		}
   124  	}
   125  	if len(supportedBases) == 0 {
   126  		return nil, errors.NotSupportedf("the charm defined bases %q", printBases(supportedCharmBases))
   127  	}
   128  	return supportedBases, nil
   129  }
   130  
   131  // CharmBase determines what base to use with a charm.
   132  // Order of preference is:
   133  //   - user requested with --base or defined by bundle when deploying
   134  //   - model default, if set, acts like --base
   135  //   - juju default ubuntu LTS from charm manifest
   136  //   - first base listed in the charm manifest
   137  //   - in the case of local charms with no manifest nor base in metadata,
   138  //     base must be provided by the user.
   139  func (s BaseSelector) CharmBase() (selectedBase base.Base, err error) {
   140  	// TODO(sidecar): handle systems
   141  
   142  	// TODO (hml) 2023-05-16
   143  	// BaseSelector needs refinement. It is currently a copy of
   144  	// SeriesSelector, however it does too much for too many
   145  	// cases.
   146  
   147  	// User has requested a base with --base.
   148  	if !s.requestedBase.Empty() {
   149  		return s.userRequested(s.requestedBase)
   150  	}
   151  
   152  	// Use model default base, if explicitly set and supported by the charm.
   153  	// Cannot guarantee that the requestedBase is either a user supplied base or
   154  	// the DefaultBase model config if supplied.
   155  	if s.explicitDefaultBase {
   156  		return s.userRequested(s.defaultBase)
   157  	}
   158  
   159  	// Prefer latest Ubuntu LTS.
   160  	preferredBase, err := BaseForCharm(base.LatestLTSBase(), s.supportedBases)
   161  	if err == nil {
   162  		s.logger.Infof(msgLatestLTSBase, base.LatestLTSBase())
   163  		return preferredBase, nil
   164  	} else if errors.Is(err, MissingBaseError) {
   165  		return base.Base{}, err
   166  	}
   167  
   168  	// Try juju's current default supported Ubuntu LTS
   169  	jujuDefaultBase, err := BaseForCharm(version.DefaultSupportedLTSBase(), s.supportedBases)
   170  	if err == nil {
   171  		s.logger.Infof(msgLatestLTSBase, version.DefaultSupportedLTSBase())
   172  		return jujuDefaultBase, nil
   173  	}
   174  
   175  	// Last chance, the first base in the charm's manifest
   176  	return BaseForCharm(base.Base{}, s.supportedBases)
   177  }
   178  
   179  // userRequested checks the base the user has requested, and returns it if it
   180  // is supported, or if they used --force.
   181  func (s BaseSelector) userRequested(requestedBase base.Base) (base.Base, error) {
   182  	// TODO(sidecar): handle computed base
   183  	b, err := BaseForCharm(requestedBase, s.supportedBases)
   184  	if s.force && IsUnsupportedBaseError(err) && s.jujuSupportedBases.Contains(requestedBase.String()) {
   185  		// If the base is unsupported by juju, using force will not
   186  		// apply.
   187  		b = requestedBase
   188  	} else if err != nil {
   189  		if !s.jujuSupportedBases.Contains(requestedBase.String()) {
   190  			return base.Base{}, errors.NewNotSupported(nil, fmt.Sprintf("base: %s", requestedBase))
   191  		}
   192  		if IsUnsupportedBaseError(err) {
   193  			return base.Base{}, errors.Errorf(
   194  				"base %q is not supported, supported bases are: %s",
   195  				requestedBase, printBases(s.supportedBases),
   196  			)
   197  		}
   198  		return base.Base{}, err
   199  	}
   200  	s.logger.Infof(msgUserRequestedBase, b)
   201  	return b, nil
   202  }
   203  
   204  func printBases(bases []base.Base) string {
   205  	baseStrings := make([]string, len(bases))
   206  	for i, base := range bases {
   207  		baseStrings[i] = base.DisplayString()
   208  	}
   209  	return strings.Join(baseStrings, ", ")
   210  }