Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🌱 [clusterctl] Allow single image override #3480

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 35 additions & 21 deletions cmd/clusterctl/client/config/imagemeta_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ limitations under the License.
package config

import (
"fmt"
"strings"

"github.com/pkg/errors"
Expand Down Expand Up @@ -50,24 +51,29 @@ func newImageMetaClient(reader Reader) *imageMetaClient {
}
}

func (p *imageMetaClient) AlterImage(component, image string) (string, error) {
// Gets the image meta that applies to the selected component; if none, returns early
meta, err := p.getImageMetaByComponent(component)
func (p *imageMetaClient) AlterImage(component, imageString string) (string, error) {
image, err := container.ImageFromString(imageString)
if err != nil {
return "", err
}

// Gets the image meta that applies to the selected component/image; if none, returns early
meta, err := p.getImageMeta(component, image.Name)
if err != nil {
return "", err
}
if meta == nil {
return image, nil
return imageString, nil
}

// Apply the image meta to image name
return meta.ApplyToImage(image)
return meta.ApplyToImage(image), nil
}

// getImageMetaByComponent returns the image meta that applies to the selected component
func (p *imageMetaClient) getImageMetaByComponent(component string) (*imageMeta, error) {
// getImageMeta returns the image meta that applies to the selected component/image
func (p *imageMetaClient) getImageMeta(component, imageName string) (*imageMeta, error) {
// if the image meta for the component is already known, return it
if im, ok := p.imageMetaCache[component]; ok {
if im, ok := p.imageMetaCache[imageMetaCacheKey(component, imageName)]; ok {
return im, nil
}

Expand All @@ -79,23 +85,37 @@ func (p *imageMetaClient) getImageMetaByComponent(component string) (*imageMeta,

// If there are not image override configurations, return.
if meta == nil {
p.imageMetaCache[component] = nil
p.imageMetaCache[imageMetaCacheKey(component, imageName)] = nil
return nil, nil
}

// Gets the image configuration and to the specific component, and returns the union of the two.
// Gets the image configuration for:
// - all the components,
// - the component (and to all its images)
// - the selected component/image
// and returns the union of all the above.
m := &imageMeta{}
if allMeta, ok := meta[allImageConfig]; ok {
m.Union(&allMeta)
}

if componentMeta, ok := meta[component]; ok {
m.Union(&componentMeta)
}

p.imageMetaCache[component] = m

if imageNameMeta, ok := meta[imageMetaCacheKey(component, imageName)]; ok {
m.Union(&imageNameMeta)
}
p.imageMetaCache[imageMetaCacheKey(component, imageName)] = m

return m, nil
}

func imageMetaCacheKey(component, imageName string) string {
return fmt.Sprintf("%s/%s", component, imageName)
}

// imageMeta allows to define transformations to apply to the image contained in the YAML manifests.
type imageMeta struct {
// repository sets the container registry to pull images from.
Expand All @@ -117,21 +137,15 @@ func (i *imageMeta) Union(other *imageMeta) {
}

// ApplyToImage changes an image name applying the transformations defined in the current imageMeta.
func (i *imageMeta) ApplyToImage(image string) (string, error) {

newImage, err := container.ImageFromString(image)
if err != nil {
return "", err
}

func (i *imageMeta) ApplyToImage(image container.Image) string {
// apply transformations
if i.Repository != "" {
newImage.Repository = strings.TrimSuffix(i.Repository, "/")
image.Repository = strings.TrimSuffix(i.Repository, "/")
}
if i.Tag != "" {
newImage.Tag = i.Tag
image.Tag = i.Tag
}

// returns the resulting image name
return newImage.String(), nil
return image.String()
}
121 changes: 116 additions & 5 deletions cmd/clusterctl/client/config/imagemeta_client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ func Test_imageMetaClient_AlterImage(t *testing.T) {
wantErr bool
}{
{
name: "no image config, image should not be changes",
name: "no image config: images should not be changes",
fields: fields{
reader: test.NewFakeReader(),
},
Expand All @@ -52,7 +52,31 @@ func Test_imageMetaClient_AlterImage(t *testing.T) {
wantErr: false,
},
{
name: "image config for cert-manager, image for the cert-manager should be changed",
name: "image config for cert-manager/cert-manager-cainjector: image for the cert-manager/cert-manager-cainjector should be changed",
fields: fields{
reader: test.NewFakeReader().WithImageMeta("cert-manager/cert-manager-cainjector", "foo-repository.io", "foo-tag"),
},
args: args{
component: "cert-manager",
image: "quay.io/jetstack/cert-manager-cainjector:v0.11.0",
},
want: "foo-repository.io/cert-manager-cainjector:foo-tag",
wantErr: false,
},
{
name: "image config for cert-manager/cert-manager-cainjector: image for the cert-manager/cert-manager-webhook should not be changed",
fields: fields{
reader: test.NewFakeReader().WithImageMeta("cert-manager/cert-manager-cainjector", "foo-repository.io", "foo-tag"),
},
args: args{
component: "cert-manager",
image: "quay.io/jetstack/cert-manager-webhook:v0.11.0",
},
want: "quay.io/jetstack/cert-manager-webhook:v0.11.0",
wantErr: false,
},
{
name: "image config for cert-manager: images for the cert-manager should be changed",
fields: fields{
reader: test.NewFakeReader().WithImageMeta("cert-manager", "foo-repository.io", "foo-tag"),
},
Expand All @@ -64,7 +88,49 @@ func Test_imageMetaClient_AlterImage(t *testing.T) {
wantErr: false,
},
{
name: "image config for all, image for the cert-manager should be changed",
name: "image config for cert-manager/cert-manager-cainjector and for cert-manager: images for the cert-manager/cert-manager-cainjector should be changed according to the most specific",
fields: fields{
reader: test.NewFakeReader().
WithImageMeta("cert-manager/cert-manager-cainjector", "foo-repository.io", "foo-tag").
WithImageMeta("cert-manager", "bar-repository.io", "bar-tag"),
},
args: args{
component: "cert-manager",
image: "quay.io/jetstack/cert-manager-cainjector:v0.11.0",
},
want: "foo-repository.io/cert-manager-cainjector:foo-tag",
wantErr: false,
},
{
name: "image config for cert-manager/cert-manager-cainjector and for cert-manager: images for the cert-manager/cert-manager-cainjector should be changed according to the most specific (mixed case)",
fields: fields{
reader: test.NewFakeReader().
WithImageMeta("cert-manager/cert-manager-cainjector", "foo-repository.io", "").
WithImageMeta("cert-manager", "", "bar-tag"),
},
args: args{
component: "cert-manager",
image: "quay.io/jetstack/cert-manager-cainjector:v0.11.0",
},
want: "foo-repository.io/cert-manager-cainjector:bar-tag",
wantErr: false,
},
{
name: "image config for cert-manager/cert-manager-cainjector and for cert-manager: images for the cert-manager/cert-manager-webhook should be changed according to the most generic",
fields: fields{
reader: test.NewFakeReader().
WithImageMeta("cert-manager/cert-manager-cainjector", "foo-repository.io", "foo-tag").
WithImageMeta("cert-manager", "bar-repository.io", "bar-tag"),
},
args: args{
component: "cert-manager",
image: "quay.io/jetstack/cert-manager-webhook:v0.11.0",
},
want: "bar-repository.io/cert-manager-webhook:bar-tag",
wantErr: false,
},
{
name: "image config for all: images for the cert-manager should be changed",
fields: fields{
reader: test.NewFakeReader().WithImageMeta(allImageConfig, "foo-repository.io", "foo-tag"),
},
Expand All @@ -76,7 +142,7 @@ func Test_imageMetaClient_AlterImage(t *testing.T) {
wantErr: false,
},
{
name: "image config for all and image config for cert-manager, image for the cert-manager should be changed according to the most specific",
name: "image config for all and for cert-manager: images for the cert-manager should be changed according to the most specific",
fields: fields{
reader: test.NewFakeReader().
WithImageMeta(allImageConfig, "foo-repository.io", "foo-tag").
Expand All @@ -90,7 +156,7 @@ func Test_imageMetaClient_AlterImage(t *testing.T) {
wantErr: false,
},
{
name: "image config for all and image config for cert-manager, image for the cert-manager should be changed according to the most specific (mixed case)",
name: "image config for all and for cert-manager: images for the cert-manager should be changed according to the most specific (mixed case)",
fields: fields{
reader: test.NewFakeReader().
WithImageMeta(allImageConfig, "foo-repository.io", "").
Expand All @@ -103,6 +169,51 @@ func Test_imageMetaClient_AlterImage(t *testing.T) {
want: "foo-repository.io/cert-manager-cainjector:bar-tag",
wantErr: false,
},
{
name: "image config for cert-manager/cert-manager-cainjector, for cert-manager and for all: images for the cert-manager/cert-manager-cainjector should be changed according to the most specific",
fields: fields{
reader: test.NewFakeReader().
WithImageMeta("cert-manager/cert-manager-cainjector", "foo-repository.io", "foo-tag").
WithImageMeta("cert-manager", "bar-repository.io", "bar-tag").
WithImageMeta(allImageConfig, "baz-repository.io", "baz-tag"),
},
args: args{
component: "cert-manager",
image: "quay.io/jetstack/cert-manager-cainjector:v0.11.0",
},
want: "foo-repository.io/cert-manager-cainjector:foo-tag",
wantErr: false,
},
{
name: "image config for cert-manager/cert-manager-cainjector, for cert-manager and for all: images for the cert-manager/cert-manager-cainjector should be changed according to the most specific (mixed case)",
fields: fields{
reader: test.NewFakeReader().
WithImageMeta("cert-manager/cert-manager-cainjector", "foo-repository.io", "").
WithImageMeta("cert-manager", "", "bar-tag").
WithImageMeta(allImageConfig, "baz-repository.io", "baz-tag"),
},
args: args{
component: "cert-manager",
image: "quay.io/jetstack/cert-manager-cainjector:v0.11.0",
},
want: "foo-repository.io/cert-manager-cainjector:bar-tag",
wantErr: false,
},
{
name: "image config for cert-manager/cert-manager-cainjector, for cert-manager and for all: images for the cert-manager/cert-manager-webhook should be changed according to the most generic",
fields: fields{
reader: test.NewFakeReader().
WithImageMeta("cert-manager/cert-manager-cainjector", "foo-repository.io", "foo-tag").
WithImageMeta("cert-manager", "bar-repository.io", "").
WithImageMeta(allImageConfig, "baz-repository.io", "baz-tag"),
},
args: args{
component: "cert-manager",
image: "quay.io/jetstack/cert-manager-webhook:v0.11.0",
},
want: "bar-repository.io/cert-manager-webhook:baz-tag",
wantErr: false,
},
{
name: "fails if wrong image config",
fields: fields{
Expand Down
3 changes: 2 additions & 1 deletion cmd/clusterctl/client/repository/components_client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ var controllerYaml = []byte("apiVersion: apps/v1\n" +
" template:\n" +
" spec:\n" +
" containers:\n" +
" - name: manager\n")
" - name: manager\n" +
" image: docker.io/library/image:latest\n")

const namespaceName = "capa-system"

Expand Down
10 changes: 10 additions & 0 deletions docs/book/src/clusterctl/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,16 @@ images:
In this example we are overriding the image repository for all the components and the image tag for
all the images in the cert-manager component.

If required to alter only a specific image you can use:

```yaml
images:
all:
repository: myorg.io/local-repo
cert-manager/cert-manager-cainjector:
tag: v0.11.1
```

## Cert-Manager timeout override

For situations when resources are limited or the network is slow, the cert-manager wait time to be running can be customized by adding a field to the clusterctl config file, for example:
Expand Down