diff --git a/cmd/juju/application/deploy_test.go b/cmd/juju/application/deploy_test.go index 016c9aadeeee..901b69f04bbf 100644 --- a/cmd/juju/application/deploy_test.go +++ b/cmd/juju/application/deploy_test.go @@ -10,6 +10,7 @@ import ( "path/filepath" "regexp" "runtime" + "runtime/debug" "sort" "strings" "time" @@ -19,6 +20,7 @@ import ( "github.com/juju/cmd/v3" "github.com/juju/cmd/v3/cmdtesting" "github.com/juju/collections/set" + "github.com/juju/collections/transform" "github.com/juju/errors" "github.com/juju/gnuflag" "github.com/juju/loggo" @@ -148,15 +150,9 @@ func (s *DeploySuiteBase) SetUpTest(c *gc.C) { } s.RepoSuite.SetUpTest(c) - // TODO: remove this patch once we removed all the old series from tests in current package. - s.PatchValue(&deployer.SupportedJujuSeries, - func(time.Time, string, string) (set.Strings, error) { - return set.NewStrings( - "centos7", "centos9", "genericlinux", "kubernetes", - "jammy", "focal", "jammy", "xenial", - ), nil - }, - ) + s.PatchValue(&deployer.SupportedJujuBases, func(time.Time, corebase.Base, string) ([]corebase.Base, error) { + return transform.SliceOrErr([]string{"ubuntu@22.04", "ubuntu@20.04", "ubuntu@18.04"}, corebase.ParseBaseFromString) + }) s.CmdBlockHelper = coretesting.NewCmdBlockHelper(s.APIState) c.Assert(s.CmdBlockHelper, gc.NotNil) @@ -366,9 +362,9 @@ func (s *DeploySuite) TestDeployFromPathOldCharm(c *gc.C) { } func (s *DeploySuite) TestDeployFromPathOldCharmMissingSeries(c *gc.C) { - path := testcharms.RepoWithSeries("bionic").ClonedDirPath(c.MkDir(), "dummy") + path := testcharms.RepoWithSeries("bionic").ClonedDirPath(c.MkDir(), "dummy-no-series") err := s.runDeploy(c, path) - c.Assert(err, gc.ErrorMatches, "series not specified and charm does not define any") + c.Assert(err, gc.ErrorMatches, "charm does not define any bases, not valid") } func (s *DeploySuite) TestDeployFromPathOldCharmMissingSeriesUseDefaultSeries(c *gc.C) { @@ -415,31 +411,27 @@ func (s *DeploySuite) TestDeployFromPath(c *gc.C) { } func (s *DeploySuite) TestDeployFromPathUnsupportedSeriesHaveOverlap(c *gc.C) { - // Donot remove this because we want to test: series supported by the charm and series supported by Juju have overlap. - s.PatchValue(&deployer.SupportedJujuSeries, - func(time.Time, string, string) (set.Strings, error) { - return set.NewStrings( - "jammy", "focal", - ), nil - }, - ) + // Do not remove this because we want to test: bases supported by the charm and bases supported by Juju have overlap. + s.PatchValue(&deployer.SupportedJujuBases, func(time.Time, corebase.Base, string) ([]corebase.Base, error) { + return transform.SliceOrErr([]string{"ubuntu@22.04", "ubuntu@20.04", "ubuntu@12.10"}, corebase.ParseBaseFromString) + }) path := testcharms.RepoWithSeries("bionic").ClonedDirPath(c.MkDir(), "multi-series") err := s.runDeploy(c, path, "--base", "ubuntu@12.10") - c.Assert(err, gc.ErrorMatches, `series "quantal" is not supported, supported series are: focal,jammy`) + c.Assert(err, gc.ErrorMatches, `base "ubuntu@12.10/stable" is not supported, supported bases are: .*`) } -func (s *DeploySuite) TestDeployFromPathUnsupportedSeriesHaveNoOverlap(c *gc.C) { - // Donot remove this because we want to test: series supported by the charm and series supported by Juju have NO overlap. - s.PatchValue(&deployer.SupportedJujuSeries, - func(time.Time, string, string) (set.Strings, error) { - return set.NewStrings("kinetic"), nil +func (s *DeploySuite) TestDeployFromPathUnsupportedBaseHaveNoOverlap(c *gc.C) { + // Do not remove this because we want to test: bases supported by the charm and bases supported by Juju have NO overlap. + s.PatchValue(&deployer.SupportedJujuBases, + func(time.Time, corebase.Base, string) ([]corebase.Base, error) { + return []corebase.Base{corebase.MustParseBaseFromString("ubuntu@22.10")}, nil }, ) path := testcharms.RepoWithSeries("bionic").ClonedDirPath(c.MkDir(), "multi-series") - err := s.runDeploy(c, path, "--base", "ubuntu@12.10") - c.Assert(err, gc.ErrorMatches, `multi-series is not available on the following series: quantal`) + err := s.runDeploy(c, path) + c.Assert(err, gc.ErrorMatches, `the charm defined bases ".*" not supported`) } func (s *DeploySuite) TestDeployFromPathUnsupportedSeriesForce(c *gc.C) { @@ -448,14 +440,9 @@ func (s *DeploySuite) TestDeployFromPathUnsupportedSeriesForce(c *gc.C) { withLocalCharmDeployable(s.fakeAPI, curl, charmDir, false) withCharmDeployable(s.fakeAPI, curl, corebase.MustParseBaseFromString("ubuntu@20.04"), charmDir.Meta(), charmDir.Metrics(), false, false, 1, nil, nil) - // TODO: remove this patch once we removed all the old series from tests in current package. - s.PatchValue(&deployer.SupportedJujuSeries, - func(time.Time, string, string) (set.Strings, error) { - return set.NewStrings( - "jammy", "focal", "jammy", "xenial", "quantal", - ), nil - }, - ) + s.PatchValue(&deployer.SupportedJujuBases, func(time.Time, corebase.Base, string) ([]corebase.Base, error) { + return transform.SliceOrErr([]string{"ubuntu@22.04", "ubuntu@20.04", "ubuntu@18.04", "ubuntu@12.10"}, corebase.ParseBaseFromString) + }) err := s.runDeployForState(c, charmDir.Path, "--base", "ubuntu@12.10", "--force") c.Assert(err, jc.ErrorIsNil) @@ -468,14 +455,9 @@ func (s *DeploySuite) TestDeployFromPathUnsupportedLXDProfileForce(c *gc.C) { withLocalCharmDeployable(s.fakeAPI, curl, charmDir, false) withCharmDeployable(s.fakeAPI, curl, corebase.MustParseBaseFromString("ubuntu@20.04"), charmDir.Meta(), charmDir.Metrics(), false, true, 1, nil, nil) - // TODO: remove this patch once we removed all the old series from tests in current package. - s.PatchValue(&deployer.SupportedJujuSeries, - func(time.Time, string, string) (set.Strings, error) { - return set.NewStrings( - "jammy", "focal", "jammy", "xenial", "quantal", - ), nil - }, - ) + s.PatchValue(&deployer.SupportedJujuBases, func(time.Time, corebase.Base, string) ([]corebase.Base, error) { + return transform.SliceOrErr([]string{"ubuntu@22.04", "ubuntu@20.04", "ubuntu@18.04", "ubuntu@12.10"}, corebase.ParseBaseFromString) + }) err := s.runDeployForState(c, charmDir.Path, "--base", "ubuntu@12.10", "--force") c.Assert(err, jc.ErrorIsNil) @@ -1038,15 +1020,10 @@ func (s *CAASDeploySuiteBase) expectDeployer(c *gc.C, cfg deployer.DeployerConfi func (s *CAASDeploySuiteBase) SetUpTest(c *gc.C) { - // TODO: remove this patch once we removed all the old series from tests in current package. - s.PatchValue(&deployer.SupportedJujuSeries, - func(time.Time, string, string) (set.Strings, error) { - return set.NewStrings( - "centos7", "centos9", "genericlinux", "kubernetes", - "jammy", "focal", "jammy", "xenial", - ), nil - }, - ) + s.PatchValue(&deployer.SupportedJujuBases, func(time.Time, corebase.Base, string) ([]corebase.Base, error) { + return transform.SliceOrErr([]string{"ubuntu@22.04", "ubuntu@20.04", "ubuntu@18.04"}, corebase.ParseBaseFromString) + }) + cookiesFile := filepath.Join(c.MkDir(), ".go-cookies") s.PatchEnvironment("JUJU_COOKIEFILE", cookiesFile) @@ -1217,9 +1194,9 @@ func (s *CAASDeploySuite) TestDevices(c *gc.C) { func (s *DeploySuite) TestDeployStorageFailContainer(c *gc.C) { charmDir := testcharms.RepoWithSeries("bionic").ClonedDir(c.MkDir(), "multi-series") - curl := charm.MustParseURL("local:focal/multi-series-1") + curl := charm.MustParseURL("local:jammy/multi-series-1") withLocalCharmDeployable(s.fakeAPI, curl, charmDir, false) - withCharmDeployable(s.fakeAPI, curl, corebase.MustParseBaseFromString("ubuntu@20.04"), charmDir.Meta(), charmDir.Metrics(), false, false, 1, nil, nil) + withCharmDeployable(s.fakeAPI, curl, corebase.MustParseBaseFromString("ubuntu@22.04"), charmDir.Meta(), charmDir.Metrics(), false, false, 1, nil, nil) machine, err := s.State.AddMachine(state.UbuntuBase("22.04"), state.JobHostUnits) c.Assert(err, jc.ErrorIsNil) @@ -1446,24 +1423,19 @@ func (s *DeploySuite) TestDeployLocalWithSeriesMismatchReturnsError(c *gc.C) { _, _, err := s.runDeployWithOutput(c, charmDir.Path, "--base", "ubuntu@12.10") - c.Check(err, gc.ErrorMatches, `terms1 is not available on the following series: quantal not supported`) + c.Check(err, gc.ErrorMatches, `terms1 is not available on the following base: ubuntu@12.10/stable`) } func (s *DeploySuite) TestDeployLocalWithSeriesAndForce(c *gc.C) { + s.PatchValue(&deployer.SupportedJujuBases, func(time.Time, corebase.Base, string) ([]corebase.Base, error) { + return transform.SliceOrErr([]string{"ubuntu@22.04", "ubuntu@20.04", "ubuntu@18.04", "ubuntu@12.10"}, corebase.ParseBaseFromString) + }) + charmDir := testcharms.RepoWithSeries("quantal").ClonedDir(c.MkDir(), "terms1") curl := charm.MustParseURL("local:quantal/terms1-1") withLocalCharmDeployable(s.fakeAPI, curl, charmDir, true) withCharmDeployable(s.fakeAPI, curl, defaultBase, charmDir.Meta(), charmDir.Metrics(), false, true, 1, nil, nil) - // TODO: remove this patch once we removed all the old series from tests in current package. - s.PatchValue(&deployer.SupportedJujuSeries, - func(time.Time, string, string) (set.Strings, error) { - return set.NewStrings( - "jammy", "focal", "jammy", "xenial", "quantal", - ), nil - }, - ) - err := s.runDeployForState(c, charmDir.Path, "--base", "ubuntu@12.10", "--force") c.Assert(err, jc.ErrorIsNil) s.AssertApplication(c, "terms1", curl, 1, 0) @@ -1479,15 +1451,9 @@ func (s *DeploySuite) setupNonESMBase(c *gc.C) (corebase.Base, string) { supportedNotEMS := supported.Difference(set.NewStrings(corebase.ESMSupportedJujuSeries()...)) c.Assert(supportedNotEMS.Size(), jc.GreaterThan, 0) - // TODO: remove this patch once we removed all the old series from tests in current package. - s.PatchValue(&deployer.SupportedJujuSeries, - func(time.Time, string, string) (set.Strings, error) { - return set.NewStrings( - "centos7", "centos9", "genericlinux", "kubernetes", - "jammy", "focal", "jammy", "xenial", - ), nil - }, - ) + s.PatchValue(&deployer.SupportedJujuBases, func(time.Time, corebase.Base, string) ([]corebase.Base, error) { + return transform.SliceOrErr([]string{"centos@7", "centos@9", "ubuntu@22.04", "ubuntu@20.04", "ubuntu@16.04"}, corebase.ParseBaseFromString) + }) nonEMSSeries := supportedNotEMS.SortedValues()[0] @@ -1524,6 +1490,7 @@ func (s *DeploySuite) setupNonESMBase(c *gc.C) (corebase.Base, string) { // TODO (stickupkid): Remove this test once we remove series in 3.2 func (s *DeploySuite) TestDeployLocalWithSupportedNonESMSeries(c *gc.C) { nonEMSBase, loggingPath := s.setupNonESMBase(c) + fmt.Println(nonEMSBase) err := s.runDeploy(c, loggingPath, "--base", nonEMSBase.String()) c.Logf("%+v", s.fakeAPI.Calls()) c.Assert(err, jc.ErrorIsNil) @@ -1533,7 +1500,7 @@ func (s *DeploySuite) TestDeployLocalWithSupportedNonESMSeries(c *gc.C) { func (s *DeploySuite) TestDeployLocalWithNotSupportedNonESMSeries(c *gc.C) { _, loggingPath := s.setupNonESMBase(c) err := s.runDeploy(c, loggingPath, "--base", "ubuntu@17.10") - c.Assert(err, gc.ErrorMatches, "logging is not available on the following series: artful not supported") + c.Assert(err, gc.ErrorMatches, "logging is not available on the following base: ubuntu@17.10/stable") } // setupConfigFile creates a configuration file for testing set @@ -1907,15 +1874,9 @@ var _ = gc.Suite(&DeployUnitTestSuite{}) func (s *DeployUnitTestSuite) SetUpTest(c *gc.C) { s.IsolationSuite.SetUpTest(c) - // TODO: remove this patch once we removed all the old series from tests in current package. - s.PatchValue(&deployer.SupportedJujuSeries, - func(time.Time, string, string) (set.Strings, error) { - return set.NewStrings( - "centos7", "centos9", "genericlinux", "kubernetes", - "jammy", "focal", "jammy", "xenial", - ), nil - }, - ) + s.PatchValue(&deployer.SupportedJujuBases, func(time.Time, corebase.Base, string) ([]corebase.Base, error) { + return transform.SliceOrErr([]string{"ubuntu@22.04", "ubuntu@20.04", "ubuntu@18.04"}, corebase.ParseBaseFromString) + }) cookiesFile := filepath.Join(c.MkDir(), ".go-cookies") s.PatchEnvironment("JUJU_COOKIEFILE", cookiesFile) @@ -2292,6 +2253,7 @@ func (f *fakeDeployAPI) ModelUUID() (string, bool) { func (f *fakeDeployAPI) AddLocalCharm(url *charm.URL, ch charm.Charm, force bool) (*charm.URL, error) { results := f.MethodCall(f, "AddLocalCharm", url, ch, force) if results == nil { + debug.PrintStack() return nil, errors.NotFoundf("registered API call AddLocalCharm %v", url) } return results[0].(*charm.URL), jujutesting.TypeAssertError(results[1]) diff --git a/cmd/juju/application/deployer/bundlehandler.go b/cmd/juju/application/deployer/bundlehandler.go index 6ffb004a9b22..608a921d1dc2 100644 --- a/cmd/juju/application/deployer/bundlehandler.go +++ b/cmd/juju/application/deployer/bundlehandler.go @@ -711,11 +711,7 @@ func (h *bundleHandler) addLocalCharm(chParams bundlechanges.AddCharmParams, chB charmPath = filepath.Join(h.bundleDir, charmPath) } - chSeries, err := corebase.GetSeriesFromBase(chBase) - if err != nil { - return errors.Annotatef(err, "cannot deploy local charm at %q", charmPath) - } - ch, curl, err := corecharm.NewCharmAtPathForceSeries(charmPath, chSeries, h.force) + ch, curl, err := corecharm.NewCharmAtPathForceBase(charmPath, chBase, h.force) if err != nil { return errors.Annotatef(err, "cannot deploy local charm at %q", charmPath) } diff --git a/cmd/juju/application/deployer/charm.go b/cmd/juju/application/deployer/charm.go index e3e084bac129..1e854d727396 100644 --- a/cmd/juju/application/deployer/charm.go +++ b/cmd/juju/application/deployer/charm.go @@ -10,7 +10,6 @@ import ( "github.com/juju/charm/v11" jujuclock "github.com/juju/clock" "github.com/juju/cmd/v3" - "github.com/juju/collections/transform" "github.com/juju/errors" "github.com/juju/gnuflag" @@ -51,7 +50,7 @@ type deployCharm struct { storage map[string]storage.Constraints trust bool - validateCharmSeriesWithName func(series, name string, imageStream string) error + validateCharmBaseWithName func(base corebase.Base, name string, imageStream string) error validateResourcesNeededForLocalDeploy func(charmMeta *charm.Meta) error } @@ -225,7 +224,11 @@ func (d *predeployedLocalCharm) PrepareAndDeploy(ctx *cmd.Context, deployAPI Dep } // Avoid deploying charm if it's not valid for the model. - if err := d.validateCharmSeriesWithName(userCharmURL.Series, userCharmURL.Name, modelCfg.ImageStream()); err != nil { + base, err := corebase.GetBaseFromSeries(d.userCharmURL.Series) + if err != nil { + return errors.Trace(err) + } + if err := d.validateCharmBaseWithName(base, userCharmURL.Name, modelCfg.ImageStream()); err != nil { return errors.Trace(err) } @@ -244,11 +247,6 @@ func (d *predeployedLocalCharm) PrepareAndDeploy(ctx *cmd.Context, deployAPI Dep return errors.Trace(err) } - base, err := corebase.GetBaseFromSeries(d.userCharmURL.Series) - if err != nil { - return errors.Trace(err) - } - platform := utils.MakePlatform(d.constraints, base, d.modelConstraints) origin, err := utils.DeduceOrigin(userCharmURL, charm.Channel{}, platform) if err != nil { @@ -426,7 +424,7 @@ func (c *repositoryCharm) compatibilityPrepareAndDeploy(ctx *cmd.Context, deploy ctx.Verbosef("Preparing to deploy %q from the %s", userRequestedURL.Name, location) - modelCfg, workloadSeries, err := seriesSelectorRequirements(deployAPI, c.clock, userRequestedURL) + modelCfg, err := getModelConfig(deployAPI) if err != nil { return errors.Trace(err) } @@ -435,20 +433,20 @@ func (c *repositoryCharm) compatibilityPrepareAndDeploy(ctx *cmd.Context, deploy // deploy using the store but pass in the origin command line // argument so users can target a specific origin. origin := c.id.Origin - var usingDefaultSeries bool + var usingDefaultBase bool if defaultBase, ok := modelCfg.DefaultBase(); ok && origin.Base.Channel.Empty() { base, err := corebase.ParseBaseFromString(defaultBase) if err != nil { return errors.Trace(err) } origin.Base = base - usingDefaultSeries = true + usingDefaultBase = true } storeCharmOrBundleURL, origin, supportedBases, err := resolver.ResolveCharm(userRequestedURL, origin, false) // no --switch possible. if charm.IsUnsupportedSeriesError(err) { msg := fmt.Sprintf("%v. Use --force to deploy the charm anyway.", err) - if usingDefaultSeries { - msg += " Used the default-series." + if usingDefaultBase { + msg += " Used the default-base." } return errors.Errorf(msg) } else if err != nil { @@ -458,53 +456,42 @@ func (c *repositoryCharm) compatibilityPrepareAndDeploy(ctx *cmd.Context, deploy return errors.Trace(err) } - var seriesFlag string - if !c.baseFlag.Empty() { - var err error - seriesFlag, err = corebase.GetSeriesFromBase(c.baseFlag) - if err != nil { - return errors.Trace(err) - } - } - supportedSeries, err := transform.SliceOrErr(supportedBases, corebase.GetSeriesFromBase) + imageStream := modelCfg.ImageStream() + workloadBases, err := SupportedJujuBases(c.clock.Now(), c.baseFlag, imageStream) if err != nil { return errors.Trace(err) } - selector := corecharm.SeriesSelector{ - CharmURLSeries: userRequestedURL.Series, - SeriesFlag: seriesFlag, - SupportedSeries: supportedSeries, - SupportedJujuSeries: workloadSeries, + baseSelector, err := corecharm.ConfigureBaseSelector(corecharm.SelectorConfig{ + Config: modelCfg, Force: c.force, - Conf: modelCfg, - FromBundle: false, Logger: logger, + RequestedBase: c.baseFlag, + SupportedCharmBases: supportedBases, + WorkloadBases: workloadBases, UsingImageID: (c.constraints.HasImageID() || c.modelConstraints.HasImageID()), - } - err = selector.Validate() + }) if err != nil { return errors.Trace(err) } - // Get the series to use. - series, err := selector.CharmSeries() + // Get the base to use. + base, err := baseSelector.CharmBase() - logger.Tracef("Using series %q from %v to deploy %v", series, supportedSeries, userRequestedURL) + logger.Tracef("Using base %q from %v to deploy %v", base, supportedBases, userRequestedURL) - imageStream := modelCfg.ImageStream() // Avoid deploying charm if it's not valid for the model. // We check this first before possibly suggesting --force. if err == nil { - if err2 := c.validateCharmSeriesWithName(series, storeCharmOrBundleURL.Name, imageStream); err2 != nil { + if err2 := c.validateCharmBaseWithName(base, storeCharmOrBundleURL.Name, imageStream); err2 != nil { return errors.Trace(err2) } } if charm.IsUnsupportedSeriesError(err) { msg := fmt.Sprintf("%v. Use --force to deploy the charm anyway.", err) - if usingDefaultSeries { - msg += " Used the default-series." + if usingDefaultBase { + msg += " Used the default-base." } return errors.Errorf(msg) } @@ -513,15 +500,6 @@ func (c *repositoryCharm) compatibilityPrepareAndDeploy(ctx *cmd.Context, deploy } // Ensure we save the origin. - var base corebase.Base - if series == corebase.Kubernetes.String() { - base = corebase.LegacyKubernetesBase() - } else { - base, err = corebase.GetBaseFromSeries(series) - if err != nil { - return errors.Trace(err) - } - } origin = origin.WithBase(&base) // In-order for the url to represent the following updates to the origin @@ -567,16 +545,20 @@ func (c *repositoryCharm) compatibilityPrepareAndDeploy(ctx *cmd.Context, deploy } ctx.Infof(formatLocatedText(curl, csOrigin)) - // If the original series was empty, so we couldn't validate the original - // charm series, but the charm url wasn't nil, we can check and validate + // If the original base was empty, so we couldn't validate the original + // charm base, but the charm url wasn't nil, we can check and validate // what that one says. // // Note: it's interesting that the charm url and the series can diverge and // tell different things when deploying a charm and in sake of understanding // what we deploy, we should converge the two so that both report identical // values. - if curl != nil && series == "" { - if err := c.validateCharmSeriesWithName(curl.Series, curl.Name, imageStream); err != nil { + if curl != nil && base.Empty() { + b, err := corebase.GetBaseFromSeries(curl.Series) + if err != nil { + return errors.Trace(err) + } + if err := c.validateCharmBaseWithName(b, curl.Name, imageStream); err != nil { return errors.Trace(err) } } diff --git a/cmd/juju/application/deployer/charm_test.go b/cmd/juju/application/deployer/charm_test.go index 834a2b506e99..033c76386446 100644 --- a/cmd/juju/application/deployer/charm_test.go +++ b/cmd/juju/application/deployer/charm_test.go @@ -79,7 +79,7 @@ func (s *charmSuite) TestRepositoryCharmDeployDryRunCompatibility(c *gc.C) { dCharm := s.newDeployCharm() dCharm.dryRun = true - dCharm.validateCharmSeriesWithName = func(series, name string, imageStream string) error { + dCharm.validateCharmBaseWithName = func(_ corebase.Base, _ string, _ string) error { return nil } repoCharm := &repositoryCharm{ @@ -102,7 +102,7 @@ func (s *charmSuite) TestRepositoryCharmDeployDryRunImageIdNoBase(c *gc.C) { dCharm := s.newDeployCharm() dCharm.dryRun = true - dCharm.validateCharmSeriesWithName = func(series, name string, imageStream string) error { + dCharm.validateCharmBaseWithName = func(_ corebase.Base, _ string, _ string) error { return nil } dCharm.constraints = constraints.Value{ @@ -129,7 +129,7 @@ func (s *charmSuite) TestRepositoryCharmDeployDryRunDefaultSeriesForce(c *gc.C) dCharm := s.newDeployCharm() dCharm.dryRun = true dCharm.force = true - dCharm.validateCharmSeriesWithName = func(series, name string, imageStream string) error { + dCharm.validateCharmBaseWithName = func(_ corebase.Base, _ string, _ string) error { return nil } repoCharm := &repositoryCharm{ diff --git a/cmd/juju/application/deployer/deployer.go b/cmd/juju/application/deployer/deployer.go index 0153816ca881..465b7424734d 100644 --- a/cmd/juju/application/deployer/deployer.go +++ b/cmd/juju/application/deployer/deployer.go @@ -15,7 +15,6 @@ import ( charmresource "github.com/juju/charm/v11/resource" jujuclock "github.com/juju/clock" "github.com/juju/cmd/v3" - "github.com/juju/collections/set" "github.com/juju/errors" "github.com/juju/gnuflag" "github.com/juju/loggo" @@ -56,7 +55,7 @@ type localPreDeployerKind struct { // localCharmDeployerKind represents a local charm deployment type localCharmDeployerKind struct { - seriesName string + base corebase.Base imageStream string ch charm.Charm curl *charm.URL @@ -232,13 +231,13 @@ func (d *factory) localCharmDeployer(getter ModelConfigGetter) (DeployerKind, er if isLocalSchema(charmOrBundle) { charmOrBundle = charmOrBundle[6:] } - seriesName, imageStream, seriesErr := d.determineSeriesForLocalCharm(charmOrBundle, getter) + base, imageStream, seriesErr := d.determineBaseForLocalCharm(charmOrBundle, getter) if seriesErr != nil { return nil, errors.Trace(seriesErr) } // Charm may have been supplied via a path reference. - ch, curl, err := corecharm.NewCharmAtPathForceSeries(charmOrBundle, seriesName, d.force) + ch, curl, err := corecharm.NewCharmAtPathForceBase(charmOrBundle, base, d.force) // We check for several types of known error which indicate // that the supplied reference was indeed a path but there was // an issue reading the charm located there. @@ -259,7 +258,7 @@ func (d *factory) localCharmDeployer(getter ModelConfigGetter) (DeployerKind, er logger.Debugf("cannot interpret as local charm: %v", err) return nil, nil } else { - return &localCharmDeployerKind{seriesName, imageStream, ch, curl}, nil + return &localCharmDeployerKind{base, imageStream, ch, curl}, nil } } @@ -277,10 +276,10 @@ func (d *factory) localPreDeployedCharmDeployer() (DeployerKind, error) { return &localPreDeployerKind{userCharmURL: userCharmURL}, nil } -func (d *factory) determineSeriesForLocalCharm(charmOrBundle string, getter ModelConfigGetter) (string, string, error) { - // TODO (cderici): check the validity of the comments belowe +func (d *factory) determineBaseForLocalCharm(charmOrBundle string, getter ModelConfigGetter) (corebase.Base, string, error) { + // TODO (cderici): check the validity of the comments below // NOTE: Here we select the series using the algorithm defined by - // `seriesSelector.charmSeries`. This serves to override the algorithm found + // `baseSelector.charmBase`. This serves to override the algorithm found // in `charmrepo.NewCharmAtPath` which is outdated (but must still be // called since the code is coupled with path interpretation logic which // cannot easily be factored out). @@ -292,59 +291,49 @@ func (d *factory) determineSeriesForLocalCharm(charmOrBundle string, getter Mode // get the correct series. A proper refactoring of the charmrepo package is // needed for a more elegant fix. var ( - imageStream string - seriesName string + selectedBase corebase.Base + imageStream string ) - if !d.base.Empty() { - var err error - seriesName, err = corebase.GetSeriesFromBase(d.base) - if err != nil { - return "", "", errors.Trace(err) - } - } - ch, err := d.charmReader.ReadCharm(charmOrBundle) if err == nil { modelCfg, err := getModelConfig(getter) if err != nil { - return "", "", errors.Trace(err) + return corebase.Base{}, "", errors.Trace(err) } imageStream = modelCfg.ImageStream() - workloadSeries, err := SupportedJujuSeries(d.clock.Now(), seriesName, imageStream) + workloadBases, err := SupportedJujuBases(d.clock.Now(), d.base, imageStream) if err != nil { - return "", "", errors.Trace(err) + return corebase.Base{}, "", errors.Trace(err) } - supportedSeries, err := corecharm.ComputedSeries(ch) + supportedBases, err := corecharm.ComputedBases(ch) if err != nil { - return "", "", errors.Trace(err) + return corebase.Base{}, "", errors.Trace(err) } - if len(supportedSeries) == 0 { - logger.Warningf("%s does not declare supported series in metadata.yml", ch.Meta().Name) + if len(supportedBases) == 0 { + logger.Warningf("%s does not declare supported bases", ch.Meta().Name) } - seriesSelector := corecharm.SeriesSelector{ - SeriesFlag: seriesName, - SupportedSeries: supportedSeries, - SupportedJujuSeries: workloadSeries, + baseSelector, err := corecharm.ConfigureBaseSelector(corecharm.SelectorConfig{ + Config: modelCfg, Force: d.force, - Conf: modelCfg, - FromBundle: false, Logger: logger, + RequestedBase: d.base, + SupportedCharmBases: supportedBases, + WorkloadBases: workloadBases, UsingImageID: d.constraints.HasImageID() || d.modelConstraints.HasImageID(), - } - err = seriesSelector.Validate() + }) if err != nil { - return "", "", errors.Trace(err) + return corebase.Base{}, "", errors.Trace(err) } - seriesName, err = seriesSelector.CharmSeries() + selectedBase, err = baseSelector.CharmBase() if err = charmValidationError(ch.Meta().Name, errors.Trace(err)); err != nil { - return "", "", errors.Trace(err) + return corebase.Base{}, "", errors.Trace(err) } } - return seriesName, imageStream, nil + return selectedBase, imageStream, nil } func (d *factory) checkHandleRevision(userCharmURL *charm.URL, charmHubSchemaCheck bool) (*charm.URL, error) { @@ -520,7 +509,7 @@ func (d *factory) newDeployCharm() deployCharm { storage: d.storage, trust: d.trust, - validateCharmSeriesWithName: d.validateCharmSeriesWithName, + validateCharmBaseWithName: d.validateCharmBaseWithName, validateResourcesNeededForLocalDeploy: d.validateResourcesNeededForLocalDeploy, } } @@ -583,7 +572,7 @@ func (dk *localCharmDeployerKind) CreateDeployer(d factory) (Deployer, error) { // Avoid deploying charm if the charm series is not correct for the // available image streams. var err error - if err = d.validateCharmSeriesWithName(dk.seriesName, dk.curl.Name, dk.imageStream); err != nil { + if err = d.validateCharmBaseWithName(dk.base, dk.curl.Name, dk.imageStream); err != nil { return nil, errors.Trace(err) } if err := d.validateResourcesNeededForLocalDeploy(dk.ch.Meta()); err != nil { @@ -668,25 +657,6 @@ func appsRequiringTrust(appSpecList map[string]*charm.ApplicationSpec) []string return tl } -func seriesSelectorRequirements(api ModelConfigGetter, cl jujuclock.Clock, chURL *charm.URL) (*config.Config, set.Strings, error) { - // resolver.resolve potentially updates the series of anything - // passed in. Store this for use in seriesSelector. - userRequestedSeries := chURL.Series - - modelCfg, err := getModelConfig(api) - if err != nil { - return nil, nil, errors.Trace(err) - } - - imageStream := modelCfg.ImageStream() - workloadSeries, err := SupportedJujuSeries(cl.Now(), userRequestedSeries, imageStream) - if err != nil { - return nil, nil, errors.Trace(err) - } - - return modelCfg, workloadSeries, nil -} - var getModelConfig = func(api ModelConfigGetter) (*config.Config, error) { // Separated into a variable for easy overrides attrs, err := api.ModelGet() @@ -697,27 +667,32 @@ var getModelConfig = func(api ModelConfigGetter) (*config.Config, error) { return config.New(config.NoDefaults, attrs) } -func (d *factory) validateCharmSeries(seriesName string, imageStream string) error { +func (d *factory) validateCharmBase(base corebase.Base, imageStream string) error { // TODO(sidecar): handle systems - // attempt to locate the charm series from the list of known juju series + // attempt to locate the charm base from the list of known juju series // that we currently support. - workloadSeries, err := SupportedJujuSeries(d.clock.Now(), seriesName, imageStream) + if d.force { + return nil + } + workloadBases, err := SupportedJujuBases(d.clock.Now(), base, imageStream) if err != nil { return errors.Trace(err) } - if !workloadSeries.Contains(seriesName) && !d.force { - return errors.NotSupportedf("series: %s", seriesName) + for _, workloadBase := range workloadBases { + if workloadBase == base { + return nil + } } - return nil + return errors.NotSupportedf("base: %s", base) } -// validateCharmSeriesWithName calls the validateCharmSeries, but handles the +// validateCharmBaseWithName calls the validateCharmBase, but handles the // error return value to check for NotSupported error and returns a custom error // message if that's found. -func (d *factory) validateCharmSeriesWithName(series, name string, imageStream string) error { - err := d.validateCharmSeries(series, imageStream) +func (d *factory) validateCharmBaseWithName(base corebase.Base, name string, imageStream string) error { + err := d.validateCharmBase(base, imageStream) return charmValidationError(name, errors.Trace(err)) } diff --git a/cmd/juju/application/deployer/deployer_test.go b/cmd/juju/application/deployer/deployer_test.go index 37fc0a1ee14d..47d1bd18c2dc 100644 --- a/cmd/juju/application/deployer/deployer_test.go +++ b/cmd/juju/application/deployer/deployer_test.go @@ -13,7 +13,7 @@ import ( charmresource "github.com/juju/charm/v11/resource" "github.com/juju/clock" "github.com/juju/cmd/v3" - "github.com/juju/collections/set" + "github.com/juju/collections/transform" "github.com/juju/errors" "github.com/juju/gnuflag" "github.com/juju/testing" @@ -55,15 +55,9 @@ var _ = gc.Suite(&deployerSuite{}) func (s *deployerSuite) SetUpTest(_ *gc.C) { s.deployResourceIDs = make(map[string]string) - // TODO: remove this patch once we removed all the old series from tests in current package. - s.PatchValue(&SupportedJujuSeries, - func(time.Time, string, string) (set.Strings, error) { - return set.NewStrings( - "centos7", "centos9", "genericlinux", "kubernetes", - "jammy", "focal", "bionic", "xenial", - ), nil - }, - ) + s.PatchValue(&SupportedJujuBases, func(time.Time, corebase.Base, string) ([]corebase.Base, error) { + return transform.SliceOrErr([]string{"ubuntu@22.04", "ubuntu@20.04", "ubuntu@18.04"}, corebase.ParseBaseFromString) + }) } func (s *deployerSuite) TestGetDeployerPredeployedLocalCharm(c *gc.C) { diff --git a/cmd/juju/application/deployer/interface.go b/cmd/juju/application/deployer/interface.go index 99332d121812..47444d795c44 100644 --- a/cmd/juju/application/deployer/interface.go +++ b/cmd/juju/application/deployer/interface.go @@ -69,10 +69,6 @@ type ConsumeDetails interface { Close() error } -// For testing. -// TODO: unexport it if we don't need to patch it anymore. -var SupportedJujuSeries = corebase.WorkloadSeries - // For testing. // TODO: unexport it if we don't need to patch it anymore. var SupportedJujuBases = corebase.WorkloadBases diff --git a/cmd/juju/application/export_test.go b/cmd/juju/application/export_test.go index 3dc161bc9cd4..43b201a1e249 100644 --- a/cmd/juju/application/export_test.go +++ b/cmd/juju/application/export_test.go @@ -4,12 +4,8 @@ package application import ( - "time" - "github.com/juju/charm/v11" "github.com/juju/cmd/v3" - "github.com/juju/collections/set" - gc "gopkg.in/check.v1" "github.com/juju/juju/api" "github.com/juju/juju/api/base" @@ -18,7 +14,6 @@ import ( "github.com/juju/juju/cmd/juju/application/store" "github.com/juju/juju/cmd/juju/application/utils" "github.com/juju/juju/cmd/modelcmd" - jujutesting "github.com/juju/juju/juju/testing" "github.com/juju/juju/jujuclient" ) @@ -235,23 +230,3 @@ func NewConfigCommandForTest(api ApplicationAPI, store jujuclient.ClientStore) m c.SetClientStore(store) return c } - -// RepoSuiteBaseSuite allows the patching of the supported juju suite for -// each test. -type RepoSuiteBaseSuite struct { - jujutesting.RepoSuite -} - -func (s *RepoSuiteBaseSuite) SetUpTest(c *gc.C) { - s.RepoSuite.SetUpTest(c) - - // TODO: remove this patch once we removed all the old series from tests in current package. - s.PatchValue(&deployer.SupportedJujuSeries, - func(time.Time, string, string) (set.Strings, error) { - return set.NewStrings( - "centos7", "centos9", "genericlinux", "kubernetes", - "jammy", "focal", "bionic", "xenial", - ), nil - }, - ) -} diff --git a/cmd/juju/application/refresh_test.go b/cmd/juju/application/refresh_test.go index 89cde361c69b..780950d5fcf6 100644 --- a/cmd/juju/application/refresh_test.go +++ b/cmd/juju/application/refresh_test.go @@ -18,7 +18,7 @@ import ( charmresource "github.com/juju/charm/v11/resource" "github.com/juju/cmd/v3" "github.com/juju/cmd/v3/cmdtesting" - "github.com/juju/collections/set" + "github.com/juju/collections/transform" "github.com/juju/errors" "github.com/juju/names/v4" "github.com/juju/testing" @@ -430,16 +430,9 @@ func (s *RefreshErrorsStateSuite) SetUpSuite(c *gc.C) { c.Skip("Mongo failures on macOS") } s.RepoSuite.SetUpSuite(c) - - // TODO: remove this patch once we removed all the old series from tests in current package. - s.PatchValue(&deployer.SupportedJujuSeries, - func(time.Time, string, string) (set.Strings, error) { - return set.NewStrings( - "centos7", "centos9", "genericlinux", "kubernetes", - "jammy", "focal", "bionic", "xenial", - ), nil - }, - ) + s.PatchValue(&deployer.SupportedJujuBases, func(time.Time, corebase.Base, string) ([]corebase.Base, error) { + return transform.SliceOrErr([]string{"ubuntu@22.04", "ubuntu@20.04", "ubuntu@18.04"}, corebase.ParseBaseFromString) + }) } func (s *RefreshErrorsStateSuite) SetUpTest(c *gc.C) { @@ -488,7 +481,7 @@ func (s *RefreshErrorsStateSuite) deployApplication(c *gc.C) { withLocalCharmDeployable(s.fakeAPI, curl, charmDir, false) withCharmDeployable(s.fakeAPI, curl, corebase.MustParseBaseFromString("ubuntu@18.04"), charmDir.Meta(), charmDir.Metrics(), false, false, 1, nil, nil) - err := runDeploy(c, charmDir.Path, "riak", "--series", "bionic") + err := runDeploy(c, charmDir.Path, "riak", "--base", "ubuntu@18.04") c.Assert(err, jc.ErrorIsNil) } @@ -539,6 +532,10 @@ func (s *RefreshSuccessStateSuite) SetUpSuite(c *gc.C) { c.Skip("Mongo failures on macOS") } s.RepoSuite.SetUpSuite(c) + s.PatchValue(&deployer.SupportedJujuBases, func(time.Time, corebase.Base, string) ([]corebase.Base, error) { + return transform.SliceOrErr([]string{"ubuntu@22.04", "ubuntu@20.04", "ubuntu@18.04"}, corebase.ParseBaseFromString) + }) + } func (s *RefreshSuccessStateSuite) assertUpgraded(c *gc.C, riak *state.Application, revision int, forced bool) *charm.URL { @@ -561,16 +558,6 @@ func (s *RefreshSuccessStateSuite) SetUpTest(c *gc.C) { } s.RepoSuite.SetUpTest(c) - // TODO: remove this patch once we removed all the old series from tests in current package. - s.PatchValue(&deployer.SupportedJujuSeries, - func(time.Time, string, string) (set.Strings, error) { - return set.NewStrings( - "centos7", "centos9", "genericlinux", "kubernetes", - "jammy", "focal", "bionic", "xenial", - ), nil - }, - ) - s.charmClient = mockCharmClient{} s.cmd = NewRefreshCommandForStateTest( newCharmAdder, diff --git a/cmd/juju/application/refresher/interface.go b/cmd/juju/application/refresher/interface.go index 924e26738615..7db76a5c5c01 100644 --- a/cmd/juju/application/refresher/interface.go +++ b/cmd/juju/application/refresher/interface.go @@ -44,16 +44,16 @@ type CharmResolver interface { // CharmRepository defines methods for interaction with a charm repo. type CharmRepository interface { - // NewCharmAtPathForceSeries returns the charm represented by this path, - // and a URL that describes it. If the series is empty, - // the charm's default series is used, if any. - // Otherwise, the series is validated against those the + // NewCharmAtPathForceBase returns the charm represented by this path, + // and a URL that describes it. If the base is empty, + // the charm's default base is used, if any. + // Otherwise, the base is validated against those the // charm declares it supports. If force is true, then any - // series validation errors are ignored and the requested - // series is used regardless. Note though that is it still - // an error if the series is not specified and the charm does not + // base validation errors are ignored and the requested + // base is used regardless. Note though that is it still + // an error if the base is not specified and the charm does not // define any. - NewCharmAtPathForceSeries(path, series string, force bool) (charm.Charm, *charm.URL, error) + NewCharmAtPathForceBase(path string, base base.Base, force bool) (charm.Charm, *charm.URL, error) } // CommandLogger represents a logger which follows the logging diff --git a/cmd/juju/application/refresher/refresher.go b/cmd/juju/application/refresher/refresher.go index d5672c867e96..0d8fb026f726 100644 --- a/cmd/juju/application/refresher/refresher.go +++ b/cmd/juju/application/refresher/refresher.go @@ -169,14 +169,7 @@ func (d *localCharmRefresher) Allowed(_ RefresherConfig) (bool, error) { // Refresh a given local charm. // Bundles are not supported as there is no physical representation in Juju. func (d *localCharmRefresher) Refresh() (*CharmID, error) { - var deployedSeries string - if !d.deployedBase.Channel.Empty() { - var err error - if deployedSeries, err = corebase.GetSeriesFromBase(d.deployedBase); err != nil { - return nil, errors.Trace(err) - } - } - ch, newURL, err := d.charmRepo.NewCharmAtPathForceSeries(d.charmRef, deployedSeries, d.forceBase) + ch, newURL, err := d.charmRepo.NewCharmAtPathForceBase(d.charmRef, d.deployedBase, d.forceBase) if err == nil { newName := ch.Meta().Name if newName != d.charmURL.Name { @@ -319,8 +312,8 @@ func stdOriginResolver(curl *charm.URL, origin corecharm.Origin, channel charm.C type defaultCharmRepo struct{} -func (defaultCharmRepo) NewCharmAtPathForceSeries(path, series string, force bool) (charm.Charm, *charm.URL, error) { - return corecharm.NewCharmAtPathForceSeries(path, series, force) +func (defaultCharmRepo) NewCharmAtPathForceBase(path string, base corebase.Base, force bool) (charm.Charm, *charm.URL, error) { + return corecharm.NewCharmAtPathForceBase(path, base, force) } // charmHubOriginResolver attempts to resolve the origin required to resolve diff --git a/cmd/juju/application/refresher/refresher_mock_test.go b/cmd/juju/application/refresher/refresher_mock_test.go index 38b791bd913e..be9d492550a7 100644 --- a/cmd/juju/application/refresher/refresher_mock_test.go +++ b/cmd/juju/application/refresher/refresher_mock_test.go @@ -181,18 +181,18 @@ func (m *MockCharmRepository) EXPECT() *MockCharmRepositoryMockRecorder { return m.recorder } -// NewCharmAtPathForceSeries mocks base method. -func (m *MockCharmRepository) NewCharmAtPathForceSeries(arg0, arg1 string, arg2 bool) (charm.Charm, *charm.URL, error) { +// NewCharmAtPathForceBase mocks base method. +func (m *MockCharmRepository) NewCharmAtPathForceBase(arg0 string, arg1 base.Base, arg2 bool) (charm.Charm, *charm.URL, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "NewCharmAtPathForceSeries", arg0, arg1, arg2) + ret := m.ctrl.Call(m, "NewCharmAtPathForceBase", arg0, arg1, arg2) ret0, _ := ret[0].(charm.Charm) ret1, _ := ret[1].(*charm.URL) ret2, _ := ret[2].(error) return ret0, ret1, ret2 } -// NewCharmAtPathForceSeries indicates an expected call of NewCharmAtPathForceSeries. -func (mr *MockCharmRepositoryMockRecorder) NewCharmAtPathForceSeries(arg0, arg1, arg2 interface{}) *gomock.Call { +// NewCharmAtPathForceBase indicates an expected call of NewCharmAtPathForceBase. +func (mr *MockCharmRepositoryMockRecorder) NewCharmAtPathForceBase(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewCharmAtPathForceSeries", reflect.TypeOf((*MockCharmRepository)(nil).NewCharmAtPathForceSeries), arg0, arg1, arg2) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewCharmAtPathForceBase", reflect.TypeOf((*MockCharmRepository)(nil).NewCharmAtPathForceBase), arg0, arg1, arg2) } diff --git a/cmd/juju/application/refresher/refresher_test.go b/cmd/juju/application/refresher/refresher_test.go index 920c30a06a24..e8e4c9825781 100644 --- a/cmd/juju/application/refresher/refresher_test.go +++ b/cmd/juju/application/refresher/refresher_test.go @@ -244,7 +244,7 @@ func (s *localCharmRefresherSuite) TestRefresh(c *gc.C) { charmAdder.EXPECT().AddLocalCharm(curl, ch, false).Return(curl, nil) charmRepo := NewMockCharmRepository(ctrl) - charmRepo.EXPECT().NewCharmAtPathForceSeries(ref, "", false).Return(ch, curl, nil) + charmRepo.EXPECT().NewCharmAtPathForceBase(ref, corebase.Base{}, false).Return(ch, curl, nil) cfg := basicRefresherConfig(curl, ref) @@ -267,7 +267,7 @@ func (s *localCharmRefresherSuite) TestRefreshBecomesExhausted(c *gc.C) { charmAdder := NewMockCharmAdder(ctrl) charmRepo := NewMockCharmRepository(ctrl) - charmRepo.EXPECT().NewCharmAtPathForceSeries(ref, "", false).Return(nil, nil, os.ErrNotExist) + charmRepo.EXPECT().NewCharmAtPathForceBase(ref, corebase.Base{}, false).Return(nil, nil, os.ErrNotExist) cfg := basicRefresherConfig(curl, ref) @@ -288,7 +288,7 @@ func (s *localCharmRefresherSuite) TestRefreshDoesNotFindLocal(c *gc.C) { charmAdder := NewMockCharmAdder(ctrl) charmRepo := NewMockCharmRepository(ctrl) - charmRepo.EXPECT().NewCharmAtPathForceSeries(ref, "", false).Return(nil, nil, errors.NotFoundf("fail")) + charmRepo.EXPECT().NewCharmAtPathForceBase(ref, corebase.Base{}, false).Return(nil, nil, errors.NotFoundf("fail")) cfg := basicRefresherConfig(curl, ref) diff --git a/cmd/juju/application/unexpose_test.go b/cmd/juju/application/unexpose_test.go index 9da892c1bf6b..4c0c1f8dced0 100644 --- a/cmd/juju/application/unexpose_test.go +++ b/cmd/juju/application/unexpose_test.go @@ -9,12 +9,13 @@ import ( "github.com/juju/charm/v11" "github.com/juju/cmd/v3/cmdtesting" - "github.com/juju/collections/set" + "github.com/juju/collections/transform" "github.com/juju/errors" jc "github.com/juju/testing/checkers" gc "gopkg.in/check.v1" "github.com/juju/juju/cmd/juju/application/deployer" + corebase "github.com/juju/juju/core/base" jujutesting "github.com/juju/juju/juju/testing" "github.com/juju/juju/rpc" "github.com/juju/juju/testcharms" @@ -32,15 +33,9 @@ func (s *UnexposeSuite) SetUpTest(c *gc.C) { } s.RepoSuite.SetUpTest(c) - // TODO: remove this patch once we removed all the old series from tests in current package. - s.PatchValue(&deployer.SupportedJujuSeries, - func(time.Time, string, string) (set.Strings, error) { - return set.NewStrings( - "centos7", "centos9", "genericlinux", "kubernetes", - "jammy", "focal", "bionic", "xenial", - ), nil - }, - ) + s.PatchValue(&deployer.SupportedJujuBases, func(time.Time, corebase.Base, string) ([]corebase.Base, error) { + return transform.SliceOrErr([]string{"ubuntu@22.04", "ubuntu@20.04", "ubuntu@18.04"}, corebase.ParseBaseFromString) + }) s.CmdBlockHelper = testing.NewCmdBlockHelper(s.APIState) c.Assert(s.CmdBlockHelper, gc.NotNil) diff --git a/core/bundle/changes/handlers.go b/core/bundle/changes/handlers.go index 9b5c27d723f1..502c7e714632 100644 --- a/core/bundle/changes/handlers.go +++ b/core/bundle/changes/handlers.go @@ -1288,7 +1288,15 @@ func getSeries(application *charm.ApplicationSpec, defaultSeries string) (string // Handle local charm paths. if charm.IsValidLocalCharmOrBundlePath(application.Charm) { - _, charmURL, err := corecharm.NewCharmAtPath(application.Charm, defaultSeries) + var b corebase.Base + if defaultSeries != "" { + var err error + b, err = corebase.GetBaseFromSeries(defaultSeries) + if err != nil { + return "", errors.Trace(err) + } + } + _, charmURL, err := corecharm.NewCharmAtPath(application.Charm, b) if corecharm.IsMissingSeriesError(err) { // local charm path is valid but the charm doesn't declare a default series. return defaultSeries, nil diff --git a/core/charm/baseselector.go b/core/charm/baseselector.go index e50a7d2ebe36..a9f15d0a0be9 100644 --- a/core/charm/baseselector.go +++ b/core/charm/baseselector.go @@ -19,6 +19,12 @@ const ( msgLatestLTSBase = "with the latest LTS base %q" ) +// SelectorLogger defines the logging methods needed +type SelectorLogger interface { + Infof(string, ...interface{}) + Tracef(string, ...interface{}) +} + // BaseSelector is a helper type that determines what base the charm should // be deployed to. type BaseSelector struct { @@ -185,7 +191,7 @@ func (s BaseSelector) userRequested(requestedBase base.Base) (base.Base, error) } if IsUnsupportedBaseError(err) { return base.Base{}, errors.Errorf( - "base %q is not supported, base series are: %s", + "base %q is not supported, supported bases are: %s", requestedBase, printBases(s.supportedBases), ) } diff --git a/core/charm/baseselector_test.go b/core/charm/baseselector_test.go index 59476f0d200e..50e11c53dfee 100644 --- a/core/charm/baseselector_test.go +++ b/core/charm/baseselector_test.go @@ -291,3 +291,21 @@ func (s *baseSelectorSuite) TestConfigureBaseSelectorDefaultBaseFail(c *gc.C) { _, err = baseSelector.CharmBase() c.Assert(err, gc.ErrorMatches, `base: ubuntu@18.04/stable`) } + +type mockModelCfg struct { + base string + explicit bool +} + +func (d mockModelCfg) DefaultBase() (string, bool) { + return d.base, d.explicit +} + +func (d mockModelCfg) ImageStream() string { + return "released" +} + +type noOpLogger struct{} + +func (noOpLogger) Infof(string, ...interface{}) {} +func (noOpLogger) Tracef(string, ...interface{}) {} diff --git a/core/charm/charmpath.go b/core/charm/charmpath.go index 0363eb2c7e74..a32a3c4f46ac 100644 --- a/core/charm/charmpath.go +++ b/core/charm/charmpath.go @@ -10,31 +10,30 @@ import ( "strings" "github.com/juju/charm/v11" - "github.com/juju/collections/set" "github.com/juju/errors" "github.com/juju/juju/core/base" ) // NewCharmAtPath returns the charm represented by this path, -// and a URL that describes it. If the series is empty, -// the charm's default series is used, if any. -// Otherwise, the series is validated against those the +// and a URL that describes it. If the base is empty, +// the charm's default base is used, if any. +// Otherwise, the base is validated against those the // charm declares it supports. -func NewCharmAtPath(path, series string) (charm.Charm, *charm.URL, error) { - return NewCharmAtPathForceSeries(path, series, false) +func NewCharmAtPath(path string, b base.Base) (charm.Charm, *charm.URL, error) { + return NewCharmAtPathForceBase(path, b, false) } -// NewCharmAtPathForceSeries returns the charm represented by this path, -// and a URL that describes it. If the series is empty, -// the charm's default series is used, if any. -// Otherwise, the series is validated against those the +// NewCharmAtPathForceBase returns the charm represented by this path, +// and a URL that describes it. If the base is empty, +// the charm's default base is used, if any. +// Otherwise, the base is validated against those the // charm declares it supports. If force is true, then any -// series validation errors are ignored and the requested -// series is used regardless. Note though that is it still -// an error if the series is not specified and the charm does not +// base validation errors are ignored and the requested +// base is used regardless. Note though that is it still +// an error if the base is not specified and the charm does not // define any. -func NewCharmAtPathForceSeries(path, series string, force bool) (charm.Charm, *charm.URL, error) { +func NewCharmAtPathForceBase(path string, b base.Base, force bool) (charm.Charm, *charm.URL, error) { if path == "" { return nil, nil, errors.New("empty charm path") } @@ -57,7 +56,11 @@ func NewCharmAtPathForceSeries(path, series string, force bool) (charm.Charm, *c } _, name := filepath.Split(absPath) - seriesToUse, err := charmSeries(series, force, ch) + baseToUse, err := charmBase(b, force, ch) + if err != nil { + return nil, nil, err + } + seriesToUse, err := base.GetSeriesFromBase(baseToUse) if err != nil { return nil, nil, err } @@ -71,34 +74,35 @@ func NewCharmAtPathForceSeries(path, series string, force bool) (charm.Charm, *c return ch, url, nil } -func charmSeries(series string, force bool, cm charm.CharmMeta) (string, error) { - if force && series != "" { - return series, nil +func charmBase(b base.Base, force bool, cm charm.CharmMeta) (base.Base, error) { + if force && !b.Empty() { + return b, nil } - computedSeries, err := ComputedSeries(cm) - logger.Tracef("series %q, %v", series, computedSeries) + computedBases, err := ComputedBases(cm) + logger.Tracef("base %q, %v", b, computedBases) if err != nil { - return "", err + return base.Base{}, err } - if len(computedSeries) == 0 { - if series == "" { - return "", errors.New("series not specified and charm does not define any") + if len(computedBases) == 0 { + if b.Empty() { + return base.Base{}, errors.New("base not specified and charm does not define any") } else { - // old style charm with no series in metadata. - return series, nil + // old style charm with no base in metadata. + return b, nil } } - if series != "" { - if set.NewStrings(computedSeries...).Contains(series) { - return series, nil - } else { - return "", NewUnsupportedSeriesError(series, computedSeries) + if !b.Empty() { + for _, computedBase := range computedBases { + if b == computedBase { + return b, nil + } } + return base.Base{}, NewUnsupportedBaseError(b, computedBases) } - if len(computedSeries) > 0 { - return computedSeries[0], nil + if len(computedBases) > 0 { + return computedBases[0], nil } - return "", errors.Errorf("series not specified and charm does not define any") + return base.Base{}, errors.Errorf("base not specified and charm does not define any") } func isNotExistsError(err error) bool { diff --git a/core/charm/charmpath_test.go b/core/charm/charmpath_test.go index 250bda413fbf..93d038556fc4 100644 --- a/core/charm/charmpath_test.go +++ b/core/charm/charmpath_test.go @@ -11,6 +11,7 @@ import ( jc "github.com/juju/testing/checkers" gc "gopkg.in/check.v1" + "github.com/juju/juju/core/base" corecharm "github.com/juju/juju/core/charm" "github.com/juju/juju/testcharms" ) @@ -30,22 +31,22 @@ func (s *charmPathSuite) cloneCharmDir(path, name string) string { } func (s *charmPathSuite) TestNoPath(c *gc.C) { - _, _, err := corecharm.NewCharmAtPath("", "jammy") + _, _, err := corecharm.NewCharmAtPath("", base.MustParseBaseFromString("ubuntu@22.04")) c.Assert(err, gc.ErrorMatches, "empty charm path") } func (s *charmPathSuite) TestInvalidPath(c *gc.C) { - _, _, err := corecharm.NewCharmAtPath("/foo", "jammy") + _, _, err := corecharm.NewCharmAtPath("/foo", base.MustParseBaseFromString("ubuntu@22.04")) c.Assert(err, gc.Equals, os.ErrNotExist) } func (s *charmPathSuite) TestRepoURL(c *gc.C) { - _, _, err := corecharm.NewCharmAtPath("ch:foo", "jammy") + _, _, err := corecharm.NewCharmAtPath("ch:foo", base.MustParseBaseFromString("ubuntu@22.04")) c.Assert(err, gc.Equals, os.ErrNotExist) } func (s *charmPathSuite) TestInvalidRelativePath(c *gc.C) { - _, _, err := corecharm.NewCharmAtPath("./foo", "jammy") + _, _, err := corecharm.NewCharmAtPath("./foo", base.MustParseBaseFromString("ubuntu@22.04")) c.Assert(err, gc.Equals, os.ErrNotExist) } @@ -55,19 +56,19 @@ func (s *charmPathSuite) TestRelativePath(c *gc.C) { c.Assert(err, jc.ErrorIsNil) defer func() { _ = os.Chdir(cwd) }() c.Assert(os.Chdir(s.repoPath), jc.ErrorIsNil) - _, _, err = corecharm.NewCharmAtPath("mysql", "jammy") + _, _, err = corecharm.NewCharmAtPath("mysql", base.MustParseBaseFromString("ubuntu@22.04")) c.Assert(corecharm.IsInvalidPathError(err), jc.IsTrue) } func (s *charmPathSuite) TestNoCharmAtPath(c *gc.C) { - _, _, err := corecharm.NewCharmAtPath(c.MkDir(), "jammy") + _, _, err := corecharm.NewCharmAtPath(c.MkDir(), base.MustParseBaseFromString("ubuntu@22.04")) c.Assert(err, gc.ErrorMatches, "charm not found.*") } func (s *charmPathSuite) TestCharm(c *gc.C) { charmDir := filepath.Join(s.repoPath, "mysql") s.cloneCharmDir(s.repoPath, "mysql") - ch, url, err := corecharm.NewCharmAtPath(charmDir, "focal") + ch, url, err := corecharm.NewCharmAtPath(charmDir, base.MustParseBaseFromString("ubuntu@20.04")) c.Assert(err, jc.ErrorIsNil) c.Assert(ch.Meta().Name, gc.Equals, "mysql") c.Assert(ch.Revision(), gc.Equals, 1) @@ -77,65 +78,65 @@ func (s *charmPathSuite) TestCharm(c *gc.C) { func (s *charmPathSuite) TestCharmWithManifest(c *gc.C) { repo := testcharms.RepoForSeries("focal") charmDir := repo.CharmDir("cockroach") - ch, url, err := corecharm.NewCharmAtPath(charmDir.Path, "") + ch, url, err := corecharm.NewCharmAtPath(charmDir.Path, base.Base{}) c.Assert(err, jc.ErrorIsNil) c.Assert(ch.Meta().Name, gc.Equals, "cockroachdb") c.Assert(ch.Revision(), gc.Equals, 0) c.Assert(url, gc.DeepEquals, charm.MustParseURL("local:focal/cockroach-0")) } -func (s *charmPathSuite) TestNoSeriesSpecified(c *gc.C) { +func (s *charmPathSuite) TestNoBaseSpecified(c *gc.C) { charmDir := filepath.Join(s.repoPath, "mysql") s.cloneCharmDir(s.repoPath, "mysql") - _, _, err := corecharm.NewCharmAtPath(charmDir, "") - c.Assert(err, gc.ErrorMatches, "series not specified and charm does not define any") + _, _, err := corecharm.NewCharmAtPath(charmDir, base.Base{}) + c.Assert(err, gc.ErrorMatches, "base not specified and charm does not define any") } -func (s *charmPathSuite) TestNoSeriesSpecifiedForceStillFails(c *gc.C) { +func (s *charmPathSuite) TestNoBaseSpecifiedForceStillFails(c *gc.C) { charmDir := filepath.Join(s.repoPath, "mysql") s.cloneCharmDir(s.repoPath, "mysql") - _, _, err := corecharm.NewCharmAtPathForceSeries(charmDir, "", true) - c.Assert(err, gc.ErrorMatches, "series not specified and charm does not define any") + _, _, err := corecharm.NewCharmAtPathForceBase(charmDir, base.Base{}, true) + c.Assert(err, gc.ErrorMatches, "base not specified and charm does not define any") } -func (s *charmPathSuite) TestMultiSeriesDefault(c *gc.C) { +func (s *charmPathSuite) TestMultiBaseDefault(c *gc.C) { charmDir := filepath.Join(s.repoPath, "multi-series-charmpath") s.cloneCharmDir(s.repoPath, "multi-series-charmpath") - ch, url, err := corecharm.NewCharmAtPath(charmDir, "") + ch, url, err := corecharm.NewCharmAtPath(charmDir, base.Base{}) c.Assert(err, gc.IsNil) c.Assert(ch.Meta().Name, gc.Equals, "new-charm-with-multi-series") c.Assert(ch.Revision(), gc.Equals, 7) c.Assert(url, gc.DeepEquals, charm.MustParseURL("local:jammy/multi-series-charmpath-7")) } -func (s *charmPathSuite) TestMultiSeries(c *gc.C) { +func (s *charmPathSuite) TestMultiBase(c *gc.C) { charmDir := filepath.Join(s.repoPath, "multi-series-charmpath") s.cloneCharmDir(s.repoPath, "multi-series-charmpath") - ch, url, err := corecharm.NewCharmAtPath(charmDir, "focal") + ch, url, err := corecharm.NewCharmAtPath(charmDir, base.MustParseBaseFromString("ubuntu@20.04")) c.Assert(err, gc.IsNil) c.Assert(ch.Meta().Name, gc.Equals, "new-charm-with-multi-series") c.Assert(ch.Revision(), gc.Equals, 7) c.Assert(url, gc.DeepEquals, charm.MustParseURL("local:focal/multi-series-charmpath-7")) } -func (s *charmPathSuite) TestUnsupportedSeries(c *gc.C) { +func (s *charmPathSuite) TestUnsupportedBase(c *gc.C) { charmDir := filepath.Join(s.repoPath, "multi-series-charmpath") s.cloneCharmDir(s.repoPath, "multi-series-charmpath") - _, _, err := corecharm.NewCharmAtPath(charmDir, "wily") - c.Assert(err, gc.ErrorMatches, `series "wily" not supported by charm, the charm supported series are.*`) + _, _, err := corecharm.NewCharmAtPath(charmDir, base.MustParseBaseFromString("ubuntu@15.10")) + c.Assert(err, gc.ErrorMatches, `base "ubuntu@15.10" not supported by charm, the charm supported bases are.*`) } -func (s *charmPathSuite) TestUnsupportedSeriesNoForce(c *gc.C) { +func (s *charmPathSuite) TestUnsupportedBaseNoForce(c *gc.C) { charmDir := filepath.Join(s.repoPath, "multi-series-charmpath") s.cloneCharmDir(s.repoPath, "multi-series-charmpath") - _, _, err := corecharm.NewCharmAtPathForceSeries(charmDir, "wily", false) - c.Assert(err, gc.ErrorMatches, `series "wily" not supported by charm, the charm supported series are.*`) + _, _, err := corecharm.NewCharmAtPathForceBase(charmDir, base.MustParseBaseFromString("ubuntu@15.10"), false) + c.Assert(err, gc.ErrorMatches, `base "ubuntu@15.10" not supported by charm, the charm supported bases are.*`) } -func (s *charmPathSuite) TestUnsupportedSeriesForce(c *gc.C) { +func (s *charmPathSuite) TestUnsupportedBaseForce(c *gc.C) { charmDir := filepath.Join(s.repoPath, "multi-series-charmpath") s.cloneCharmDir(s.repoPath, "multi-series-charmpath") - ch, url, err := corecharm.NewCharmAtPathForceSeries(charmDir, "wily", true) + ch, url, err := corecharm.NewCharmAtPathForceBase(charmDir, base.MustParseBaseFromString("ubuntu@15.10"), true) c.Assert(err, jc.ErrorIsNil) c.Assert(ch.Meta().Name, gc.Equals, "new-charm-with-multi-series") c.Assert(ch.Revision(), gc.Equals, 7) @@ -149,7 +150,7 @@ func (s *charmPathSuite) TestFindsSymlinks(c *gc.C) { err := os.Symlink(realPath, linkPath) c.Assert(err, gc.IsNil) - ch, url, err := corecharm.NewCharmAtPath(filepath.Join(charmsPath, "dummy"), "quantal") + ch, url, err := corecharm.NewCharmAtPath(filepath.Join(charmsPath, "dummy"), base.MustParseBaseFromString("ubuntu@12.10")) c.Assert(err, gc.IsNil) c.Assert(ch.Revision(), gc.Equals, 1) c.Assert(ch.Meta().Name, gc.Equals, "dummy") diff --git a/core/charm/seriesselector.go b/core/charm/seriesselector.go deleted file mode 100644 index 4282eef78c69..000000000000 --- a/core/charm/seriesselector.go +++ /dev/null @@ -1,198 +0,0 @@ -// Copyright 2016 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package charm - -import ( - "fmt" - "strings" - - "github.com/juju/collections/set" - "github.com/juju/errors" - - corebase "github.com/juju/juju/core/base" - "github.com/juju/juju/version" -) - -const ( - msgUserRequestedSeries = "with the user specified series %q" - msgBundleSeries = "with the series %q defined by the bundle" - msgLatestLTSSeries = "with the latest LTS series %q" -) - -type modelConfig interface { - // DefaultBase returns the configured default base - // for the environment, and whether the default base was - // explicitly configured on the environment. - DefaultBase() (string, bool) -} - -// SelectorLogger defines the logging methods needed -type SelectorLogger interface { - Infof(string, ...interface{}) - Tracef(string, ...interface{}) -} - -// SeriesSelector is a helper type that determines what series the charm should -// be deployed to. -type SeriesSelector struct { - // SeriesFlag is the series passed to the --series flag on the command line. - SeriesFlag string - // CharmURLSeries is the series specified as part of the charm URL, i.e. - // ch:jammy/ubuntu. - CharmURLSeries string - // Conf is the configuration for the model we're deploying to. - Conf modelConfig - // SupportedSeries is the list of series the charm supports. - SupportedSeries []string - // SupportedJujuSeries is the list of series that juju supports. - SupportedJujuSeries set.Strings - // Force indicates the user explicitly wants to deploy to a requested - // series, regardless of whether the charm says it supports that series. - Force bool - // from bundle specifies the deploy request comes from a bundle spec. - FromBundle bool - Logger SelectorLogger - // UsingImageID is true when the user is using the image-id constraint - // when deploying the charm. This is needed to validate that in that - // case the user is also explicitly providing a base. - UsingImageID bool -} - -// TODO(nvinuesa): The Force flag is only valid if the SeriesFlag is specified -// or to force the deploy of a LXD profile that doesn't pass validation, this -// should be added to these validation checks. -func (s SeriesSelector) Validate() error { - // If the image-id constraint is provided then base must be explicitly - // provided either by flag either by model-config default base. - _, explicit := s.Conf.DefaultBase() - if s.UsingImageID && - s.SeriesFlag == "" && - s.CharmURLSeries == "" && - !explicit { - return errors.Forbiddenf("base must be explicitly provided when image-id constraint is used") - } - - return nil -} - -// charmSeries determines what series to use with a charm. -// Order of preference is: -// - user requested with --series or defined by bundle when deploying -// - user requested in charm's url (e.g. juju deploy jammy/ubuntu) -// - model default, if set, acts like --series -// - default from charm metadata supported series / series in url -// - default LTS -func (s SeriesSelector) CharmSeries() (selectedSeries string, err error) { - // TODO(sidecar): handle systems - - // User has requested a series with --series. - if s.SeriesFlag != "" { - return s.userRequested(s.SeriesFlag) - } - - // User specified a series in the charm URL, e.g. - // juju deploy precise/ubuntu. - if s.CharmURLSeries != "" { - return s.userRequested(s.CharmURLSeries) - } - - // No series explicitly requested by the user. - // Use model default series, if explicitly set and supported by the charm. - if defaultBase, explicit := s.Conf.DefaultBase(); explicit { - base, err := corebase.ParseBaseFromString(defaultBase) - if err != nil { - return "", errors.Trace(err) - } - - defaultSeries, err := corebase.GetSeriesFromBase(base) - if err != nil { - return "", errors.Trace(err) - } - return s.userRequested(defaultSeries) - } - - // Next fall back to the charm's list of series, filtered to what's supported - // by Juju. Preserve the order of the supported series from the charm - // metadata, as the order could be out of order compared to Ubuntu series - // order (precise, xenial, bionic, trusty, etc). - var supportedSeries []string - for _, charmSeries := range s.SupportedSeries { - if s.SupportedJujuSeries.Contains(charmSeries) { - supportedSeries = append(supportedSeries, charmSeries) - } - } - defaultSeries, err := SeriesForCharm("", supportedSeries) - if err == nil { - return defaultSeries, nil - } - - // Charm hasn't specified a default (likely due to being a local charm - // deployed by path). Last chance, best we can do is default to LTS. - - // At this point, because we have no idea what series the charm supports, - // *everything* requires --Force. - if !s.Force { - s.Logger.Tracef("juju supported series %s", s.SupportedJujuSeries.SortedValues()) - s.Logger.Tracef("charm supported series %s", s.SupportedSeries) - if IsMissingSeriesError(err) && len(s.SupportedSeries) > 0 { - return "", errors.Errorf("the charm defined series %q not supported", strings.Join(s.SupportedSeries, ", ")) - } - - // We know err is not nil due to above, so return the error - // returned to us from the charm call. - return "", err - } - - latestLTS := version.DefaultSupportedLTS() - s.Logger.Infof(msgLatestLTSSeries, latestLTS) - return latestLTS, nil -} - -// userRequested checks the series the user has requested, and returns it if it -// is supported, or if they used --Force. -func (s SeriesSelector) userRequested(requestedSeries string) (string, error) { - // TODO(sidecar): handle computed series - series, err := SeriesForCharm(requestedSeries, s.SupportedSeries) - if s.Force { - series = requestedSeries - } else if err != nil { - if IsUnsupportedSeriesError(err) { - supported := s.SupportedJujuSeries.Intersection(set.NewStrings(s.SupportedSeries...)) - if supported.IsEmpty() { - return "", errors.NewNotSupported(nil, fmt.Sprintf("series: %s", requestedSeries)) - } - return "", errors.Errorf( - "series %q is not supported, supported series are: %s", - requestedSeries, strings.Join(supported.SortedValues(), ","), - ) - } - return "", err - } - - // validate the series we get from the charm - if err := s.validateSeries(series); err != nil { - return "", err - } - - // either it's a supported series or the user used --Force, so just - // give them what they asked for. - if s.FromBundle { - s.Logger.Infof(msgBundleSeries, series) - return series, nil - } - s.Logger.Infof(msgUserRequestedSeries, series) - return series, nil -} - -func (s SeriesSelector) validateSeries(seriesName string) error { - if len(s.SupportedJujuSeries) == 0 { - // programming error - return errors.Errorf("expected supported juju series to exist") - } - - if !s.SupportedJujuSeries.Contains(seriesName) { - return errors.NotSupportedf("series: %s", seriesName) - } - return nil -} diff --git a/core/charm/seriesselector_test.go b/core/charm/seriesselector_test.go deleted file mode 100644 index f19644fa38b8..000000000000 --- a/core/charm/seriesselector_test.go +++ /dev/null @@ -1,365 +0,0 @@ -// Copyright 2016 Canonical Ltd. -// Licensed under the AGPLv3, see LICENCE file for details. - -package charm - -import ( - "github.com/juju/collections/set" - jc "github.com/juju/testing/checkers" - gc "gopkg.in/check.v1" - - corebase "github.com/juju/juju/core/base" -) - -type SeriesSelectorSuite struct{} - -var _ = gc.Suite(&SeriesSelectorSuite{}) - -func (s *SeriesSelectorSuite) TestCharmSeries(c *gc.C) { - deploySeriesTests := []struct { - title string - - SeriesSelector - - expectedSeries string - err string - }{ - { - // Simple selectors first, no supported series. - - title: "juju deploy simple # no default series, no supported series", - SeriesSelector: SeriesSelector{ - Conf: mockModelCfg{}, - }, - err: "series not specified and charm does not define any", - }, { - title: "juju deploy simple # default series set, no supported series", - SeriesSelector: SeriesSelector{ - Conf: mockModelCfg{"ubuntu@18.04", true}, - SupportedJujuSeries: set.NewStrings("bionic", "cosmic"), - }, - expectedSeries: "bionic", - }, - { - title: "juju deploy simple with old series # default series set, no supported series", - SeriesSelector: SeriesSelector{ - Conf: mockModelCfg{"ubuntu@15.10", true}, - SupportedJujuSeries: set.NewStrings("bionic", "cosmic"), - }, - err: "series: wily not supported", - }, - { - title: "juju deploy simple --series=precise # default series set, no supported series", - SeriesSelector: SeriesSelector{ - SeriesFlag: "precise", - Conf: mockModelCfg{"ubuntu@15.10", true}, - SupportedJujuSeries: set.NewStrings("bionic", "cosmic"), - }, - err: "series: precise not supported", - }, { - title: "juju deploy simple --series=bionic # default series set, no supported series, no supported juju series", - SeriesSelector: SeriesSelector{ - SeriesFlag: "bionic", - Conf: mockModelCfg{"ubuntu@15.10", true}, - }, - err: "expected supported juju series to exist", - }, - { - title: "juju deploy simple --series=bionic # default series set, no supported series", - SeriesSelector: SeriesSelector{ - SeriesFlag: "bionic", - Conf: mockModelCfg{"ubuntu@15.10", true}, - SupportedJujuSeries: set.NewStrings("bionic", "cosmic"), - }, - expectedSeries: "bionic", - }, - { - title: "juju deploy trusty/simple # charm series set, default series set, no supported series", - SeriesSelector: SeriesSelector{ - CharmURLSeries: "trusty", - Conf: mockModelCfg{"ubuntu@15.10", true}, - SupportedJujuSeries: set.NewStrings("bionic", "cosmic"), - }, - err: "series: trusty not supported", - }, - { - title: "juju deploy bionic/simple # charm series set, default series set, no supported series", - SeriesSelector: SeriesSelector{ - CharmURLSeries: "bionic", - Conf: mockModelCfg{"ubuntu@15.10", true}, - SupportedJujuSeries: set.NewStrings("bionic", "cosmic"), - }, - expectedSeries: "bionic", - }, - { - title: "juju deploy cosmic/simple --series=bionic # series specified, charm series set, default series set, no supported series", - SeriesSelector: SeriesSelector{ - SeriesFlag: "bionic", - CharmURLSeries: "cosmic", - Conf: mockModelCfg{"ubuntu@15.10", true}, - SupportedJujuSeries: set.NewStrings("bionic", "cosmic"), - }, - expectedSeries: "bionic", - }, - { - title: "juju deploy simple --force # no default series, no supported series, use LTS (jammy)", - SeriesSelector: SeriesSelector{ - Force: true, - Conf: mockModelCfg{}, - }, - expectedSeries: "jammy", - }, - - // Now charms with supported series. - - { - title: "juju deploy multiseries # use charm default, nothing specified, no default series", - SeriesSelector: SeriesSelector{ - SupportedSeries: []string{"bionic", "cosmic"}, - Conf: mockModelCfg{}, - SupportedJujuSeries: set.NewStrings("bionic", "cosmic"), - }, - expectedSeries: "bionic", - }, - { - title: "juju deploy multiseries with invalid series # use charm default, nothing specified, no default series", - SeriesSelector: SeriesSelector{ - SupportedSeries: []string{"precise", "bionic", "cosmic"}, - Conf: mockModelCfg{}, - SupportedJujuSeries: set.NewStrings("bionic", "cosmic"), - }, - expectedSeries: "bionic", - }, - { - title: "juju deploy multiseries with invalid serie # use charm default, nothing specified, no default series", - SeriesSelector: SeriesSelector{ - SupportedSeries: []string{"precise"}, - Conf: mockModelCfg{}, - SupportedJujuSeries: set.NewStrings("bionic", "cosmic"), - }, - err: `the charm defined series "precise" not supported`, - }, - { - title: "juju deploy multiseries # use charm defaults used if default series doesn't match, nothing specified", - SeriesSelector: SeriesSelector{ - SupportedSeries: []string{"bionic", "cosmic"}, - Conf: mockModelCfg{"ubuntu@15.10", true}, - SupportedJujuSeries: set.NewStrings("bionic", "cosmic"), - }, - err: `series "wily" is not supported, supported series are: bionic,cosmic`, - }, - { - title: "juju deploy multiseries # use model series defaults if supported by charm", - SeriesSelector: SeriesSelector{ - SupportedSeries: []string{"bionic", "cosmic", "disco"}, - Conf: mockModelCfg{"ubuntu@19.04", true}, - SupportedJujuSeries: set.NewStrings("bionic", "cosmic", "disco"), - }, - expectedSeries: "disco", - }, - { - title: "juju deploy multiseries # use model series defaults if supported by charm", - SeriesSelector: SeriesSelector{ - SupportedSeries: []string{"bionic", "cosmic", "disco"}, - Conf: mockModelCfg{"ubuntu@19.04", true}, - SupportedJujuSeries: set.NewStrings("bionic", "cosmic"), - }, - err: "series: disco not supported", - }, - { - title: "juju deploy multiseries --series=bionic # use supported requested", - SeriesSelector: SeriesSelector{ - SeriesFlag: "bionic", - SupportedSeries: []string{"utopic", "vivid", "bionic"}, - Conf: mockModelCfg{}, - SupportedJujuSeries: set.NewStrings("bionic", "cosmic"), - }, - expectedSeries: "bionic", - }, - { - title: "juju deploy multiseries --series=bionic # use supported requested", - SeriesSelector: SeriesSelector{ - SeriesFlag: "bionic", - SupportedSeries: []string{"cosmic", "bionic"}, - Conf: mockModelCfg{}, - SupportedJujuSeries: set.NewStrings("bionic", "cosmic"), - }, - expectedSeries: "bionic", - }, - { - title: "juju deploy multiseries --series=bionic # unsupported requested", - SeriesSelector: SeriesSelector{ - SeriesFlag: "bionic", - SupportedSeries: []string{"utopic", "vivid"}, - Conf: mockModelCfg{}, - SupportedJujuSeries: set.NewStrings("bionic", "cosmic"), - }, - err: `series: bionic`, - }, - { - title: "juju deploy multiseries --series=bionic --force # unsupported forced", - SeriesSelector: SeriesSelector{ - SeriesFlag: "bionic", - SupportedSeries: []string{"utopic", "vivid"}, - Force: true, - Conf: mockModelCfg{}, - }, - err: "expected supported juju series to exist", - }, - { - title: "juju deploy bionic/multiseries # non-default but supported series", - SeriesSelector: SeriesSelector{ - CharmURLSeries: "bionic", - SupportedSeries: []string{"utopic", "vivid", "bionic"}, - Conf: mockModelCfg{}, - SupportedJujuSeries: set.NewStrings("bionic", "cosmic"), - }, - expectedSeries: "bionic", - }, - { - title: "juju deploy bionic/multiseries # non-default but supported series", - SeriesSelector: SeriesSelector{ - CharmURLSeries: "bionic", - SupportedSeries: []string{"utopic", "vivid", "bionic"}, - Conf: mockModelCfg{}, - }, - err: "expected supported juju series to exist", - }, - { - title: "juju deploy bionic/multiseries --series=cosmic # non-default but supported series", - SeriesSelector: SeriesSelector{ - SeriesFlag: "cosmic", - CharmURLSeries: "bionic", - SupportedSeries: []string{"utopic", "vivid", "bionic", "cosmic"}, - Conf: mockModelCfg{}, - SupportedJujuSeries: set.NewStrings("bionic", "cosmic"), - }, - expectedSeries: "cosmic", - }, - { - title: "juju deploy bionic/multiseries --series=cosmic # unsupported series", - SeriesSelector: SeriesSelector{ - SeriesFlag: "cosmic", - CharmURLSeries: "bionic", - SupportedSeries: []string{"bionic", "utopic", "vivid"}, - Conf: mockModelCfg{}, - }, - err: `series: cosmic`, - }, - { - title: "juju deploy bionic/multiseries --series=cosmic # unsupported series", - SeriesSelector: SeriesSelector{ - SeriesFlag: "cosmic", - CharmURLSeries: "bionic", - SupportedSeries: []string{"bionic", "utopic", "vivid", "cosmic"}, - Conf: mockModelCfg{}, - SupportedJujuSeries: set.NewStrings("bionic", "cosmic"), - }, - expectedSeries: "cosmic", - }, - { - title: "juju deploy bionic/multiseries --series=precise --force # unsupported series forced", - SeriesSelector: SeriesSelector{ - SeriesFlag: "precise", - CharmURLSeries: "bionic", - SupportedSeries: []string{"bionic", "utopic", "vivid"}, - Force: true, - Conf: mockModelCfg{}, - }, - err: "expected supported juju series to exist", - }, - } - - // Use bionic for LTS for all calls. - previous := corebase.SetLatestLtsForTesting("bionic") - defer corebase.SetLatestLtsForTesting(previous) - - for i, test := range deploySeriesTests { - c.Logf("test %d [%s]", i, test.title) - test.SeriesSelector.Logger = &noOpLogger{} - series, err := test.SeriesSelector.CharmSeries() - if test.err != "" { - c.Check(err, gc.ErrorMatches, test.err) - } else { - c.Check(err, jc.ErrorIsNil) - c.Check(series, gc.Equals, test.expectedSeries) - } - } -} - -func (s *SeriesSelectorSuite) TestValidate(c *gc.C) { - deploySeriesTests := []struct { - title string - selector SeriesSelector - err string - }{ - { - title: "should fail when image-id constraint is used and no base is explicitly set", - selector: SeriesSelector{ - Conf: mockModelCfg{ - explicit: false, - }, - UsingImageID: true, - }, - err: "base must be explicitly provided when image-id constraint is used", - }, - { - title: "should return no errors when using image-id and series flag", - selector: SeriesSelector{ - Conf: mockModelCfg{ - explicit: false, - }, - SeriesFlag: "jammy", - UsingImageID: true, - }, - }, - { - title: "should return no errors when using image-id and charms url series is set", - selector: SeriesSelector{ - Conf: mockModelCfg{ - explicit: false, - }, - CharmURLSeries: "jammy", - UsingImageID: true, - }, - }, - { - title: "should return no errors when using image-id and explicit base from conf", - selector: SeriesSelector{ - Conf: mockModelCfg{ - explicit: true, - }, - UsingImageID: true, - }, - }, - } - - for i, test := range deploySeriesTests { - c.Logf("test %d [%s]", i, test.title) - test.selector.Logger = &noOpLogger{} - err := test.selector.Validate() - if test.err != "" { - c.Check(err, gc.ErrorMatches, test.err) - } else { - c.Check(err, jc.ErrorIsNil) - } - } -} - -type mockModelCfg struct { - base string - explicit bool -} - -func (d mockModelCfg) DefaultBase() (string, bool) { - return d.base, d.explicit -} - -func (d mockModelCfg) ImageStream() string { - return "released" -} - -type noOpLogger struct{} - -func (noOpLogger) Infof(string, ...interface{}) {} -func (noOpLogger) Tracef(string, ...interface{}) {} diff --git a/testcharms/charm-repo/bionic/dummy-no-series/.dir/ignored b/testcharms/charm-repo/bionic/dummy-no-series/.dir/ignored new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/testcharms/charm-repo/bionic/dummy-no-series/.ignored b/testcharms/charm-repo/bionic/dummy-no-series/.ignored new file mode 100644 index 000000000000..4287ca861797 --- /dev/null +++ b/testcharms/charm-repo/bionic/dummy-no-series/.ignored @@ -0,0 +1 @@ +# \ No newline at end of file diff --git a/testcharms/charm-repo/bionic/dummy-no-series/actions.yaml b/testcharms/charm-repo/bionic/dummy-no-series/actions.yaml new file mode 100644 index 000000000000..3ab89e5f9ea8 --- /dev/null +++ b/testcharms/charm-repo/bionic/dummy-no-series/actions.yaml @@ -0,0 +1,7 @@ +snapshot: + description: Take a snapshot of the database. + params: + outfile: + description: The file to write out to. + type: string + default: foo.bz2 diff --git a/testcharms/charm-repo/bionic/dummy-no-series/build/ignored b/testcharms/charm-repo/bionic/dummy-no-series/build/ignored new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/testcharms/charm-repo/bionic/dummy-no-series/config.yaml b/testcharms/charm-repo/bionic/dummy-no-series/config.yaml new file mode 100644 index 000000000000..a164f0a19a3d --- /dev/null +++ b/testcharms/charm-repo/bionic/dummy-no-series/config.yaml @@ -0,0 +1,5 @@ +options: + title: {default: My Title, description: A descriptive title used for the application., type: string} + outlook: {description: No default outlook., type: string} + username: {default: admin001, description: The name of the initial account (given admin permissions)., type: string} + skill-level: {description: A number indicating skill., type: int} diff --git a/testcharms/charm-repo/bionic/dummy-no-series/empty/.gitkeep b/testcharms/charm-repo/bionic/dummy-no-series/empty/.gitkeep new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/testcharms/charm-repo/bionic/dummy-no-series/hooks/install b/testcharms/charm-repo/bionic/dummy-no-series/hooks/install new file mode 100755 index 000000000000..1344650090d1 --- /dev/null +++ b/testcharms/charm-repo/bionic/dummy-no-series/hooks/install @@ -0,0 +1,2 @@ +#!/bin/bash +echo "Done!" diff --git a/testcharms/charm-repo/bionic/dummy-no-series/hooks/remove b/testcharms/charm-repo/bionic/dummy-no-series/hooks/remove new file mode 100755 index 000000000000..0d92f636d6ae --- /dev/null +++ b/testcharms/charm-repo/bionic/dummy-no-series/hooks/remove @@ -0,0 +1,2 @@ +#!/bin/bash +echo "Removing service files" diff --git a/testcharms/charm-repo/bionic/dummy-no-series/hooks/stop b/testcharms/charm-repo/bionic/dummy-no-series/hooks/stop new file mode 100755 index 000000000000..1ab881305616 --- /dev/null +++ b/testcharms/charm-repo/bionic/dummy-no-series/hooks/stop @@ -0,0 +1,2 @@ +#!/bin/bash +echo "Stopping service" diff --git a/testcharms/charm-repo/bionic/dummy-no-series/metadata.yaml b/testcharms/charm-repo/bionic/dummy-no-series/metadata.yaml new file mode 100644 index 000000000000..477ee93e9b8d --- /dev/null +++ b/testcharms/charm-repo/bionic/dummy-no-series/metadata.yaml @@ -0,0 +1,6 @@ +name: dummy +summary: "That's a dummy charm." +description: | + This is a longer description which + potentially contains multiple lines. +minjujuversion: "1.0.0" diff --git a/testcharms/charm-repo/bionic/dummy-no-series/revision b/testcharms/charm-repo/bionic/dummy-no-series/revision new file mode 100644 index 000000000000..56a6051ca2b0 --- /dev/null +++ b/testcharms/charm-repo/bionic/dummy-no-series/revision @@ -0,0 +1 @@ +1 \ No newline at end of file diff --git a/testcharms/charm-repo/bionic/dummy-no-series/src/hello.c b/testcharms/charm-repo/bionic/dummy-no-series/src/hello.c new file mode 100644 index 000000000000..9e37d3329254 --- /dev/null +++ b/testcharms/charm-repo/bionic/dummy-no-series/src/hello.c @@ -0,0 +1,7 @@ +#include + +main() +{ + printf ("Hello World!\n"); + return 0; +} diff --git a/testcharms/charm-repo/bionic/dummy-no-series/version b/testcharms/charm-repo/bionic/dummy-no-series/version new file mode 100644 index 000000000000..de5345805361 --- /dev/null +++ b/testcharms/charm-repo/bionic/dummy-no-series/version @@ -0,0 +1 @@ +revision-id: dummy-146-g725cfd3-dirty \ No newline at end of file diff --git a/testcharms/charm-repo/bionic/dummy/metadata.yaml b/testcharms/charm-repo/bionic/dummy/metadata.yaml index 265f1b6aabfc..f3835b0aac24 100644 --- a/testcharms/charm-repo/bionic/dummy/metadata.yaml +++ b/testcharms/charm-repo/bionic/dummy/metadata.yaml @@ -3,4 +3,8 @@ summary: "That's a dummy charm." description: | This is a longer description which potentially contains multiple lines. -minjujuversion: "1.0.0" \ No newline at end of file +minjujuversion: "1.0.0" +series: + - jammy + - focal + - bionic diff --git a/testcharms/charm-repo/bionic/logging/metadata.yaml b/testcharms/charm-repo/bionic/logging/metadata.yaml index c9cd2ff0f954..9d4bd6067211 100644 --- a/testcharms/charm-repo/bionic/logging/metadata.yaml +++ b/testcharms/charm-repo/bionic/logging/metadata.yaml @@ -14,3 +14,7 @@ requires: info: interface: juju-info scope: container +series: + - jammy + - focal + - bionic diff --git a/testcharms/charm-repo/bionic/riak/metadata.yaml b/testcharms/charm-repo/bionic/riak/metadata.yaml index 0b0eaa622b0a..c1a0f5c58554 100644 --- a/testcharms/charm-repo/bionic/riak/metadata.yaml +++ b/testcharms/charm-repo/bionic/riak/metadata.yaml @@ -9,3 +9,7 @@ provides: peers: ring: interface: riak +series: + - bionic + - focal + - jammy diff --git a/testcharms/charm-repo/bionic/terms1/metadata.yaml b/testcharms/charm-repo/bionic/terms1/metadata.yaml index 65468d9e2f14..9bafd6d13037 100644 --- a/testcharms/charm-repo/bionic/terms1/metadata.yaml +++ b/testcharms/charm-repo/bionic/terms1/metadata.yaml @@ -3,4 +3,8 @@ summary: "That's a dummy charm with terms." description: | This is a longer description which potentially contains multiple lines. -terms: ["term1/1", "term3/1"] \ No newline at end of file +terms: ["term1/1", "term3/1"] +series: + - jammy + - focal + - bionic diff --git a/testcharms/charm-repo/quantal/terms1/metadata.yaml b/testcharms/charm-repo/quantal/terms1/metadata.yaml index 65468d9e2f14..9bafd6d13037 100644 --- a/testcharms/charm-repo/quantal/terms1/metadata.yaml +++ b/testcharms/charm-repo/quantal/terms1/metadata.yaml @@ -3,4 +3,8 @@ summary: "That's a dummy charm with terms." description: | This is a longer description which potentially contains multiple lines. -terms: ["term1/1", "term3/1"] \ No newline at end of file +terms: ["term1/1", "term3/1"] +series: + - jammy + - focal + - bionic