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 }