diff --git a/cmd/juju/application/bundle/bundle.go b/cmd/juju/application/bundle/bundle.go index 7f339ff8d3b..7ca7f0ad57a 100644 --- a/cmd/juju/application/bundle/bundle.go +++ b/cmd/juju/application/bundle/bundle.go @@ -10,6 +10,7 @@ import ( "strings" "github.com/juju/charm/v12" + "github.com/juju/cmd/v3" "github.com/juju/errors" "github.com/juju/names/v5" @@ -250,7 +251,7 @@ func applicationConfigValue(key string, valueMap interface{}) (interface{}, erro // combined bundle data. Returns a slice of errors encountered while // processing the bundle. They are for informational purposes and do // not require failing the bundle deployment. -func ComposeAndVerifyBundle(base BundleDataSource, pathToOverlays []string) (*charm.BundleData, []error, error) { +func ComposeAndVerifyBundle(ctx *cmd.Context, base BundleDataSource, pathToOverlays []string) (*charm.BundleData, []error, error) { verifyConstraints := func(s string) error { _, err := constraints.Parse(s) @@ -286,6 +287,8 @@ func ComposeAndVerifyBundle(base BundleDataSource, pathToOverlays []string) (*ch return nil, nil, errors.Trace(err) } + deprecationWarningForSeries(ctx, bundleData) + return bundleData, unMarshallErrors, nil } @@ -355,6 +358,29 @@ func verifyBundle(data *charm.BundleData, bundleDir string, verifyConstraints fu return errors.Trace(verifyError) } +func deprecationWarningForSeries(ctx *cmd.Context, data *charm.BundleData) { + includeSeries := false + if data.Series != "" { + includeSeries = true + } + for _, m := range data.Machines { + if m != nil && m.Series != "" { + includeSeries = true + break + } + } + for _, app := range data.Applications { + if app != nil && app.Series != "" { + includeSeries = true + break + } + } + + if includeSeries { + ctx.Warningf("series in being deprecated in favour of bases. For more information about the transition to bases see https://discourse.charmhub.io/t/transition-from-series-to-base-in-juju-4-0/14127") + } +} + func verifyMixedSeriesBasesMatch(data *charm.BundleData) error { if data == nil { return nil diff --git a/cmd/juju/application/bundle/bundle_test.go b/cmd/juju/application/bundle/bundle_test.go index ae7e51db040..cf8bcbc815f 100644 --- a/cmd/juju/application/bundle/bundle_test.go +++ b/cmd/juju/application/bundle/bundle_test.go @@ -9,6 +9,7 @@ import ( "strings" "github.com/juju/charm/v12" + "github.com/juju/cmd/v3" "github.com/juju/errors" jc "github.com/juju/testing/checkers" "go.uber.org/mock/gomock" @@ -213,8 +214,10 @@ func (s *composeAndVerifyRepSuite) TestComposeAndVerifyBundleEmpty(c *gc.C) { defer s.setupMocks(c).Finish() s.expectEmptyParts() s.expectBasePath() + ctx, err := cmd.DefaultContext() + c.Assert(err, jc.ErrorIsNil) - obtained, _, err := ComposeAndVerifyBundle(s.bundleDataSource, nil) + obtained, _, err := ComposeAndVerifyBundle(ctx, s.bundleDataSource, nil) c.Assert(err, gc.ErrorMatches, ".*bundle is empty not valid") c.Assert(obtained, gc.IsNil) } @@ -225,8 +228,10 @@ func (s *composeAndVerifyRepSuite) TestComposeAndVerifyBundleUnsupportedConstrai c.Assert(err, jc.ErrorIsNil) s.expectParts(&charm.BundleDataPart{Data: bundleData}) s.expectBasePath() + ctx, err := cmd.DefaultContext() + c.Assert(err, jc.ErrorIsNil) - obtained, _, err := ComposeAndVerifyBundle(s.bundleDataSource, nil) + obtained, _, err := ComposeAndVerifyBundle(ctx, s.bundleDataSource, nil) c.Assert(err, gc.ErrorMatches, "*'image-id' constraint in a base bundle not supported") c.Assert(obtained, gc.IsNil) } @@ -237,8 +242,10 @@ func (s *composeAndVerifyRepSuite) TestComposeAndVerifyBundleNoOverlay(c *gc.C) c.Assert(err, jc.ErrorIsNil) s.expectParts(&charm.BundleDataPart{Data: bundleData}) s.expectBasePath() + ctx, err := cmd.DefaultContext() + c.Assert(err, jc.ErrorIsNil) - obtained, _, err := ComposeAndVerifyBundle(s.bundleDataSource, nil) + obtained, _, err := ComposeAndVerifyBundle(ctx, s.bundleDataSource, nil) c.Assert(err, jc.ErrorIsNil) c.Assert(obtained, gc.DeepEquals, bundleData) } @@ -250,13 +257,15 @@ func (s *composeAndVerifyRepSuite) TestComposeAndVerifyBundleOverlay(c *gc.C) { s.expectParts(&charm.BundleDataPart{Data: bundleData}) s.expectBasePath() s.setupOverlayFile(c) + ctx, err := cmd.DefaultContext() + c.Assert(err, jc.ErrorIsNil) expected := *bundleData expected.Applications["wordpress"].Options = map[string]interface{}{ "blog-title": "magic bundle config", } - obtained, _, err := ComposeAndVerifyBundle(s.bundleDataSource, []string{s.overlayFile}) + obtained, _, err := ComposeAndVerifyBundle(ctx, s.bundleDataSource, []string{s.overlayFile}) c.Assert(err, jc.ErrorIsNil) c.Assert(obtained, gc.DeepEquals, &expected) } @@ -268,13 +277,15 @@ func (s *composeAndVerifyRepSuite) TestComposeAndVerifyBundleOverlayUnsupportedC s.expectParts(&charm.BundleDataPart{Data: bundleData}) s.expectBasePath() s.setupOverlayFile(c) + ctx, err := cmd.DefaultContext() + c.Assert(err, jc.ErrorIsNil) expected := *bundleData expected.Applications["wordpress"].Options = map[string]interface{}{ "blog-title": "magic bundle config", } - obtained, _, err := ComposeAndVerifyBundle(s.bundleDataSource, []string{s.overlayFile}) + obtained, _, err := ComposeAndVerifyBundle(ctx, s.bundleDataSource, []string{s.overlayFile}) c.Assert(err, gc.ErrorMatches, "*'image-id' constraint in a base bundle not supported") c.Assert(obtained, gc.IsNil) } @@ -290,13 +301,15 @@ func (s *composeAndVerifyRepSuite) TestComposeAndVerifyBundleOverlayUnmarshallEr }) s.expectBasePath() s.setupOverlayFile(c) + ctx, err := cmd.DefaultContext() + c.Assert(err, jc.ErrorIsNil) expected := *bundleData expected.Applications["wordpress"].Options = map[string]interface{}{ "blog-title": "magic bundle config", } - obtained, unmarshallErrors, err := ComposeAndVerifyBundle(s.bundleDataSource, []string{s.overlayFile}) + obtained, unmarshallErrors, err := ComposeAndVerifyBundle(ctx, s.bundleDataSource, []string{s.overlayFile}) c.Assert(err, jc.ErrorIsNil) c.Assert(obtained, gc.DeepEquals, &expected) c.Assert(unmarshallErrors, gc.HasLen, 1) @@ -309,8 +322,10 @@ func (s *composeAndVerifyRepSuite) TestComposeAndVerifyBundleMixingBaseAndSeries c.Assert(err, jc.ErrorIsNil) s.expectParts(&charm.BundleDataPart{Data: bundleData}) s.expectBasePath() + ctx, err := cmd.DefaultContext() + c.Assert(err, jc.ErrorIsNil) - obtained, _, err := ComposeAndVerifyBundle(s.bundleDataSource, nil) + obtained, _, err := ComposeAndVerifyBundle(ctx, s.bundleDataSource, nil) c.Assert(err, jc.ErrorIsNil) c.Assert(obtained, gc.DeepEquals, bundleData) } @@ -321,8 +336,10 @@ func (s *composeAndVerifyRepSuite) TestComposeAndVerifyBundleMixingBaseAndSeries c.Assert(err, jc.ErrorIsNil) s.expectParts(&charm.BundleDataPart{Data: bundleData}) s.expectBasePath() + ctx, err := cmd.DefaultContext() + c.Assert(err, jc.ErrorIsNil) - obtained, _, err := ComposeAndVerifyBundle(s.bundleDataSource, []string{s.overlayFile}) + obtained, _, err := ComposeAndVerifyBundle(ctx, s.bundleDataSource, []string{s.overlayFile}) c.Assert(err, gc.ErrorMatches, `(?s)the provided bundle has the following errors:.*application "wordpress" series "jammy" and base "ubuntu@20.04" must match if both supplied.*invalid constraints.*`) c.Assert(obtained, gc.IsNil) } diff --git a/cmd/juju/application/deployer/bundle.go b/cmd/juju/application/deployer/bundle.go index 00745477aac..168aa5da267 100644 --- a/cmd/juju/application/deployer/bundle.go +++ b/cmd/juju/application/deployer/bundle.go @@ -84,7 +84,7 @@ func (d *deployBundle) deploy( d.accountUser = accountDetails.User // Compose bundle to be deployed and check its validity. - bundleData, unmarshalErrors, err := bundle.ComposeAndVerifyBundle(d.bundleDataSource, d.bundleOverlayFile) + bundleData, unmarshalErrors, err := bundle.ComposeAndVerifyBundle(ctx, d.bundleDataSource, d.bundleOverlayFile) if err != nil { return errors.Annotatef(err, "cannot deploy bundle") } diff --git a/cmd/juju/application/diffbundle.go b/cmd/juju/application/diffbundle.go index 2c24d152865..789d4e4cc9a 100644 --- a/cmd/juju/application/diffbundle.go +++ b/cmd/juju/application/diffbundle.go @@ -230,7 +230,7 @@ func (c *diffBundleCommand) Run(ctx *cmd.Context) error { return errors.Trace(err) } - bundle, _, err := appbundle.ComposeAndVerifyBundle(baseSrc, c.bundleOverlays) + bundle, _, err := appbundle.ComposeAndVerifyBundle(ctx, baseSrc, c.bundleOverlays) if err != nil { return errors.Trace(err) } diff --git a/cmd/juju/cloud/addcredential.go b/cmd/juju/cloud/addcredential.go index a5a4d42aa83..9dc5f1b368d 100644 --- a/cmd/juju/cloud/addcredential.go +++ b/cmd/juju/cloud/addcredential.go @@ -91,7 +91,6 @@ Use --controller option to upload a credential to a controller. Use --client option to add a credential to the current client. - ` const usageAddCredentialExamples = ` diff --git a/cmd/juju/cloud/addcredential_test.go b/cmd/juju/cloud/addcredential_test.go index a42a3ff041c..aebef437042 100644 --- a/cmd/juju/cloud/addcredential_test.go +++ b/cmd/juju/cloud/addcredential_test.go @@ -917,7 +917,6 @@ Enter credential name: Using auth-type "jsonfile". Enter path to the .json file containing a service account key for your project -(detailed instructions available at https://discourse.charmhub.io/t/1508). Path: `[1:] stderr := ` @@ -973,7 +972,6 @@ Enter your choice, or type Q|q to quit: Enter credential name: Using auth-type "jsonfile". Enter path to the .json file containing a service account key for your project -(detailed instructions available at https://discourse.charmhub.io/t/1508). Path: Credential "blah" added locally for cloud "remote". @@ -1002,7 +1000,6 @@ Enter credential name: Using auth-type "jsonfile". Enter path to the .json file containing a service account key for your project -(detailed instructions available at https://discourse.charmhub.io/t/1508). Path: `[1:] stderr := ` diff --git a/core/base/base.go b/core/base/base.go index 5b8f68c3957..4693c4c9411 100644 --- a/core/base/base.go +++ b/core/base/base.go @@ -124,6 +124,25 @@ func (b Base) IsCompatible(other Base) bool { return b.OS == other.OS && b.Channel.Track == other.Channel.Track } +// ubuntuLTSes lists the Ubuntu LTS releases that +// this version of Juju knows about +var ubuntuLTSes = []Base{ + MakeDefaultBase(UbuntuOS, "20.04"), + MakeDefaultBase(UbuntuOS, "22.04"), + MakeDefaultBase(UbuntuOS, "24.04"), +} + +// IsUbuntuLTS returns true if this base is a recognised +// Ubuntu LTS. +func (b Base) IsUbuntuLTS() bool { + for _, ubuntuLTS := range ubuntuLTSes { + if b.IsCompatible(ubuntuLTS) { + return true + } + } + return false +} + // DisplayString returns the base string ignoring risk. func (b Base) DisplayString() string { if b.Channel.Track == "" || b.OS == "" { diff --git a/core/base/base_test.go b/core/base/base_test.go index 72df1071df8..1a1f78cf3eb 100644 --- a/core/base/base_test.go +++ b/core/base/base_test.go @@ -100,3 +100,41 @@ func (s *BaseSuite) TestParseManifestBases(c *gc.C) { } c.Assert(obtained, jc.DeepEquals, expected) } + +var ubuntuLTS = []Base{ + MustParseBaseFromString("ubuntu@20.04"), + MustParseBaseFromString("ubuntu@22.04"), + MustParseBaseFromString("ubuntu@24.04"), + MustParseBaseFromString("ubuntu@24.04/stable"), + MustParseBaseFromString("ubuntu@24.04/edge"), +} + +func (s *BaseSuite) TestIsUbuntuLTSForLTSes(c *gc.C) { + for i, lts := range ubuntuLTS { + c.Logf("Checking index %d base %v", i, lts) + c.Check(lts.IsUbuntuLTS(), jc.IsTrue) + } +} + +var nonUbuntuLTS = []Base{ + MustParseBaseFromString("ubuntu@17.04"), + MustParseBaseFromString("ubuntu@19.04"), + MustParseBaseFromString("ubuntu@21.04"), + + MustParseBaseFromString("ubuntu@18.10"), + MustParseBaseFromString("ubuntu@20.10"), + MustParseBaseFromString("ubuntu@22.10"), + + MustParseBaseFromString("ubuntu@22.04-blah"), + MustParseBaseFromString("ubuntu@22.04.1234"), + + MustParseBaseFromString("centos@7"), + MustParseBaseFromString("centos@20.04"), +} + +func (s *BaseSuite) TestIsUbuntuLTSForNonLTSes(c *gc.C) { + for i, lts := range nonUbuntuLTS { + c.Logf("Checking index %d base %v", i, lts) + c.Check(lts.IsUbuntuLTS(), jc.IsFalse) + } +} diff --git a/provider/azure/environ.go b/provider/azure/environ.go index 93c3cc364fd..76978dfcc53 100644 --- a/provider/azure/environ.go +++ b/provider/azure/environ.go @@ -1085,9 +1085,6 @@ func newOSProfile( } instOS := ostype.OSTypeForName(instanceConfig.Base.OS) - if err != nil { - return nil, ostype.Unknown, errors.Trace(err) - } switch instOS { case ostype.Ubuntu, ostype.CentOS: // SSH keys are handled by custom data, but must also be diff --git a/provider/azure/instancetype.go b/provider/azure/instancetype.go index f4a075b708e..8f375a3eacc 100644 --- a/provider/azure/instancetype.go +++ b/provider/azure/instancetype.go @@ -536,7 +536,7 @@ func (env *azureEnviron) findInstanceSpec( if err != nil { return nil, errors.Trace(err) } - image, err := imageutils.SeriesImage(ctx, constraint.Base, imageStream, constraint.Region, client) + image, err := imageutils.BaseImage(ctx, constraint.Base, imageStream, constraint.Region, client) if err != nil { return nil, errors.Trace(err) } diff --git a/provider/azure/internal/imageutils/images.go b/provider/azure/internal/imageutils/images.go index 3f9d03017e1..490096d52f6 100644 --- a/provider/azure/internal/imageutils/images.go +++ b/provider/azure/internal/imageutils/images.go @@ -31,15 +31,17 @@ const ( ubuntuPublisher = "Canonical" dailyStream = "daily" + + plan = "server-gen1" ) -// SeriesImage gets an instances.Image for the specified series, image stream +// BaseImage gets an instances.Image for the specified base, image stream // and location. The resulting Image's ID is in the URN format expected by // Azure Resource Manager. // // For Ubuntu, we query the SKUs to determine the most recent point release // for a series. -func SeriesImage( +func BaseImage( ctx context.ProviderCallContext, base jujubase.Base, stream, location string, client *armcompute.VirtualMachineImagesClient, @@ -77,19 +79,91 @@ func SeriesImage( }, nil } -func offerForUbuntuSeries(series string) string { - return fmt.Sprintf("0001-com-ubuntu-server-%s", series) +// legacyUbuntuBases is a slice of bases which use the old-style offer +// id formatted like "0001-com-ubuntu-server-${series}". +// +// Recently Canonical changed the format for images offer ids +// and SKUs in Azure. The threshold for this change was noble, so if +// we want to deploy bases before noble, we must branch and use the +// old format. +// +// The old format offer ids have format `0001-com-ubuntu-server-${series}` +// or `001-com-ubuntu-server-${series}-daily` and have SKUs formatted +// `${version_number}-lts`, `${version_number}-gen2`, etc. +// +// The new format offer ids have format `ubuntu-${version_number}`, +// `ubuntu-${version_number}-lts`, `ubuntu-${version_number}-lts-daily`, +// etc. and have SKUs `server`, `server-gen1`m, `server-arm64`, etc. +// +// Since there are only a finte number of Ubuntu versions we support +// before Noble, we hardcode this list. So when new versions of Ubuntu +// are +// +// All Ubuntu images we support outside of this list have offer +// id like "ubuntu-${version}-lts" or "ubuntu-${version}" +var legacyUbuntuBases = []jujubase.Base{ + jujubase.MustParseBaseFromString("ubuntu@20.04"), + jujubase.MustParseBaseFromString("ubuntu@20.10"), + jujubase.MustParseBaseFromString("ubuntu@21.04"), + jujubase.MustParseBaseFromString("ubuntu@21.10"), + jujubase.MustParseBaseFromString("ubuntu@22.04"), + jujubase.MustParseBaseFromString("ubuntu@22.10"), + jujubase.MustParseBaseFromString("ubuntu@23.04"), + jujubase.MustParseBaseFromString("ubuntu@23.10"), +} + +func ubuntuBaseIslegacy(base jujubase.Base) bool { + for _, oldBase := range legacyUbuntuBases { + if base.IsCompatible(oldBase) { + return true + } + } + return false } // ubuntuSKU returns the best SKU for the Canonical:UbuntuServer offering, // matching the given series. +// +// TODO(jack-w-shaw): Selecting the 'daily' stream is currently broken for legacy +// Ubuntu SKU selection. See the following lp bug: +// https://bugs.launchpad.net/juju/+bug/2067717 func ubuntuSKU(ctx context.ProviderCallContext, base jujubase.Base, stream, location string, client *armcompute.VirtualMachineImagesClient) (string, string, error) { + if ubuntuBaseIslegacy(base) { + return legacyUbuntuSKU(ctx, base, stream, location, client) + } + + offer := fmt.Sprintf("ubuntu-%s", strings.ReplaceAll(base.Channel.Track, ".", "_")) + if base.IsUbuntuLTS() { + offer = fmt.Sprintf("%s-lts", offer) + } + if stream == dailyStream { + offer = fmt.Sprintf("%s-daily", offer) + } + + logger.Debugf("listing SKUs: Location=%s, Publisher=%s, Offer=%s", location, ubuntuPublisher, offer) + result, err := client.ListSKUs(ctx, location, ubuntuPublisher, offer, nil) + if err != nil { + return "", "", errorutils.HandleCredentialError(errors.Annotate(err, "listing Ubuntu SKUs"), ctx) + } + for _, img := range result.VirtualMachineImageResourceArray { + skuName := *img.Name + if skuName == plan { + logger.Debugf("found Azure SKU Name: %v", skuName) + return skuName, offer, nil + } + logger.Debugf("ignoring Azure SKU Name: %v", skuName) + } + return "", "", errors.NotFoundf("ubuntu %q SKUs for %v stream", base, stream) +} + +func legacyUbuntuSKU(ctx context.ProviderCallContext, base jujubase.Base, stream, location string, client *armcompute.VirtualMachineImagesClient) (string, string, error) { series, err := jujubase.GetSeriesFromBase(base) if err != nil { return "", "", errors.Trace(err) } - offer := offerForUbuntuSeries(series) - version := strings.ReplaceAll(base.Channel.Track, ".", "_") + offer := fmt.Sprintf("0001-com-ubuntu-server-%s", series) + desiredSKUPrefix := strings.ReplaceAll(base.Channel.Track, ".", "_") + logger.Debugf("listing SKUs: Location=%s, Publisher=%s, Offer=%s", location, ubuntuPublisher, offer) result, err := client.ListSKUs(ctx, location, ubuntuPublisher, offer, nil) if err != nil { @@ -99,12 +173,12 @@ func ubuntuSKU(ctx context.ProviderCallContext, base jujubase.Base, stream, loca var versions ubuntuVersions for _, img := range result.VirtualMachineImageResourceArray { skuName := *img.Name - logger.Debugf("Found Azure SKU Name: %v", skuName) - if !strings.HasPrefix(skuName, version) { - logger.Debugf("ignoring SKU %q (does not match base %q with version %q)", skuName, base, version) + logger.Debugf("found Azure SKU Name: %v", skuName) + if !strings.HasPrefix(skuName, desiredSKUPrefix) { + logger.Debugf("ignoring SKU %q (does not match series %q)", skuName, series) continue } - version, tag, err := parseUbuntuSKU(skuName) + version, tag, err := parselegacyUbuntuSKU(skuName) if err != nil { logger.Errorf("ignoring SKU %q (failed to parse: %s)", skuName, err) continue @@ -125,7 +199,7 @@ func ubuntuSKU(ctx context.ProviderCallContext, base jujubase.Base, stream, loca versions = append(versions, version) } if len(versions) == 0 { - return "", "", errors.NotFoundf("Ubuntu SKUs for %s stream", stream) + return "", "", errors.NotFoundf("legacy ubuntu %q SKUs for %s stream", series, stream) } sort.Sort(versions) bestVersion := versions[len(versions)-1] @@ -138,9 +212,9 @@ type ubuntuVersion struct { Point int } -// parseUbuntuSKU splits an UbuntuServer SKU into its +// parselegacyUbuntuSKU splits an UbuntuServer SKU into its // version ("22_04.3") and tag ("LTS") parts. -func parseUbuntuSKU(sku string) (ubuntuVersion, string, error) { +func parselegacyUbuntuSKU(sku string) (ubuntuVersion, string, error) { var version ubuntuVersion var tag string var err error diff --git a/provider/azure/internal/imageutils/images_test.go b/provider/azure/internal/imageutils/images_test.go index 8d7bc9d0573..072b757d209 100644 --- a/provider/azure/internal/imageutils/images_test.go +++ b/provider/azure/internal/imageutils/images_test.go @@ -45,11 +45,11 @@ func (s *imageutilsSuite) SetUpTest(c *gc.C) { s.callCtx = context.NewEmptyCloudCallContext() } -func (s *imageutilsSuite) TestSeriesImage(c *gc.C) { +func (s *imageutilsSuite) TestBaseImageOldStyle(c *gc.C) { s.mockSender.AppendResponse(azuretesting.NewResponseWithContent( `[{"name": "20_04"}, {"name": "20_04-LTS"}, {"name": "19_04"}]`, )) - image, err := imageutils.SeriesImage(s.callCtx, corebase.MakeDefaultBase("ubuntu", "20.04"), "released", "westus", s.client) + image, err := imageutils.BaseImage(s.callCtx, corebase.MakeDefaultBase("ubuntu", "20.04"), "released", "westus", s.client) c.Assert(err, jc.ErrorIsNil) c.Assert(image, gc.NotNil) c.Assert(image, jc.DeepEquals, &instances.Image{ @@ -59,11 +59,11 @@ func (s *imageutilsSuite) TestSeriesImage(c *gc.C) { }) } -func (s *imageutilsSuite) TestSeriesImageInvalidSKU(c *gc.C) { +func (s *imageutilsSuite) TestBaseImageOldStyleInvalidSKU(c *gc.C) { s.mockSender.AppendResponse(azuretesting.NewResponseWithContent( `[{"name": "22_04_invalid"}, {"name": "22_04_5-LTS"}]`, )) - image, err := imageutils.SeriesImage(s.callCtx, corebase.MakeDefaultBase("ubuntu", "22.04"), "released", "westus", s.client) + image, err := imageutils.BaseImage(s.callCtx, corebase.MakeDefaultBase("ubuntu", "22.04"), "released", "westus", s.client) c.Assert(err, jc.ErrorIsNil) c.Assert(image, gc.NotNil) c.Assert(image, jc.DeepEquals, &instances.Image{ @@ -73,14 +73,42 @@ func (s *imageutilsSuite) TestSeriesImageInvalidSKU(c *gc.C) { }) } -func (s *imageutilsSuite) TestSeriesImageCentOS(c *gc.C) { +func (s *imageutilsSuite) TestBaseImage(c *gc.C) { + s.mockSender.AppendResponse(azuretesting.NewResponseWithContent( + `[{"name": "server"}, {"name": "server-gen1"}, {"name": "server-arm64"}]`, + )) + image, err := imageutils.BaseImage(s.callCtx, corebase.MakeDefaultBase("ubuntu", "24.04"), "released", "westus", s.client) + c.Assert(err, jc.ErrorIsNil) + c.Assert(image, gc.NotNil) + c.Assert(image, jc.DeepEquals, &instances.Image{ + Id: "Canonical:ubuntu-24_04-lts:server-gen1:latest", + Arch: arch.AMD64, + VirtType: "Hyper-V", + }) +} + +func (s *imageutilsSuite) TestBaseImageNonLTS(c *gc.C) { + s.mockSender.AppendResponse(azuretesting.NewResponseWithContent( + `[{"name": "server"}, {"name": "server-gen1"}, {"name": "server-arm64"}]`, + )) + image, err := imageutils.BaseImage(s.callCtx, corebase.MakeDefaultBase("ubuntu", "25.04"), "released", "westus", s.client) + c.Assert(err, jc.ErrorIsNil) + c.Assert(image, gc.NotNil) + c.Assert(image, jc.DeepEquals, &instances.Image{ + Id: "Canonical:ubuntu-25_04:server-gen1:latest", + Arch: arch.AMD64, + VirtType: "Hyper-V", + }) +} + +func (s *imageutilsSuite) TestBaseImageCentOS(c *gc.C) { for _, cseries := range []string{"7", "8"} { base := corebase.MakeDefaultBase("centos", cseries) s.assertImageId(c, base, "released", "OpenLogic:CentOS:7.3:latest") } } -func (s *imageutilsSuite) TestSeriesImageStream(c *gc.C) { +func (s *imageutilsSuite) TestBaseImageStream(c *gc.C) { s.mockSender.AppendAndRepeatResponse(azuretesting.NewResponseWithContent( `[{"name": "22_04_2"}, {"name": "22_04_3-DAILY"}, {"name": "22_04_1-LTS"}]`), 2) base := corebase.MakeDefaultBase("ubuntu", "22.04") @@ -88,20 +116,27 @@ func (s *imageutilsSuite) TestSeriesImageStream(c *gc.C) { s.assertImageId(c, base, "released", "Canonical:0001-com-ubuntu-server-jammy:22_04_2:latest") } -func (s *imageutilsSuite) TestSeriesImageNotFound(c *gc.C) { +func (s *imageutilsSuite) TestBaseImageOldStyleNotFound(c *gc.C) { + s.mockSender.AppendResponse(azuretesting.NewResponseWithContent(`[]`)) + image, err := imageutils.BaseImage(s.callCtx, corebase.MakeDefaultBase("ubuntu", "22.04"), "released", "westus", s.client) + c.Assert(err, gc.ErrorMatches, `selecting SKU for ubuntu@22.04: legacy ubuntu "jammy" SKUs for released stream not found`) + c.Assert(image, gc.IsNil) +} + +func (s *imageutilsSuite) TestBaseImageNotFound(c *gc.C) { s.mockSender.AppendResponse(azuretesting.NewResponseWithContent(`[]`)) - image, err := imageutils.SeriesImage(s.callCtx, corebase.MakeDefaultBase("ubuntu", "22.04"), "released", "westus", s.client) - c.Assert(err, gc.ErrorMatches, "selecting SKU for ubuntu@22.04: Ubuntu SKUs for released stream not found") + image, err := imageutils.BaseImage(s.callCtx, corebase.MakeDefaultBase("ubuntu", "24.04"), "released", "westus", s.client) + c.Assert(err, gc.ErrorMatches, `selecting SKU for ubuntu@24.04: ubuntu "ubuntu@24.04/stable" SKUs for released stream not found`) c.Assert(image, gc.IsNil) } -func (s *imageutilsSuite) TestSeriesImageStreamNotFound(c *gc.C) { +func (s *imageutilsSuite) TestBaseImageStreamNotFound(c *gc.C) { s.mockSender.AppendResponse(azuretesting.NewResponseWithContent(`[{"name": "22_04-beta1"}]`)) - _, err := imageutils.SeriesImage(s.callCtx, corebase.MakeDefaultBase("ubuntu", "22.04"), "whatever", "westus", s.client) - c.Assert(err, gc.ErrorMatches, "selecting SKU for ubuntu@22.04: Ubuntu SKUs for whatever stream not found") + _, err := imageutils.BaseImage(s.callCtx, corebase.MakeDefaultBase("ubuntu", "22.04"), "whatever", "westus", s.client) + c.Assert(err, gc.ErrorMatches, `selecting SKU for ubuntu@22.04: legacy ubuntu "jammy" SKUs for whatever stream not found`) } -func (s *imageutilsSuite) TestSeriesImageStreamThrewCredentialError(c *gc.C) { +func (s *imageutilsSuite) TestBaseImageStreamThrewCredentialError(c *gc.C) { s.mockSender.AppendResponse(azuretesting.NewResponseWithStatus("401 Unauthorized", http.StatusUnauthorized)) called := false s.callCtx.InvalidateCredentialFunc = func(string) error { @@ -109,12 +144,12 @@ func (s *imageutilsSuite) TestSeriesImageStreamThrewCredentialError(c *gc.C) { return nil } - _, err := imageutils.SeriesImage(s.callCtx, corebase.MakeDefaultBase("ubuntu", "22.04"), "whatever", "westus", s.client) + _, err := imageutils.BaseImage(s.callCtx, corebase.MakeDefaultBase("ubuntu", "22.04"), "whatever", "westus", s.client) c.Assert(err.Error(), jc.Contains, "RESPONSE 401") c.Assert(called, jc.IsTrue) } -func (s *imageutilsSuite) TestSeriesImageStreamThrewNonCredentialError(c *gc.C) { +func (s *imageutilsSuite) TestBaseImageStreamThrewNonCredentialError(c *gc.C) { s.mockSender.AppendResponse(azuretesting.NewResponseWithStatus("308 Permanent Redirect", http.StatusPermanentRedirect)) called := false s.callCtx.InvalidateCredentialFunc = func(string) error { @@ -122,13 +157,13 @@ func (s *imageutilsSuite) TestSeriesImageStreamThrewNonCredentialError(c *gc.C) return nil } - _, err := imageutils.SeriesImage(s.callCtx, corebase.MakeDefaultBase("ubuntu", "22.04"), "whatever", "westus", s.client) + _, err := imageutils.BaseImage(s.callCtx, corebase.MakeDefaultBase("ubuntu", "22.04"), "whatever", "westus", s.client) c.Assert(err.Error(), jc.Contains, "RESPONSE 308") c.Assert(called, jc.IsFalse) } func (s *imageutilsSuite) assertImageId(c *gc.C, base corebase.Base, stream, id string) { - image, err := imageutils.SeriesImage(s.callCtx, base, stream, "westus", s.client) + image, err := imageutils.BaseImage(s.callCtx, base, stream, "westus", s.client) c.Assert(err, jc.ErrorIsNil) c.Assert(image.Id, gc.Equals, id) } diff --git a/provider/gce/credentials.go b/provider/gce/credentials.go index 4ea7b031f7d..a59cd77ce5e 100644 --- a/provider/gce/credentials.go +++ b/provider/gce/credentials.go @@ -51,7 +51,7 @@ func (environProviderCredentials) CredentialSchemas() map[cloud.AuthType]cloud.C cloud.JSONFileAuthType: {{ Name: credAttrFile, CredentialAttr: cloud.CredentialAttr{ - Description: "path to the .json file containing a service account key for your project\n(detailed instructions available at https://discourse.charmhub.io/t/1508).\nPath", + Description: "path to the .json file containing a service account key for your project\nPath", FilePath: true, }, }}, diff --git a/scripts/find-bad-doc-comments.py b/scripts/find-bad-doc-comments.py index 42bdee56536..196ca628459 100755 --- a/scripts/find-bad-doc-comments.py +++ b/scripts/find-bad-doc-comments.py @@ -31,8 +31,8 @@ def find_go_files(root): yield path.join(directory, filename) DOC_COMMENT_PATT = '\n\n//.+\n(//.+\n)*func.+\n' -FIRST_WORD_PATT = '// *(\w+)' -FUNC_NAME_PATT = 'func(?: \([^)]+\))? (\S+)\(' +FIRST_WORD_PATT = '// *(\\w+)' +FUNC_NAME_PATT = 'func(?: \\([^)]+\\))? (\\S+)\\(' def extract_doc_comments(text): for match in re.finditer(DOC_COMMENT_PATT, text, re.MULTILINE): diff --git a/scripts/leadershipclaimer/count-leadership.py b/scripts/leadershipclaimer/count-leadership.py index ddc66e425f0..c872213815b 100644 --- a/scripts/leadershipclaimer/count-leadership.py +++ b/scripts/leadershipclaimer/count-leadership.py @@ -13,9 +13,9 @@ def main(args): p.add_argument("--tick", type=float, default=1.0, help="seconds between printing status ticks") opts = p.parse_args(args) - actionsRE = re.compile("\s*(?P