From 1df6a7d73c0c794a13502bbdc3d0069c06165dc6 Mon Sep 17 00:00:00 2001 From: Yang Kelvin Date: Fri, 7 Jun 2024 19:14:39 +1000 Subject: [PATCH 1/2] docs: fix SeeAlso and Example docstrings for juju commands; --- cmd/juju/action/cancel.go | 22 ++++++++-- cmd/juju/action/exec.go | 22 ++++++++-- cmd/juju/action/showtask.go | 1 + cmd/juju/agree/agree/agree.go | 3 ++ .../agree/listagreements/listagreements.go | 18 ++++---- cmd/juju/application/bind.go | 5 +++ cmd/juju/application/config.go | 27 ++++++++++-- cmd/juju/application/expose.go | 27 ++++++++++-- cmd/juju/application/refresh.go | 24 +++++++++-- cmd/juju/application/removeapplication.go | 4 ++ cmd/juju/application/resolved.go | 18 ++++++-- cmd/juju/application/scaleapplication.go | 5 +++ cmd/juju/application/showunit.go | 18 ++++++++ cmd/juju/application/unexpose.go | 37 +++++++++------- cmd/juju/backups/download.go | 16 +++++-- cmd/juju/commands/bootstrap.go | 7 +++- cmd/juju/commands/helptool.go | 1 + cmd/juju/commands/helptool_test.go | 3 ++ cmd/juju/commands/list_sshkeys.go | 4 ++ cmd/juju/commands/version.go | 9 ++-- cmd/juju/controller/addmodel.go | 6 +++ cmd/juju/machine/show.go | 3 ++ cmd/juju/metricsdebug/collectmetrics.go | 16 +++++-- cmd/juju/metricsdebug/metrics.go | 1 + cmd/juju/model/grantrevoke.go | 1 + cmd/juju/model/grantrevokecloud.go | 1 + cmd/juju/model/retryprovisioning.go | 16 +++++-- cmd/juju/model/show.go | 3 ++ cmd/juju/resource/list.go | 15 +++++++ cmd/juju/resource/upload.go | 7 ++++ cmd/juju/secrets/list.go | 6 +++ cmd/juju/secrets/show.go | 5 +++ cmd/juju/space/reload.go | 17 ++++++-- cmd/juju/ssh/debugcode.go | 4 ++ cmd/juju/ssh/debughooks.go | 4 ++ cmd/juju/status/history.go | 42 +++++++++++++++++-- cmd/juju/storage/detach.go | 4 ++ cmd/juju/storage/import.go | 1 + cmd/juju/storage/list.go | 34 +++++++++++---- cmd/juju/storage/poollist.go | 27 ++++++++++-- cmd/juju/storage/remove.go | 8 ++++ cmd/juju/storage/show.go | 19 +++++++-- cmd/juju/subnet/list.go | 25 ++++++++--- cmd/juju/waitfor/waitfor.go | 15 +++---- 44 files changed, 453 insertions(+), 98 deletions(-) diff --git a/cmd/juju/action/cancel.go b/cmd/juju/action/cancel.go index 7a81432702f..0436ffd9c10 100644 --- a/cmd/juju/action/cancel.go +++ b/cmd/juju/action/cancel.go @@ -37,12 +37,26 @@ func (c *cancelCommand) SetFlags(f *gnuflag.FlagSet) { const cancelDoc = ` Cancel pending or running tasks matching given IDs or partial ID prefixes.` +const cancelExamples = ` +To cancel a task by ID: + + juju cancel-task 1 + +To cancel multiple tasks by ID: + + juju cancel-task 1 2 3 +` + func (c *cancelCommand) Info() *cmd.Info { info := &cmd.Info{ - Name: "cancel-task", - Args: "(|) [...]", - Purpose: "Cancel pending or running tasks.", - Doc: cancelDoc, + Name: "cancel-task", + Args: "(|) [...]", + Purpose: "Cancel pending or running tasks.", + Doc: cancelDoc, + Examples: cancelExamples, + SeeAlso: []string{ + "show-task", + }, } return jujucmd.Info(info) } diff --git a/cmd/juju/action/exec.go b/cmd/juju/action/exec.go index 5d0374b5425..20ee473295a 100644 --- a/cmd/juju/action/exec.go +++ b/cmd/juju/action/exec.go @@ -123,13 +123,27 @@ those arguments. For example: ` +const example = ` + + juju exec --all -- hostname -f + + juju exec --unit hello/0 env + + juju exec --unit controller/0 juju-engine-report +` + // Info implements Command.Info. func (c *execCommand) Info() *cmd.Info { info := jujucmd.Info(&cmd.Info{ - Name: "exec", - Args: "", - Purpose: "Run the commands on the remote targets specified.", - Doc: execDoc, + Name: "exec", + Args: "", + Purpose: "Run the commands on the remote targets specified.", + Doc: execDoc, + Examples: example, + SeeAlso: []string{ + "run", + "ssh", + }, }) return info } diff --git a/cmd/juju/action/showtask.go b/cmd/juju/action/showtask.go index cea72246758..583fd897bd9 100644 --- a/cmd/juju/action/showtask.go +++ b/cmd/juju/action/showtask.go @@ -88,6 +88,7 @@ func (c *showTaskCommand) Info() *cmd.Info { Doc: showTaskDoc, Examples: showTaskExamples, SeeAlso: []string{ + "cancel-task", "run", "operations", "show-operation", diff --git a/cmd/juju/agree/agree/agree.go b/cmd/juju/agree/agree/agree.go index e5471008443..b0fcaddc6a1 100644 --- a/cmd/juju/agree/agree/agree.go +++ b/cmd/juju/agree/agree/agree.go @@ -86,6 +86,9 @@ func (c *agreeCommand) Info() *cmd.Info { Purpose: "Agree to terms.", Doc: agreeDoc, Examples: agreeExamples, + SeeAlso: []string{ + "agreements", + }, }) } diff --git a/cmd/juju/agree/listagreements/listagreements.go b/cmd/juju/agree/listagreements/listagreements.go index a133e01cb78..55df4e8e2ad 100644 --- a/cmd/juju/agree/listagreements/listagreements.go +++ b/cmd/juju/agree/listagreements/listagreements.go @@ -39,10 +39,10 @@ In other words, some applications may only be installed if a user agrees to accept some terms defined by the charm. This command lists the terms that the user has agreed to. +` -See also: - agree - +const listAgreementsExamples = ` + juju agreements ` // NewListAgreementsCommand returns a new command that can be @@ -73,10 +73,14 @@ func (c *listAgreementsCommand) SetFlags(f *gnuflag.FlagSet) { // Info implements Command.Info. func (c *listAgreementsCommand) Info() *cmd.Info { return jujucmd.Info(&cmd.Info{ - Name: "agreements", - Purpose: "List user's agreements.", - Doc: listAgreementsDoc, - Aliases: []string{"list-agreements"}, + Name: "agreements", + Purpose: "List user's agreements.", + Doc: listAgreementsDoc, + Aliases: []string{"list-agreements"}, + Examples: listAgreementsExamples, + SeeAlso: []string{ + "agree", + }, }) } diff --git a/cmd/juju/application/bind.go b/cmd/juju/application/bind.go index 02fc96ca7b7..b070135ab9f 100644 --- a/cmd/juju/application/bind.go +++ b/cmd/juju/application/bind.go @@ -84,6 +84,11 @@ func (c *bindCommand) Info() *cmd.Info { Purpose: "Change bindings for a deployed application.", Doc: bindCmdDoc, Examples: bindCmdExamples, + SeeAlso: []string{ + "spaces", + "show-space", + "show-application", + }, }) } diff --git a/cmd/juju/application/config.go b/cmd/juju/application/config.go index d0419c5c52c..20d981a4e27 100644 --- a/cmd/juju/application/config.go +++ b/cmd/juju/application/config.go @@ -94,6 +94,24 @@ settings back to their default value as defined in the charm metadata: juju config apache2 --reset servername juju config apache2 --reset servername,lb_balancer_timeout +` + + examples = ` +To view all configuration values for an application, run + + juju config mysql --format json + +To set a configuration value for an application, run + + juju config mysql foo=bar + +To set some keys and reset others: + + juju config mysql key1=val1 key2=val2 --reset key3,key4 + +To set a configuration value for an application from a file: + + juju config mysql --file=path/to/cfg.yaml ` ) @@ -130,10 +148,11 @@ type ApplicationAPI interface { // Info is part of the cmd.Command interface. func (c *configCommand) Info() *cmd.Info { return jujucmd.Info(&cmd.Info{ - Name: "config", - Args: " [--branch ] [--reset ] [][=] ...]", - Purpose: configSummary, - Doc: configDetails, + Name: "config", + Args: " [--branch ] [--reset ] [][=] ...]", + Purpose: configSummary, + Doc: configDetails, + Examples: examples, SeeAlso: []string{ "deploy", "status", diff --git a/cmd/juju/application/expose.go b/cmd/juju/application/expose.go index 5dfeea8bb38..0e310161f28 100644 --- a/cmd/juju/application/expose.go +++ b/cmd/juju/application/expose.go @@ -66,6 +66,24 @@ juju expose apache2 --endpoints logs --to-cidrs 192.168.0.0/24 `[1:] +const example = ` +To expose an application: + + juju expose apache2 + +To expose an application to one or multiple spaces: + + juju expose apache2 --to-spaces public + +To expose an application to one or multiple endpoints: + + juju expose apache2 --endpoints logs + +To expose an application to one or multiple CIDRs: + + juju expose apache2 --to-cidrs 10.0.0.0/24 +` + // NewExposeCommand returns a command to expose applications. func NewExposeCommand() modelcmd.ModelCommand { return modelcmd.Wrap(&exposeCommand{}) @@ -82,10 +100,11 @@ type exposeCommand struct { func (c *exposeCommand) Info() *cmd.Info { return jujucmd.Info(&cmd.Info{ - Name: "expose", - Args: "", - Purpose: usageExposeSummary, - Doc: usageExposeDetails, + Name: "expose", + Args: "", + Purpose: usageExposeSummary, + Doc: usageExposeDetails, + Examples: example, SeeAlso: []string{ "unexpose", }, diff --git a/cmd/juju/application/refresh.go b/cmd/juju/application/refresh.go index e72d83dd04f..4fafbc183af 100644 --- a/cmd/juju/application/refresh.go +++ b/cmd/juju/application/refresh.go @@ -237,6 +237,20 @@ application; overriding profiles on the container may cause unexpected behavior. ` +const refreshExamples = ` +To refresh the storage constraints for the application foo: + + juju refresh foo --storage cache=ssd,10G + +To refresh the application config from a file for application foo: + + juju refresh foo --config config.yaml + +To refresh the resources for application foo: + + juju refresh foo --resource bar=/some/file.tgz --resource baz=./docs/cfg.xml +` + const upgradedApplicationHasUnitsMessage = ` Upgrading from an older PodSpec style charm to a newer Sidecar charm requires that the application be scaled down to 0 units. @@ -249,10 +263,12 @@ all those units to disappear before continuing. func (c *refreshCommand) Info() *cmd.Info { return jujucmd.Info(&cmd.Info{ - Name: "refresh", - Args: "", - Purpose: "Refresh an application's charm.", - Doc: refreshDoc, + Name: "refresh", + Args: "", + Purpose: "Refresh an application's charm.", + Doc: refreshDoc, + SeeAlso: []string{"deploy"}, + Examples: refreshExamples, }) } diff --git a/cmd/juju/application/removeapplication.go b/cmd/juju/application/removeapplication.go index 30abe341a6b..069636dba7c 100644 --- a/cmd/juju/application/removeapplication.go +++ b/cmd/juju/application/removeapplication.go @@ -93,6 +93,10 @@ func (c *removeApplicationCommand) Info() *cmd.Info { Purpose: helpSummaryRmApp, Doc: helpDetailsRmApp, Examples: helpExamplesRmApp, + SeeAlso: []string{ + "scale-application", + "show-application", + }, }) } diff --git a/cmd/juju/application/resolved.go b/cmd/juju/application/resolved.go index 69cca1cd66e..fd834ddcdf5 100644 --- a/cmd/juju/application/resolved.go +++ b/cmd/juju/application/resolved.go @@ -29,12 +29,22 @@ type resolvedCommand struct { All bool } +const resolvedCommandExamples = ` + + juju resolved mysql/0 + + juju resolved mysql/0 mysql/1 + + juju resolved --all +` + func (c *resolvedCommand) Info() *cmd.Info { return jujucmd.Info(&cmd.Info{ - Name: "resolved", - Args: "[ ...]", - Purpose: "Marks unit errors resolved and re-executes failed hooks.", - Aliases: []string{"resolve"}, + Name: "resolved", + Args: "[ ...]", + Purpose: "Marks unit errors resolved and re-executes failed hooks.", + Aliases: []string{"resolve"}, + Examples: resolvedCommandExamples, }) } diff --git a/cmd/juju/application/scaleapplication.go b/cmd/juju/application/scaleapplication.go index e7fd3a80fbc..5dcbefe13bb 100644 --- a/cmd/juju/application/scaleapplication.go +++ b/cmd/juju/application/scaleapplication.go @@ -58,6 +58,11 @@ func (c *scaleApplicationCommand) Info() *cmd.Info { Purpose: "Set the desired number of k8s application units.", Doc: scaleApplicationDoc, Examples: scaleApplicationExamples, + SeeAlso: []string{ + "remove-application", + "add-unit", + "remove-unit", + }, }) } diff --git a/cmd/juju/application/showunit.go b/cmd/juju/application/showunit.go index ef37b094dd4..fd496c0a996 100644 --- a/cmd/juju/application/showunit.go +++ b/cmd/juju/application/showunit.go @@ -25,10 +25,24 @@ or related unit may be shown, or just the application data. ` const showUnitExamples = ` +To show information about a unit: + juju show-unit mysql/0 + +To show information about multiple units: + juju show-unit mysql/0 wordpress/1 + +To show only the application relation data for a unit: + juju show-unit mysql/0 --app + +To show only the relation data for a specific endpoint: + juju show-unit mysql/0 --endpoint db + +To show only the relation data for a specific related unit: + juju show-unit mysql/0 --related-unit wordpress/2 ` @@ -61,6 +75,10 @@ func (c *showUnitCommand) Info() *cmd.Info { Purpose: "Displays information about a unit.", Doc: showUnitDoc, Examples: showUnitExamples, + SeeAlso: []string{ + "add-unit", + "remove-unit", + }, } return jujucmd.Info(showCmd) } diff --git a/cmd/juju/application/unexpose.go b/cmd/juju/application/unexpose.go index 388a17c0f89..5a27794d32d 100644 --- a/cmd/juju/application/unexpose.go +++ b/cmd/juju/application/unexpose.go @@ -26,24 +26,15 @@ the "juju expose" command, they can be unexposed by running the "juju unexpose" command. If no additional options are specified, the command will unexpose the -application (if exposed). For example, to unexpose the apache2 application, -you can run: - -juju unexpose apache2 +application (if exposed). The --endpoints option may be used to restrict the effect of this command to -the list of ports opened for a comma-delimited list of endpoints. For instance, -to only unexpose the ports opened by apache2 for the "www" endpoint, you can -run: - -juju unexpose apache2 --endpoints www +the list of ports opened for a comma-delimited list of endpoints. Note that when the --endpoints option is provided, the application will still remain exposed if any other of its endpoints are still exposed. However, if none of its endpoints remain exposed, the application will be instead unexposed. - -See also: - expose`[1:] +`[1:] // NewUnexposeCommand returns a command to unexpose applications. func NewUnexposeCommand() modelcmd.ModelCommand { @@ -57,12 +48,26 @@ type unexposeCommand struct { ExposedEndpointsList string } +const unexposeCommandExample = ` + juju unexpose apache2 + +To unexpose only the ports that charms have opened for the "www", or "www" and "logs" endpoints: + + juju unexpose apache2 --endpoints www + + juju unexpose apache2 --endpoints www,logs +` + func (c *unexposeCommand) Info() *cmd.Info { return jujucmd.Info(&cmd.Info{ - Name: "unexpose", - Args: "", - Purpose: usageUnexposeSummary, - Doc: usageUnexposeDetails, + Name: "unexpose", + Args: "", + Purpose: usageUnexposeSummary, + Doc: usageUnexposeDetails, + Examples: unexposeCommandExample, + SeeAlso: []string{ + "expose", + }, }) } diff --git a/cmd/juju/backups/download.go b/cmd/juju/backups/download.go index 8c28f6916b6..9833672190a 100644 --- a/cmd/juju/backups/download.go +++ b/cmd/juju/backups/download.go @@ -23,6 +23,10 @@ If --filename is not used, the archive is downloaded to a temporary location and the filename is printed to stdout. ` +const examples = ` + juju download-backup /full/path/to/backup/on/controller +` + // NewDownloadCommand returns a commant used to download backups. func NewDownloadCommand() cmd.Command { return modelcmd.Wrap(&downloadCommand{}) @@ -40,10 +44,14 @@ type downloadCommand struct { // Info implements Command.Info. func (c *downloadCommand) Info() *cmd.Info { return jujucmd.Info(&cmd.Info{ - Name: "download-backup", - Args: "/full/path/to/backup/on/controller", - Purpose: "Download a backup archive file.", - Doc: downloadDoc, + Name: "download-backup", + Args: "/full/path/to/backup/on/controller", + Purpose: "Download a backup archive file.", + Doc: downloadDoc, + Examples: examples, + SeeAlso: []string{ + "create-backup", + }, }) } diff --git a/cmd/juju/commands/bootstrap.go b/cmd/juju/commands/bootstrap.go index e5077a209cc..f175050baf5 100644 --- a/cmd/juju/commands/bootstrap.go +++ b/cmd/juju/commands/bootstrap.go @@ -150,18 +150,21 @@ GKE or EKS. When bootstrapping to a k8s cluster Juju does not recognise, there's no guarantee a load balancer is available, so Juju defaults to a controller -service type of ClusterIP. This may not be suitable, so there's 3 bootstrap +service type of ClusterIP. This may not be suitable, so there are three bootstrap options available to tell Juju how to set up the controller service. Part of the solution may require a load balancer for the cluster to be set up manually first, or perhaps an external k8s service via a FQDN will be used (this is a cluster specific implementation decision which Juju needs to be -informed about so it can set things up correctly). The 3 relevant bootstrap +informed about so it can set things up correctly). The three relevant bootstrap options are (see list of bootstrap config items below for a full explanation): - controller-service-type - controller-external-name - controller-external-ips +Juju advertises those addresses to other controllers, so they must be resolveable from +other controllers for cross-model (cross-controller, actually) relations to work. + If a storage pool is specified using --storage-pool, this will be created in the controller model. ` diff --git a/cmd/juju/commands/helptool.go b/cmd/juju/commands/helptool.go index 116189e7e11..0468363d152 100644 --- a/cmd/juju/commands/helptool.go +++ b/cmd/juju/commands/helptool.go @@ -117,6 +117,7 @@ func (t *helpToolCommand) Info() *cmd.Info { Purpose: "Show help on a Juju charm hook tool.", Doc: helpToolDoc, Examples: helpToolExamples, + SeeAlso: []string{"help"}, }) } diff --git a/cmd/juju/commands/helptool_test.go b/cmd/juju/commands/helptool_test.go index 199838e24fd..2e059ad1018 100644 --- a/cmd/juju/commands/helptool_test.go +++ b/cmd/juju/commands/helptool_test.go @@ -103,6 +103,9 @@ Examples: For help on a specific tool, supply the name of that tool, for example: juju help-tool unit-get + +See also: + - help `) } diff --git a/cmd/juju/commands/list_sshkeys.go b/cmd/juju/commands/list_sshkeys.go index 8d2b2222dc3..6c349066ae5 100644 --- a/cmd/juju/commands/list_sshkeys.go +++ b/cmd/juju/commands/list_sshkeys.go @@ -58,6 +58,10 @@ func (c *listKeysCommand) Info() *cmd.Info { Doc: usageListSSHKeysDetails, Aliases: []string{"list-ssh-keys"}, Examples: usageListSSHKeysExamples, + SeeAlso: []string{ + "add-ssh-key", + "remove-ssh-key", + }, }) } diff --git a/cmd/juju/commands/version.go b/cmd/juju/commands/version.go index 77e43a4e91e..2a877e91d5a 100644 --- a/cmd/juju/commands/version.go +++ b/cmd/juju/commands/version.go @@ -17,11 +17,12 @@ const versionDoc = ` Print only the Juju CLI client version.` const versionExamplesDoc = ` -To see the version of Juju running on a particular controller, use - juju show-controller + juju version -To see the version of Juju running on a particular model, use - juju show-model` +Print all version information: + + juju version --all +` // versionDetail is populated with version information from juju/juju/cmd // and passed into each SuperCommand. It can be printed using `juju version --all`. diff --git a/cmd/juju/controller/addmodel.go b/cmd/juju/controller/addmodel.go index 4f174e11e5d..6da34570af5 100644 --- a/cmd/juju/controller/addmodel.go +++ b/cmd/juju/controller/addmodel.go @@ -116,6 +116,12 @@ func (c *addModelCommand) Info() *cmd.Info { Purpose: "Adds a workload model.", Doc: strings.TrimSpace(addModelHelpDoc), Examples: addModelHelpExamples, + SeeAlso: []string{ + "model-config", + "model-defaults", + "add-credential", + "autoload-credentials", + }, }) } diff --git a/cmd/juju/machine/show.go b/cmd/juju/machine/show.go index 6326595f932..5afe9594be2 100644 --- a/cmd/juju/machine/show.go +++ b/cmd/juju/machine/show.go @@ -46,6 +46,9 @@ func (c *showMachineCommand) Info() *cmd.Info { Purpose: "Show a machine's status.", Doc: showMachineCommandDoc, Examples: showMachineExamples, + SeeAlso: []string{ + "add-machine", + }, }) } diff --git a/cmd/juju/metricsdebug/collectmetrics.go b/cmd/juju/metricsdebug/collectmetrics.go index 93525bd6e7a..adb8ea212ef 100644 --- a/cmd/juju/metricsdebug/collectmetrics.go +++ b/cmd/juju/metricsdebug/collectmetrics.go @@ -32,6 +32,12 @@ You may abort this command and it will continue to run asynchronously. Results may be checked by 'juju show-task'. ` +const examples = ` + juju collect-metrics myapp + + juju collect-metrics myapp/0 +` + const ( // commandTimeout represents the timeout for executing the command itself commandTimeout = 3 * time.Second @@ -55,10 +61,12 @@ func NewCollectMetricsCommand() cmd.Command { // Info implements Command.Info. func (c *collectMetricsCommand) Info() *cmd.Info { return jujucmd.Info(&cmd.Info{ - Name: "collect-metrics", - Args: "[application or unit]", - Purpose: "Collect metrics on the given unit/application.", - Doc: collectMetricsDoc, + Name: "collect-metrics", + Args: "[application or unit]", + Purpose: "Collect metrics on the given unit/application.", + Doc: collectMetricsDoc, + Examples: examples, + SeeAlso: []string{"metrics"}, }) } diff --git a/cmd/juju/metricsdebug/metrics.go b/cmd/juju/metricsdebug/metrics.go index 66aca1ca286..3a36b86ec7b 100644 --- a/cmd/juju/metricsdebug/metrics.go +++ b/cmd/juju/metricsdebug/metrics.go @@ -47,6 +47,7 @@ func (c *MetricsCommand) Info() *cmd.Info { Args: "[tag1[...tagN]]", Purpose: "Retrieve metrics collected by specified entities.", Doc: metricsDoc, + SeeAlso: []string{"collect-metrics"}, }) } diff --git a/cmd/juju/model/grantrevoke.go b/cmd/juju/model/grantrevoke.go index 4fc85f7c63c..8ae410596b2 100644 --- a/cmd/juju/model/grantrevoke.go +++ b/cmd/juju/model/grantrevoke.go @@ -196,6 +196,7 @@ func (c *grantCommand) Info() *cmd.Info { SeeAlso: []string{ "revoke", "add-user", + "grant-cloud", }, }) } diff --git a/cmd/juju/model/grantrevokecloud.go b/cmd/juju/model/grantrevokecloud.go index 82944057e4a..6dfd1a4ed41 100644 --- a/cmd/juju/model/grantrevokecloud.go +++ b/cmd/juju/model/grantrevokecloud.go @@ -112,6 +112,7 @@ func (c *grantCloudCommand) Info() *cmd.Info { Doc: usageGrantCloudDetails, Examples: usageGrantCloudExamples, SeeAlso: []string{ + "grant", "revoke-cloud", "add-user", }, diff --git a/cmd/juju/model/retryprovisioning.go b/cmd/juju/model/retryprovisioning.go index b3c14971a76..5428bb0a15a 100644 --- a/cmd/juju/model/retryprovisioning.go +++ b/cmd/juju/model/retryprovisioning.go @@ -40,11 +40,21 @@ type RetryProvisioningAPI interface { RetryProvisioning(all bool, machines ...names.MachineTag) ([]params.ErrorResult, error) } +const retryProvisioningCommandExamples = ` + + juju retry-provisioning 0 + + juju retry-provisioning 0 1 + + juju retry-provisioning --all +` + func (c *retryProvisioningCommand) Info() *cmd.Info { return jujucmd.Info(&cmd.Info{ - Name: "retry-provisioning", - Args: " [...]", - Purpose: "Retries provisioning for failed machines.", + Name: "retry-provisioning", + Args: " [...]", + Purpose: "Retries provisioning for failed machines.", + Examples: retryProvisioningCommandExamples, }) } diff --git a/cmd/juju/model/show.go b/cmd/juju/model/show.go index f48420692b3..166281ffb06 100644 --- a/cmd/juju/model/show.go +++ b/cmd/juju/model/show.go @@ -61,6 +61,9 @@ func (c *showModelCommand) Info() *cmd.Info { Args: "", Purpose: "Shows information about the current or specified model.", Doc: showModelCommandDoc, + SeeAlso: []string{ + "add-model", + }, }) } diff --git a/cmd/juju/resource/list.go b/cmd/juju/resource/list.go index fd0dfafbf83..1ad8e2c69ab 100644 --- a/cmd/juju/resource/list.go +++ b/cmd/juju/resource/list.go @@ -50,6 +50,20 @@ func NewListCommand() modelcmd.ModelCommand { return modelcmd.Wrap(c) } +const listResourcesExamples = ` +To list resources for an application: + + juju resources mysql + +To list resources for a unit: + + juju resources mysql/0 + +To show detailed information about resources used by a unit: + + juju resources mysql/0 --details +` + // Info implements cmd.Command.Info. func (c *ListCommand) Info() *cmd.Info { return jujucmd.Info(&cmd.Info{ @@ -66,6 +80,7 @@ This command shows the resources required by and those in use by an existing application or unit in your model. When run for an application, it will also show any updates available for resources from a store. `, + Examples: listResourcesExamples, }) } diff --git a/cmd/juju/resource/upload.go b/cmd/juju/resource/upload.go index be576ae137b..c75d570f92f 100644 --- a/cmd/juju/resource/upload.go +++ b/cmd/juju/resource/upload.go @@ -86,6 +86,13 @@ Note: If you choose (2b): You will need to specify: (i) the local path to the private OCI image as well as (ii) the username/password required to access the private OCI image. +` + attachExample = ` + juju attach-resource mysql resource-name=foo + + juju attach-resource ubuntu-k8s ubuntu_image=ubuntu + + juju attach-resource redis-k8s redis-image=redis ` ) diff --git a/cmd/juju/secrets/list.go b/cmd/juju/secrets/list.go index ddaaae39281..cc79dfc2f8b 100644 --- a/cmd/juju/secrets/list.go +++ b/cmd/juju/secrets/list.go @@ -70,6 +70,12 @@ func (c *listSecretsCommand) Info() *cmd.Info { Doc: listSecretsDoc, Examples: listSecretsExamples, Aliases: []string{"list-secrets"}, + SeeAlso: []string{ + "add-secret", + "remove-secret", + "show-secret", + "update-secret", + }, }) } diff --git a/cmd/juju/secrets/show.go b/cmd/juju/secrets/show.go index 6e28c7d01f0..cf6eb345359 100644 --- a/cmd/juju/secrets/show.go +++ b/cmd/juju/secrets/show.go @@ -70,6 +70,11 @@ func (c *showSecretsCommand) Info() *cmd.Info { Purpose: "Shows details for a specific secret.", Doc: showSecretsDoc, Examples: showSecretsExamples, + SeeAlso: []string{ + "add-secret", + "update-secret", + "remove-secret", + }, }) } diff --git a/cmd/juju/space/reload.go b/cmd/juju/space/reload.go index cb70419c1bf..0c95885e438 100644 --- a/cmd/juju/space/reload.go +++ b/cmd/juju/space/reload.go @@ -28,12 +28,23 @@ const ReloadCommandDoc = ` Reloades spaces and subnets from substrate. ` +const ReloadCommandExamples = ` + juju reload-spaces +` + // Info is defined on the cmd.Command interface. func (c *ReloadCommand) Info() *cmd.Info { return jujucmd.Info(&cmd.Info{ - Name: "reload-spaces", - Purpose: "Reloads spaces and subnets from substrate.", - Doc: strings.TrimSpace(ReloadCommandDoc), + Name: "reload-spaces", + Purpose: "Reloads spaces and subnets from substrate.", + Doc: strings.TrimSpace(ReloadCommandDoc), + Examples: ReloadCommandExamples, + SeeAlso: []string{ + "spaces", + "add-space", + "show-space", + "move-to-space", + }, }) } diff --git a/cmd/juju/ssh/debugcode.go b/cmd/juju/ssh/debugcode.go index fc2cd64be4a..72731884fc8 100644 --- a/cmd/juju/ssh/debugcode.go +++ b/cmd/juju/ssh/debugcode.go @@ -79,6 +79,10 @@ func (c *debugCodeCommand) Info() *cmd.Info { Purpose: "Launch a tmux session to debug hooks and/or actions.", Doc: debugCodeDoc, Examples: usageDebugCodeExamples, + SeeAlso: []string{ + "ssh", + "debug-hooks", + }, }) } diff --git a/cmd/juju/ssh/debughooks.go b/cmd/juju/ssh/debughooks.go index c49a41983db..6ebf8f7223c 100644 --- a/cmd/juju/ssh/debughooks.go +++ b/cmd/juju/ssh/debughooks.go @@ -93,6 +93,10 @@ func (c *debugHooksCommand) Info() *cmd.Info { Doc: debugHooksDoc, Examples: usageDebugHooksExamples, Aliases: []string{"debug-hook"}, + SeeAlso: []string{ + "ssh", + "debug-code", + }, }) } diff --git a/cmd/juju/status/history.go b/cmd/juju/status/history.go index 93fdf2f9318..647b41c995c 100644 --- a/cmd/juju/status/history.go +++ b/cmd/juju/status/history.go @@ -62,12 +62,46 @@ The statuses are available for the following types. The default is unit. `, supportedHistoryKindDescs()) +const statusHistoryExamples = ` +Show the status history for the specified unit: + + juju show-status-log mysql/0 + +Show the status history for the specified unit with the last 30 logs: + + juju show-status-log mysql/0 -n 30 + +Show the status history for the specified unit with the logs for the past 2 days: + + juju show-status-log mysql/0 -days 2 + +Show the status history for the specified unit with the logs for any date after 2020-01-01: + + juju show-status-log mysql/0 --from-date 2020-01-01 + +Show the status history for the specified application: + + juju show-status-log -type application wordpress + +Show the status history for the specified machine: + + juju show-status-log 0 + +Show the status history for the model: + + juju show-status-log -type model +` + func (c *statusHistoryCommand) Info() *cmd.Info { return jujucmd.Info(&cmd.Info{ - Name: "show-status-log", - Args: "", - Purpose: "Output past statuses for the specified entity.", - Doc: statusHistoryDoc, + Name: "show-status-log", + Args: "", + Purpose: "Output past statuses for the specified entity.", + Doc: statusHistoryDoc, + Examples: statusHistoryExamples, + SeeAlso: []string{ + "status", + }, }) } diff --git a/cmd/juju/storage/detach.go b/cmd/juju/storage/detach.go index e94d3b29994..45d838c834e 100644 --- a/cmd/juju/storage/detach.go +++ b/cmd/juju/storage/detach.go @@ -89,6 +89,10 @@ func (c *detachStorageCommand) Info() *cmd.Info { Doc: detachStorageCommandDoc, Examples: detachStorageCommandExamples, Args: detachStorageCommandArgs, + SeeAlso: []string{ + "storage", + "attach-storage", + }, }) } diff --git a/cmd/juju/storage/import.go b/cmd/juju/storage/import.go index 2551f97107b..83e21540577 100644 --- a/cmd/juju/storage/import.go +++ b/cmd/juju/storage/import.go @@ -123,6 +123,7 @@ func (c *importFilesystemCommand) Info() *cmd.Info { Doc: importFilesystemCommandDoc, Args: importFilesystemCommandAgs, Examples: importFilesystemCommandExamples, + SeeAlso: []string{"storage"}, }) } diff --git a/cmd/juju/storage/list.go b/cmd/juju/storage/list.go index c2c9dc394b3..56651fd3894 100644 --- a/cmd/juju/storage/list.go +++ b/cmd/juju/storage/list.go @@ -29,6 +29,20 @@ const listCommandDoc = ` List information about storage. ` +const listCommandExample = ` +List all storage: + + juju storage + +List only filesystem storage: + + juju storage --filesystem + +List only volume storage: + + juju storage --volume +` + // listCommand returns storage instances. type listCommand struct { StorageCommandBase @@ -42,11 +56,17 @@ type listCommand struct { // Info implements Command.Info. func (c *listCommand) Info() *cmd.Info { return jujucmd.Info(&cmd.Info{ - Name: "storage", - Args: " ...", - Purpose: "Lists storage details.", - Doc: listCommandDoc, - Aliases: []string{"list-storage"}, + Name: "storage", + Args: " ...", + Purpose: "Lists storage details.", + Doc: listCommandDoc, + Aliases: []string{"list-storage"}, + Examples: listCommandExample, + SeeAlso: []string{ + "show-storage", + "add-storage", + "remove-storage", + }, }) } @@ -60,8 +80,8 @@ func (c *listCommand) SetFlags(f *gnuflag.FlagSet) { }) // TODO(axw) deprecate these flags, and introduce separate commands // for listing just filesystems or volumes. - f.BoolVar(&c.filesystem, "filesystem", false, "List filesystem storage") - f.BoolVar(&c.volume, "volume", false, "List volume storage") + f.BoolVar(&c.filesystem, "filesystem", false, "List filesystem storage(deprecated)") + f.BoolVar(&c.volume, "volume", false, "List volume storage(deprecated)") } // Init implements Command.Init. diff --git a/cmd/juju/storage/poollist.go b/cmd/juju/storage/poollist.go index 85aba603507..d7de0aef521 100644 --- a/cmd/juju/storage/poollist.go +++ b/cmd/juju/storage/poollist.go @@ -47,6 +47,20 @@ Both pool types and names must be valid. Valid pool types are pool types that are registered for Juju model. ` +const poolListCommandExample = ` +List all storage pools: + + juju storage-pools + +List only pools of type kubernetes, azure, ebs: + + juju storage-pools --provider kubernetes,azure,ebs + +List only pools named pool1 and pool2: + + juju storage-pools --name pool1,pool2 +` + // NewPoolListCommand returns a command that lists storage pools on a model func NewPoolListCommand() cmd.Command { cmd := &poolListCommand{} @@ -73,10 +87,15 @@ func (c *poolListCommand) Init(args []string) (err error) { // Info implements Command.Info. func (c *poolListCommand) Info() *cmd.Info { return jujucmd.Info(&cmd.Info{ - Name: "storage-pools", - Purpose: "List storage pools.", - Doc: poolListCommandDoc, - Aliases: []string{"list-storage-pools"}, + Name: "storage-pools", + Purpose: "List storage pools.", + Doc: poolListCommandDoc, + Aliases: []string{"list-storage-pools"}, + Examples: poolListCommandExample, + SeeAlso: []string{ + "create-storage-pool", + "remove-storage-pool", + }, }) } diff --git a/cmd/juju/storage/remove.go b/cmd/juju/storage/remove.go index 0a47f5704d6..fa3f03770bb 100644 --- a/cmd/juju/storage/remove.go +++ b/cmd/juju/storage/remove.go @@ -75,6 +75,14 @@ func (c *removeStorageCommand) Info() *cmd.Info { Doc: removeStorageCommandDoc, Args: removeStorageCommandArgs, Examples: removeStorageCommandExamples, + SeeAlso: []string{ + "add-storage", + "attach-storage", + "detach-storage", + "list-storage", + "show-storage", + "storage", + }, }) } diff --git a/cmd/juju/storage/show.go b/cmd/juju/storage/show.go index f80114a3143..04da0dcbe6f 100644 --- a/cmd/juju/storage/show.go +++ b/cmd/juju/storage/show.go @@ -33,6 +33,10 @@ separated when more than one ID is desired. ` +const showCommandExample = ` + juju show-storage storage-id +` + // showCommand attempts to release storage instance. type showCommand struct { StorageCommandBase @@ -53,10 +57,17 @@ func (c *showCommand) Init(args []string) (err error) { // Info implements Command.Info. func (c *showCommand) Info() *cmd.Info { return jujucmd.Info(&cmd.Info{ - Name: "show-storage", - Args: " [...]", - Purpose: "Shows storage instance information.", - Doc: showCommandDoc, + Name: "show-storage", + Args: " [...]", + Purpose: "Shows storage instance information.", + Doc: showCommandDoc, + Examples: showCommandExample, + SeeAlso: []string{ + "storage", + "attach-storage", + "detach-storage", + "remove-storage", + }, }) } diff --git a/cmd/juju/subnet/list.go b/cmd/juju/subnet/list.go index e691ac00f32..a277986d8be 100644 --- a/cmd/juju/subnet/list.go +++ b/cmd/juju/subnet/list.go @@ -47,14 +47,29 @@ output formats include "yaml" (default) and "json". To redirect the output to a file, use --output. ` +const listCommandExample = ` +To list all subnets known to Juju: + + juju subnets + +To list subnets associated with a specific network space: + + juju subnets --space my-space + +To list subnets associated with a specific availability zone: + + juju subnets --zone my-zone +` + // Info is defined on the cmd.Command interface. func (c *ListCommand) Info() *cmd.Info { return jujucmd.Info(&cmd.Info{ - Name: "subnets", - Args: "[--space ] [--zone ] [--format yaml|json] [--output ]", - Purpose: "List subnets known to Juju.", - Doc: strings.TrimSpace(listCommandDoc), - Aliases: []string{"list-subnets"}, + Name: "subnets", + Args: "[--space ] [--zone ] [--format yaml|json] [--output ]", + Purpose: "List subnets known to Juju.", + Doc: strings.TrimSpace(listCommandDoc), + Aliases: []string{"list-subnets"}, + Examples: listCommandExample, }) } diff --git a/cmd/juju/waitfor/waitfor.go b/cmd/juju/waitfor/waitfor.go index dcd77d8d6e4..a7e5e15d74e 100644 --- a/cmd/juju/waitfor/waitfor.go +++ b/cmd/juju/waitfor/waitfor.go @@ -32,8 +32,14 @@ Built-in functions are provided to help define the goal state. The built-in functions are defined in the query package. Examples of built-in functions include len, print, forEach (lambda), startsWith and endsWith. -Examples: +See also: + wait-for model + wait-for application + wait-for machine + wait-for unit +` +const waitForExamples = ` Waits for the mysql/0 unit to be created and active. juju wait-for unit mysql/0 @@ -45,12 +51,6 @@ Waits for the mysql application to be active or idle. Waits for the model units to all start with ubuntu. juju wait-for model default --query='forEach(units, unit => startsWith(unit.name, "ubuntu"))' - -See also: - wait-for model - wait-for application - wait-for machine - wait-for unit ` // NewWaitForCommand creates the wait-for supercommand and registers the @@ -61,6 +61,7 @@ func NewWaitForCommand() cmd.Command { UsagePrefix: "juju", Doc: waitForDoc, Purpose: "Wait for an entity to reach a specified state.", + Examples: waitForExamples, }) waitFor.Register(newApplicationCommand()) From f4d5b4578c7de7077ad98ab6d0006adfc9c48c67 Mon Sep 17 00:00:00 2001 From: wallyworld Date: Thu, 18 Jul 2024 12:23:58 +1000 Subject: [PATCH 2/2] fix(upgrade): fix broken upgrade on k8s Upgrades on k8s were broken duue to the oci image fat manifest format not being supported by Juju. --- apiserver/facades/client/client/client.go | 12 +++- .../facades/client/client/client_test.go | 31 ++++++--- .../client/modelupgrader/findagents.go | 12 +++- .../client/modelupgrader/upgrader_test.go | 43 ++++++++---- docker/registry/internal/acr.go | 12 ++-- docker/registry/internal/acr_test.go | 21 +++++- docker/registry/internal/base_client.go | 8 +-- docker/registry/internal/base_manifests.go | 68 +++++++++++++------ .../registry/internal/base_manifests_test.go | 49 ++++++++----- docker/registry/internal/base_tags.go | 4 +- docker/registry/internal/ecr.go | 14 ++-- docker/registry/internal/ecr_test.go | 21 +++++- docker/registry/internal/interface.go | 2 +- docker/registry/internal/package_test.go | 19 +++--- docker/registry/mocks/registry_mock.go | 14 ++-- 15 files changed, 221 insertions(+), 109 deletions(-) diff --git a/apiserver/facades/client/client/client.go b/apiserver/facades/client/client/client.go index 1680e7220b8..329d841de09 100644 --- a/apiserver/facades/client/client/client.go +++ b/apiserver/facades/client/client/client.go @@ -16,6 +16,7 @@ import ( "github.com/juju/juju/apiserver/facade" "github.com/juju/juju/cloudconfig/podcfg" "github.com/juju/juju/controller" + "github.com/juju/juju/core/arch" "github.com/juju/juju/core/cache" "github.com/juju/juju/core/leadership" "github.com/juju/juju/core/multiwatcher" @@ -327,6 +328,11 @@ func (c *Client) toolVersionsForCAAS(args params.FindToolsParams, streamsVersion if err != nil { return result, errors.Trace(err) } + + wantArch := args.Arch + if wantArch == "" { + wantArch = arch.DefaultArchitecture + } for _, tag := range tags { number := tag.AgentVersion() if number.Compare(current) <= 0 { @@ -351,21 +357,21 @@ func (c *Client) toolVersionsForCAAS(args params.FindToolsParams, streamsVersion continue } } - arch, err := reg.GetArchitecture(imageName, number.String()) + arches, err := reg.GetArchitectures(imageName, number.String()) if errors.IsNotFound(err) { continue } if err != nil { return result, errors.Annotatef(err, "cannot get architecture for %s:%s", imageName, number.String()) } - if args.Arch != "" && arch != args.Arch { + if !set.NewStrings(arches...).Contains(wantArch) { continue } tools := tools.Tools{ Version: version.Binary{ Number: number, Release: coreos.HostOSTypeName(), - Arch: arch, + Arch: wantArch, }, } result.List = append(result.List, &tools) diff --git a/apiserver/facades/client/client/client_test.go b/apiserver/facades/client/client/client_test.go index 799e3b8ba33..25002b1d4fa 100644 --- a/apiserver/facades/client/client/client_test.go +++ b/apiserver/facades/client/client/client_test.go @@ -488,7 +488,15 @@ func (s *findToolsSuite) getModelConfig(c *gc.C, agentVersion string) *config.Co return mCfg } +func (s *findToolsSuite) TestFindToolsCAASReleasedDefault(c *gc.C) { + s.assertFindToolsCAASReleased(c, "", "amd64") +} + func (s *findToolsSuite) TestFindToolsCAASReleased(c *gc.C) { + s.assertFindToolsCAASReleased(c, "arm64", "arm64") +} + +func (s *findToolsSuite) assertFindToolsCAASReleased(c *gc.C, wantArch, expectArch string) { ctrl := gomock.NewController(c) defer ctrl.Finish() @@ -502,6 +510,9 @@ func (s *findToolsSuite) TestFindToolsCAASReleased(c *gc.C) { {Version: version.MustParseBinary("2.9.9-ubuntu-amd64")}, {Version: version.MustParseBinary("2.9.10-ubuntu-amd64")}, {Version: version.MustParseBinary("2.9.11-ubuntu-amd64")}, + {Version: version.MustParseBinary("2.9.9-ubuntu-arm64")}, + {Version: version.MustParseBinary("2.9.10-ubuntu-arm64")}, + {Version: version.MustParseBinary("2.9.11-ubuntu-arm64")}, } s.PatchValue(&coreos.HostOS, func() coreos.OSType { return coreos.Ubuntu }) @@ -513,7 +524,7 @@ func (s *findToolsSuite) TestFindToolsCAASReleased(c *gc.C) { authorizer.EXPECT().HasPermission(permission.WriteAccess, coretesting.ModelTag).Return(nil), backend.EXPECT().Model().Return(model, nil), - toolsFinder.EXPECT().FindAgents(common.FindAgentsParams{MajorVersion: 2}). + toolsFinder.EXPECT().FindAgents(common.FindAgentsParams{MajorVersion: 2, Arch: wantArch}). Return(simpleStreams, nil), model.EXPECT().Type().Return(state.ModelTypeCAAS), model.EXPECT().Config().Return(s.getModelConfig(c, "2.9.9"), nil), @@ -536,8 +547,8 @@ func (s *findToolsSuite) TestFindToolsCAASReleased(c *gc.C) { image.NewImageInfo(version.MustParse("2.9.11")), image.NewImageInfo(version.MustParse("2.9.12")), // skip: it's not released in simplestream yet. }, nil), - registryProvider.EXPECT().GetArchitecture("jujud-operator", "2.9.10").Return("amd64", nil), - registryProvider.EXPECT().GetArchitecture("jujud-operator", "2.9.11").Return("amd64", nil), + registryProvider.EXPECT().GetArchitectures("jujud-operator", "2.9.10").Return([]string{"amd64", "arm64"}, nil), + registryProvider.EXPECT().GetArchitectures("jujud-operator", "2.9.11").Return([]string{"amd64", "arm64"}, nil), registryProvider.EXPECT().Close().Return(nil), ) @@ -558,12 +569,12 @@ func (s *findToolsSuite) TestFindToolsCAASReleased(c *gc.C) { }, ) c.Assert(err, jc.ErrorIsNil) - result, err := api.FindTools(params.FindToolsParams{MajorVersion: 2}) + result, err := api.FindTools(params.FindToolsParams{MajorVersion: 2, Arch: wantArch}) c.Assert(err, jc.ErrorIsNil) c.Assert(result, gc.DeepEquals, params.FindToolsResult{ List: []*tools.Tools{ - {Version: version.MustParseBinary("2.9.10-ubuntu-amd64")}, - {Version: version.MustParseBinary("2.9.11-ubuntu-amd64")}, + {Version: version.MustParseBinary("2.9.10-ubuntu-" + expectArch)}, + {Version: version.MustParseBinary("2.9.11-ubuntu-" + expectArch)}, }, }) } @@ -618,10 +629,10 @@ func (s *findToolsSuite) TestFindToolsCAASNonReleased(c *gc.C) { image.NewImageInfo(version.MustParse("2.9.12")), image.NewImageInfo(version.MustParse("2.9.13")), // skip: it's not released in simplestream yet. }, nil), - registryProvider.EXPECT().GetArchitecture("jujud-operator", "2.9.10.1").Return("amd64", nil), - registryProvider.EXPECT().GetArchitecture("jujud-operator", "2.9.10").Return("amd64", nil), - registryProvider.EXPECT().GetArchitecture("jujud-operator", "2.9.11").Return("amd64", nil), - registryProvider.EXPECT().GetArchitecture("jujud-operator", "2.9.12").Return("", errors.NotFoundf("2.9.12")), // This can only happen on a non-official registry account. + registryProvider.EXPECT().GetArchitectures("jujud-operator", "2.9.10.1").Return([]string{"amd64", "arm64"}, nil), + registryProvider.EXPECT().GetArchitectures("jujud-operator", "2.9.10").Return([]string{"amd64", "arm64"}, nil), + registryProvider.EXPECT().GetArchitectures("jujud-operator", "2.9.11").Return([]string{"amd64", "arm64"}, nil), + registryProvider.EXPECT().GetArchitectures("jujud-operator", "2.9.12").Return(nil, errors.NotFoundf("2.9.12")), // This can only happen on a non-official registry account. registryProvider.EXPECT().Close().Return(nil), ) diff --git a/apiserver/facades/client/modelupgrader/findagents.go b/apiserver/facades/client/modelupgrader/findagents.go index a121469c82d..5c5f0b7be52 100644 --- a/apiserver/facades/client/modelupgrader/findagents.go +++ b/apiserver/facades/client/modelupgrader/findagents.go @@ -11,6 +11,7 @@ import ( "github.com/juju/juju/apiserver/common" "github.com/juju/juju/cloudconfig/podcfg" "github.com/juju/juju/controller" + "github.com/juju/juju/core/arch" coreos "github.com/juju/juju/core/os" "github.com/juju/juju/docker" envtools "github.com/juju/juju/environs/tools" @@ -121,6 +122,11 @@ func (m *ModelUpgraderAPI) agentVersionsForCAAS( if err != nil { return nil, errors.Trace(err) } + + wantArch := args.Arch + if wantArch == "" { + wantArch = arch.DefaultArchitecture + } for _, tag := range tags { number := tag.AgentVersion() if args.MajorVersion > 0 { @@ -145,21 +151,21 @@ func (m *ModelUpgraderAPI) agentVersionsForCAAS( continue } } - arch, err := reg.GetArchitecture(imageName, number.String()) + arches, err := reg.GetArchitectures(imageName, number.String()) if errors.Is(err, errors.NotFound) { continue } if err != nil { return nil, errors.Annotatef(err, "cannot get architecture for %s:%s", imageName, number.String()) } - if args.Arch != "" && arch != args.Arch { + if !set.NewStrings(arches...).Contains(wantArch) { continue } tools := coretools.Tools{ Version: version.Binary{ Number: number, Release: coreos.HostOSTypeName(), - Arch: arch, + Arch: wantArch, }, } result = append(result, &tools) diff --git a/apiserver/facades/client/modelupgrader/upgrader_test.go b/apiserver/facades/client/modelupgrader/upgrader_test.go index 08bc2ac10a1..ca429f0a112 100644 --- a/apiserver/facades/client/modelupgrader/upgrader_test.go +++ b/apiserver/facades/client/modelupgrader/upgrader_test.go @@ -774,7 +774,15 @@ func (s *modelUpgradeSuite) TestFindToolsIAAS(c *gc.C) { }) } +func (s *modelUpgradeSuite) TestFindToolsCAASReleasedDefault(c *gc.C) { + s.assertFindToolsCAASReleased(c, "", "amd64") +} + func (s *modelUpgradeSuite) TestFindToolsCAASReleased(c *gc.C) { + s.assertFindToolsCAASReleased(c, "arm64", "arm64") +} + +func (s *modelUpgradeSuite) assertFindToolsCAASReleased(c *gc.C, wantArch, expectArch string) { ctrl, api := s.getModelUpgraderAPI(c) defer ctrl.Finish() @@ -786,6 +794,9 @@ func (s *modelUpgradeSuite) TestFindToolsCAASReleased(c *gc.C) { {Version: version.MustParseBinary("2.9.9-ubuntu-amd64")}, {Version: version.MustParseBinary("2.9.10-ubuntu-amd64")}, {Version: version.MustParseBinary("2.9.11-ubuntu-amd64")}, + {Version: version.MustParseBinary("2.9.9-ubuntu-arm64")}, + {Version: version.MustParseBinary("2.9.10-ubuntu-arm64")}, + {Version: version.MustParseBinary("2.9.11-ubuntu-arm64")}, } s.PatchValue(&coreos.HostOS, func() coreos.OSType { return coreos.Ubuntu }) @@ -793,6 +804,7 @@ func (s *modelUpgradeSuite) TestFindToolsCAASReleased(c *gc.C) { s.toolsFinder.EXPECT().FindAgents(common.FindAgentsParams{ MajorVersion: 2, MinorVersion: 9, ModelType: state.ModelTypeCAAS, + Arch: wantArch, }).Return(simpleStreams, nil), s.registryProvider.EXPECT().Tags("jujud-operator").Return(coretools.Versions{ image.NewImageInfo(version.MustParse("2.9.8")), @@ -802,20 +814,21 @@ func (s *modelUpgradeSuite) TestFindToolsCAASReleased(c *gc.C) { image.NewImageInfo(version.MustParse("2.9.11")), image.NewImageInfo(version.MustParse("2.9.12")), // skip: it's not released in simplestream yet. }, nil), - s.registryProvider.EXPECT().GetArchitecture("jujud-operator", "2.9.9").Return("amd64", nil), - s.registryProvider.EXPECT().GetArchitecture("jujud-operator", "2.9.10.1").Return("amd64", nil), - s.registryProvider.EXPECT().GetArchitecture("jujud-operator", "2.9.10").Return("amd64", nil), - s.registryProvider.EXPECT().GetArchitecture("jujud-operator", "2.9.11").Return("amd64", nil), + s.registryProvider.EXPECT().GetArchitectures("jujud-operator", "2.9.9").Return([]string{"amd64", "arm64"}, nil), + s.registryProvider.EXPECT().GetArchitectures("jujud-operator", "2.9.10.1").Return([]string{"amd64", "arm64"}, nil), + s.registryProvider.EXPECT().GetArchitectures("jujud-operator", "2.9.10").Return([]string{"amd64", "arm64"}, nil), + s.registryProvider.EXPECT().GetArchitectures("jujud-operator", "2.9.11").Return([]string{"amd64", "arm64"}, nil), s.registryProvider.EXPECT().Close().Return(nil), ) - result, err := api.FindAgents(common.FindAgentsParams{MajorVersion: 2, MinorVersion: 9, ModelType: state.ModelTypeCAAS}) + result, err := api.FindAgents(common.FindAgentsParams{ + MajorVersion: 2, MinorVersion: 9, ModelType: state.ModelTypeCAAS, Arch: wantArch}) c.Assert(err, jc.ErrorIsNil) c.Assert(result, gc.DeepEquals, coretools.Versions{ - &coretools.Tools{Version: version.MustParseBinary("2.9.9-ubuntu-amd64")}, - &coretools.Tools{Version: version.MustParseBinary("2.9.10.1-ubuntu-amd64")}, - &coretools.Tools{Version: version.MustParseBinary("2.9.10-ubuntu-amd64")}, - &coretools.Tools{Version: version.MustParseBinary("2.9.11-ubuntu-amd64")}, + &coretools.Tools{Version: version.MustParseBinary("2.9.9-ubuntu-" + expectArch)}, + &coretools.Tools{Version: version.MustParseBinary("2.9.10.1-ubuntu-" + expectArch)}, + &coretools.Tools{Version: version.MustParseBinary("2.9.10-ubuntu-" + expectArch)}, + &coretools.Tools{Version: version.MustParseBinary("2.9.11-ubuntu-" + expectArch)}, }) } @@ -845,7 +858,7 @@ func (s *modelUpgradeSuite) TestFindToolsCAASReleasedExact(c *gc.C) { image.NewImageInfo(version.MustParse("2.9.11")), image.NewImageInfo(version.MustParse("2.9.12")), }, nil), - s.registryProvider.EXPECT().GetArchitecture("jujud-operator", "2.9.10").Return("amd64", nil), + s.registryProvider.EXPECT().GetArchitectures("jujud-operator", "2.9.10").Return([]string{"amd64"}, nil), s.registryProvider.EXPECT().Close().Return(nil), ) @@ -887,11 +900,11 @@ func (s *modelUpgradeSuite) TestFindToolsCAASNonReleased(c *gc.C) { image.NewImageInfo(version.MustParse("2.9.12")), image.NewImageInfo(version.MustParse("2.9.13")), // skip: it's not released in simplestream yet. }, nil), - s.registryProvider.EXPECT().GetArchitecture("jujud-operator", "2.9.9").Return("amd64", nil), - s.registryProvider.EXPECT().GetArchitecture("jujud-operator", "2.9.10.1").Return("amd64", nil), - s.registryProvider.EXPECT().GetArchitecture("jujud-operator", "2.9.10").Return("amd64", nil), - s.registryProvider.EXPECT().GetArchitecture("jujud-operator", "2.9.11").Return("amd64", nil), - s.registryProvider.EXPECT().GetArchitecture("jujud-operator", "2.9.12").Return("", errors.NotFoundf("2.9.12")), // This can only happen on a non-official registry account. + s.registryProvider.EXPECT().GetArchitectures("jujud-operator", "2.9.9").Return([]string{"amd64", "arm64"}, nil), + s.registryProvider.EXPECT().GetArchitectures("jujud-operator", "2.9.10.1").Return([]string{"amd64", "arm64"}, nil), + s.registryProvider.EXPECT().GetArchitectures("jujud-operator", "2.9.10").Return([]string{"amd64", "arm64"}, nil), + s.registryProvider.EXPECT().GetArchitectures("jujud-operator", "2.9.11").Return([]string{"amd64", "arm64"}, nil), + s.registryProvider.EXPECT().GetArchitectures("jujud-operator", "2.9.12").Return(nil, errors.NotFoundf("2.9.12")), // This can only happen on a non-official registry account. s.registryProvider.EXPECT().Close().Return(nil), ) diff --git a/docker/registry/internal/acr.go b/docker/registry/internal/acr.go index 6d4dfc08379..654a6a78102 100644 --- a/docker/registry/internal/acr.go +++ b/docker/registry/internal/acr.go @@ -33,16 +33,16 @@ func normalizeRepoDetailsAzure(repoDetails *docker.ImageRepoDetails) error { return nil } -func (c *azureContainerRegistry) String() string { +func (c azureContainerRegistry) String() string { return "azurecr.io" } // Match checks if the repository details matches current provider format. -func (c *azureContainerRegistry) Match() bool { +func (c azureContainerRegistry) Match() bool { return strings.Contains(c.repoDetails.ServerAddress, "azurecr.io") } -func (c *azureContainerRegistry) WrapTransport(...TransportWrapper) error { +func (c azureContainerRegistry) WrapTransport(...TransportWrapper) error { if c.repoDetails.BasicAuthConfig.Empty() { return errors.NewNotValid(nil, fmt.Sprintf(`username and password are required for registry %q`, c.repoDetails.Repository)) } @@ -57,9 +57,9 @@ func (c azureContainerRegistry) Tags(imageName string) (versions tools.Versions, return c.fetchTags(url, &response) } -// GetArchitecture returns the archtecture of the image for the specified tag. -func (c azureContainerRegistry) GetArchitecture(imageName, tag string) (string, error) { - return getArchitecture(imageName, tag, c) +// GetArchitectures returns the architectures of the image for the specified tag. +func (c azureContainerRegistry) GetArchitectures(imageName, tag string) ([]string, error) { + return getArchitectures(imageName, tag, c) } // GetManifests returns the manifests of the image for the specified tag. diff --git a/docker/registry/internal/acr_test.go b/docker/registry/internal/acr_test.go index 5c0d0aa9c84..83e572a73d9 100644 --- a/docker/registry/internal/acr_test.go +++ b/docker/registry/internal/acr_test.go @@ -294,10 +294,10 @@ func (s *azureContainerRegistrySuite) assertGetManifestsSchemaVersion1(c *gc.C, func (s *azureContainerRegistrySuite) TestGetManifestsSchemaVersion1(c *gc.C) { s.assertGetManifestsSchemaVersion1(c, ` -{ "schemaVersion": 1, "name": "jujuqa/jujud-operator", "tag": "2.9.13", "architecture": "amd64"} +{ "schemaVersion": 1, "name": "jujuqa/jujud-operator", "tag": "2.9.13", "architecture": "ppc64le"} `[1:], `application/vnd.docker.distribution.manifest.v1+prettyjws`, - &internal.ManifestsResult{Architecture: "amd64"}, + &internal.ManifestsResult{Architectures: []string{"ppc64el"}}, ) } @@ -319,6 +319,23 @@ func (s *azureContainerRegistrySuite) TestGetManifestsSchemaVersion2(c *gc.C) { ) } +func (s *azureContainerRegistrySuite) TestGetManifestsSchemaVersion2List(c *gc.C) { + s.assertGetManifestsSchemaVersion1(c, + ` +{ + "schemaVersion": 2, + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "manifests": [ + {"platform": {"architecture": "amd64"}}, + {"platform": {"architecture": "ppc64le"}} + ] +} +`[1:], + `application/vnd.docker.distribution.manifest.list.v2+prettyjws`, + &internal.ManifestsResult{Architectures: []string{"amd64", "ppc64el"}}, + ) +} + func (s *azureContainerRegistrySuite) TestGetBlobs(c *gc.C) { // Use v2 for private repository. s.isPrivate = true diff --git a/docker/registry/internal/base_client.go b/docker/registry/internal/base_client.go index 5b6c9d32126..5b0a0c30052 100644 --- a/docker/registry/internal/base_client.go +++ b/docker/registry/internal/base_client.go @@ -194,12 +194,12 @@ func commonURLGetter(version APIVersion, url url.URL, pathTemplate string, args return url.String() } -func (c baseClient) url(pathTemplate string, args ...interface{}) string { +func (c *baseClient) url(pathTemplate string, args ...interface{}) string { return commonURLGetter(c.APIVersion(), *c.baseURL, pathTemplate, args...) } // Ping pings the baseClient endpoint. -func (c baseClient) Ping() error { +func (c *baseClient) Ping() error { url := c.url("/") logger.Debugf("baseClient ping %q", url) resp, err := c.client.Get(url) @@ -209,7 +209,7 @@ func (c baseClient) Ping() error { return errors.Trace(unwrapNetError(err)) } -func (c baseClient) ImageRepoDetails() (o docker.ImageRepoDetails) { +func (c *baseClient) ImageRepoDetails() (o docker.ImageRepoDetails) { if c.repoDetails != nil { return *c.repoDetails } @@ -224,7 +224,7 @@ func (c *baseClient) Close() error { return nil } -func (c baseClient) getPaginatedJSON(url string, response interface{}) (string, error) { +func (c *baseClient) getPaginatedJSON(url string, response interface{}) (string, error) { resp, err := c.client.Get(url) logger.Tracef("getPaginatedJSON for %q, err %v", url, err) if err != nil { diff --git a/docker/registry/internal/base_manifests.go b/docker/registry/internal/base_manifests.go index 0170ae1ac43..4ed99284439 100644 --- a/docker/registry/internal/base_manifests.go +++ b/docker/registry/internal/base_manifests.go @@ -10,6 +10,8 @@ import ( "strings" "github.com/juju/errors" + + "github.com/juju/juju/core/arch" ) //go:generate go run go.uber.org/mock/mockgen -package mocks -destination mocks/manifests_mock.go github.com/juju/juju/docker/registry/internal ArchitectureGetter @@ -28,10 +30,23 @@ type manifestsResponseV2 struct { Config manifestsResponseV2Config `json:"config"` } +type manifestResponseV2Platform struct { + Architecture string `json:"architecture"` +} + +type manifestResponseV2Manifest struct { + Platform manifestResponseV2Platform `json:"platform"` +} + +type manifestsResponseListV2 struct { + SchemaVersion int `json:"schemaVersion"` + Manifests []manifestResponseV2Manifest `json:"manifests"` +} + // ManifestsResult is the result of GetManifests. type ManifestsResult struct { - Architecture string - Digest string + Architectures []string + Digest string } // BlobsResponse is the result of GetBlobs. @@ -45,38 +60,38 @@ type ArchitectureGetter interface { GetBlobs(imageName, digest string) (*BlobsResponse, error) } -// GetArchitecture returns the archtecture of the image for the specified tag. -func (c baseClient) GetArchitecture(imageName, tag string) (string, error) { - return getArchitecture(imageName, tag, c) +// GetArchitectures returns the architectures of the image for the specified tag. +func (c *baseClient) GetArchitectures(imageName, tag string) ([]string, error) { + return getArchitectures(imageName, tag, c) } -func getArchitecture(imageName, tag string, client ArchitectureGetter) (string, error) { +func getArchitectures(imageName, tag string, client ArchitectureGetter) ([]string, error) { manifests, err := client.GetManifests(imageName, tag) if err != nil { - return "", errors.Annotatef(err, "can not get manifests for %s:%s", imageName, tag) + return nil, errors.Annotatef(err, "can not get manifests for %s:%s", imageName, tag) } - if manifests.Architecture == "" && manifests.Digest == "" { - return "", errors.New(fmt.Sprintf("faild to get manifests for %q %q", imageName, tag)) + if len(manifests.Architectures) == 0 && manifests.Digest == "" { + return nil, errors.New(fmt.Sprintf("faild to get manifests for %q %q", imageName, tag)) } - if manifests.Architecture != "" { - return manifests.Architecture, nil + if len(manifests.Architectures) > 0 { + return manifests.Architectures, nil } blobs, err := client.GetBlobs(imageName, manifests.Digest) if err != nil { - return "", errors.Trace(err) + return nil, errors.Trace(err) } - return blobs.Architecture, nil + return []string{arch.NormaliseArch(blobs.Architecture)}, nil } // GetManifests returns the manifests of the image for the specified tag. -func (c baseClient) GetManifests(imageName, tag string) (*ManifestsResult, error) { +func (c *baseClient) GetManifests(imageName, tag string) (*ManifestsResult, error) { repo := getRepositoryOnly(c.ImageRepoDetails().Repository) url := c.url("/%s/%s/manifests/%s", repo, imageName, tag) return c.GetManifestsCommon(url) } // GetManifestsCommon returns manifests result for the provided url. -func (c baseClient) GetManifestsCommon(url string) (*ManifestsResult, error) { +func (c *baseClient) GetManifestsCommon(url string) (*ManifestsResult, error) { resp, err := c.client.Get(url) if err != nil { return nil, errors.Trace(unwrapNetError(err)) @@ -86,8 +101,9 @@ func (c baseClient) GetManifestsCommon(url string) (*ManifestsResult, error) { } const ( - manifestContentTypeV1 = "application/vnd.docker.distribution.manifest.v1" - manifestContentTypeV2 = "application/vnd.docker.distribution.manifest.v2" + manifestContentTypeV1 = "application/vnd.docker.distribution.manifest.v1" + manifestContentTypeV2 = "application/vnd.docker.distribution.manifest.v2" + manifestContentTypeListV2 = "application/vnd.docker.distribution.manifest.list.v2" ) func processManifestsResponse(resp *http.Response) (*ManifestsResult, error) { @@ -107,27 +123,37 @@ func processManifestsResponse(resp *http.Response) (*ManifestsResult, error) { if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { return nil, errors.Trace(err) } - return &ManifestsResult{Architecture: data.Architecture}, nil + return &ManifestsResult{Architectures: []string{arch.NormaliseArch(data.Architecture)}}, nil case manifestContentTypeV2: var data manifestsResponseV2 if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { return nil, errors.Trace(err) } return &ManifestsResult{Digest: data.Config.Digest}, nil + case manifestContentTypeListV2: + var data manifestsResponseListV2 + if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { + return nil, errors.Trace(err) + } + archs := make([]string, len(data.Manifests)) + for i, manifest := range data.Manifests { + archs[i] = arch.NormaliseArch(manifest.Platform.Architecture) + } + return &ManifestsResult{Architectures: archs}, nil default: return nil, notSupportedAPIVersionError } } -// GetBlobs gets the archtecture of the image for the specified tag via blobs API. -func (c baseClient) GetBlobs(imageName, digest string) (*BlobsResponse, error) { +// GetBlobs gets the architecture of the image for the specified tag via blobs API. +func (c *baseClient) GetBlobs(imageName, digest string) (*BlobsResponse, error) { repo := getRepositoryOnly(c.ImageRepoDetails().Repository) url := c.url("/%s/%s/blobs/%s", repo, imageName, digest) return c.GetBlobsCommon(url) } // GetBlobsCommon returns blobs result for the provided url. -func (c baseClient) GetBlobsCommon(url string) (*BlobsResponse, error) { +func (c *baseClient) GetBlobsCommon(url string) (*BlobsResponse, error) { resp, err := c.client.Get(url) logger.Tracef("getting blobs for %q, err %v", url, err) if err != nil { diff --git a/docker/registry/internal/base_manifests_test.go b/docker/registry/internal/base_manifests_test.go index 399c278390c..384a89ee96e 100644 --- a/docker/registry/internal/base_manifests_test.go +++ b/docker/registry/internal/base_manifests_test.go @@ -74,13 +74,13 @@ func (s *baseSuite) assertGetManifestsSchemaVersion1(c *gc.C, responseData, cont func (s *baseSuite) TestGetManifestsSchemaVersion1(c *gc.C) { s.assertGetManifestsSchemaVersion1(c, ` -{ "schemaVersion": 1, "name": "jujuqa/jujud-operator", "tag": "2.9.13", "architecture": "amd64"} +{ "schemaVersion": 1, "name": "jujuqa/jujud-operator", "tag": "2.9.13", "architecture": "ppc64le"} `[1:], `application/vnd.docker.distribution.manifest.v1+prettyjws`, http.StatusOK, func(result *internal.ManifestsResult, err error) { c.Assert(err, jc.ErrorIsNil) - c.Assert(result, jc.DeepEquals, &internal.ManifestsResult{Architecture: "amd64"}) + c.Assert(result, jc.DeepEquals, &internal.ManifestsResult{Architectures: []string{"ppc64el"}}) }, ) } @@ -107,6 +107,27 @@ func (s *baseSuite) TestGetManifestsSchemaVersion2(c *gc.C) { ) } +func (s *baseSuite) TestGetManifestsSchemaVersion2List(c *gc.C) { + s.assertGetManifestsSchemaVersion1(c, + ` +{ + "schemaVersion": 2, + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "manifests": [ + {"platform": {"architecture": "amd64"}}, + {"platform": {"architecture": "ppc64le"}} + ] +} +`[1:], + `application/vnd.docker.distribution.manifest.list.v2+prettyjws`, + http.StatusOK, + func(result *internal.ManifestsResult, err error) { + c.Assert(err, jc.ErrorIsNil) + c.Assert(result, jc.DeepEquals, &internal.ManifestsResult{Architectures: []string{"amd64", "ppc64el"}}) + }, + ) +} + func (s *baseSuite) TestGetManifestsSchemaVersion2NotFound(c *gc.C) { s.assertGetManifestsSchemaVersion1(c, ` @@ -191,14 +212,12 @@ func (s *baseSuite) TestGetArchitectureV1(c *gc.C) { ctrl := gomock.NewController(c) client := mocks.NewMockArchitectureGetter(ctrl) - gomock.InOrder( - client.EXPECT().GetManifests("jujud-operator", "2.9.12").Return( - &internal.ManifestsResult{Architecture: "amd64"}, nil, - ), + client.EXPECT().GetManifests("jujud-operator", "2.9.12").Return( + &internal.ManifestsResult{Architectures: []string{"amd64", "arm64"}}, nil, ) - arch, err := internal.GetArchitecture("jujud-operator", "2.9.12", client) + arch, err := internal.GetArchitectures("jujud-operator", "2.9.12", client) c.Assert(err, jc.ErrorIsNil) - c.Assert(arch, jc.DeepEquals, "amd64") + c.Assert(arch, jc.DeepEquals, []string{"amd64", "arm64"}) } func (s *baseSuite) TestGetArchitectureV2(c *gc.C) { @@ -210,23 +229,21 @@ func (s *baseSuite) TestGetArchitectureV2(c *gc.C) { &internal.ManifestsResult{Digest: "sha256:f0609d8a844f7271411c1a9c5d7a898fd9f9c5a4844e3bc7db6d725b54671ac1"}, nil, ), client.EXPECT().GetBlobs("jujud-operator", "sha256:f0609d8a844f7271411c1a9c5d7a898fd9f9c5a4844e3bc7db6d725b54671ac1").Return( - &internal.BlobsResponse{Architecture: "amd64"}, nil, + &internal.BlobsResponse{Architecture: "ppc64le"}, nil, ), ) - arch, err := internal.GetArchitecture("jujud-operator", "2.9.12", client) + arch, err := internal.GetArchitectures("jujud-operator", "2.9.12", client) c.Assert(err, jc.ErrorIsNil) - c.Assert(arch, jc.DeepEquals, "amd64") + c.Assert(arch, jc.DeepEquals, []string{"ppc64el"}) } func (s *baseSuite) TestGetArchitectureInvalidResponse(c *gc.C) { ctrl := gomock.NewController(c) client := mocks.NewMockArchitectureGetter(ctrl) - gomock.InOrder( - client.EXPECT().GetManifests("jujud-operator", "2.9.12").Return( - &internal.ManifestsResult{}, nil, - ), + client.EXPECT().GetManifests("jujud-operator", "2.9.12").Return( + &internal.ManifestsResult{}, nil, ) - _, err := internal.GetArchitecture("jujud-operator", "2.9.12", client) + _, err := internal.GetArchitectures("jujud-operator", "2.9.12", client) c.Assert(err, gc.ErrorMatches, `faild to get manifests for "jujud-operator" "2.9.12"`) } diff --git a/docker/registry/internal/base_tags.go b/docker/registry/internal/base_tags.go index 071b658221f..4ea1dfef4d7 100644 --- a/docker/registry/internal/base_tags.go +++ b/docker/registry/internal/base_tags.go @@ -48,7 +48,7 @@ func fetchTagsV2(c tagFetcher, imageName string) (tools.Versions, error) { } // Tags fetches tags for an OCI image. -func (c baseClient) Tags(imageName string) (tools.Versions, error) { +func (c *baseClient) Tags(imageName string) (tools.Versions, error) { switch c.APIVersion() { case APIVersionV2: return fetchTagsV2(c, imageName) @@ -57,7 +57,7 @@ func (c baseClient) Tags(imageName string) (tools.Versions, error) { } } -func (c baseClient) fetchTags(url string, res tagsGetter) (versions tools.Versions, err error) { +func (c *baseClient) fetchTags(url string, res tagsGetter) (versions tools.Versions, err error) { pushVersions := func(tags []string) { for _, tag := range tags { v, err := version.Parse(tag) diff --git a/docker/registry/internal/ecr.go b/docker/registry/internal/ecr.go index 1511c38b79b..69745a98766 100644 --- a/docker/registry/internal/ecr.go +++ b/docker/registry/internal/ecr.go @@ -183,31 +183,31 @@ func (c *elasticContainerRegistry) WrapTransport(...TransportWrapper) (err error } // Ping pings the ecr endpoint. -func (c elasticContainerRegistry) Ping() error { +func (c *elasticContainerRegistry) Ping() error { // No ping endpoint available for ecr. return nil } // Tags fetches tags for an OCI image. -func (c elasticContainerRegistry) Tags(imageName string) (versions tools.Versions, err error) { +func (c *elasticContainerRegistry) Tags(imageName string) (versions tools.Versions, err error) { url := c.url("/%s/tags/list", imageName) var response tagsResponseV2 return c.fetchTags(url, &response) } -// GetArchitecture returns the archtecture of the image for the specified tag. -func (c elasticContainerRegistry) GetArchitecture(imageName, tag string) (string, error) { - return getArchitecture(imageName, tag, c) +// GetArchitectures returns the architectures of the image for the specified tag. +func (c *elasticContainerRegistry) GetArchitectures(imageName, tag string) ([]string, error) { + return getArchitectures(imageName, tag, c) } // GetManifests returns the manifests of the image for the specified tag. -func (c elasticContainerRegistry) GetManifests(imageName, tag string) (*ManifestsResult, error) { +func (c *elasticContainerRegistry) GetManifests(imageName, tag string) (*ManifestsResult, error) { url := c.url("/%s/manifests/%s", imageName, tag) return c.GetManifestsCommon(url) } // GetBlobs gets the archtecture of the image for the specified tag via blobs API. -func (c elasticContainerRegistry) GetBlobs(imageName, digest string) (*BlobsResponse, error) { +func (c *elasticContainerRegistry) GetBlobs(imageName, digest string) (*BlobsResponse, error) { url := c.url("/%s/blobs/%s", imageName, digest) return c.GetBlobsCommon(url) } diff --git a/docker/registry/internal/ecr_test.go b/docker/registry/internal/ecr_test.go index df61c06f647..bf4822ae44c 100644 --- a/docker/registry/internal/ecr_test.go +++ b/docker/registry/internal/ecr_test.go @@ -336,10 +336,10 @@ func (s *elasticContainerRegistrySuite) assertGetManifestsSchemaVersion1(c *gc.C func (s *elasticContainerRegistrySuite) TestGetManifestsSchemaVersion1(c *gc.C) { s.assertGetManifestsSchemaVersion1(c, ` -{ "schemaVersion": 1, "name": "jujuqa/jujud-operator", "tag": "2.9.13", "architecture": "amd64"} +{ "schemaVersion": 1, "name": "jujuqa/jujud-operator", "tag": "2.9.13", "architecture": "ppc64le"} `[1:], `application/vnd.docker.distribution.manifest.v1+prettyjws`, - &internal.ManifestsResult{Architecture: "amd64"}, + &internal.ManifestsResult{Architectures: []string{"ppc64el"}}, ) } @@ -361,6 +361,23 @@ func (s *elasticContainerRegistrySuite) TestGetManifestsSchemaVersion2(c *gc.C) ) } +func (s *elasticContainerRegistrySuite) TestGetManifestsSchemaVersion2List(c *gc.C) { + s.assertGetManifestsSchemaVersion1(c, + ` +{ + "schemaVersion": 2, + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "manifests": [ + {"platform": {"architecture": "amd64"}}, + {"platform": {"architecture": "ppc64le"}} + ] +} +`[1:], + `application/vnd.docker.distribution.manifest.list.v2+prettyjws`, + &internal.ManifestsResult{Architectures: []string{"amd64", "ppc64el"}}, + ) +} + func (s *elasticContainerRegistrySuite) TestGetBlobs(c *gc.C) { // Use v2 for private repository. s.isPrivate = true diff --git a/docker/registry/internal/interface.go b/docker/registry/internal/interface.go index a047ccf3468..053bf6ed69e 100644 --- a/docker/registry/internal/interface.go +++ b/docker/registry/internal/interface.go @@ -16,7 +16,7 @@ import ( type Registry interface { String() string Tags(string) (tools.Versions, error) - GetArchitecture(imageName, tag string) (string, error) + GetArchitectures(imageName, tag string) ([]string, error) Close() error Ping() error ImageRepoDetails() docker.ImageRepoDetails diff --git a/docker/registry/internal/package_test.go b/docker/registry/internal/package_test.go index 96ce8307f67..a27294c9ba5 100644 --- a/docker/registry/internal/package_test.go +++ b/docker/registry/internal/package_test.go @@ -16,15 +16,14 @@ func TestPackage(t *testing.T) { } type ( - AzureContainerRegistry = azureContainerRegistry - BaseClient = baseClient - Dockerhub = dockerhub - GoogleContainerRegistry = googleContainerRegistry - GithubContainerRegistry = githubContainerRegistry - GitlabContainerRegistry = gitlabContainerRegistry - QuayContainerRegistry = quayContainerRegistry - ElasticContainerRegistry = elasticContainerRegistry - ElasticContainerRegistryPublic = elasticContainerRegistryPublic + AzureContainerRegistry = azureContainerRegistry + BaseClient = baseClient + Dockerhub = dockerhub + GoogleContainerRegistry = googleContainerRegistry + GithubContainerRegistry = githubContainerRegistry + GitlabContainerRegistry = gitlabContainerRegistry + QuayContainerRegistry = quayContainerRegistry + ElasticContainerRegistry = elasticContainerRegistry ) var ( @@ -34,7 +33,7 @@ var ( NewTokenTransport = newTokenTransport NewElasticContainerRegistryForTest = newElasticContainerRegistryForTest NewAzureContainerRegistry = newAzureContainerRegistry - GetArchitecture = getArchitecture + GetArchitectures = getArchitectures UnwrapNetError = unwrapNetError ) diff --git a/docker/registry/mocks/registry_mock.go b/docker/registry/mocks/registry_mock.go index da62cb20f8b..e6f8a3070c9 100644 --- a/docker/registry/mocks/registry_mock.go +++ b/docker/registry/mocks/registry_mock.go @@ -55,19 +55,19 @@ func (mr *MockRegistryMockRecorder) Close() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockRegistry)(nil).Close)) } -// GetArchitecture mocks base method. -func (m *MockRegistry) GetArchitecture(arg0, arg1 string) (string, error) { +// GetArchitectures mocks base method. +func (m *MockRegistry) GetArchitectures(arg0, arg1 string) ([]string, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetArchitecture", arg0, arg1) - ret0, _ := ret[0].(string) + ret := m.ctrl.Call(m, "GetArchitectures", arg0, arg1) + ret0, _ := ret[0].([]string) ret1, _ := ret[1].(error) return ret0, ret1 } -// GetArchitecture indicates an expected call of GetArchitecture. -func (mr *MockRegistryMockRecorder) GetArchitecture(arg0, arg1 any) *gomock.Call { +// GetArchitectures indicates an expected call of GetArchitectures. +func (mr *MockRegistryMockRecorder) GetArchitectures(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetArchitecture", reflect.TypeOf((*MockRegistry)(nil).GetArchitecture), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetArchitectures", reflect.TypeOf((*MockRegistry)(nil).GetArchitectures), arg0, arg1) } // ImageRepoDetails mocks base method.