diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index 690e1480e13..b860a8b50e2 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -55,7 +55,7 @@ jobs: libdqlite-dev \ libsqlite3-dev \ sqlite3 - curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.53.3 + curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.54.2 sudo curl -sSfL https://github.com/mvdan/sh/releases/download/v3.7.0/shfmt_v3.7.0_linux_$(go env GOARCH) -o /usr/bin/shfmt sudo chmod +x /usr/bin/shfmt diff --git a/Makefile b/Makefile index cef1b583be8..e90f74d1dfe 100644 --- a/Makefile +++ b/Makefile @@ -508,8 +508,9 @@ ifeq ($(shell if [ "$(GO_INSTALLED_VERSION)" \< "$(GO_MOD_VERSION)" ]; then echo endif endif -WAIT_FOR_DPKG=sh -c '. "${PROJECT_DIR}/make_functions.sh"; wait_for_dpkg "$$@"' wait_for_dpkg -JUJU_DB_CHANNEL=4.4/stable +WAIT_FOR_DPKG=bash -c '. "${PROJECT_DIR}/make_functions.sh"; wait_for_dpkg "$$@"' wait_for_dpkg +JUJU_DB_VERSION=4.4 +JUJU_DB_CHANNEL=${JUJU_DB_VERSION}/stable .PHONY: install-mongo-dependencies install-mongo-dependencies: @@ -552,7 +553,8 @@ check-deps: # CAAS related targets -DOCKER_USERNAME ?= jujusolutions +export OCI_BUILDER ?= $(shell (which podman 2>&1 > /dev/null && echo podman) || echo docker ) +DOCKER_USERNAME ?= docker.io/jujusolutions DOCKER_BUILDX_CONTEXT ?= juju-make DOCKER_STAGING_DIR ?= ${BUILD_DIR}/docker-staging JUJUD_STAGING_DIR ?= ${DOCKER_STAGING_DIR}/jujud-operator @@ -565,6 +567,7 @@ BUILD_OPERATOR_IMAGE=bash -c '. "${PROJECT_DIR}/make_functions.sh"; build_push_o OPERATOR_IMAGE_PATH=bash -c '. "${PROJECT_DIR}/make_functions.sh"; operator_image_path "$$@"' operator_image_path OPERATOR_IMAGE_RELEASE_PATH=bash -c '. "${PROJECT_DIR}/make_functions.sh"; operator_image_release_path "$$@"' operator_image_release_path UPDATE_MICROK8S_OPERATOR=bash -c '. "${PROJECT_DIR}/make_functions.sh"; microk8s_operator_update "$$@"' microk8s_operator_update +SEED_REPOSITORY=bash -c '. "${PROJECT_DIR}/make_functions.sh"; seed_repository "$$@"' seed_repository image_check_prereq=image-check-build ifneq ($(OPERATOR_IMAGE_BUILD_SRC),true) @@ -585,7 +588,9 @@ image-check-build-skip: .PHONY: docker-builder docker-builder: ## docker-builder: Makes sure that there is a buildx context for building the oci images +ifeq ($(OCI_BUILDER),docker) -@docker buildx create --name ${DOCKER_BUILDX_CONTEXT} +endif .PHONY: image-check operator-image: image-check docker-builder @@ -609,12 +614,16 @@ push-operator-image-undefined: push-operator-image: $(push_operator_image_prereq) ## push-operator-image: Push up the newly built operator image via docker - .PHONY: push-release-operator-image push-release-operator-image: PUSH_IMAGE=true push-release-operator-image: operator-image ## push-release-operator-image: Push up the newly built release operator image via docker +.PHONY: seed-repository +seed-repository: +## seed-repository: Copy required juju images from docker.io/jujusolutions + JUJU_DB_VERSION=$(JUJU_DB_VERSION) $(SEED_REPOSITORY) + .PHONY: host-install host-install: @@ -623,18 +632,19 @@ host-install: .PHONY: minikube-operator-update minikube-operator-update: host-install operator-image -## minikube-operator-update: Push up the newly built operator image for use with minikube - docker save "$(shell ${OPERATOR_IMAGE_PATH})" | minikube image load --overwrite=true - +## minikube-operator-update: Inject the newly built operator image into minikube + $(OCI_BUILDER) save "$(shell ${OPERATOR_IMAGE_PATH})" | minikube image load --overwrite=true - .PHONY: microk8s-operator-update microk8s-operator-update: host-install operator-image -## microk8s-operator-update: Push up the newly built operator image for use with microk8s +## microk8s-operator-update: Inject the newly built operator image into microk8s @${UPDATE_MICROK8S_OPERATOR} .PHONY: k3s-operator-update k3s-operator-update: host-install operator-image -## k3s-operator-update: Push up the newly built operator image for use with k3s - docker save "$(shell ${OPERATOR_IMAGE_PATH})" | sudo k3s ctr images import - +## k3s-operator-update: Inject the newly built operator image into k3s + $(OCI_BUILDER) save "$(shell ${OPERATOR_IMAGE_PATH})" | sudo k3s ctr images import - + .PHONY: check-k8s-model check-k8s-model: @@ -646,7 +656,7 @@ check-k8s-model: local-operator-update: check-k8s-model operator-image ## local-operator-update: Build then update local operator image $(eval kubeworkers != juju status -m ${JUJU_K8S_MODEL} kubernetes-worker --format json | jq -c '.machines | keys' | tr -c '[:digit:]' ' ' 2>&1) - docker save "$(shell ${OPERATOR_IMAGE_PATH})" | gzip > ${DOCKER_STAGING_DIR}/jujud-operator-image.tar.gz + $(OCI_BUILDER) save "$(shell ${OPERATOR_IMAGE_PATH})" | gzip > ${DOCKER_STAGING_DIR}/jujud-operator-image.tar.gz $(foreach wm,$(kubeworkers), juju scp -m ${JUJU_K8S_MODEL} ${DOCKER_STAGING_DIR}/jujud-operator-image.tar.gz $(wm):/tmp/jujud-operator-image.tar.gz ; ) $(foreach wm,$(kubeworkers), juju ssh -m ${JUJU_K8S_MODEL} $(wm) -- "zcat /tmp/jujud-operator-image.tar.gz | docker load" ; ) diff --git a/api/controller/caasmodelconfigmanager/client.go b/api/controller/caasmodelconfigmanager/client.go index 6b333416618..b871528be2e 100644 --- a/api/controller/caasmodelconfigmanager/client.go +++ b/api/controller/caasmodelconfigmanager/client.go @@ -8,6 +8,9 @@ import ( "github.com/juju/juju/api/base" "github.com/juju/juju/api/common" + apiwatcher "github.com/juju/juju/api/watcher" + "github.com/juju/juju/core/watcher" + "github.com/juju/juju/rpc/params" ) // Client allows access to the CAAS model config manager API endpoint. @@ -28,3 +31,15 @@ func NewClient(caller base.APICaller) (*Client, error) { ControllerConfigAPI: common.NewControllerConfig(facadeCaller), }, nil } + +// WatchControllerConfig provides a watcher for changes on controller config. +func (c *Client) WatchControllerConfig() (watcher.NotifyWatcher, error) { + var result params.NotifyWatchResult + if err := c.facade.FacadeCall("WatchControllerConfig", nil, &result); err != nil { + return nil, err + } + if result.Error != nil { + return nil, result.Error + } + return apiwatcher.NewNotifyWatcher(c.facade.RawAPICaller(), result), nil +} diff --git a/apiserver/facades/client/client/client.go b/apiserver/facades/client/client/client.go index ec82a323447..526ba91edfa 100644 --- a/apiserver/facades/client/client/client.go +++ b/apiserver/facades/client/client/client.go @@ -15,6 +15,7 @@ import ( apiservererrors "github.com/juju/juju/apiserver/errors" "github.com/juju/juju/apiserver/facade" "github.com/juju/juju/cloudconfig/podcfg" + "github.com/juju/juju/controller" "github.com/juju/juju/core/cache" "github.com/juju/juju/core/leadership" "github.com/juju/juju/core/multiwatcher" @@ -292,13 +293,15 @@ func (c *Client) toolVersionsForCAAS(args params.FindToolsParams, streamsVersion if err != nil { return result, errors.Trace(err) } - imageRepoDetails := controllerCfg.CAASImageRepo() + imageRepoDetails, err := docker.NewImageRepoDetails(controllerCfg.CAASImageRepo()) + if err != nil { + return result, errors.Annotatef(err, "parsing %s", controller.CAASImageRepo) + } if imageRepoDetails.Empty() { - repoDetails, err := docker.NewImageRepoDetails(podcfg.JujudOCINamespace) + imageRepoDetails, err = docker.NewImageRepoDetails(podcfg.JujudOCINamespace) if err != nil { return result, errors.Trace(err) } - imageRepoDetails = *repoDetails } reg, err := c.registryAPIFunc(imageRepoDetails) if err != nil { diff --git a/apiserver/facades/client/controller/controller.go b/apiserver/facades/client/controller/controller.go index 8ac16e84200..d494985c1a7 100644 --- a/apiserver/facades/client/controller/controller.go +++ b/apiserver/facades/client/controller/controller.go @@ -32,6 +32,7 @@ import ( "github.com/juju/juju/core/model" "github.com/juju/juju/core/multiwatcher" "github.com/juju/juju/core/permission" + "github.com/juju/juju/docker" "github.com/juju/juju/environs/bootstrap" "github.com/juju/juju/migration" "github.com/juju/juju/pubsub/controller" @@ -728,6 +729,50 @@ func (c *ControllerAPI) ConfigSet(args params.ControllerConfigSet) error { if err := c.checkIsSuperUser(); err != nil { return errors.Trace(err) } + + currentCfg, err := c.state.ControllerConfig() + if err != nil { + return errors.Trace(err) + } + + // TODO(dqlite): move this business logic out of the facade. + if newValue, ok := args.Config[corecontroller.CAASImageRepo]; ok { + var newCAASImageRepo docker.ImageRepoDetails + if v, ok := newValue.(string); ok { + newCAASImageRepo, err = docker.NewImageRepoDetails(v) + if err != nil { + return fmt.Errorf("cannot parse %s: %s%w", corecontroller.CAASImageRepo, err.Error(), + errors.Hide(errors.NotValid)) + } + } else { + return fmt.Errorf("%s expected a string got %v%w", corecontroller.CAASImageRepo, v, + errors.Hide(errors.NotValid)) + } + + var currentCAASImageRepo docker.ImageRepoDetails + if currentValue, ok := currentCfg[corecontroller.CAASImageRepo]; !ok { + return fmt.Errorf("cannot change %s as it is not currently set%w", corecontroller.CAASImageRepo, + errors.Hide(errors.NotValid)) + } else if v, ok := currentValue.(string); !ok { + return fmt.Errorf("existing %s expected a string", corecontroller.CAASImageRepo) + } else { + currentCAASImageRepo, err = docker.NewImageRepoDetails(v) + if err != nil { + return fmt.Errorf("cannot parse existing %s: %w", corecontroller.CAASImageRepo, err) + } + } + // TODO: when podspec is removed, implement changing caas-image-repo. + if newCAASImageRepo.Repository != currentCAASImageRepo.Repository { + return fmt.Errorf("cannot change %s: repository read-only, only authentication can be updated", corecontroller.CAASImageRepo) + } + if !newCAASImageRepo.IsPrivate() && currentCAASImageRepo.IsPrivate() { + return fmt.Errorf("cannot change %s: unable to remove authentication details", corecontroller.CAASImageRepo) + } + if newCAASImageRepo.IsPrivate() && !currentCAASImageRepo.IsPrivate() { + return fmt.Errorf("cannot change %s: unable to add authentication details", corecontroller.CAASImageRepo) + } + } + if err := c.state.UpdateControllerConfig(args.Config, nil); err != nil { return errors.Trace(err) } diff --git a/apiserver/facades/client/controller/controller_test.go b/apiserver/facades/client/controller/controller_test.go index 480336d9d28..853f6173301 100644 --- a/apiserver/facades/client/controller/controller_test.go +++ b/apiserver/facades/client/controller/controller_test.go @@ -33,6 +33,7 @@ import ( "github.com/juju/juju/core/cache" coremultiwatcher "github.com/juju/juju/core/multiwatcher" "github.com/juju/juju/core/permission" + "github.com/juju/juju/docker" "github.com/juju/juju/environs" environscloudspec "github.com/juju/juju/environs/cloudspec" "github.com/juju/juju/environs/config" @@ -1020,6 +1021,54 @@ func (s *controllerSuite) TestConfigSetPublishesEvent(c *gc.C) { c.Assert(config.Features().SortedValues(), jc.DeepEquals, []string{"bar", "foo"}) } +func (s *controllerSuite) TestConfigSetCAASImageRepo(c *gc.C) { + config, err := s.State.ControllerConfig() + c.Assert(err, jc.ErrorIsNil) + c.Assert(config.CAASImageRepo(), gc.Equals, "") + + err = s.controller.ConfigSet(params.ControllerConfigSet{Config: map[string]interface{}{ + "caas-image-repo": "juju-repo.local", + }}) + c.Assert(err, gc.ErrorMatches, `cannot change caas-image-repo as it is not currently set`) + + err = s.State.UpdateControllerConfig(map[string]interface{}{ + "caas-image-repo": "jujusolutions", + }, nil) + c.Assert(err, jc.ErrorIsNil) + + err = s.controller.ConfigSet(params.ControllerConfigSet{Config: map[string]interface{}{ + "caas-image-repo": "juju-repo.local", + }}) + c.Assert(err, gc.ErrorMatches, `cannot change caas-image-repo: repository read-only, only authentication can be updated`) + + err = s.controller.ConfigSet(params.ControllerConfigSet{Config: map[string]interface{}{ + "caas-image-repo": `{"repository":"jujusolutions","username":"foo","password":"bar"}`, + }}) + c.Assert(err, gc.ErrorMatches, `cannot change caas-image-repo: unable to add authentication details`) + + err = s.State.UpdateControllerConfig(map[string]interface{}{ + "caas-image-repo": `{"repository":"jujusolutions","username":"bar","password":"foo"}`, + }, nil) + c.Assert(err, jc.ErrorIsNil) + + err = s.controller.ConfigSet(params.ControllerConfigSet{Config: map[string]interface{}{ + "caas-image-repo": `{"repository":"jujusolutions","username":"foo","password":"bar"}`, + }}) + c.Assert(err, jc.ErrorIsNil) + + config, err = s.State.ControllerConfig() + c.Assert(err, jc.ErrorIsNil) + repoDetails, err := docker.NewImageRepoDetails(config.CAASImageRepo()) + c.Assert(err, jc.ErrorIsNil) + c.Assert(repoDetails, gc.DeepEquals, docker.ImageRepoDetails{ + Repository: "jujusolutions", + BasicAuthConfig: docker.BasicAuthConfig{ + Username: "foo", + Password: "bar", + }, + }) +} + func (s *controllerSuite) TestMongoVersion(c *gc.C) { result, err := s.controller.MongoVersion() c.Assert(err, jc.ErrorIsNil) diff --git a/apiserver/facades/client/modelupgrader/findagents.go b/apiserver/facades/client/modelupgrader/findagents.go index 55364d5a7bd..a121469c82d 100644 --- a/apiserver/facades/client/modelupgrader/findagents.go +++ b/apiserver/facades/client/modelupgrader/findagents.go @@ -10,6 +10,7 @@ import ( "github.com/juju/juju/apiserver/common" "github.com/juju/juju/cloudconfig/podcfg" + "github.com/juju/juju/controller" coreos "github.com/juju/juju/core/os" "github.com/juju/juju/docker" envtools "github.com/juju/juju/environs/tools" @@ -95,13 +96,15 @@ func (m *ModelUpgraderAPI) agentVersionsForCAAS( streamsAgents coretools.List, ) (coretools.Versions, error) { result := coretools.Versions{} - imageRepoDetails := args.ControllerCfg.CAASImageRepo() + imageRepoDetails, err := docker.NewImageRepoDetails(args.ControllerCfg.CAASImageRepo()) + if err != nil { + return nil, errors.Annotatef(err, "parsing %s", controller.CAASImageRepo) + } if imageRepoDetails.Empty() { - repoDetails, err := docker.NewImageRepoDetails(podcfg.JujudOCINamespace) + imageRepoDetails, err = docker.NewImageRepoDetails(podcfg.JujudOCINamespace) if err != nil { return nil, errors.Trace(err) } - imageRepoDetails = *repoDetails } reg, err := m.registryAPIFunc(imageRepoDetails) if err != nil { diff --git a/apiserver/facades/controller/caasapplicationprovisioner/provisioner.go b/apiserver/facades/controller/caasapplicationprovisioner/provisioner.go index 983e1ff6351..ded0b99ba16 100644 --- a/apiserver/facades/controller/caasapplicationprovisioner/provisioner.go +++ b/apiserver/facades/controller/caasapplicationprovisioner/provisioner.go @@ -31,6 +31,7 @@ import ( "github.com/juju/juju/core/network" "github.com/juju/juju/core/resources" "github.com/juju/juju/core/status" + "github.com/juju/juju/docker" "github.com/juju/juju/environs/bootstrap" "github.com/juju/juju/environs/config" "github.com/juju/juju/environs/tags" @@ -353,6 +354,10 @@ func (a *API) provisioningInfo(appName names.ApplicationTag) (*params.CAASApplic return nil, errors.Annotatef(err, "getting application config") } base := app.Base() + imageRepoDetails, err := docker.NewImageRepoDetails(cfg.CAASImageRepo()) + if err != nil { + return nil, errors.Annotatef(err, "parsing %s", controller.CAASImageRepo) + } return ¶ms.CAASApplicationProvisioningInfo{ Version: vers, APIAddresses: addrs, @@ -362,7 +367,7 @@ func (a *API) provisioningInfo(appName names.ApplicationTag) (*params.CAASApplic Devices: devices, Constraints: mergedCons, Base: params.Base{Name: base.OS, Channel: base.Channel}, - ImageRepo: params.NewDockerImageInfo(cfg.CAASImageRepo(), imagePath), + ImageRepo: params.NewDockerImageInfo(imageRepoDetails, imagePath), CharmModifiedVersion: app.CharmModifiedVersion(), CharmURL: *charmURL, Trust: appConfig.GetBool(application.TrustConfigOptionName, false), diff --git a/apiserver/facades/controller/caasmodelconfigmanager/facade.go b/apiserver/facades/controller/caasmodelconfigmanager/facade.go index 86b7d4c9022..7ff0dde2e8f 100644 --- a/apiserver/facades/controller/caasmodelconfigmanager/facade.go +++ b/apiserver/facades/controller/caasmodelconfigmanager/facade.go @@ -7,14 +7,37 @@ import ( "github.com/juju/juju/apiserver/common" "github.com/juju/juju/apiserver/facade" "github.com/juju/juju/rpc/params" + "github.com/juju/juju/state" + "github.com/juju/juju/state/watcher" ) +//go:generate go run go.uber.org/mock/mockgen -package mocks -destination mocks/context_mock.go github.com/juju/juju/apiserver/facade Authorizer,Context,Resources + +// State provides required state for the Facade. +type State interface { + WatchControllerConfig() state.NotifyWatcher +} + // Facade allows model config manager clients to watch controller config changes and fetch controller config. type Facade struct { - auth facade.Authorizer + auth facade.Authorizer + resources facade.Resources + + ctrlState State controllerConfigAPI *common.ControllerConfigAPI } func (f *Facade) ControllerConfig() (params.ControllerConfigResult, error) { return f.controllerConfigAPI.ControllerConfig() } + +func (f *Facade) WatchControllerConfig() (params.NotifyWatchResult, error) { + result := params.NotifyWatchResult{} + w := f.ctrlState.WatchControllerConfig() + if _, ok := <-w.Changes(); ok { + result.NotifyWatcherId = f.resources.Register(w) + } else { + return result, watcher.EnsureErr(w) + } + return result, nil +} diff --git a/apiserver/facades/controller/caasmodelconfigmanager/register.go b/apiserver/facades/controller/caasmodelconfigmanager/register.go index c9751f0f0cf..502f07f62d8 100644 --- a/apiserver/facades/controller/caasmodelconfigmanager/register.go +++ b/apiserver/facades/controller/caasmodelconfigmanager/register.go @@ -33,6 +33,8 @@ func newFacade(ctx facade.Context) (*Facade, error) { } return &Facade{ auth: authorizer, + resources: ctx.Resources(), controllerConfigAPI: common.NewStateControllerConfig(systemState), + ctrlState: systemState, }, nil } diff --git a/apiserver/facades/controller/caasmodeloperator/operator.go b/apiserver/facades/controller/caasmodeloperator/operator.go index 2f4edd0e59f..79256d1486d 100644 --- a/apiserver/facades/controller/caasmodeloperator/operator.go +++ b/apiserver/facades/controller/caasmodeloperator/operator.go @@ -14,6 +14,8 @@ import ( apiservererrors "github.com/juju/juju/apiserver/errors" "github.com/juju/juju/apiserver/facade" "github.com/juju/juju/cloudconfig/podcfg" + "github.com/juju/juju/controller" + "github.com/juju/juju/docker" "github.com/juju/juju/rpc/params" "github.com/juju/juju/state/watcher" ) @@ -118,7 +120,12 @@ func (a *API) ModelOperatorProvisioningInfo() (params.ModelOperatorInfo, error) if err != nil { return result, errors.Trace(err) } - imageInfo := params.NewDockerImageInfo(controllerConf.CAASImageRepo(), registryPath) + + imageRepoDetails, err := docker.NewImageRepoDetails(controllerConf.CAASImageRepo()) + if err != nil { + return result, errors.Annotatef(err, "parsing %s", controller.CAASImageRepo) + } + imageInfo := params.NewDockerImageInfo(imageRepoDetails, registryPath) logger.Tracef("image info %v", imageInfo) result = params.ModelOperatorInfo{ diff --git a/apiserver/facades/controller/caasoperatorprovisioner/provisioner.go b/apiserver/facades/controller/caasoperatorprovisioner/provisioner.go index fb92a64f2f7..5c7fe55f3b6 100644 --- a/apiserver/facades/controller/caasoperatorprovisioner/provisioner.go +++ b/apiserver/facades/controller/caasoperatorprovisioner/provisioner.go @@ -18,6 +18,8 @@ import ( "github.com/juju/juju/caas/kubernetes/provider" k8sconstants "github.com/juju/juju/caas/kubernetes/provider/constants" "github.com/juju/juju/cloudconfig/podcfg" + "github.com/juju/juju/controller" + "github.com/juju/juju/docker" "github.com/juju/juju/environs/config" "github.com/juju/juju/environs/tags" "github.com/juju/juju/pki" @@ -132,7 +134,10 @@ func (a *API) OperatorProvisioningInfo(args params.Entities) (params.OperatorPro modelConfig, ) - imageRepo := cfg.CAASImageRepo() + imageRepo, err := docker.NewImageRepoDetails(cfg.CAASImageRepo()) + if err != nil { + return result, errors.Annotatef(err, "parsing %s", controller.CAASImageRepo) + } registryPath, err := podcfg.GetJujuOCIImagePath(cfg, vers) if err != nil { return result, errors.Trace(err) diff --git a/apiserver/facades/controller/caasunitprovisioner/provisioner.go b/apiserver/facades/controller/caasunitprovisioner/provisioner.go index d049f02c51c..7faee85f6a3 100644 --- a/apiserver/facades/controller/caasunitprovisioner/provisioner.go +++ b/apiserver/facades/controller/caasunitprovisioner/provisioner.go @@ -26,6 +26,7 @@ import ( "github.com/juju/juju/cloudconfig/podcfg" "github.com/juju/juju/controller" "github.com/juju/juju/core/status" + "github.com/juju/juju/docker" "github.com/juju/juju/environs/config" "github.com/juju/juju/environs/tags" "github.com/juju/juju/rpc/params" @@ -392,7 +393,12 @@ func (f *Facade) provisioningInfo(model Model, tagString string) (*params.Kubern if err != nil { return nil, errors.Trace(err) } - imageRepo := params.NewDockerImageInfo(controllerCfg.CAASImageRepo(), registryPath) + + imageRepoDetails, err := docker.NewImageRepoDetails(controllerCfg.CAASImageRepo()) + if err != nil { + return nil, errors.Annotatef(err, "parsing %s", controller.CAASImageRepo) + } + imageRepo := params.NewDockerImageInfo(imageRepoDetails, registryPath) logger.Tracef("imageRepo %v", imageRepo) filesystemParams, err := f.applicationFilesystemParams(app, controllerCfg, modelConfig) if err != nil { diff --git a/apiserver/facades/schema.json b/apiserver/facades/schema.json index 2fab8ed55bb..3fb36f9994b 100644 --- a/apiserver/facades/schema.json +++ b/apiserver/facades/schema.json @@ -10498,6 +10498,14 @@ "$ref": "#/definitions/ControllerConfigResult" } } + }, + "WatchControllerConfig": { + "type": "object", + "properties": { + "Result": { + "$ref": "#/definitions/NotifyWatchResult" + } + } } }, "definitions": { @@ -10518,6 +10526,46 @@ "required": [ "config" ] + }, + "Error": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "info": { + "type": "object", + "patternProperties": { + ".*": { + "type": "object", + "additionalProperties": true + } + } + }, + "message": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "message", + "code" + ] + }, + "NotifyWatchResult": { + "type": "object", + "properties": { + "NotifyWatcherId": { + "type": "string" + }, + "error": { + "$ref": "#/definitions/Error" + } + }, + "additionalProperties": false, + "required": [ + "NotifyWatcherId" + ] } } } diff --git a/caas/Dockerfile b/caas/Dockerfile index 007dda7e6ac..87868d09bbb 100644 --- a/caas/Dockerfile +++ b/caas/Dockerfile @@ -1,7 +1,6 @@ FROM public.ecr.aws/ubuntu/ubuntu:22.04 ARG TARGETOS ARG TARGETARCH -ARG BUILDOS # Add the syslog user for audit logging. RUN useradd --system -M syslog diff --git a/caas/kubernetes/provider/bootstrap.go b/caas/kubernetes/provider/bootstrap.go index 0e396928e2f..2c476526974 100644 --- a/caas/kubernetes/provider/bootstrap.go +++ b/caas/kubernetes/provider/bootstrap.go @@ -40,9 +40,12 @@ import ( "github.com/juju/juju/cloud" "github.com/juju/juju/cloudconfig" "github.com/juju/juju/cloudconfig/podcfg" + "github.com/juju/juju/controller" k8sannotations "github.com/juju/juju/core/annotations" corebase "github.com/juju/juju/core/base" "github.com/juju/juju/core/watcher" + "github.com/juju/juju/docker" + "github.com/juju/juju/docker/registry" "github.com/juju/juju/environs" environsbootstrap "github.com/juju/juju/environs/bootstrap" "github.com/juju/juju/juju/osenv" @@ -307,9 +310,30 @@ func newcontrollerStack( cs.resourceNameVolBootstrapParams = cs.getResourceName(cloudconfig.FileNameBootstrapParams) cs.resourceNameVolAgentConf = cs.getResourceName(agentconstants.AgentConfigFilename) - if cs.dockerAuthSecretData, err = pcfg.Controller.CAASImageRepo().SecretData(); err != nil { - return nil, errors.Trace(err) + // Initialize registry. + repoDetails, err := docker.NewImageRepoDetails(pcfg.Controller.CAASImageRepo()) + if err != nil { + return nil, errors.Annotatef(err, "parsing %s", controller.CAASImageRepo) } + if !repoDetails.Empty() { + reg, err := registry.New(repoDetails) + if err != nil { + return nil, errors.Trace(err) + } + defer func() { _ = reg.Close() }() + err = reg.RefreshAuth() + if err != nil { + return nil, errors.Trace(err) + } + err = reg.Ping() + if err != nil { + return nil, errors.Trace(err) + } + if cs.dockerAuthSecretData, err = reg.ImageRepoDetails().SecretData(); err != nil { + return nil, errors.Trace(err) + } + } + return cs, nil } @@ -1531,7 +1555,10 @@ func (c *controllerStack) buildContainerSpecForCommands(setupCmd, machineCmd str if err != nil { return nil, errors.Trace(err) } - repo := c.pcfg.Controller.CAASOperatorImagePath() + repo, err := docker.NewImageRepoDetails(c.pcfg.Controller.CAASImageRepo()) + if err != nil { + return nil, errors.Annotatef(err, "parsing %s", controller.CAASImageRepo) + } charmBaseImage, err := podcfg.ImageForBase(repo.Repository, charm.Base{ Name: strings.ToLower(os.String()), Channel: charm.Channel{ diff --git a/caas/kubernetes/provider/bootstrap_test.go b/caas/kubernetes/provider/bootstrap_test.go index 4eea3399a48..00a21b68b15 100644 --- a/caas/kubernetes/provider/bootstrap_test.go +++ b/caas/kubernetes/provider/bootstrap_test.go @@ -39,6 +39,7 @@ import ( "github.com/juju/juju/controller" k8sannotations "github.com/juju/juju/core/annotations" "github.com/juju/juju/core/constraints" + "github.com/juju/juju/docker" "github.com/juju/juju/environs/config" envtesting "github.com/juju/juju/environs/testing" "github.com/juju/juju/feature" @@ -447,7 +448,9 @@ func (s *bootstrapSuite) TestBootstrap(c *gc.C) { }, } - secretCAASImageRepoData, err := s.controllerCfg.CAASImageRepo().SecretData() + repoDetails, err := docker.NewImageRepoDetails(s.controllerCfg.CAASImageRepo()) + c.Assert(err, jc.ErrorIsNil) + secretCAASImageRepoData, err := repoDetails.SecretData() c.Assert(err, jc.ErrorIsNil) secretCAASImageRepo := &core.Secret{ diff --git a/caas/kubernetes/provider/export_test.go b/caas/kubernetes/provider/export_test.go index 7834d10190d..cf8be9d3ba8 100644 --- a/caas/kubernetes/provider/export_test.go +++ b/caas/kubernetes/provider/export_test.go @@ -109,7 +109,10 @@ func NewcontrollerStackForTest( pcfg *podcfg.ControllerPodConfig, ) (ControllerStackerForTest, error) { cs, err := newcontrollerStack(ctx, stackName, storageClass, broker, pcfg) - return cs.(*controllerStack), err + if err != nil { + return nil, err + } + return cs.(*controllerStack), nil } func Pod(u *workloadSpec) k8sspecs.PodSpecWithAnnotations { diff --git a/cloudconfig/podcfg/image.go b/cloudconfig/podcfg/image.go index 03dc0111dae..2eb19f31170 100644 --- a/cloudconfig/podcfg/image.go +++ b/cloudconfig/podcfg/image.go @@ -13,6 +13,7 @@ import ( "github.com/juju/version/v2" "github.com/juju/juju/controller" + "github.com/juju/juju/docker" ) const ( @@ -35,7 +36,11 @@ func (cfg *ControllerPodConfig) dbVersion() (version.Number, error) { // GetJujuDbOCIImagePath returns the juju-db oci image path. func (cfg *ControllerPodConfig) GetJujuDbOCIImagePath() (string, error) { - imageRepo := cfg.Controller.CAASImageRepo().Repository + details, err := docker.NewImageRepoDetails(cfg.Controller.CAASImageRepo()) + if err != nil { + return "", errors.Annotatef(err, "parsing %s", controller.CAASImageRepo) + } + imageRepo := details.Repository if imageRepo == "" { imageRepo = JujudOCINamespace } @@ -62,16 +67,20 @@ func IsCharmBaseImage(imagePath string) bool { func GetJujuOCIImagePath(controllerCfg controller.Config, ver version.Number) (string, error) { // First check the deprecated "caas-operator-image-path" config. imagePath, err := RebuildOldOperatorImagePath( - controllerCfg.CAASOperatorImagePath().Repository, ver, + controllerCfg.CAASOperatorImagePath(), ver, ) if imagePath != "" || err != nil { return imagePath, err } + details, err := docker.NewImageRepoDetails(controllerCfg.CAASImageRepo()) + if err != nil { + return "", errors.Annotatef(err, "parsing %s", controller.CAASImageRepo) + } tag := "" if ver != version.Zero { tag = ver.String() } - return imageRepoToPath(controllerCfg.CAASImageRepo().Repository, tag) + return imageRepoToPath(details.Repository, tag) } // RebuildOldOperatorImagePath returns a updated image path for the specified juju version. diff --git a/cmd/containeragent/initialize/package_test.go b/cmd/containeragent/initialize/package_test.go index 0718b48a26f..e5772c0f8d4 100644 --- a/cmd/containeragent/initialize/package_test.go +++ b/cmd/containeragent/initialize/package_test.go @@ -30,25 +30,25 @@ func (*importSuite) TestImports(c *gc.C) { "agent/constants", "api", "api/agent/agent", - "api/base", "api/agent/caasapplication", + "api/agent/keyupdater", + "api/base", "api/common", "api/common/cloudspec", - "api/agent/keyupdater", "api/watcher", "apiserver/errors", - "rpc/params", - "cmd/containeragent/utils", "caas/kubernetes/provider/constants", - "cmd/containeragent/utils", "charmhub", "charmhub/path", "charmhub/transport", "cloud", "cmd", "cmd/constants", + "cmd/containeragent/utils", + "cmd/containeragent/utils", "controller", "core/arch", + "core/base", "core/charm/metrics", "core/constraints", "core/devices", @@ -68,13 +68,9 @@ func (*importSuite) TestImports(c *gc.C) { "core/relation", "core/resources", "core/secrets", - "core/base", "core/status", "core/watcher", "docker", - "docker/registry", - "docker/registry/image", - "docker/registry/internal", "environs/cloudspec", "environs/config", "environs/context", @@ -94,6 +90,7 @@ func (*importSuite) TestImports(c *gc.C) { "proxy", "rpc", "rpc/jsoncodec", + "rpc/params", "service/common", "service/pebble/plan", "service/snap", diff --git a/cmd/juju/commands/bootstrap.go b/cmd/juju/commands/bootstrap.go index 7c4650bedf2..c580b21d261 100644 --- a/cmd/juju/commands/bootstrap.go +++ b/cmd/juju/commands/bootstrap.go @@ -42,6 +42,7 @@ import ( "github.com/juju/juju/core/instance" "github.com/juju/juju/core/model" "github.com/juju/juju/core/network" + "github.com/juju/juju/docker" "github.com/juju/juju/environs" "github.com/juju/juju/environs/bootstrap" environscloudspec "github.com/juju/juju/environs/cloudspec" @@ -1603,6 +1604,21 @@ func (c *bootstrapCommand) bootstrapConfigs( return bootstrapConfigs{}, errors.Annotate(err, "constructing bootstrap config") } + // Pre-process controller attributes. + if _, ok := controllerConfigAttrs[controller.CAASOperatorImagePath]; ok { + return bootstrapConfigs{}, fmt.Errorf("%q is no longer supported controller configuration", + controller.CAASOperatorImagePath) + } + if v, ok := controllerConfigAttrs[controller.CAASImageRepo]; ok { + if v, ok := v.(string); ok { + repoDetails, err := docker.LoadImageRepoDetails(v) + if err != nil { + return bootstrapConfigs{}, errors.Annotatef(err, "processing %s", controller.CAASImageRepo) + } + controllerConfigAttrs[controller.CAASImageRepo] = repoDetails.Content() + } + } + controllerConfig, err := controller.NewConfig( controllerUUID.String(), bootstrapConfig.CACert, diff --git a/cmd/juju/ssh/ssh_machine.go b/cmd/juju/ssh/ssh_machine.go index a481ba5c965..9968f0459e5 100644 --- a/cmd/juju/ssh/ssh_machine.go +++ b/cmd/juju/ssh/ssh_machine.go @@ -88,8 +88,8 @@ func (t *resolvedTarget) isAgent() bool { return targetIsAgent(t.entity) } -// SSHPort is the TCP port used for SSH connections. -const SSHPort = 22 +// sshPort is the TCP port used for SSH connections. +const sshPort = 22 func (c *sshMachine) SetFlags(f *gnuflag.FlagSet) { f.BoolVar(&c.proxy, "proxy", false, "Proxy through the API server") @@ -535,7 +535,7 @@ func (c *sshMachine) reachableAddressGetter(entity string) (string, error) { } } - usable := network.NewMachineHostPorts(SSHPort, addresses...).HostPorts().FilterUnusable() + usable := network.NewMachineHostPorts(sshPort, addresses...).HostPorts().FilterUnusable() best, err := c.hostChecker.FindHost(usable, publicKeys) if err != nil { return "", errors.Trace(err) diff --git a/controller/config.go b/controller/config.go index 4a641d9a75e..9bf950d3f1b 100755 --- a/controller/config.go +++ b/controller/config.go @@ -12,20 +12,15 @@ import ( "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" "github.com/juju/collections/set" "github.com/juju/errors" - "github.com/juju/loggo" "github.com/juju/names/v4" "github.com/juju/romulus" "github.com/juju/utils/v3" "gopkg.in/juju/environschema.v1" "gopkg.in/yaml.v2" - "github.com/juju/juju/docker" - "github.com/juju/juju/docker/registry" "github.com/juju/juju/pki" ) -var logger = loggo.GetLogger("juju.controller") - const ( // MongoProfLow represents the most conservative mongo memory profile. MongoProfLow = "low" @@ -455,23 +450,32 @@ var ( // config attributes that are allowed to be updated after the // controller has been created. AllowedUpdateConfigAttributes = set.NewStrings( - AgentLogfileMaxSize, AgentLogfileMaxBackups, + AgentLogfileMaxSize, AgentRateLimitMax, AgentRateLimitRate, APIPortOpenDelay, + ApplicationResourceDownloadLimit, AuditingEnabled, AuditLogCaptureArgs, AuditLogExcludeMethods, AuditLogMaxBackups, AuditLogMaxSize, + CAASImageRepo, // TODO Juju 3.0: ControllerAPIPort should be required and treated // more like api-port. ControllerAPIPort, ControllerName, + ControllerResourceDownloadLimit, + Features, + JujuHASpace, + JujuManagementSpace, + MaxAgentStateSize, + MaxCharmStateSize, MaxDebugLogDuration, MaxPruneTxnBatchSize, MaxPruneTxnPasses, + MigrationMinionWaitMax, ModelLogfileMaxBackups, ModelLogfileMaxSize, ModelLogsSize, @@ -479,14 +483,6 @@ var ( PruneTxnQueryCount, PruneTxnSleepTime, PublicDNSAddress, - JujuHASpace, - JujuManagementSpace, - Features, - MaxCharmStateSize, - MaxAgentStateSize, - MigrationMinionWaitMax, - ApplicationResourceDownloadLimit, - ControllerResourceDownloadLimit, QueryTracingEnabled, QueryTracingThreshold, ) @@ -935,51 +931,15 @@ func (c Config) JujuManagementSpace() string { // CAASOperatorImagePath sets the url of the docker image // used for the application operator. -func (c Config) CAASOperatorImagePath() (o docker.ImageRepoDetails) { - str := c.asString(CAASOperatorImagePath) - repoDetails, err := docker.NewImageRepoDetails(str) - if repoDetails != nil { - return *repoDetails - } - // This should not happen since we have done validation in c.Valiate(). - logger.Tracef("parsing controller config %q: %q, err %v", CAASOperatorImagePath, str, err) - return o -} - -func validateCAASImageRepo(imageRepo string) (string, error) { - if imageRepo == "" { - return "", nil - } - imageDetails, err := docker.NewImageRepoDetails(imageRepo) - if err != nil { - return "", errors.Trace(err) - } - if err = imageDetails.Validate(); err != nil { - return "", errors.Trace(err) - } - r, err := registry.New(*imageDetails) - if err != nil { - return "", errors.Trace(err) - } - defer func() { _ = r.Close() }() - - if err = r.Ping(); err != nil { - return "", errors.Trace(err) - } - return r.ImageRepoDetails().Content(), nil +// Deprecated: use CAASImageRepo +func (c Config) CAASOperatorImagePath() string { + return c.asString(CAASOperatorImagePath) } // CAASImageRepo sets the url of the docker repo // used for the jujud operator and mongo images. -func (c Config) CAASImageRepo() (o docker.ImageRepoDetails) { - str := c.asString(CAASImageRepo) - repoDetails, err := docker.NewImageRepoDetails(str) - if repoDetails != nil { - return *repoDetails - } - // This should not happen since we have done validation in c.Valiate(). - logger.Tracef("parsing controller config %q: %q, err %v", CAASImageRepo, str, err) - return o +func (c Config) CAASImageRepo() string { + return c.asString(CAASImageRepo) } // MeteringURL returns the URL to use for metering api calls. @@ -1169,19 +1129,6 @@ func Validate(c Config) error { return errors.Trace(err) } - var err error - if v, ok := c[CAASOperatorImagePath].(string); ok && v != "" { - if c[CAASOperatorImagePath], err = validateCAASImageRepo(v); err != nil { - return errors.Trace(err) - } - } - - if v, ok := c[CAASImageRepo].(string); ok && v != "" { - if c[CAASImageRepo], err = validateCAASImageRepo(v); err != nil { - return errors.Trace(err) - } - } - var auditLogMaxSize int if v, ok := c[AuditLogMaxSize].(string); ok { if size, err := utils.ParseSize(v); err != nil { diff --git a/controller/config_test.go b/controller/config_test.go index fddb32101f1..b0a1d71e94b 100755 --- a/controller/config_test.go +++ b/controller/config_test.go @@ -7,8 +7,6 @@ import ( "encoding/base64" "encoding/json" "fmt" - "io" - "net/http" stdtesting "testing" "time" @@ -20,6 +18,7 @@ import ( gc "gopkg.in/check.v1" "github.com/juju/juju/controller" + "github.com/juju/juju/docker" "github.com/juju/juju/docker/registry" "github.com/juju/juju/docker/registry/mocks" "github.com/juju/juju/testing" @@ -179,48 +178,6 @@ var newConfigTests = []struct { controller.ModelLogsSize: "0", }, expectError: "model logs size less than 1 MB not valid", -}, { - about: "invalid CAAS docker image repo", - config: controller.Config{ - controller.CAASImageRepo: "foo?bar", - }, - expectError: `docker image path "foo\?bar": invalid reference format`, -}, { - about: "empty CAAS docker image repo", - config: controller.Config{ - controller.CAASImageRepo: `{"foo": "bar"}`, - }, - expectError: `empty repository not valid`, -}, { - about: "invalid CAAS operator docker image repo - leading colon", - config: controller.Config{ - controller.CAASImageRepo: ":foo", - }, - expectError: `docker image path ":foo": invalid reference format`, -}, { - about: "invalid CAAS docker image repo - trailing colon", - config: controller.Config{ - controller.CAASImageRepo: `{"foo":""}`, - }, - expectError: `empty repository not valid`, -}, { - about: "invalid CAAS docker image repo - extra colon", - config: controller.Config{ - controller.CAASImageRepo: "foo::bar", - }, - expectError: `docker image path "foo::bar": invalid reference format`, -}, { - about: "invalid CAAS docker image repo - leading /", - config: controller.Config{ - controller.CAASImageRepo: "/foo", - }, - expectError: `docker image path "/foo": invalid reference format`, -}, { - about: "invalid CAAS docker image repo - extra /", - config: controller.Config{ - controller.CAASImageRepo: "foo//bar", - }, - expectError: `docker image path "foo//bar": invalid reference format`, }, { about: "negative controller-api-port", config: controller.Config{ @@ -673,42 +630,9 @@ func (s *ConfigSuite) TestConfigNoSpacesNilSpaceConfigPreserved(c *gc.C) { func (s *ConfigSuite) TestCAASImageRepo(c *gc.C) { ctrl := gomock.NewController(c) defer ctrl.Finish() + + // Ensure no requests are made from controller config code. mockRoundTripper := mocks.NewMockRoundTripper(ctrl) - gomock.InOrder( - mockRoundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn( - func(req *http.Request) (*http.Response, error) { - c.Assert(req.Method, gc.Equals, `GET`) - c.Assert(req.URL.String(), gc.Equals, `https://index.docker.io/v2`) - resps := &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(nil), - } - return resps, nil - }, - ), - mockRoundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn( - func(req *http.Request) (*http.Response, error) { - c.Assert(req.Method, gc.Equals, `GET`) - c.Assert(req.URL.String(), gc.Equals, `https://registry.foo.com/v2`) - resps := &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(nil), - } - return resps, nil - }, - ), - mockRoundTripper.EXPECT().RoundTrip(gomock.Any()).DoAndReturn( - func(req *http.Request) (*http.Response, error) { - c.Assert(req.Method, gc.Equals, `GET`) - c.Assert(req.URL.String(), gc.Equals, `https://ghcr.io/v2/`) - resps := &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(nil), - } - return resps, nil - }, - ), - ) s.PatchValue(®istry.DefaultTransport, mockRoundTripper) type tc struct { @@ -743,7 +667,9 @@ func (s *ConfigSuite) TestCAASImageRepo(c *gc.C) { }, ) c.Check(err, jc.ErrorIsNil) - c.Check(cfg.CAASImageRepo().Repository, gc.Equals, imageRepo.expected) + imageRepoDetails, err := docker.NewImageRepoDetails(cfg.CAASImageRepo()) + c.Check(err, jc.ErrorIsNil) + c.Check(imageRepoDetails.Repository, gc.Equals, imageRepo.expected) } } diff --git a/docker/auth.go b/docker/auth.go index 181bfd69337..496e71d1dd9 100644 --- a/docker/auth.go +++ b/docker/auth.go @@ -109,10 +109,6 @@ func (ac *TokenAuthConfig) Validate() error { return nil } -func (ac *TokenAuthConfig) init() error { - return nil -} - // BasicAuthConfig contains authorization information for basic auth. type BasicAuthConfig struct { // Auth is the base64 encoded "username:password" string. @@ -135,16 +131,6 @@ func (ba *BasicAuthConfig) Validate() error { return nil } -func (ba *BasicAuthConfig) init() error { - if ba.Empty() { - return nil - } - if ba.Auth.Empty() { - ba.Auth = NewToken(base64.StdEncoding.EncodeToString([]byte(ba.Username + ":" + ba.Password))) - } - return nil -} - // ImageRepoDetails contains authorization information for connecting to a Registry. type ImageRepoDetails struct { BasicAuthConfig `json:",inline" yaml:",inline"` @@ -181,6 +167,10 @@ func (rid ImageRepoDetails) SecretData() ([]byte, error) { return nil, nil } rid.Repository = "" + if !rid.BasicAuthConfig.Empty() && rid.BasicAuthConfig.Auth.Empty() { + rid.BasicAuthConfig.Auth = NewToken( + base64.StdEncoding.EncodeToString([]byte(rid.BasicAuthConfig.Username + ":" + rid.BasicAuthConfig.Password))) + } o := dockerConfigData{ Auths: map[string]ImageRepoDetails{ rid.ServerAddress: rid, @@ -191,6 +181,12 @@ func (rid ImageRepoDetails) SecretData() ([]byte, error) { // Content returns the json marshalled string with raw credentials. func (rid ImageRepoDetails) Content() string { + copy := rid + copy.Repository = "" + if copy.Empty() { + // If only repository is set, return it. + return rid.Repository + } d, _ := json.Marshal(rid) return string(d) } @@ -213,16 +209,6 @@ func (rid *ImageRepoDetails) Validate() error { return nil } -func (rid *ImageRepoDetails) init() error { - if err := rid.BasicAuthConfig.init(); err != nil { - return errors.Annotatef(err, "initializing basic auth config for repository %q", rid.Repository) - } - if err := rid.TokenAuthConfig.init(); err != nil { - return errors.Annotatef(err, "initializing token auth config for repository %q", rid.Repository) - } - return nil -} - // Empty checks if the auth information is empty. func (rid ImageRepoDetails) Empty() bool { return rid == ImageRepoDetails{} @@ -239,8 +225,25 @@ func fileExists(p string) (bool, error) { return !info.IsDir(), nil } -// NewImageRepoDetails tries to parse a file path or file content and returns an instance of ImageRepoDetails. -func NewImageRepoDetails(contentOrPath string) (o *ImageRepoDetails, err error) { +// NewImageRepoDetails tries to parse as json or basic repository path and returns an instance of ImageRepoDetails. +func NewImageRepoDetails(repo string) (o ImageRepoDetails, err error) { + if repo == "" { + return o, nil + } + data := []byte(repo) + err = json.Unmarshal(data, &o) + if err != nil { + logger.Tracef("unmarshalling %q, err %#v", repo, err) + return ImageRepoDetails{Repository: repo}, nil + } + if err = o.Validate(); err != nil { + return o, errors.Trace(err) + } + return o, nil +} + +// LoadImageRepoDetails tries to parse a file path or file content and returns an instance of ImageRepoDetails. +func LoadImageRepoDetails(contentOrPath string) (o ImageRepoDetails, err error) { if contentOrPath == "" { return o, nil } @@ -250,21 +253,8 @@ func NewImageRepoDetails(contentOrPath string) (o *ImageRepoDetails, err error) logger.Debugf("reading image repository information from %q", contentOrPath) data, err = os.ReadFile(contentOrPath) if err != nil { - return nil, errors.Trace(err) + return o, errors.Trace(err) } } - o = &ImageRepoDetails{} - err = json.Unmarshal(data, o) - if err != nil { - logger.Tracef("unmarshalling %q, err %#v", contentOrPath, err) - return &ImageRepoDetails{Repository: contentOrPath}, nil - } - - if err = o.Validate(); err != nil { - return nil, errors.Trace(err) - } - if err = o.init(); err != nil { - return nil, errors.Trace(err) - } - return o, nil + return NewImageRepoDetails(string(data)) } diff --git a/docker/auth_test.go b/docker/auth_test.go index 9b832c6f227..54e1095b8c9 100644 --- a/docker/auth_test.go +++ b/docker/auth_test.go @@ -4,7 +4,6 @@ package docker_test import ( - "encoding/base64" "os" "path/filepath" @@ -47,9 +46,9 @@ func (s *authSuite) TestNewImageRepoDetailsReadFromFile(c *gc.C) { fullpath := filepath.Join(dir, filename) err := os.WriteFile(fullpath, []byte(quayContent), 0644) c.Assert(err, jc.ErrorIsNil) - imageRepoDetails, err := docker.NewImageRepoDetails(fullpath) + imageRepoDetails, err := docker.LoadImageRepoDetails(fullpath) c.Assert(err, jc.ErrorIsNil) - c.Assert(imageRepoDetails, jc.DeepEquals, &docker.ImageRepoDetails{ + c.Assert(imageRepoDetails, jc.DeepEquals, docker.ImageRepoDetails{ Repository: "test-account", ServerAddress: "quay.io", BasicAuthConfig: docker.BasicAuthConfig{ @@ -61,7 +60,7 @@ func (s *authSuite) TestNewImageRepoDetailsReadFromFile(c *gc.C) { func (s *authSuite) TestNewImageRepoDetailsReadFromContent(c *gc.C) { imageRepoDetails, err := docker.NewImageRepoDetails(quayContent) c.Assert(err, jc.ErrorIsNil) - c.Assert(imageRepoDetails, jc.DeepEquals, &docker.ImageRepoDetails{ + c.Assert(imageRepoDetails, jc.DeepEquals, docker.ImageRepoDetails{ Repository: "test-account", ServerAddress: "quay.io", BasicAuthConfig: docker.BasicAuthConfig{ @@ -71,14 +70,13 @@ func (s *authSuite) TestNewImageRepoDetailsReadFromContent(c *gc.C) { imageRepoDetails, err = docker.NewImageRepoDetails(ecrContent) c.Assert(err, jc.ErrorIsNil) - c.Assert(imageRepoDetails, jc.DeepEquals, &docker.ImageRepoDetails{ + c.Assert(imageRepoDetails, jc.DeepEquals, docker.ImageRepoDetails{ Repository: "test-account", ServerAddress: "66668888.dkr.ecr.eu-west-1.amazonaws.com", Region: "ap-southeast-2", BasicAuthConfig: docker.BasicAuthConfig{ Username: "aws_access_key_id", Password: "aws_secret_access_key", - Auth: docker.NewToken(base64.StdEncoding.EncodeToString([]byte("aws_access_key_id:aws_secret_access_key"))), }, TokenAuthConfig: docker.TokenAuthConfig{ IdentityToken: docker.NewToken("xxxxx=="), @@ -95,7 +93,7 @@ func (s *authSuite) TestNewImageRepoDetailsReadDefaultServerAddress(c *gc.C) { `[1:] imageRepoDetails, err := docker.NewImageRepoDetails(data) c.Assert(err, jc.ErrorIsNil) - c.Assert(imageRepoDetails, jc.DeepEquals, &docker.ImageRepoDetails{ + c.Assert(imageRepoDetails, jc.DeepEquals, docker.ImageRepoDetails{ Repository: "qabot", BasicAuthConfig: docker.BasicAuthConfig{ Auth: docker.NewToken("xxxxx=="), diff --git a/docker/registry/internal/acr.go b/docker/registry/internal/acr.go index 81c398dec94..de18f22f187 100644 --- a/docker/registry/internal/acr.go +++ b/docker/registry/internal/acr.go @@ -29,6 +29,10 @@ func normalizeRepoDetailsAzure(repoDetails *docker.ImageRepoDetails) { } } +func (c *azureContainerRegistry) String() string { + return "azurecr.io" +} + // Match checks if the repository details matches current provider format. func (c *azureContainerRegistry) Match() bool { return strings.Contains(c.repoDetails.ServerAddress, "azurecr.io") diff --git a/docker/registry/internal/base_client.go b/docker/registry/internal/base_client.go index f1d2649eae9..c799f6eb1e5 100644 --- a/docker/registry/internal/base_client.go +++ b/docker/registry/internal/base_client.go @@ -77,9 +77,13 @@ func normalizeRepoDetailsCommon(repoDetails *docker.ImageRepoDetails) { } } +func (c *baseClient) String() string { + return "generic" +} + // ShouldRefreshAuth checks if the repoDetails should be refreshed. -func (c *baseClient) ShouldRefreshAuth() (bool, *time.Duration) { - return false, nil +func (c *baseClient) ShouldRefreshAuth() (bool, time.Duration) { + return false, time.Duration(0) } // RefreshAuth refreshes the repoDetails. diff --git a/docker/registry/internal/dockerhub.go b/docker/registry/internal/dockerhub.go index 2cbe9aa00ee..2517abfa35d 100644 --- a/docker/registry/internal/dockerhub.go +++ b/docker/registry/internal/dockerhub.go @@ -26,6 +26,10 @@ func newDockerhub(repoDetails docker.ImageRepoDetails, transport http.RoundTripp return &dockerhub{c} } +func (c *dockerhub) String() string { + return "docker.io" +} + // Match checks if the repository details matches current provider format. func (c *dockerhub) Match() bool { return c.repoDetails.ServerAddress == "" || strings.Contains(c.repoDetails.ServerAddress, "docker.io") diff --git a/docker/registry/internal/ecr.go b/docker/registry/internal/ecr.go index e95851656f9..aec2388d991 100644 --- a/docker/registry/internal/ecr.go +++ b/docker/registry/internal/ecr.go @@ -86,6 +86,10 @@ func normalizeRepoDetailsElasticContainerRegistry(repoDetails *docker.ImageRepoD } } +func (c *elasticContainerRegistry) String() string { + return "*.dkr.ecr.*.amazonaws.com" +} + // Match checks if the repository details matches current provider format. func (c *elasticContainerRegistry) Match() bool { return strings.Contains(c.repoDetails.ServerAddress, "amazonaws.com") @@ -123,16 +127,15 @@ func (c *elasticContainerRegistry) refreshTokenForElasticContainerRegistry(image } // ShouldRefreshAuth checks if the repoDetails should be refreshed. -func (c *elasticContainerRegistry) ShouldRefreshAuth() (bool, *time.Duration) { +func (c *elasticContainerRegistry) ShouldRefreshAuth() (bool, time.Duration) { if c.repoDetails.Auth.Empty() || c.repoDetails.Auth.ExpiresAt == nil { - return true, nil + return true, time.Duration(0) } d := time.Until(*c.repoDetails.Auth.ExpiresAt) if d <= advanceExpiry { - return true, nil + return true, time.Duration(0) } - nextCheckDuration := d - advanceExpiry - return false, &nextCheckDuration + return false, d - advanceExpiry } // RefreshAuth refreshes the repoDetails. @@ -146,13 +149,23 @@ func (c *elasticContainerRegistry) elasticContainerRegistryTransport( if repoDetails.BasicAuthConfig.Empty() { return nil, errors.NewNotValid(nil, "empty credential for elastic container registry") } - if err := c.refreshTokenForElasticContainerRegistry(repoDetails); err != nil { - return nil, errors.Trace(err) + if repoDetails.Region == "" { + return nil, errors.NewNotValid(nil, "region is required") } - if repoDetails.Auth.Empty() { - return nil, errors.NewNotValid(nil, "empty identity token for elastic container registry") + if repoDetails.Username == "" || repoDetails.Password == "" { + return nil, errors.NewNotValid(nil, + fmt.Sprintf("username and password are required for registry %q", repoDetails.Repository), + ) } - return newBasicTransport(transport, "", "", repoDetails.Auth.Value), nil + return dynamicTransportFunc(func() (http.RoundTripper, error) { + if err := c.refreshTokenForElasticContainerRegistry(repoDetails); err != nil { + return nil, errors.Trace(err) + } + if repoDetails.Auth.Empty() { + return nil, errors.NewNotValid(nil, "empty identity token for elastic container registry") + } + return newBasicTransport(transport, "", "", repoDetails.Auth.Value), nil + }), nil } func (c *elasticContainerRegistry) WrapTransport(...TransportWrapper) (err error) { @@ -204,6 +217,10 @@ func newElasticContainerRegistryPublic(repoDetails docker.ImageRepoDetails, tran return &elasticContainerRegistryPublic{c} } +func (c *elasticContainerRegistryPublic) String() string { + return "public.ecr.aws" +} + // Match checks if the repository details matches current provider format. func (c *elasticContainerRegistryPublic) Match() bool { return strings.Contains(c.repoDetails.ServerAddress, "public.ecr.aws") diff --git a/docker/registry/internal/ecr_test.go b/docker/registry/internal/ecr_test.go index 8ce05c0a11c..2e250f67511 100644 --- a/docker/registry/internal/ecr_test.go +++ b/docker/registry/internal/ecr_test.go @@ -67,7 +67,7 @@ func (s *elasticContainerRegistrySuite) getRegistry(c *gc.C, ensureAsserts func( {AuthorizationToken: aws.String(`xxxx===`)}, }, }, nil, - ) + ).AnyTimes() } } @@ -153,7 +153,7 @@ func (s *elasticContainerRegistrySuite) TestShouldRefreshAuthAuthTokenMissing(c } setImageRepoDetails(c, reg, repoDetails) shouldRefreshAuth, tick := reg.ShouldRefreshAuth() - c.Assert(tick, gc.IsNil) + c.Assert(tick, gc.Equals, time.Duration(0)) c.Assert(shouldRefreshAuth, jc.IsTrue) } @@ -172,7 +172,7 @@ func (s *elasticContainerRegistrySuite) TestShouldRefreshNoExpireTime(c *gc.C) { repoDetails.Auth = docker.NewToken(`xxx===`) setImageRepoDetails(c, reg, repoDetails) shouldRefreshAuth, tick := reg.ShouldRefreshAuth() - c.Assert(tick, gc.IsNil) + c.Assert(tick, gc.Equals, time.Duration(0)) c.Assert(shouldRefreshAuth, jc.IsTrue) } @@ -196,7 +196,7 @@ func (s *elasticContainerRegistrySuite) TestShouldRefreshTokenExpired(c *gc.C) { } setImageRepoDetails(c, reg, repoDetails) shouldRefreshAuth, tick := reg.ShouldRefreshAuth() - c.Assert(tick, gc.IsNil) + c.Assert(tick, gc.Equals, time.Duration(0)) c.Assert(shouldRefreshAuth, jc.IsTrue) // // already expired. @@ -207,7 +207,7 @@ func (s *elasticContainerRegistrySuite) TestShouldRefreshTokenExpired(c *gc.C) { } setImageRepoDetails(c, reg, repoDetails) shouldRefreshAuth, tick = reg.ShouldRefreshAuth() - c.Assert(tick, gc.IsNil) + c.Assert(tick, gc.Equals, time.Duration(0)) c.Assert(shouldRefreshAuth, jc.IsTrue) } diff --git a/docker/registry/internal/gcr.go b/docker/registry/internal/gcr.go index 44e430cd6f8..90333251c5d 100644 --- a/docker/registry/internal/gcr.go +++ b/docker/registry/internal/gcr.go @@ -22,6 +22,10 @@ func newGoogleContainerRegistry(repoDetails docker.ImageRepoDetails, transport h return &googleContainerRegistry{c} } +func (c *googleContainerRegistry) String() string { + return "gcr.io" +} + // Match checks if the repository details matches current provider format. func (c *googleContainerRegistry) Match() bool { return strings.Contains(c.repoDetails.ServerAddress, "gcr.io") diff --git a/docker/registry/internal/github.go b/docker/registry/internal/github.go index c34c1d4ac0f..a407dcc5f59 100644 --- a/docker/registry/internal/github.go +++ b/docker/registry/internal/github.go @@ -22,6 +22,10 @@ func newGithubContainerRegistry(repoDetails docker.ImageRepoDetails, transport h return &githubContainerRegistry{c} } +func (c *githubContainerRegistry) String() string { + return "ghcr.io" +} + // Match checks if the repository details matches current provider format. func (c *githubContainerRegistry) Match() bool { return strings.Contains(c.repoDetails.ServerAddress, "ghcr.io") diff --git a/docker/registry/internal/gitlab.go b/docker/registry/internal/gitlab.go index 591043703fd..7addcbc4bd7 100644 --- a/docker/registry/internal/gitlab.go +++ b/docker/registry/internal/gitlab.go @@ -19,6 +19,10 @@ func newGitlabContainerRegistry(repoDetails docker.ImageRepoDetails, transport h return &gitlabContainerRegistry{c} } +func (c *gitlabContainerRegistry) String() string { + return "registry.gitlab.com" +} + // Match checks if the repository details matches current provider format. func (c *gitlabContainerRegistry) Match() bool { return strings.Contains(c.repoDetails.ServerAddress, "registry.gitlab.com") diff --git a/docker/registry/internal/interface.go b/docker/registry/internal/interface.go index f37fc34a820..a047ccf3468 100644 --- a/docker/registry/internal/interface.go +++ b/docker/registry/internal/interface.go @@ -14,12 +14,13 @@ import ( // Registry provides APIs to interact with the OCI provider client. type Registry interface { + String() string Tags(string) (tools.Versions, error) GetArchitecture(imageName, tag string) (string, error) Close() error Ping() error ImageRepoDetails() docker.ImageRepoDetails - ShouldRefreshAuth() (bool, *time.Duration) + ShouldRefreshAuth() (bool, time.Duration) RefreshAuth() error } diff --git a/docker/registry/internal/provider.go b/docker/registry/internal/provider.go index 4b612b796c2..99954506e53 100644 --- a/docker/registry/internal/provider.go +++ b/docker/registry/internal/provider.go @@ -20,13 +20,13 @@ func NewBase(repoDetails docker.ImageRepoDetails, transport http.RoundTripper) * func Providers() []func(docker.ImageRepoDetails, http.RoundTripper) RegistryInternal { return []func(docker.ImageRepoDetails, http.RoundTripper) RegistryInternal{ newAzureContainerRegistry, - newDockerhub, newGitlabContainerRegistry, newGithubContainerRegistry, newQuayContainerRegistry, newGoogleContainerRegistry, newElasticContainerRegistry, newElasticContainerRegistryPublic, + newDockerhub, // DockerHub must be last as it matches on default domain. } } diff --git a/docker/registry/internal/quay.go b/docker/registry/internal/quay.go index 4466c45fb38..c58c5262d62 100644 --- a/docker/registry/internal/quay.go +++ b/docker/registry/internal/quay.go @@ -21,6 +21,10 @@ func newQuayContainerRegistry(repoDetails docker.ImageRepoDetails, transport htt return &quayContainerRegistry{c} } +func (c *quayContainerRegistry) String() string { + return "quay.io" +} + // Match checks if the repository details matches current provider format. func (c *quayContainerRegistry) Match() bool { return strings.Contains(c.repoDetails.ServerAddress, "quay.io") diff --git a/docker/registry/internal/transports.go b/docker/registry/internal/transports.go index 11acd47c3f7..ab7866b99e1 100644 --- a/docker/registry/internal/transports.go +++ b/docker/registry/internal/transports.go @@ -16,6 +16,17 @@ import ( "github.com/juju/errors" ) +type dynamicTransportFunc func() (http.RoundTripper, error) + +// RoundTrip executes a single HTTP transaction, returning a Response for the provided Request. +func (f dynamicTransportFunc) RoundTrip(req *http.Request) (*http.Response, error) { + transport, err := f() + if err != nil { + return nil, err + } + return transport.RoundTrip(req) +} + type basicTransport struct { transport http.RoundTripper username string diff --git a/docker/registry/mocks/registry_mock.go b/docker/registry/mocks/registry_mock.go index 87282ad68a6..05b72b19c72 100644 --- a/docker/registry/mocks/registry_mock.go +++ b/docker/registry/mocks/registry_mock.go @@ -108,11 +108,11 @@ func (mr *MockRegistryMockRecorder) RefreshAuth() *gomock.Call { } // ShouldRefreshAuth mocks base method. -func (m *MockRegistry) ShouldRefreshAuth() (bool, *time.Duration) { +func (m *MockRegistry) ShouldRefreshAuth() (bool, time.Duration) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ShouldRefreshAuth") ret0, _ := ret[0].(bool) - ret1, _ := ret[1].(*time.Duration) + ret1, _ := ret[1].(time.Duration) return ret0, ret1 } @@ -122,6 +122,20 @@ func (mr *MockRegistryMockRecorder) ShouldRefreshAuth() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ShouldRefreshAuth", reflect.TypeOf((*MockRegistry)(nil).ShouldRefreshAuth)) } +// String mocks base method. +func (m *MockRegistry) String() string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "String") + ret0, _ := ret[0].(string) + return ret0 +} + +// String indicates an expected call of String. +func (mr *MockRegistryMockRecorder) String() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "String", reflect.TypeOf((*MockRegistry)(nil).String)) +} + // Tags mocks base method. func (m *MockRegistry) Tags(arg0 string) (tools.Versions, error) { m.ctrl.T.Helper() diff --git a/docker/registry/registry_test.go b/docker/registry/registry_test.go index 5f9865b65aa..63d9b9e81a1 100644 --- a/docker/registry/registry_test.go +++ b/docker/registry/registry_test.go @@ -4,12 +4,55 @@ package registry_test import ( - "github.com/juju/testing" + jc "github.com/juju/testing/checkers" gc "gopkg.in/check.v1" + + "github.com/juju/juju/docker" + "github.com/juju/juju/docker/registry" ) type registrySuite struct { - testing.IsolationSuite } var _ = gc.Suite(®istrySuite{}) + +func (s *registrySuite) TestSelectsAWSPrivate(c *gc.C) { + reg, err := registry.New(docker.ImageRepoDetails{ + Repository: "123456.dkr.ecr.eu-west-1.amazonaws.com", + BasicAuthConfig: docker.BasicAuthConfig{ + Username: "access key id", + Password: "secret key", + }, + Region: "us-west-1", + }) + c.Assert(err, jc.ErrorIsNil) + c.Assert(reg, gc.NotNil) + c.Assert(reg.String(), gc.Equals, "*.dkr.ecr.*.amazonaws.com") +} + +func (s *registrySuite) TestSelectsDockerHub(c *gc.C) { + reg, err := registry.New(docker.ImageRepoDetails{ + Repository: "jujusolutions", + }) + c.Assert(err, jc.ErrorIsNil) + c.Assert(reg, gc.NotNil) + c.Assert(reg.String(), gc.Equals, "docker.io") +} + +func (s *registrySuite) TestSelectsGithubContainerRegistry(c *gc.C) { + reg, err := registry.New(docker.ImageRepoDetails{ + Repository: "ghcr.io/juju", + }) + c.Assert(err, jc.ErrorIsNil) + c.Assert(reg, gc.NotNil) + c.Assert(reg.String(), gc.Equals, "ghcr.io") +} + +func (s *registrySuite) TestSelectsAWSPublic(c *gc.C) { + reg, err := registry.New(docker.ImageRepoDetails{ + Repository: "public.ecr.aws/juju", + }) + c.Assert(err, jc.ErrorIsNil) + c.Assert(reg, gc.NotNil) + c.Assert(reg.String(), gc.Equals, "public.ecr.aws") +} diff --git a/make_functions.sh b/make_functions.sh index 179c0be9132..749bf765e92 100755 --- a/make_functions.sh +++ b/make_functions.sh @@ -9,12 +9,14 @@ JUJUD_BIN_DIR=${JUJUD_BIN_DIR:-${BUILD_DIR}/bin} # Versioning variables JUJU_BUILD_NUMBER=${JUJU_BUILD_NUMBER:-} +JUJU_DB_VERSION=${JUJU_DB_VERSION:-} # Docker variables -DOCKER_USERNAME=${DOCKER_USERNAME:-jujusolutions} +OCI_BUILDER=${OCI_BUILDER:-docker} +DOCKER_USERNAME=${DOCKER_USERNAME:-docker.io/jujusolutions} DOCKER_BUILDX_CONTEXT=${DOCKER_BUILDX_CONTEXT:-juju-make} DOCKER_STAGING_DIR="${BUILD_DIR}/docker-staging" -DOCKER_BIN=${DOCKER_BIN:-$(which docker || true)} +DOCKER_BIN=${DOCKER_BIN:-$(which ${OCI_BUILDER} || true)} readonly docker_staging_dir="docker-staging" @@ -22,10 +24,10 @@ readonly docker_staging_dir="docker-staging" # Docker staging directory under the build path. The staging directory's path # is returned as the output of this function. _make_docker_staging_dir() { - dir="${BUILD_DIR}/${docker_staging_dir}" - rm -rf "$dir" - mkdir -p "$dir" - echo "$dir" + dir="${BUILD_DIR}/${docker_staging_dir}" + rm -rf "$dir" + mkdir -p "$dir" + echo "$dir" } _juju_version() { @@ -39,23 +41,23 @@ _image_version() { } microk8s_operator_update() { - echo "Uploading image $(operator_image_path) to microk8s" - # For macos we have to push the image into the microk8s multipass vm because - # we can't use the ctr to stream off the local machine. - if [[ $(uname) = "Darwin" ]]; then - tmp_docker_image="/tmp/juju-operator-image-${RANDOM}.image" - docker save $(operator_image_path) | multipass transfer - microk8s-vm:${tmp_docker_image} - microk8s ctr --namespace k8s.io image import ${tmp_docker_image} - multipass exec microk8s-vm rm "${tmp_docker_image}" - return - fi - - # Linux we can stream the file like normal. - docker save "$(operator_image_path)" | microk8s.ctr --namespace k8s.io image import - + echo "Uploading image $(operator_image_path) to microk8s" + # For macos we have to push the image into the microk8s multipass vm because + # we can't use the ctr to stream off the local machine. + if [[ $(uname) = "Darwin" ]]; then + tmp_docker_image="/tmp/juju-operator-image-${RANDOM}.image" + "${DOCKER_BIN}" save $(operator_image_path) | multipass transfer - microk8s-vm:${tmp_docker_image} + microk8s ctr --namespace k8s.io image import ${tmp_docker_image} + multipass exec microk8s-vm rm "${tmp_docker_image}" + return + fi + + # Linux we can stream the file like normal. + "${DOCKER_BIN}" save "$(operator_image_path)" | microk8s.ctr --namespace k8s.io image import - } juju_version() { - (cd "${PROJECT_DIR}" && go run version/helper/main.go) + (cd "${PROJECT_DIR}" && go run version/helper/main.go) } operator_image_release_path() { @@ -85,52 +87,84 @@ operator_image_path() { build_push_operator_image() { build_multi_osarch=${1-""} if [[ -z "$build_multi_osarch" ]]; then - build_multi_osarch="$(go env GOOS)/$(go env GOARCH)" + build_multi_osarch="$(go env GOOS)/$(go env GOARCH)" fi # We need to find any ppc64el references and move the build artefacts over # to ppc64le so that it works with Docker. for platform in $build_multi_osarch; do - if [[ "$platform" = *"ppc64el"* ]]; then - echo "detected operator image build for ppc64el \"${platform}\"" - new_platform=$(echo "$platform" | sed 's/ppc64el/ppc64le/g') - echo "changing platform \"${platform}\" to platform \"${new_platform}\"" - - platform_dir="${BUILD_DIR}/$(echo "$platform" | sed 's/\//_/g')" - new_platform_dir="${BUILD_DIR}/$(echo "$new_platform" | sed 's/\//_/g')" - if ! [[ -d "$platform_dir" ]]; then - echo "platform build directory \"${platform_dir}\" does not exist" - exit 1 + if [[ "$platform" = *"ppc64el"* ]]; then + echo "detected operator image build for ppc64el \"${platform}\"" + new_platform=$(echo "$platform" | sed 's/ppc64el/ppc64le/g') + echo "changing platform \"${platform}\" to platform \"${new_platform}\"" + + platform_dir="${BUILD_DIR}/$(echo "$platform" | sed 's/\//_/g')" + new_platform_dir="${BUILD_DIR}/$(echo "$new_platform" | sed 's/\//_/g')" + if ! [[ -d "$platform_dir" ]]; then + echo "platform build directory \"${platform_dir}\" does not exist" + exit 1 + fi + + echo "copying platform build directory \"${platform_dir}\" to \"${new_platform_dir}\"" + cp -r "$platform_dir" "$new_platform_dir" fi - - echo "copying platform build directory \"${platform_dir}\" to \"${new_platform_dir}\"" - cp -r "$platform_dir" "$new_platform_dir" - fi done build_multi_osarch=$(echo "$build_multi_osarch" | sed 's/ppc64el/ppc64le/g') push_image=${2:-"false"} - output="-o type=oci,dest=${BUILD_DIR}/oci.tar.gz" - if [[ "$push_image" = true ]]; then - output="-o type=image,push=true" - elif [[ $(echo "$build_multi_osarch" | wc -w) -eq 1 ]]; then - output="-o type=docker" - fi build_multi_osarch=$(echo $build_multi_osarch | sed 's/ /,/g') WORKDIR=$(_make_docker_staging_dir) cp "${PROJECT_DIR}/caas/Dockerfile" "${WORKDIR}/" cp "${PROJECT_DIR}/caas/requirements.txt" "${WORKDIR}/" - BUILDX_NO_DEFAULT_ATTESTATIONS=true DOCKER_BUILDKIT=1 "$DOCKER_BIN" buildx build \ - --builder "$DOCKER_BUILDX_CONTEXT" \ - -f "${WORKDIR}/Dockerfile" \ - -t "$(operator_image_path)" \ - --platform="$build_multi_osarch" \ - --provenance=false \ - ${output} \ - "${BUILD_DIR}" + if [[ "${OCI_BUILDER}" = "docker" ]]; then + output="-o type=oci,dest=${BUILD_DIR}/oci.tar.gz" + if [[ "$push_image" = true ]]; then + output="-o type=image,push=true" + elif [[ $(echo "$build_multi_osarch" | wc -w) -eq 1 ]]; then + output="-o type=docker" + fi + BUILDX_NO_DEFAULT_ATTESTATIONS=true DOCKER_BUILDKIT=1 "$DOCKER_BIN" buildx build \ + --builder "$DOCKER_BUILDX_CONTEXT" \ + -f "${WORKDIR}/Dockerfile" \ + -t "$(operator_image_path)" \ + --platform="$build_multi_osarch" \ + --provenance=false \ + ${output} \ + "${BUILD_DIR}" + elif [[ "${OCI_BUILDER}" = "podman" ]]; then + "$DOCKER_BIN" build \ + --jobs "4" \ + -f "${WORKDIR}/Dockerfile" \ + -t "$(operator_image_path)" \ + --platform="$build_multi_osarch" \ + "${BUILD_DIR}" + if [[ "$push_image" = true ]]; then + "$DOCKER_BIN" push "$(operator_image_path)" + fi + else + echo "unknown OCI_BUILDER=${OCI_BUILDER} expected docker or podman" + exit 1 + fi +} + +seed_repository() { + set -x + "$DOCKER_BIN" pull "docker.io/jujusolutions/juju-db:${JUJU_DB_VERSION}" + "$DOCKER_BIN" tag "docker.io/jujusolutions/juju-db:${JUJU_DB_VERSION}" "${DOCKER_USERNAME}/juju-db:${JUJU_DB_VERSION}" + "$DOCKER_BIN" push "${DOCKER_USERNAME}/juju-db:${JUJU_DB_VERSION}" + + # copy all the lts that are available + for (( i = 18; ; i += 2 )); do + if "$DOCKER_BIN" pull "docker.io/jujusolutions/charm-base:ubuntu-$i.04" ; then + "$DOCKER_BIN" tag "docker.io/jujusolutions/charm-base:ubuntu-$i.04" "${DOCKER_USERNAME}/charm-base:ubuntu-$i.04" + "$DOCKER_BIN" push "${DOCKER_USERNAME}/charm-base:ubuntu-$i.04" + else + break + fi + done } wait_for_dpkg() { diff --git a/provider/equinix/environ.go b/provider/equinix/environ.go index da3b7c639c9..9049fefa692 100644 --- a/provider/equinix/environ.go +++ b/provider/equinix/environ.go @@ -25,7 +25,6 @@ import ( "github.com/juju/juju/cloudconfig/cloudinit" "github.com/juju/juju/cloudconfig/instancecfg" "github.com/juju/juju/cloudconfig/providerinit" - "github.com/juju/juju/cmd/juju/ssh" "github.com/juju/juju/core/arch" corebase "github.com/juju/juju/core/base" "github.com/juju/juju/core/constraints" @@ -46,6 +45,8 @@ import ( var logger = loggo.GetLogger("juju.provider.equinix") +const sshPort = 22 + type environConfig struct { config *config.Config attrs map[string]interface{} @@ -295,8 +296,8 @@ func getCloudConfig(args environs.StartInstanceParams) (cloudinit.CloudConfig, e cloudCfg.AddPackage("jq") // Set a default INPUT policy of drop, permitting ssh - iptablesDefault := strings.Split(fmt.Sprintf(defaultIPTablesCommands, ssh.SSHPort), "\n") - iptablesDefault = append(iptablesDefault, fmt.Sprintf(acceptInputPort, ssh.SSHPort)) + iptablesDefault := strings.Split(fmt.Sprintf(defaultIPTablesCommands, sshPort), "\n") + iptablesDefault = append(iptablesDefault, fmt.Sprintf(acceptInputPort, sshPort)) if args.InstanceConfig.IsController() { for _, port := range []int{ args.InstanceConfig.ControllerConfig.APIPort(), diff --git a/tests/README.md b/tests/README.md index 0eb66eb512a..1538a61861d 100644 --- a/tests/README.md +++ b/tests/README.md @@ -58,7 +58,7 @@ sudo snap install expect The static analysis tests also require `golangci-lint`: ``` -go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.53.0 +go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.54.2 ``` To get started, it's best to quickly look at the help command from the runner. diff --git a/tests/suites/static_analysis/lint_go.sh b/tests/suites/static_analysis/lint_go.sh index cea4754b975..059870bc032 100644 --- a/tests/suites/static_analysis/lint_go.sh +++ b/tests/suites/static_analysis/lint_go.sh @@ -1,7 +1,7 @@ run_go() { VER=$(golangci-lint --version | tr -s ' ' | cut -d ' ' -f 4 | cut -d '.' -f 1,2) - if [[ ${VER} != "1.53" ]] && [[ ${VER} != "v1.53" ]]; then - (echo >&2 -e '\nError: golangci-lint version does not match 1.53. Please upgrade/downgrade to the right version.') + if [[ ${VER} != "1.54" ]] && [[ ${VER} != "v1.54" ]]; then + (echo >&2 -e '\nError: golangci-lint version does not match 1.54. Please upgrade/downgrade to the right version.') exit 1 fi OUT=$(golangci-lint run -c .github/golangci-lint.config.yaml 2>&1) diff --git a/worker/caasmodelconfigmanager/mocks/facade_mock.go b/worker/caasmodelconfigmanager/mocks/facade_mock.go index 1df25fded1f..4f2ef40d481 100644 --- a/worker/caasmodelconfigmanager/mocks/facade_mock.go +++ b/worker/caasmodelconfigmanager/mocks/facade_mock.go @@ -8,6 +8,7 @@ import ( reflect "reflect" controller "github.com/juju/juju/controller" + watcher "github.com/juju/juju/core/watcher" gomock "go.uber.org/mock/gomock" ) @@ -48,3 +49,18 @@ func (mr *MockFacadeMockRecorder) ControllerConfig() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ControllerConfig", reflect.TypeOf((*MockFacade)(nil).ControllerConfig)) } + +// WatchControllerConfig mocks base method. +func (m *MockFacade) WatchControllerConfig() (watcher.NotifyWatcher, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "WatchControllerConfig") + ret0, _ := ret[0].(watcher.NotifyWatcher) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// WatchControllerConfig indicates an expected call of WatchControllerConfig. +func (mr *MockFacadeMockRecorder) WatchControllerConfig() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WatchControllerConfig", reflect.TypeOf((*MockFacade)(nil).WatchControllerConfig)) +} diff --git a/worker/caasmodelconfigmanager/worker.go b/worker/caasmodelconfigmanager/worker.go index b71c0ff837e..8b20e0ff503 100644 --- a/worker/caasmodelconfigmanager/worker.go +++ b/worker/caasmodelconfigmanager/worker.go @@ -4,6 +4,7 @@ package caasmodelconfigmanager import ( + "reflect" "time" "github.com/juju/clock" @@ -16,10 +17,16 @@ import ( "github.com/juju/juju/api/base" api "github.com/juju/juju/api/controller/caasmodelconfigmanager" "github.com/juju/juju/controller" + "github.com/juju/juju/core/watcher" "github.com/juju/juju/docker" "github.com/juju/juju/docker/registry" ) +const ( + retryDuration = 1 * time.Second + refreshDuration = 30 * time.Second +) + // Logger represents the methods used by the worker to log details. type Logger interface { Debugf(string, ...interface{}) @@ -34,6 +41,7 @@ type Logger interface { //go:generate go run go.uber.org/mock/mockgen -package mocks -destination mocks/facade_mock.go github.com/juju/juju/worker/caasmodelconfigmanager Facade type Facade interface { ControllerConfig() (controller.Config, error) + WatchControllerConfig() (watcher.NotifyWatcher, error) } //go:generate go run go.uber.org/mock/mockgen -package mocks -destination mocks/broker_mock.go github.com/juju/juju/worker/caasmodelconfigmanager CAASBroker @@ -85,10 +93,6 @@ type manager struct { clock clock.Clock registryFunc func(docker.ImageRepoDetails) (registry.Registry, error) - reg registry.Registry - - nextTickDuration *time.Duration - ticker clock.Timer } // NewFacade returns a facade for caasapplicationprovisioner worker to use. @@ -129,78 +133,105 @@ func (w *manager) Wait() error { } func (w *manager) loop() (err error) { - defer func() { - if w.ticker != nil && !w.ticker.Stop() { - select { - case <-w.ticker.Chan(): - default: - } - } - }() - controllerConfig, err := w.config.Facade.ControllerConfig() + watcher, err := w.config.Facade.WatchControllerConfig() if err != nil { return errors.Trace(err) } - repoDetails := controllerConfig.CAASImageRepo() - if !repoDetails.IsPrivate() { - // No ops for public registry config. - return nil - } - w.reg, err = w.registryFunc(repoDetails) + err = w.catacomb.Add(watcher) if err != nil { return errors.Trace(err) } - if err = w.reg.Ping(); err != nil { - return errors.Trace(err) - } - if err := w.ensureImageRepoSecret(true); err != nil { - return errors.Trace(err) - } + + var ( + refresh <-chan struct{} + timeout <-chan time.Time + deadline time.Time + reg registry.Registry + lastRepoDetails docker.ImageRepoDetails + ) + first := false + signal := make(chan struct{}) + close(signal) + defer func() { + if reg != nil { + _ = reg.Close() + } + }() for { select { case <-w.catacomb.Dying(): return w.catacomb.ErrDying() - case <-w.getTickerChan(): - if err := w.ensureImageRepoSecret(false); err != nil { + case <-watcher.Changes(): + controllerConfig, err := w.config.Facade.ControllerConfig() + if err != nil { return errors.Trace(err) } - } - } -} - -func (w *manager) getTickerChan() <-chan time.Time { - d := w.getTickerDuration() - if w.ticker == nil { - w.ticker = w.clock.NewTimer(d) - } else { - if !w.ticker.Stop() { - select { - case <-w.ticker.Chan(): - default: + repoDetails, err := docker.NewImageRepoDetails(controllerConfig.CAASImageRepo()) + if err != nil { + return errors.Annotatef(err, "parsing %s", controller.CAASImageRepo) + } + if reflect.DeepEqual(repoDetails, lastRepoDetails) { + continue + } + lastRepoDetails = repoDetails + if !repoDetails.IsPrivate() { + timeout = nil + refresh = nil + continue + } + if reg != nil { + _ = reg.Close() + } + reg, err = w.registryFunc(repoDetails) + if err != nil { + return errors.Trace(err) + } + if err = reg.Ping(); err != nil { + return errors.Trace(err) + } + first = true + refresh = signal + case <-timeout: + timeout = nil + if refresh == nil { + refresh = signal + } + case <-refresh: + refresh = nil + next, err := w.ensureImageRepoSecret(reg, first) + if err != nil { + w.logger.Errorf("failed to update repository secret: %s", err.Error()) + next = retryDuration + } else { + first = false + } + if nextDeadline := w.clock.Now().Add(next); timeout == nil || nextDeadline.Before(deadline) { + deadline = nextDeadline + timeout = w.clock.After(next) } } - w.ticker.Reset(d) } - return w.ticker.Chan() } -func (w *manager) getTickerDuration() time.Duration { - if w.nextTickDuration != nil { - return *w.nextTickDuration +func (w *manager) ensureImageRepoSecret(reg registry.Registry, force bool) (time.Duration, error) { + shouldRefresh, nextRefresh := reg.ShouldRefreshAuth() + if nextRefresh == time.Duration(0) { + nextRefresh = refreshDuration + } + if !shouldRefresh && !force { + return nextRefresh, nil } - return 30 * time.Second -} -func (w *manager) ensureImageRepoSecret(isFirstCall bool) error { - var shouldRefresh bool - if shouldRefresh, w.nextTickDuration = w.reg.ShouldRefreshAuth(); !shouldRefresh && !isFirstCall { - return nil + w.logger.Debugf("refreshing auth token for %q", w.name) + if err := reg.RefreshAuth(); err != nil { + return time.Duration(0), errors.Annotatef(err, "refreshing registry auth token for %q", w.name) } - if err := w.reg.RefreshAuth(); err != nil { - return errors.Annotatef(err, "refreshing registry auth token for %q", w.name) + + w.logger.Debugf("applying refreshed auth token for %q", w.name) + err := w.config.Broker.EnsureImageRepoSecret(reg.ImageRepoDetails()) + if err != nil { + return time.Duration(0), errors.Annotatef(err, "ensuring image repository secret for %q", w.name) } - w.logger.Debugf("auth token for %q has been refreshed, applying to the secret now", w.name) - err := w.config.Broker.EnsureImageRepoSecret(w.reg.ImageRepoDetails()) - return errors.Annotatef(err, "ensuring image repository secret for %q", w.name) + return nextRefresh, nil } diff --git a/worker/caasmodelconfigmanager/worker_test.go b/worker/caasmodelconfigmanager/worker_test.go index 64fb81cbb90..386b7a2c82b 100644 --- a/worker/caasmodelconfigmanager/worker_test.go +++ b/worker/caasmodelconfigmanager/worker_test.go @@ -4,7 +4,6 @@ package caasmodelconfigmanager_test import ( - "encoding/base64" "time" "github.com/juju/clock/testclock" @@ -18,6 +17,8 @@ import ( gc "gopkg.in/check.v1" "github.com/juju/juju/controller" + "github.com/juju/juju/core/watcher" + "github.com/juju/juju/core/watcher/watchertest" "github.com/juju/juju/docker" "github.com/juju/juju/docker/registry" registrymocks "github.com/juju/juju/docker/registry/mocks" @@ -37,7 +38,7 @@ type workerSuite struct { facade *mocks.MockFacade broker *mocks.MockCAASBroker reg *registrymocks.MockRegistry - clock *testclock.Clock + clock testclock.AdvanceableClock controllerConfig controller.Config } @@ -46,7 +47,7 @@ func (s *workerSuite) SetUpTest(c *gc.C) { s.modelTag = names.NewModelTag("ffffffff-ffff-ffff-ffff-ffffffffffff") s.logger = loggo.GetLogger("test") s.controllerConfig = coretesting.FakeControllerConfig() - s.clock = testclock.NewClock(time.Time{}) + s.clock = testclock.NewDilatedWallClock(testing.ShortWait) } func (s *workerSuite) TearDownTest(c *gc.C) { @@ -121,7 +122,7 @@ func (s *workerSuite) getWorkerStarter(c *gc.C) (func(...*gomock.Call) worker.Wo Broker: s.broker, Clock: s.clock, RegistryFunc: func(i docker.ImageRepoDetails) (registry.Registry, error) { - c.Assert(i, gc.DeepEquals, s.controllerConfig.CAASImageRepo()) + c.Check(i, gc.DeepEquals, s.CAASImageRepo(c)) return s.reg, nil }, } @@ -129,9 +130,6 @@ func (s *workerSuite) getWorkerStarter(c *gc.C) (func(...*gomock.Call) worker.Wo gomock.InOrder(calls...) w, err := caasmodelconfigmanager.NewWorker(cfg) c.Assert(err, jc.ErrorIsNil) - s.AddCleanup(func(c *gc.C) { - workertest.CleanKill(c, w) - }) return w }, ctrl } @@ -146,61 +144,60 @@ func (s *workerSuite) TestWorkerTokenRefreshRequired(c *gc.C) { "region": "ap-southeast-2" }`[1:] - refreshed := s.controllerConfig.CAASImageRepo() + refreshed := s.CAASImageRepo(c) refreshed.Auth = docker.NewToken(`refreshed===`) done := make(chan struct{}, 1) startWorker, ctrl := s.getWorkerStarter(c) defer ctrl.Finish() - _ = startWorker( + controllerConfigChangedChan := make(chan struct{}, 1) + w := startWorker( + s.facade.EXPECT().WatchControllerConfig().DoAndReturn(func() (watcher.NotifyWatcher, error) { + controllerConfigChangedChan <- struct{}{} + return watchertest.NewMockNotifyWatcher(controllerConfigChangedChan), nil + }), // 1st round. s.facade.EXPECT().ControllerConfig().Return(s.controllerConfig, nil), s.reg.EXPECT().Ping().Return(nil), - s.reg.EXPECT().ShouldRefreshAuth().Return(true, nil), + s.reg.EXPECT().ShouldRefreshAuth().Return(true, time.Duration(0)), s.reg.EXPECT().RefreshAuth().Return(nil), - s.reg.EXPECT().ImageRepoDetails().DoAndReturn( - func() docker.ImageRepoDetails { - o := s.controllerConfig.CAASImageRepo() - c.Assert(o, gc.DeepEquals, docker.ImageRepoDetails{ - ServerAddress: "66668888.dkr.ecr.eu-west-1.amazonaws.com", - Repository: "66668888.dkr.ecr.eu-west-1.amazonaws.com", - Region: "ap-southeast-2", - BasicAuthConfig: docker.BasicAuthConfig{ - Username: "aws_access_key_id", - Password: "aws_secret_access_key", - Auth: docker.NewToken(base64.StdEncoding.EncodeToString([]byte("aws_access_key_id:aws_secret_access_key"))), - }, - }) - return o - }, - ), - s.broker.EXPECT().EnsureImageRepoSecret(gomock.Any()).DoAndReturn( - func(i docker.ImageRepoDetails) error { - c.Assert(i, gc.DeepEquals, s.controllerConfig.CAASImageRepo()) - return nil - }, - ), + s.reg.EXPECT().ImageRepoDetails().DoAndReturn(func() docker.ImageRepoDetails { + o := s.CAASImageRepo(c) + c.Check(o, gc.DeepEquals, docker.ImageRepoDetails{ + ServerAddress: "66668888.dkr.ecr.eu-west-1.amazonaws.com", + Repository: "66668888.dkr.ecr.eu-west-1.amazonaws.com", + Region: "ap-southeast-2", + BasicAuthConfig: docker.BasicAuthConfig{ + Username: "aws_access_key_id", + Password: "aws_secret_access_key", + }, + }) + return o + }), + s.broker.EXPECT().EnsureImageRepoSecret(gomock.Any()).DoAndReturn(func(i docker.ImageRepoDetails) error { + c.Check(i, gc.DeepEquals, s.CAASImageRepo(c)) + return nil + }), // 2nd round. - s.reg.EXPECT().ShouldRefreshAuth().Return(true, nil), + s.reg.EXPECT().ShouldRefreshAuth().Return(true, time.Duration(0)), s.reg.EXPECT().RefreshAuth().Return(nil), s.reg.EXPECT().ImageRepoDetails().Return(refreshed), - s.broker.EXPECT().EnsureImageRepoSecret(gomock.Any()).DoAndReturn( - func(i docker.ImageRepoDetails) error { - c.Assert(i, gc.DeepEquals, refreshed) - close(done) - return nil - }, - ), + s.broker.EXPECT().EnsureImageRepoSecret(gomock.Any()).DoAndReturn(func(i docker.ImageRepoDetails) error { + c.Check(i, gc.DeepEquals, refreshed) + close(done) + return nil + }), + s.reg.EXPECT().Close().Return(nil), ) - err := s.clock.WaitAdvance(30*time.Second, coretesting.ShortWait, 1) - c.Assert(err, jc.ErrorIsNil) select { case <-done: case <-time.After(coretesting.LongWait): c.Fatalf("timed out waiting for worker to start") } + + workertest.CleanKill(c, w) } func (s *workerSuite) TestWorkerTokenRefreshNotRequiredThenRetry(c *gc.C) { @@ -217,63 +214,59 @@ func (s *workerSuite) TestWorkerTokenRefreshNotRequiredThenRetry(c *gc.C) { startWorker, ctrl := s.getWorkerStarter(c) defer ctrl.Finish() - _ = startWorker( + controllerConfigChangedChan := make(chan struct{}, 1) + w := startWorker( + s.facade.EXPECT().WatchControllerConfig().DoAndReturn(func() (watcher.NotifyWatcher, error) { + controllerConfigChangedChan <- struct{}{} + return watchertest.NewMockNotifyWatcher(controllerConfigChangedChan), nil + }), // 1st round. s.facade.EXPECT().ControllerConfig().Return(s.controllerConfig, nil), s.reg.EXPECT().Ping().Return(nil), - s.reg.EXPECT().ShouldRefreshAuth().Return(true, nil), + s.reg.EXPECT().ShouldRefreshAuth().Return(true, time.Duration(0)), s.reg.EXPECT().RefreshAuth().Return(nil), - s.reg.EXPECT().ImageRepoDetails().DoAndReturn( - func() docker.ImageRepoDetails { - o := s.controllerConfig.CAASImageRepo() - c.Assert(o, gc.DeepEquals, docker.ImageRepoDetails{ - ServerAddress: "66668888.dkr.ecr.eu-west-1.amazonaws.com", - Repository: "66668888.dkr.ecr.eu-west-1.amazonaws.com", - Region: "ap-southeast-2", - BasicAuthConfig: docker.BasicAuthConfig{ - Username: "aws_access_key_id", - Password: "aws_secret_access_key", - Auth: docker.NewToken(base64.StdEncoding.EncodeToString([]byte("aws_access_key_id:aws_secret_access_key"))), - }, - }) - return o - }, - ), - s.broker.EXPECT().EnsureImageRepoSecret(gomock.Any()).DoAndReturn( - func(i docker.ImageRepoDetails) error { - c.Assert(i, gc.DeepEquals, s.controllerConfig.CAASImageRepo()) - return nil - }, - ), + s.reg.EXPECT().ImageRepoDetails().DoAndReturn(func() docker.ImageRepoDetails { + o := s.CAASImageRepo(c) + c.Check(o, gc.DeepEquals, docker.ImageRepoDetails{ + ServerAddress: "66668888.dkr.ecr.eu-west-1.amazonaws.com", + Repository: "66668888.dkr.ecr.eu-west-1.amazonaws.com", + Region: "ap-southeast-2", + BasicAuthConfig: docker.BasicAuthConfig{ + Username: "aws_access_key_id", + Password: "aws_secret_access_key", + }, + }) + return o + }), + s.broker.EXPECT().EnsureImageRepoSecret(gomock.Any()).DoAndReturn(func(i docker.ImageRepoDetails) error { + c.Check(i, gc.DeepEquals, s.CAASImageRepo(c)) + return nil + }), // 2nd round. - s.reg.EXPECT().ShouldRefreshAuth().DoAndReturn(func() (bool, *time.Duration) { - nextTick := 7 * time.Minute - return false, &nextTick + s.reg.EXPECT().ShouldRefreshAuth().DoAndReturn(func() (bool, time.Duration) { + return false, 1 * time.Second }), // 3rd round. - s.reg.EXPECT().ShouldRefreshAuth().DoAndReturn(func() (bool, *time.Duration) { - return true, nil + s.reg.EXPECT().ShouldRefreshAuth().DoAndReturn(func() (bool, time.Duration) { + return true, time.Duration(0) }), s.reg.EXPECT().RefreshAuth().Return(nil), - s.reg.EXPECT().ImageRepoDetails().Return(s.controllerConfig.CAASImageRepo()), - s.broker.EXPECT().EnsureImageRepoSecret(gomock.Any()).DoAndReturn( - func(i docker.ImageRepoDetails) error { - c.Assert(i, gc.DeepEquals, s.controllerConfig.CAASImageRepo()) - close(done) - return nil - }, - ), + s.reg.EXPECT().ImageRepoDetails().Return(s.CAASImageRepo(c)), + s.broker.EXPECT().EnsureImageRepoSecret(gomock.Any()).DoAndReturn(func(i docker.ImageRepoDetails) error { + c.Check(i, gc.DeepEquals, s.CAASImageRepo(c)) + close(done) + return nil + }), + s.reg.EXPECT().Close().Return(nil), ) - err := s.clock.WaitAdvance(30*time.Second, coretesting.ShortWait, 1) - c.Assert(err, jc.ErrorIsNil) - err = s.clock.WaitAdvance(7*time.Minute, coretesting.ShortWait, 1) - c.Assert(err, jc.ErrorIsNil) select { case <-done: case <-time.After(coretesting.LongWait): c.Fatalf("timed out waiting for worker to start") } + + workertest.CleanKill(c, w) } func (s *workerSuite) TestWorkerNoOpsForPublicRepo(c *gc.C) { @@ -288,7 +281,12 @@ func (s *workerSuite) TestWorkerNoOpsForPublicRepo(c *gc.C) { startWorker, ctrl := s.getWorkerStarter(c) defer ctrl.Finish() - _ = startWorker( + controllerConfigChangedChan := make(chan struct{}, 1) + w := startWorker( + s.facade.EXPECT().WatchControllerConfig().DoAndReturn(func() (watcher.NotifyWatcher, error) { + controllerConfigChangedChan <- struct{}{} + return watchertest.NewMockNotifyWatcher(controllerConfigChangedChan), nil + }), s.facade.EXPECT().ControllerConfig().DoAndReturn(func() (controller.Config, error) { close(done) return s.controllerConfig, nil @@ -300,4 +298,12 @@ func (s *workerSuite) TestWorkerNoOpsForPublicRepo(c *gc.C) { case <-time.After(coretesting.LongWait): c.Fatalf("timed out waiting for worker to start") } + + workertest.CleanKill(c, w) +} + +func (s *workerSuite) CAASImageRepo(c *gc.C) docker.ImageRepoDetails { + r, err := docker.NewImageRepoDetails(s.controllerConfig.CAASImageRepo()) + c.Assert(err, jc.ErrorIsNil) + return r }