diff --git a/api/common/logs.go b/api/common/logs.go index da9abf7cabe..b707f33047c 100644 --- a/api/common/logs.go +++ b/api/common/logs.go @@ -22,7 +22,7 @@ import ( // closes the connection. type DebugLogParams struct { // IncludeEntity lists entity tags to include in the response. Tags may - // finish with a '*' to match a prefix e.g.: unit-mysql-*, machine-2. If + // include '*' wildcards e.g.: unit-mysql-*, machine-2. If // none are set, then all lines are considered included. IncludeEntity []string // IncludeModule lists logging modules to include in the response. If none @@ -33,7 +33,7 @@ type DebugLogParams struct { // are set all labels are considered included. IncludeLabel []string // ExcludeEntity lists entity tags to exclude from the response. As with - // IncludeEntity the values may finish with a '*'. + // IncludeEntity the values may include '*' wildcards. ExcludeEntity []string // ExcludeModule lists logging modules to exclude from the response. If a // module is specified, all the submodules are also excluded. diff --git a/cmd/juju/application/refresh.go b/cmd/juju/application/refresh.go index c700b070fa0..d9f9ff6755e 100644 --- a/cmd/juju/application/refresh.go +++ b/cmd/juju/application/refresh.go @@ -407,7 +407,8 @@ func (c *refreshCommand) Run(ctx *cmd.Context) error { return errors.Trace(err) } charmID, err := factory.Run(cfg) - if err == nil { + switch { + case err == nil: curl := charmID.URL charmOrigin := charmID.Origin if charmOrigin.Source == corecharm.CharmStore { @@ -419,13 +420,15 @@ func (c *refreshCommand) Run(ctx *cmd.Context) error { channel = fmt.Sprintf(" in channel %s", charmID.Origin.Channel.String()) } ctx.Infof("Added %s charm %q, revision %d%s, to the model", charmOrigin.Source, curl.Name, curl.Revision, channel) - } else if errors.Is(err, refresher.ErrAlreadyUpToDate) { - if len(c.Resources) == 0 { - // Charm already up-to-date and no resources to refresh. - ctx.Infof(err.Error()) - return nil - } - } else { + case errors.Is(err, refresher.ErrAlreadyUpToDate) && c.Channel.String() != oldOrigin.CoreCharmOrigin().Channel.String(): + ctx.Infof("%s. Note: all future refreshes will now use channel %q", err.Error(), charmID.Origin.Channel.String()) + case errors.Is(err, refresher.ErrAlreadyUpToDate) && len(c.Resources) == 0: + // Charm already up-to-date and no resources to refresh. + ctx.Infof(err.Error()) + return nil + case errors.Is(err, refresher.ErrAlreadyUpToDate) && len(c.Resources) > 0: + ctx.Infof("%s. Attempt to update resources requested.", err.Error()) + default: if termErr, ok := errors.Cause(err).(*common.TermsRequiredError); ok { return errors.Trace(termErr.UserErr()) } @@ -433,7 +436,6 @@ func (c *refreshCommand) Run(ctx *cmd.Context) error { } // Next, upgrade resources. - resourceLister, err := c.NewResourceLister(apiRoot) if err != nil { return errors.Trace(err) diff --git a/cmd/juju/application/refresh_test.go b/cmd/juju/application/refresh_test.go index 5b6c16116ab..0fcb57b110e 100644 --- a/cmd/juju/application/refresh_test.go +++ b/cmd/juju/application/refresh_test.go @@ -149,6 +149,9 @@ func (s *BaseRefreshSuite) SetUpTest(c *gc.C) { bindings: map[string]string{ "": network.AlphaSpaceName, }, + charmOrigin: commoncharm.Origin{ + Risk: "stable", + }, } s.modelConfigGetter = newMockModelConfigGetter() s.resourceLister = mockResourceLister{} @@ -614,6 +617,29 @@ func (s *RefreshSuite) TestUpgradeWithChannel(c *gc.C) { }) } +func (s *RefreshSuite) TestUpgradeWithChannelNoNewCharmURL(c *gc.C) { + // Test setting a new charm channel, without an actual + // charm upgrade needed. + s.resolvedCharmURL = charm.MustParseURL("cs:quantal/foo-1") + s.resolvedChannel = csclientparams.BetaChannel + _, err := s.runRefresh(c, "foo", "--channel=beta") + c.Assert(err, jc.ErrorIsNil) + + s.charmAPIClient.CheckCallNames(c, "GetCharmURLOrigin", "Get", "SetCharm") + s.charmAPIClient.CheckCall(c, 2, "SetCharm", model.GenerationMaster, application.SetCharmConfig{ + ApplicationName: "foo", + CharmID: application.CharmID{ + URL: s.resolvedCharmURL, + Origin: commoncharm.Origin{ + Source: "charm-store", + Architecture: arch.DefaultArchitecture, + Risk: "beta", + }, + }, + EndpointBindings: map[string]string{}, + }) +} + func (s *RefreshSuite) TestRefreshShouldRespectDeployedChannelByDefault(c *gc.C) { s.resolvedChannel = csclientparams.BetaChannel _, err := s.runRefresh(c, "foo") diff --git a/cmd/juju/commands/debuglog.go b/cmd/juju/commands/debuglog.go index 302e024ba0f..b134e95deed 100644 --- a/cmd/juju/commands/debuglog.go +++ b/cmd/juju/commands/debuglog.go @@ -11,6 +11,7 @@ import ( "github.com/juju/ansiterm" "github.com/juju/cmd/v3" + "github.com/juju/collections/transform" "github.com/juju/errors" "github.com/juju/gnuflag" "github.com/juju/juju/jujuclient" @@ -46,7 +47,8 @@ machines and units can be seen in the output of `[1:] + "`juju status`" + `. The '--include' and '--exclude' options filter by entity. The entity can be a machine, unit, or application for vm models, but can be application only -for k8s models. +for k8s models. These filters support wildcards ` + "`*`" + ` if filtering on the +entity full name (prefixed by ` + "`-`" + `) The '--include-module' and '--exclude-module' options filter by (dotted) logging module name. The module name can be truncated such that all loggers @@ -204,45 +206,50 @@ func (c *debugLogCommand) Init(args []string) error { return errors.Trace(err) } isCaas := modelType == model.CAAS - c.params.IncludeEntity = c.processEntities(isCaas, c.params.IncludeEntity) - c.params.ExcludeEntity = c.processEntities(isCaas, c.params.ExcludeEntity) + if isCaas { + c.params.IncludeEntity = transform.Slice(c.params.IncludeEntity, c.parseCAASEntity) + c.params.ExcludeEntity = transform.Slice(c.params.ExcludeEntity, c.parseCAASEntity) + } else { + c.params.IncludeEntity = transform.Slice(c.params.IncludeEntity, c.parseEntity) + c.params.ExcludeEntity = transform.Slice(c.params.ExcludeEntity, c.parseEntity) + } return cmd.CheckEmpty(args) } -func (c *debugLogCommand) processEntities(isCAAS bool, entities []string) []string { - if entities == nil { - return nil +func (c *debugLogCommand) parseEntity(entity string) string { + tag, err := names.ParseTag(entity) + switch { + case strings.Contains(entity, "*"): + return entity + case err == nil && (tag.Kind() == names.ApplicationTagKind || tag.Kind() == names.MachineTagKind || tag.Kind() == names.UnitTagKind): + return tag.String() + case names.IsValidMachine(entity): + return names.NewMachineTag(entity).String() + case names.IsValidUnit(entity): + return names.NewUnitTag(entity).String() + case names.IsValidApplication(entity): + // If the user asks for --include nova-compute, we should give all + // nova-compute units for IAAS models. + return names.UnitTagKind + "-" + entity + "-*" + default: + logger.Warningf("%q was not recognised as a valid application, machine or unit name", entity) + return entity } - result := make([]string, len(entities)) - for i, entity := range entities { - // A stringified unit or machine tag never match their "IsValid" - // function from names, so if the string value passed in is a valid - // machine or unit, then convert here. - if names.IsValidMachine(entity) { - entity = names.NewMachineTag(entity).String() - } else if names.IsValidUnit(entity) { - entity = names.NewUnitTag(entity).String() - } else { - // Now we want to deal with a special case. Both stringified - // machine tags and stringified units are valid application names. - // So here we use special knowledge about how tags are serialized to - // be able to give a better user experience. If the user asks for - // --include nova-compute, we should give all nova-compute units. - if strings.HasPrefix(entity, names.UnitTagKind+"-") || - strings.HasPrefix(entity, names.MachineTagKind+"-") { - // no-op pass through - } else if names.IsValidApplication(entity) { - // Assume that the entity refers to an application. - if isCAAS { - entity = names.NewApplicationTag(entity).String() - } else { - entity = names.UnitTagKind + "-" + entity + "-*" - } - } - } - result[i] = entity +} + +func (c *debugLogCommand) parseCAASEntity(entity string) string { + tag, err := names.ParseTag(entity) + switch { + case strings.Contains(entity, "*"): + return entity + case err == nil && tag.Kind() == names.ApplicationTagKind: + return tag.String() + case names.IsValidApplication(entity): + return names.NewApplicationTag(entity).String() + default: + logger.Warningf("%q was not recognised as a valid application name. Only applications produce logs for CAAS models application", entity) + return entity } - return result } type DebugLogAPI interface { diff --git a/cmd/juju/commands/synctools.go b/cmd/juju/commands/synctools.go index e481268783c..410e005ee73 100644 --- a/cmd/juju/commands/synctools.go +++ b/cmd/juju/commands/synctools.go @@ -57,8 +57,8 @@ The online store will, of course, need to be contacted at some point to get the software. Examples: - juju sync-agent-binary --debug --version 2.0 - juju sync-agent-binary --debug --version 2.0 --local-dir=/home/ubuntu/sync-agent-binary + juju sync-agent-binary --debug --agent-version 2.0 + juju sync-agent-binary --debug --agent-version 2.0 --local-dir=/home/ubuntu/sync-agent-binary See also: upgrade-controller @@ -75,7 +75,7 @@ func (c *syncAgentBinaryCommand) Info() *cmd.Info { func (c *syncAgentBinaryCommand) SetFlags(f *gnuflag.FlagSet) { c.ModelCommandBase.SetFlags(f) - f.StringVar(&c.versionStr, "version", "", "Copy a specific major[.minor] version") + f.StringVar(&c.versionStr, "agent-version", "", "Copy a specific major[.minor] version") f.BoolVar(&c.dryRun, "dry-run", false, "Don't copy, just print what would be copied") f.BoolVar(&c.public, "public", false, "Tools are for a public cloud, so generate mirrors information") f.StringVar(&c.source, "source", "", "Local source directory") @@ -85,7 +85,7 @@ func (c *syncAgentBinaryCommand) SetFlags(f *gnuflag.FlagSet) { func (c *syncAgentBinaryCommand) Init(args []string) error { if c.versionStr == "" { - return errors.NewNotValid(nil, "--version is required") + return errors.NewNotValid(nil, "--agent-version is required") } var err error if c.targetVersion, err = version.Parse(c.versionStr); err != nil { diff --git a/cmd/juju/commands/synctools_test.go b/cmd/juju/commands/synctools_test.go index e32379ccfcc..02dabbd8fe5 100644 --- a/cmd/juju/commands/synctools_test.go +++ b/cmd/juju/commands/synctools_test.go @@ -73,21 +73,21 @@ type syncToolCommandTestCase struct { var syncToolCommandTests = []syncToolCommandTestCase{ { description: "minimum argument", - args: []string{"--version", "2.9.99", "-m", "test-target"}, + args: []string{"--agent-version", "2.9.99", "-m", "test-target"}, }, { description: "specifying also the synchronization source", - args: []string{"--version", "2.9.99", "-m", "test-target", "--source", "/foo/bar"}, + args: []string{"--agent-version", "2.9.99", "-m", "test-target", "--source", "/foo/bar"}, source: "/foo/bar", }, { description: "just make a dry run", - args: []string{"--version", "2.9.99", "-m", "test-target", "--dry-run"}, + args: []string{"--agent-version", "2.9.99", "-m", "test-target", "--dry-run"}, dryRun: true, }, { description: "specified public (ignored by API)", - args: []string{"--version", "2.9.99", "-m", "test-target", "--public"}, + args: []string{"--agent-version", "2.9.99", "-m", "test-target", "--public"}, public: false, }, } @@ -129,7 +129,8 @@ func (s *syncToolSuite) TestSyncToolsCommand(c *gc.C) { func (s *syncToolSuite) TestSyncToolsCommandTargetDirectory(c *gc.C) { dir := c.MkDir() - ctrl, run := s.getSyncAgentBinariesCommand(c, "--version", "2.9.99", "-m", "test-target", "--local-dir", dir, "--stream", "proposed") + ctrl, run := s.getSyncAgentBinariesCommand( + c, "--agent-version", "2.9.99", "-m", "test-target", "--local-dir", dir, "--stream", "proposed") defer ctrl.Finish() called := false @@ -155,7 +156,8 @@ func (s *syncToolSuite) TestSyncToolsCommandTargetDirectory(c *gc.C) { func (s *syncToolSuite) TestSyncToolsCommandTargetDirectoryPublic(c *gc.C) { dir := c.MkDir() - ctrl, run := s.getSyncAgentBinariesCommand(c, "--version", "2.9.99", "-m", "test-target", "--local-dir", dir, "--public") + ctrl, run := s.getSyncAgentBinariesCommand( + c, "--agent-version", "2.9.99", "-m", "test-target", "--local-dir", dir, "--public") defer ctrl.Finish() called := false @@ -187,7 +189,8 @@ func (s *syncToolSuite) TestAPIAdapterUploadTools(c *gc.C) { } func (s *syncToolSuite) TestAPIAdapterBlockUploadTools(c *gc.C) { - ctrl, run := s.getSyncAgentBinariesCommand(c, "-m", "test-target", "--version", "2.9.99", "--local-dir", c.MkDir(), "--stream", "released") + ctrl, run := s.getSyncAgentBinariesCommand( + c, "-m", "test-target", "--agent-version", "2.9.99", "--local-dir", c.MkDir(), "--stream", "released") defer ctrl.Finish() syncTools = func(sctx *sync.SyncContext) error { diff --git a/provider/equinix/environ.go b/provider/equinix/environ.go index 9eea7ca1398..dcd55583065 100644 --- a/provider/equinix/environ.go +++ b/provider/equinix/environ.go @@ -302,6 +302,7 @@ func getCloudConfig(args environs.StartInstanceParams) (cloudinit.CloudConfig, e } } } + iptablesDefault = append(iptablesDefault, "iptables -A INPUT -s 10.0.0.0/8 -j ACCEPT") iptablesDefault = append(iptablesDefault, "iptables -A INPUT -j DROP") cloudCfg.AddScripts( diff --git a/state/application.go b/state/application.go index 984319c5e06..6bdaaf6f859 100644 --- a/state/application.go +++ b/state/application.go @@ -1670,15 +1670,24 @@ func (a *Application) SetCharm(cfg SetCharmConfig) (err error) { }} if *a.doc.CharmURL == cfg.Charm.URL().String() { + updates := bson.D{ + {"cs-channel", channel}, + {"forcecharm", cfg.ForceUnits}, + } + // Local charms will not have a channel in their charm origin + // TODO: (hml) 2023-02-03 + // With juju 3.0, SetCharm should always have a CharmOrigin. + // Compatibility with the Update application facade method + // is no longer necessary. + if cfg.CharmOrigin != nil && cfg.CharmOrigin.Channel != nil { + updates = append(updates, bson.DocElem{"charm-origin.channel", cfg.CharmOrigin.Channel}) + } // Charm URL already set; just update the force flag and channel. ops = append(ops, txn.Op{ C: applicationsC, Id: a.doc.DocID, Assert: txn.DocExists, - Update: bson.D{{"$set", bson.D{ - {"cs-channel", channel}, - {"forcecharm", cfg.ForceUnits}, - }}}, + Update: bson.D{{"$set", updates}}, }) } else { // Check if the new charm specifies a relation max limit @@ -1709,6 +1718,10 @@ func (a *Application) SetCharm(cfg SetCharmConfig) (err error) { newCharmModifiedVersion++ } + // TODO: (hml) 2023-02-03 + // With juju 3.0, SetCharm should always have a CharmOrigin. + // Compatibility with the Update application facade method + // is no longer necessary. Modify checks appropriately. if cfg.CharmOrigin != nil { origin := a.doc.CharmOrigin // If either the charm origin ID or Hash is set before a charm is @@ -1778,7 +1791,6 @@ func (a *Application) SetCharm(cfg SetCharmConfig) (err error) { // ErrNoOperations on the other hand means there's nothing to update. return nil, errors.Trace(err) } - return ops, nil } diff --git a/state/application_test.go b/state/application_test.go index 306782c2405..09cc443ef03 100644 --- a/state/application_test.go +++ b/state/application_test.go @@ -128,6 +128,26 @@ func (s *ApplicationSuite) TestSetCharmCharmOrigin(c *gc.C) { c.Assert(obtainedOrigin, gc.DeepEquals, origin) } +func (s *ApplicationSuite) TestSetCharmUpdateChannelURLNoChange(c *gc.C) { + sch := s.AddMetaCharm(c, "mysql", metaBase, 2) + + origin := s.mysql.CharmOrigin() + origin.Channel = &state.Channel{Risk: "stable"} + + cfg := state.SetCharmConfig{ + Charm: sch, + CharmOrigin: origin, + } + err := s.mysql.SetCharm(cfg) + c.Assert(err, jc.ErrorIsNil) + c.Assert(s.mysql.CharmOrigin().Channel.Risk, gc.DeepEquals, "stable") + + cfg.CharmOrigin.Channel.Risk = "candidate" + err = s.mysql.SetCharm(cfg) + c.Assert(err, jc.ErrorIsNil) + c.Assert(s.mysql.CharmOrigin().Channel.Risk, gc.DeepEquals, "candidate") +} + func (s *ApplicationSuite) TestSetCharmCharmOriginNoChange(c *gc.C) { // Add a compatible charm. sch := s.AddMetaCharm(c, "mysql", metaBase, 2) diff --git a/state/applicationoffers.go b/state/applicationoffers.go index f138dc20457..e6724156607 100644 --- a/state/applicationoffers.go +++ b/state/applicationoffers.go @@ -143,7 +143,7 @@ func (s *applicationOffers) AllApplicationOffers() (offers []*crossmodel.Applica var docs []applicationOfferDoc err := applicationOffersCollection.Find(bson.D{}).All(&docs) if err != nil { - return nil, errors.Errorf("cannot get all application offers") + return nil, errors.Annotate(err, "getting application offer documents") } for _, doc := range docs { offer, err := s.makeApplicationOffer(doc) diff --git a/state/logs.go b/state/logs.go index 249cb7ccb4c..aa227e679d8 100644 --- a/state/logs.go +++ b/state/logs.go @@ -762,8 +762,10 @@ func makeEntityPattern(entities []string) string { var patterns []string for _, entity := range entities { // Convert * wildcard to the regex equivalent. This is safe - // because * never appears in entity names. - patterns = append(patterns, strings.Replace(entity, "*", ".*", -1)) + // because * never appears in entity names. Escape any other regex. + escaped := regexp.QuoteMeta(entity) + unescapedWildcards := strings.Replace(escaped, regexp.QuoteMeta("*"), ".*", -1) + patterns = append(patterns, unescapedWildcards) } return `^(` + strings.Join(patterns, "|") + `)$` } diff --git a/state/state.go b/state/state.go index 7e48912015b..8c27157c2a0 100644 --- a/state/state.go +++ b/state/state.go @@ -239,41 +239,14 @@ func (st *State) RemoveExportingModelDocs() error { } func (st *State) removeAllModelDocs(modelAssertion bson.D) error { - modelUUID := st.ModelUUID() - - // Gather all user permissions for the model. - // Do this first because we remove some parent docs below. - var permOps []txn.Op - permPattern := bson.M{ - "_id": bson.M{"$regex": "^" + permissionID(modelKey(modelUUID), "")}, - } - ops, err := st.removeInCollectionOps(permissionsC, permPattern) - if err != nil { - return errors.Trace(err) - } - permOps = append(permOps, ops...) - // Gather all offer permissions for the model. - ao := NewApplicationOffers(st) - allOffers, err := ao.AllApplicationOffers() - if err != nil { - return errors.Trace(err) - } - for _, offer := range allOffers { - permPattern = bson.M{ - "_id": bson.M{"$regex": "^" + permissionID(applicationOfferKey(offer.OfferUUID), "")}, - } - ops, err = st.removeInCollectionOps(permissionsC, permPattern) - if err != nil { - return errors.Trace(err) - } - permOps = append(permOps, ops...) - } - err = st.db().RunTransaction(permOps) - if err != nil { - return errors.Trace(err) + // Remove permissions first, because we potentially + // remove parent documents in the following stage. + if err := st.removeAllModelPermissions(); err != nil { + return errors.Annotate(err, "removing permissions") } // Remove each collection in its own transaction. + modelUUID := st.ModelUUID() for name, info := range st.database.Schema() { if info.global || info.rawAccess { continue @@ -316,7 +289,7 @@ func (st *State) removeAllModelDocs(modelAssertion bson.D) error { if err != nil { return errors.Trace(err) } - ops = []txn.Op{{ + ops := []txn.Op{{ // Cleanup the owner:envName unique key. C: usermodelnameC, Id: model.uniqueIndexID(), @@ -342,7 +315,42 @@ func (st *State) removeAllModelDocs(modelAssertion bson.D) error { if !st.IsController() { ops = append(ops, decHostedModelCountOp()) } - return st.db().RunTransaction(ops) + return errors.Trace(st.db().RunTransaction(ops)) +} + +// removeAllModelPermissions removes all direct permissions documents for +// this model, and all permissions for offers hosted by this model. +func (st *State) removeAllModelPermissions() error { + var permOps []txn.Op + permPattern := bson.M{ + "_id": bson.M{"$regex": "^" + permissionID(modelKey(st.ModelUUID()), "")}, + } + ops, err := st.removeInCollectionOps(permissionsC, permPattern) + if err != nil { + return errors.Trace(err) + } + permOps = append(permOps, ops...) + + applicationOffersCollection, closer := st.db().GetCollection(applicationOffersC) + defer closer() + + var offerDocs []applicationOfferDoc + if err := applicationOffersCollection.Find(bson.D{}).All(&offerDocs); err != nil { + return errors.Annotate(err, "getting application offer documents") + } + + for _, offer := range offerDocs { + permPattern = bson.M{ + "_id": bson.M{"$regex": "^" + permissionID(applicationOfferKey(offer.OfferUUID), "")}, + } + ops, err = st.removeInCollectionOps(permissionsC, permPattern) + if err != nil { + return errors.Trace(err) + } + permOps = append(permOps, ops...) + } + err = st.db().RunTransaction(permOps) + return errors.Trace(err) } // removeAllInCollectionRaw removes all the documents from the given diff --git a/tests/suites/refresh/refresh.sh b/tests/suites/refresh/refresh.sh index 6ab17d44651..360634868fb 100644 --- a/tests/suites/refresh/refresh.sh +++ b/tests/suites/refresh/refresh.sh @@ -53,7 +53,7 @@ run_refresh_local() { # shellcheck disable=SC2059 printf "${OUT}\n" - # Added local charm "ubuntu", revision 2, to the model + # format: Added charm-store charm "ubuntu", revision 21 in channel stable, to the model revision=$(echo "${OUT}" | awk 'BEGIN{FS=","} {print $2}' | awk 'BEGIN{FS=" "} {print $2}') wait_for "ubuntu" "$(charm_rev "ubuntu" "${revision}")" @@ -62,6 +62,63 @@ run_refresh_local() { destroy_model "${model_name}" } +run_refresh_channel() { + # Test juju refresh from one channel to another + echo + + model_name="test-refresh-channel" + file="${TEST_DIR}/${model_name}.log" + + ensure "${model_name}" "${file}" + + juju deploy juju-qa-test + wait_for "juju-qa-test" "$(idle_condition "juju-qa-test")" + + OUT=$(juju refresh juju-qa-test --channel 2.0/edge 2>&1 || true) + # shellcheck disable=SC2059 + printf "${OUT}\n" + + # format: Added charm-store charm "ubuntu", revision 21 in channel stable, to the model + revision=$(echo "${OUT}" | awk 'BEGIN{FS=","} {print $2}' | awk 'BEGIN{FS=" "} {print $2}') + + wait_for "juju-qa-test" "$(charm_rev "juju-qa-test" "${revision}")" + wait_for "juju-qa-test" "$(charm_channel "juju-qa-test" "2.0/edge")" + wait_for "juju-qa-test" "$(idle_condition "juju-qa-test")" + + destroy_model "${model_name}" +} + +run_refresh_channel_no_new_revision() { + # Test juju refresh from one channel to another, with no new + # charm revision. + echo + + model_name="test-refresh-channel-no-new-revision" + file="${TEST_DIR}/${model_name}.log" + + ensure "${model_name}" "${file}" + + juju deploy ubuntu + wait_for "ubuntu" "$(idle_condition "ubuntu")" + + OUT=$(juju refresh ubuntu --channel edge 2>&1 || true) + # shellcheck disable=SC2059 + printf "${OUT}\n" + + if echo "${OUT}" | grep -E -vq "all future refreshes will now use channel"; then + # shellcheck disable=SC2046 + echo $(red "failed refreshing charm: ${OUT}") + exit 5 + fi + # shellcheck disable=SC2059 + printf "${OUT}\n" + + wait_for "ubuntu" "$(charm_channel "ubuntu" "edge")" + wait_for "ubuntu" "$(idle_condition "ubuntu")" + + destroy_model "${model_name}" +} + test_basic() { if [ "$(skip 'test_basic')" ]; then echo "==> TEST SKIPPED: basic refresh" @@ -75,5 +132,7 @@ test_basic() { run "run_refresh_cs" run "run_refresh_local" + run "run_refresh_channel" + run "run_refresh_channel_no_new_revision" ) } diff --git a/tests/suites/refresh/switch.sh b/tests/suites/refresh/switch.sh index e6af164d2c1..76088fba237 100644 --- a/tests/suites/refresh/switch.sh +++ b/tests/suites/refresh/switch.sh @@ -19,7 +19,7 @@ run_refresh_switch_cs_to_ch() { # shellcheck disable=SC2059 printf "${OUT}\n" - # Added local charm "ubuntu", revision 2, to the model + # format: Added charm-store charm "ubuntu", revision 21 in channel stable, to the model revision=$(echo "${OUT}" | awk 'BEGIN{FS=","} {print $2}' | awk 'BEGIN{FS=" "} {print $2}') wait_for "ubuntu" "$(charm_rev "ubuntu" "${revision}")" @@ -28,6 +28,39 @@ run_refresh_switch_cs_to_ch() { destroy_model "${model_name}" } +run_refresh_switch_cs_to_ch_no_new_revision() { + # Test juju refresh from a charm store charm to a charm hub charm, with no new + # charm revision. + echo + + model_name="test-refresh-switch-ch-no-new-revision" + file="${TEST_DIR}/${model_name}.log" + + ensure "${model_name}" "${file}" + + OUT=$(juju deploy cs:ubuntu >&1 || true) + # shellcheck disable=SC2059 + printf "${OUT}\n" + # format: Added charm-store charm "ubuntu", revision 21 in channel stable, to the model + cs_revision=$(echo "${OUT}" | awk 'BEGIN{FS=","} {print $2}' | awk 'BEGIN{FS=" "} {print $2}') + + wait_for "ubuntu" "$(idle_condition "ubuntu")" + + OUT=$(juju refresh ubuntu --switch ch:ubuntu 2>&1 || true) + if echo "${OUT}" | grep -E -vq "Added charm-hub charm"; then + # shellcheck disable=SC2046 + echo $(red "failed refreshing charm: ${OUT}") + exit 5 + fi + # shellcheck disable=SC2059 + printf "${OUT}\n" + + wait_for "ubuntu" "$(charm_rev "ubuntu" "${cs_revision}")" + wait_for "ubuntu" "$(idle_condition "ubuntu")" + + destroy_model "${model_name}" +} + run_refresh_switch_cs_to_ch_channel() { # Test juju refresh from a charm store charm to a charm hub charm with a specific channel echo @@ -49,7 +82,7 @@ run_refresh_switch_cs_to_ch_channel() { # shellcheck disable=SC2059 printf "${OUT}\n" - # Added local charm "ubuntu", revision 2, to the model + # format: Added charm-store charm "ubuntu", revision 21 in channel stable, to the model revision=$(echo "${OUT}" | awk 'BEGIN{FS=","} {print $2}' | awk 'BEGIN{FS=" "} {print $2}') wait_for "ubuntu" "$(charm_rev "ubuntu" "${revision}")" @@ -82,7 +115,7 @@ run_refresh_switch_local_to_ch_channel() { # shellcheck disable=SC2059 printf "${OUT}\n" - # Added local charm "ubuntu", revision 2, to the model + # format: Added charm-store charm "ubuntu", revision 21 in channel stable, to the model revision=$(echo "${OUT}" | awk 'BEGIN{FS=","} {print $2}' | awk 'BEGIN{FS=" "} {print $2}') wait_for "ubuntu" "$(charm_rev "ubuntu" "${revision}")" @@ -92,32 +125,6 @@ run_refresh_switch_local_to_ch_channel() { destroy_model "${model_name}" } -run_refresh_switch_channel() { - # Test juju refresh switching from one channel to another - echo - - model_name="test-refresh-switch-channel" - file="${TEST_DIR}/${model_name}.log" - - ensure "${model_name}" "${file}" - - juju deploy juju-qa-test - wait_for "juju-qa-test" "$(idle_condition "juju-qa-test")" - - OUT=$(juju refresh juju-qa-test --channel 2.0/edge 2>&1 || true) - # shellcheck disable=SC2059 - printf "${OUT}\n" - - # Added local charm "ubuntu", revision 2, to the model - revision=$(echo "${OUT}" | awk 'BEGIN{FS=","} {print $2}' | awk 'BEGIN{FS=" "} {print $2}') - - wait_for "juju-qa-test" "$(charm_rev "juju-qa-test" "${revision}")" - wait_for "juju-qa-test" "$(charm_channel "juju-qa-test" "2.0/edge")" - wait_for "juju-qa-test" "$(idle_condition "juju-qa-test")" - - destroy_model "${model_name}" -} - test_switch() { if [ "$(skip 'test_switch')" ]; then echo "==> TEST SKIPPED: refresh switch" @@ -130,8 +137,8 @@ test_switch() { cd .. || exit run "run_refresh_switch_cs_to_ch" + run "run_refresh_switch_cs_to_ch_no_new_revision" run "run_refresh_switch_cs_to_ch_channel" run "run_refresh_switch_local_to_ch_channel" - run "run_refresh_switch_channel" ) }