Skip to content

✨clusterctl: inspect images in provider components #2104

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

Merged
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
8 changes: 8 additions & 0 deletions cmd/clusterctl/cmd/config_providers.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,14 @@ func componentsDefaultOutput(c client.Components) error {
fmt.Printf(" %s\n", v)
}
}
if len(c.Images()) > 0 {
fmt.Println("Images:")
fmt.Println(" Name")
fmt.Println(" ----")
for _, v := range c.Images() {
fmt.Printf(" %s\n", v)
}
}

return nil
}
46 changes: 44 additions & 2 deletions cmd/clusterctl/pkg/client/repository/components.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,16 +47,20 @@ var variableRegEx = regexp.MustCompile(`\${\s*([A-Z0-9_]+)\s*}`)
// 4. Set the watching namespace for the provider controller
// 5. Adds labels to all the components in order to allow easy identification of the provider objects
type Components interface {
// configuration of the provider the template belongs to.
// configuration of the provider the provider components belongs to.
config.Provider

// Version of the provider.
Version() string

// Variables required by the template.
// Variables required by the provider components.
// This value is derived by the component YAML.
Variables() []string

// Images required to install the provider components.
// This value is derived by the component YAML.
Images() []string

// TargetNamespace where the provider components will be installed.
// By default this value is derived by the component YAML, but it is possible to override it
// during the creation of the Components object.
Expand All @@ -83,6 +87,7 @@ type components struct {
config.Provider
version string
variables []string
images []string
targetNamespace string
watchingNamespace string
objs []unstructured.Unstructured
Expand All @@ -99,6 +104,10 @@ func (c *components) Variables() []string {
return c.variables
}

func (c *components) Images() []string {
return c.images
}

func (c *components) TargetNamespace() string {
return c.targetNamespace
}
Expand Down Expand Up @@ -161,6 +170,12 @@ func newComponents(provider config.Provider, version string, rawyaml []byte, con
return nil, errors.Wrap(err, "failed to parse yaml")
}

// inspect the list of objects for the images required by the provider component
images, err := inspectImages(objs)
if err != nil {
return nil, errors.Wrap(err, "failed to detect required images")
}

// inspect the list of objects for the default target namespace
// the default target namespace is the namespace object defined in the component yaml read from the repository, if any
defaultTargetNamespace, err := inspectTargetNamespace(objs)
Expand Down Expand Up @@ -215,6 +230,7 @@ func newComponents(provider config.Provider, version string, rawyaml []byte, con
Provider: provider,
version: version,
variables: variables,
images: images,
targetNamespace: targetNamespace,
watchingNamespace: watchingNamespace,
objs: objs,
Expand Down Expand Up @@ -522,6 +538,32 @@ func remove(slice []string, i int) []string {
return slice[:len(slice)-1]
}

// inspectImages identifies the container images required to install the provider.
// NB. The implemented approach is specific for the provider components YAML; it is not intended to cover
// all the possible objects used to deploy containers existing in Kubernetes.
func inspectImages(objs []unstructured.Unstructured) ([]string, error) {
images := []string{}

for _, o := range objs {
if o.GetKind() == deploymentKind {
d := &appsv1.Deployment{}
if err := scheme.Scheme.Convert(&o, d, nil); err != nil { //nolint
return nil, err
}

for _, c := range d.Spec.Template.Spec.Containers {
images = append(images, c.Image)
}

for _, c := range d.Spec.Template.Spec.InitContainers {
images = append(images, c.Image)
}
}
}

return images, nil
}

// addLabels ensures all the provider components have a consistent set of labels
func addLabels(objs []unstructured.Unstructured, name string) []unstructured.Unstructured {
for _, o := range objs {
Expand Down
119 changes: 119 additions & 0 deletions cmd/clusterctl/pkg/client/repository/components_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -737,6 +737,125 @@ func Test_fixWatchNamespace(t *testing.T) {
}
}

func Test_inspectImages(t *testing.T) {
type args struct {
objs []unstructured.Unstructured
}
tests := []struct {
name string
args args
want []string
wantErr bool
}{
{
name: "controller without the RBAC proxy",
args: args{
objs: []unstructured.Unstructured{
{
Object: map[string]interface{}{
"apiVersion": "apps/v1",
"kind": deploymentKind,
"spec": map[string]interface{}{
"template": map[string]interface{}{
"spec": map[string]interface{}{
"containers": []map[string]interface{}{
{
"name": controllerContainerName,
"image": "gcr.io/k8s-staging-cluster-api/cluster-api-controller:master",
},
},
},
},
},
},
},
},
},
want: []string{"gcr.io/k8s-staging-cluster-api/cluster-api-controller:master"},
wantErr: false,
},
{
name: "controller with the RBAC proxy",
args: args{
objs: []unstructured.Unstructured{
{
Object: map[string]interface{}{
"apiVersion": "apps/v1",
"kind": deploymentKind,
"spec": map[string]interface{}{
"template": map[string]interface{}{
"spec": map[string]interface{}{
"containers": []map[string]interface{}{
{
"name": controllerContainerName,
"image": "gcr.io/k8s-staging-cluster-api/cluster-api-controller:master",
},
{
"name": "kube-rbac-proxy",
"image": "gcr.io/kubebuilder/kube-rbac-proxy:v0.4.1",
},
},
},
},
},
},
},
},
},
want: []string{"gcr.io/k8s-staging-cluster-api/cluster-api-controller:master", "gcr.io/kubebuilder/kube-rbac-proxy:v0.4.1"},
wantErr: false,
},
{
name: "controller with init container",
args: args{
objs: []unstructured.Unstructured{
{
Object: map[string]interface{}{
"apiVersion": "apps/v1",
"kind": deploymentKind,
"spec": map[string]interface{}{
"template": map[string]interface{}{
"spec": map[string]interface{}{
"containers": []map[string]interface{}{
{
"name": controllerContainerName,
"image": "gcr.io/k8s-staging-cluster-api/cluster-api-controller:master",
},
},
"initContainers": []map[string]interface{}{
{
"name": controllerContainerName,
"image": "gcr.io/k8s-staging-cluster-api/cluster-api-controller:init",
},
},
},
},
},
},
},
},
},
want: []string{"gcr.io/k8s-staging-cluster-api/cluster-api-controller:master", "gcr.io/k8s-staging-cluster-api/cluster-api-controller:init"},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := inspectImages(tt.args.objs)
if (err != nil) != tt.wantErr {
t.Fatalf("error = %v, wantErr %v", err, tt.wantErr)
}
if tt.wantErr {
return
}

if !reflect.DeepEqual(got, tt.want) {
t.Errorf("got = %v, want %v", got, tt.want)
}
})
}
}

func Test_addLabels(t *testing.T) {
type args struct {
objs []unstructured.Unstructured
Expand Down