From 3f1734a441759413e0fbc907ea67d154c8798363 Mon Sep 17 00:00:00 2001 From: Aaron Hoffman <31711338+Aaron-9900@users.noreply.github.com> Date: Mon, 25 Nov 2024 16:29:00 +0100 Subject: [PATCH 1/8] Add GetResourceImages Signed-off-by: Aaron Hoffman <31711338+Aaron-9900@users.noreply.github.com> --- pkg/sync/common/types.go | 3 +- pkg/sync/sync_context.go | 3 +- pkg/utils/kube/kube.go | 28 +++++++++++++++++ pkg/utils/kube/kube_test.go | 62 ++++++++++++++++++++++++++++++++++++- 4 files changed, 93 insertions(+), 3 deletions(-) diff --git a/pkg/sync/common/types.go b/pkg/sync/common/types.go index b02ad8c20..c8c1e607b 100644 --- a/pkg/sync/common/types.go +++ b/pkg/sync/common/types.go @@ -116,7 +116,6 @@ func NewHookType(t string) (HookType, bool) { t == string(HookTypePostSync) || t == string(HookTypeSyncFail) || t == string(HookTypeSkip) - } type HookDeletePolicy string @@ -137,6 +136,8 @@ func NewHookDeletePolicy(p string) (HookDeletePolicy, bool) { type ResourceSyncResult struct { // holds associated resource key ResourceKey kube.ResourceKey + // holds the images associated with the resource + Images []string // holds resource version Version string // holds the execution order diff --git a/pkg/sync/sync_context.go b/pkg/sync/sync_context.go index 35981ebaa..1652d35f3 100644 --- a/pkg/sync/sync_context.go +++ b/pkg/sync/sync_context.go @@ -1248,7 +1248,7 @@ func (sc *syncContext) runTasks(tasks syncTasks, dryRun bool) runState { // finally create resources var tasksGroup syncTasks for _, task := range createTasks { - //Only wait if the type of the next task is different than the previous type + // Only wait if the type of the next task is different than the previous type if len(tasksGroup) > 0 && tasksGroup[0].targetObj.GetKind() != task.kind() { state = sc.processCreateTasks(state, tasksGroup, dryRun) tasksGroup = syncTasks{task} @@ -1308,6 +1308,7 @@ func (sc *syncContext) setResourceResult(task *syncTask, syncStatus common.Resul res := common.ResourceSyncResult{ ResourceKey: kube.GetResourceKey(task.obj()), + Images: kube.GetResourceImages(task.obj()), Version: task.version(), Status: task.syncStatus, Message: task.message, diff --git a/pkg/utils/kube/kube.go b/pkg/utils/kube/kube.go index f88ed172b..8ff1b808d 100644 --- a/pkg/utils/kube/kube.go +++ b/pkg/utils/kube/kube.go @@ -404,6 +404,34 @@ func GetDeploymentReplicas(u *unstructured.Unstructured) *int64 { return &val } +func GetResourceImages(u *unstructured.Unstructured) []string { + var images []string + + // Extract containers for resources like pods, without template + containers, found, err := unstructured.NestedSlice(u.Object, "spec", "containers") + if !found || err != nil { + // Extract containers for other resources that have template + containers, found, err = unstructured.NestedSlice(u.Object, "spec", "template", "spec", "containers") + if !found || err != nil { + return nil + } + } + + for _, container := range containers { + containerMap, ok := container.(map[string]interface{}) + if !ok { + return nil + } + image, found, err := unstructured.NestedString(containerMap, "image") + if !found || err != nil { + continue + } + images = append(images, image) + } + + return images +} + // RetryUntilSucceed keep retrying given action with specified interval until action succeed or specified context is done. func RetryUntilSucceed(ctx context.Context, interval time.Duration, desc string, log logr.Logger, action func() error) { pollErr := wait.PollUntilContextCancel(ctx, interval, true, func(ctx context.Context) (bool /*done*/, error) { diff --git a/pkg/utils/kube/kube_test.go b/pkg/utils/kube/kube_test.go index e2ecca3a3..8778644d4 100644 --- a/pkg/utils/kube/kube_test.go +++ b/pkg/utils/kube/kube_test.go @@ -56,7 +56,6 @@ func TestUnsetLabels(t *testing.T) { require.NoError(t, err) assert.Empty(t, dep.ObjectMeta.Labels) } - } func TestCleanKubectlOutput(t *testing.T) { @@ -167,6 +166,67 @@ spec: assert.Nil(t, GetDeploymentReplicas(&noDeployment)) } +func TestGetResourceImagesForResourcesWithTemplate(t *testing.T) { + manifest := []byte(` +apiVersion: extensions/v1beta2 +kind: Deployment +metadata: + name: nginx-deployment + labels: + foo: bar +spec: + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: nginx:1.7.9 + ports: + - containerPort: 80 + - name: agent + image: agent:1.0.0 +`) + + expected := []string{"nginx:1.7.9", "agent:1.0.0"} + + deployment := unstructured.Unstructured{} + err := yaml.Unmarshal([]byte(manifest), &deployment) + require.NoError(t, err) + + images := GetResourceImages(&deployment) + assert.Equal(t, expected, images) +} + +func TestGetResourceImagesForPod(t *testing.T) { + manifest := []byte(` +apiVersion: v1 +kind: Pod +metadata: + name: example-pod + labels: + app: my-app +spec: + containers: + - name: nginx-container + image: nginx:1.21 + ports: + - containerPort: 80 + - name: sidecar-container + image: busybox:1.35 + command: ["sh", "-c", "echo Hello from the sidecar; sleep 3600"] +`) + expected := []string{"nginx:1.21", "busybox:1.35"} + + pod := unstructured.Unstructured{} + err := yaml.Unmarshal([]byte(manifest), &pod) + require.NoError(t, err) + + images := GetResourceImages(&pod) + assert.Equal(t, expected, images) +} + func TestSplitYAML_SingleObject(t *testing.T) { objs, err := SplitYAML([]byte(depWithLabel)) require.NoError(t, err) From b3eb347c1ac4abd823c3fac0a1fd099db717ff93 Mon Sep 17 00:00:00 2001 From: Aaron Hoffman <31711338+Aaron-9900@users.noreply.github.com> Date: Mon, 25 Nov 2024 17:10:14 +0100 Subject: [PATCH 2/8] Use require instead of assert Signed-off-by: Aaron Hoffman <31711338+Aaron-9900@users.noreply.github.com> --- pkg/utils/kube/kube_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/utils/kube/kube_test.go b/pkg/utils/kube/kube_test.go index 8778644d4..9e39d4537 100644 --- a/pkg/utils/kube/kube_test.go +++ b/pkg/utils/kube/kube_test.go @@ -196,7 +196,7 @@ spec: require.NoError(t, err) images := GetResourceImages(&deployment) - assert.Equal(t, expected, images) + require.Equal(t, expected, images) } func TestGetResourceImagesForPod(t *testing.T) { @@ -224,7 +224,7 @@ spec: require.NoError(t, err) images := GetResourceImages(&pod) - assert.Equal(t, expected, images) + require.Equal(t, expected, images) } func TestSplitYAML_SingleObject(t *testing.T) { From ae4000b77b0eb19dec112650c7b62a88b87f3f0f Mon Sep 17 00:00:00 2001 From: Aaron Hoffman <31711338+Aaron-9900@users.noreply.github.com> Date: Wed, 27 Nov 2024 11:12:41 +0100 Subject: [PATCH 3/8] Add test for empty images case Signed-off-by: Aaron Hoffman <31711338+Aaron-9900@users.noreply.github.com> --- pkg/utils/kube/kube_test.go | 79 +++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/pkg/utils/kube/kube_test.go b/pkg/utils/kube/kube_test.go index 9e39d4537..329ec1ca9 100644 --- a/pkg/utils/kube/kube_test.go +++ b/pkg/utils/kube/kube_test.go @@ -227,6 +227,85 @@ spec: require.Equal(t, expected, images) } +func TestGetImages_NoImagesPresent(t *testing.T) { + manifests := [][]byte{ + []byte(` +apiVersion: v1 +kind: ConfigMap +metadata: + name: example-config + namespace: default + labels: + app: my-app +data: + app.properties: | + key1=value1 + key2=value2 + key3=value3 + log.level: debug +`), + []byte(` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: deployment-no-containers + labels: + foo: bar +spec: + replicas: 1 + selector: + matchLabels: + app: agent + template: + metadata: + labels: + app: agent + spec: + volumes: + - name: config-volume + configMap: + name: config +`), + []byte(` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: deployment-without-image +spec: + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: text-service + command: ["echo", "hello"] +`), + []byte(` +apiVersion: v1 +kind: Pod +metadata: + name: example-pod + labels: + app: my-app +spec: + containers: + - name: no-image-container + command: ["echo", "hello"] +`, + ), + } + + for _, manifest := range manifests { + resource := unstructured.Unstructured{} + err := yaml.Unmarshal([]byte(manifest), &resource) + require.NoError(t, err) + + images := GetResourceImages(&resource) + require.Empty(t, images) + } +} + func TestSplitYAML_SingleObject(t *testing.T) { objs, err := SplitYAML([]byte(depWithLabel)) require.NoError(t, err) From b205965a42850bfe63043f4a5a8af994711247d8 Mon Sep 17 00:00:00 2001 From: Aaron Hoffman <31711338+Aaron-9900@users.noreply.github.com> Date: Wed, 27 Nov 2024 11:15:48 +0100 Subject: [PATCH 4/8] Rename test function to match regex Signed-off-by: Aaron Hoffman <31711338+Aaron-9900@users.noreply.github.com> --- pkg/utils/kube/kube_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/utils/kube/kube_test.go b/pkg/utils/kube/kube_test.go index 329ec1ca9..3908245f6 100644 --- a/pkg/utils/kube/kube_test.go +++ b/pkg/utils/kube/kube_test.go @@ -227,7 +227,7 @@ spec: require.Equal(t, expected, images) } -func TestGetImages_NoImagesPresent(t *testing.T) { +func TestGetImagesNoImagesPresent(t *testing.T) { manifests := [][]byte{ []byte(` apiVersion: v1 From 6b9063a72199550b4fd51beabdabdc3104b1f7fb Mon Sep 17 00:00:00 2001 From: Aaron Hoffman <31711338+Aaron-9900@users.noreply.github.com> Date: Wed, 27 Nov 2024 12:21:45 +0100 Subject: [PATCH 5/8] Add support for conjobs, refactor images implementation and add test for cronjobs Signed-off-by: Aaron Hoffman <31711338+Aaron-9900@users.noreply.github.com> --- pkg/utils/kube/kube.go | 27 ++++++++++---- pkg/utils/kube/kube_test.go | 74 ++++++++++++++++++++++++------------- 2 files changed, 69 insertions(+), 32 deletions(-) diff --git a/pkg/utils/kube/kube.go b/pkg/utils/kube/kube.go index 8ff1b808d..526ff4589 100644 --- a/pkg/utils/kube/kube.go +++ b/pkg/utils/kube/kube.go @@ -405,18 +405,31 @@ func GetDeploymentReplicas(u *unstructured.Unstructured) *int64 { } func GetResourceImages(u *unstructured.Unstructured) []string { + var containers []interface{} + var found bool + var err error var images []string - // Extract containers for resources like pods, without template - containers, found, err := unstructured.NestedSlice(u.Object, "spec", "containers") - if !found || err != nil { - // Extract containers for other resources that have template - containers, found, err = unstructured.NestedSlice(u.Object, "spec", "template", "spec", "containers") - if !found || err != nil { - return nil + containerPaths := [][]string{ + // Resources without template, like pods + {"spec", "containers"}, + // Resources with template, like deployments + {"spec", "template", "spec", "containers"}, + // Cronjobs + {"spec", "jobTemplate", "spec", "template", "spec", "containers"}, + } + + for _, path := range containerPaths { + containers, found, err = unstructured.NestedSlice(u.Object, path...) + if found && err == nil { + break } } + if !found || err != nil { + return nil + } + for _, container := range containers { containerMap, ok := container.(map[string]interface{}) if !ok { diff --git a/pkg/utils/kube/kube_test.go b/pkg/utils/kube/kube_test.go index 3908245f6..10056a91b 100644 --- a/pkg/utils/kube/kube_test.go +++ b/pkg/utils/kube/kube_test.go @@ -166,8 +166,16 @@ spec: assert.Nil(t, GetDeploymentReplicas(&noDeployment)) } -func TestGetResourceImagesForResourcesWithTemplate(t *testing.T) { - manifest := []byte(` +func TestGetResourceImages(t *testing.T) { + type testCase struct { + manifest []byte + expected []string + description string + } + + testCases := []testCase{ + { + manifest: []byte(` apiVersion: extensions/v1beta2 kind: Deployment metadata: @@ -186,21 +194,12 @@ spec: ports: - containerPort: 80 - name: agent - image: agent:1.0.0 -`) - - expected := []string{"nginx:1.7.9", "agent:1.0.0"} - - deployment := unstructured.Unstructured{} - err := yaml.Unmarshal([]byte(manifest), &deployment) - require.NoError(t, err) - - images := GetResourceImages(&deployment) - require.Equal(t, expected, images) -} - -func TestGetResourceImagesForPod(t *testing.T) { - manifest := []byte(` + image: agent:1.0.0`), + expected: []string{"nginx:1.7.9", "agent:1.0.0"}, + description: "deployment with two containers", + }, + { + manifest: []byte(` apiVersion: v1 kind: Pod metadata: @@ -216,15 +215,40 @@ spec: - name: sidecar-container image: busybox:1.35 command: ["sh", "-c", "echo Hello from the sidecar; sleep 3600"] -`) - expected := []string{"nginx:1.21", "busybox:1.35"} - - pod := unstructured.Unstructured{} - err := yaml.Unmarshal([]byte(manifest), &pod) - require.NoError(t, err) +`), + expected: []string{"nginx:1.21", "busybox:1.35"}, + description: "pod with containers", + }, + { + manifest: []byte(` +apiVersion: batch/v1 +kind: CronJob +metadata: + name: hello +spec: + schedule: "* * * * *" + jobTemplate: + spec: + template: + spec: + containers: + - name: hello + image: busybox:1.28 +`), + expected: []string{"busybox:1.28"}, + description: "cronjob with containers", + }, + } - images := GetResourceImages(&pod) - require.Equal(t, expected, images) + for _, tc := range testCases { + t.Run(tc.description, func(t *testing.T) { + resource := unstructured.Unstructured{} + err := yaml.Unmarshal(tc.manifest, &resource) + require.NoError(t, err) + images := GetResourceImages(&resource) + require.Equal(t, tc.expected, images) + }) + } } func TestGetImagesNoImagesPresent(t *testing.T) { From 5b2c46fdc73d2a08fd6f3aa46c287519d9c02cd7 Mon Sep 17 00:00:00 2001 From: Aaron Hoffman <31711338+Aaron-9900@users.noreply.github.com> Date: Wed, 27 Nov 2024 12:26:47 +0100 Subject: [PATCH 6/8] Continue on container failure Signed-off-by: Aaron Hoffman <31711338+Aaron-9900@users.noreply.github.com> --- pkg/utils/kube/kube.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/utils/kube/kube.go b/pkg/utils/kube/kube.go index 526ff4589..2d8128cf0 100644 --- a/pkg/utils/kube/kube.go +++ b/pkg/utils/kube/kube.go @@ -433,7 +433,7 @@ func GetResourceImages(u *unstructured.Unstructured) []string { for _, container := range containers { containerMap, ok := container.(map[string]interface{}) if !ok { - return nil + continue } image, found, err := unstructured.NestedString(containerMap, "image") if !found || err != nil { From 1a5b26f05a75f373f358af7bd2e8a4a8737a331c Mon Sep 17 00:00:00 2001 From: Aaron Hoffman <31711338+Aaron-9900@users.noreply.github.com> Date: Wed, 27 Nov 2024 14:15:56 +0100 Subject: [PATCH 7/8] Move missing images tests to single function Signed-off-by: Aaron Hoffman <31711338+Aaron-9900@users.noreply.github.com> --- pkg/utils/kube/kube_test.go | 57 +++++++++++++++++++------------------ 1 file changed, 29 insertions(+), 28 deletions(-) diff --git a/pkg/utils/kube/kube_test.go b/pkg/utils/kube/kube_test.go index 10056a91b..37350f35d 100644 --- a/pkg/utils/kube/kube_test.go +++ b/pkg/utils/kube/kube_test.go @@ -238,22 +238,8 @@ spec: expected: []string{"busybox:1.28"}, description: "cronjob with containers", }, - } - - for _, tc := range testCases { - t.Run(tc.description, func(t *testing.T) { - resource := unstructured.Unstructured{} - err := yaml.Unmarshal(tc.manifest, &resource) - require.NoError(t, err) - images := GetResourceImages(&resource) - require.Equal(t, tc.expected, images) - }) - } -} - -func TestGetImagesNoImagesPresent(t *testing.T) { - manifests := [][]byte{ - []byte(` + { + manifest: []byte(` apiVersion: v1 kind: ConfigMap metadata: @@ -268,7 +254,11 @@ data: key3=value3 log.level: debug `), - []byte(` + expected: nil, + description: "configmap without containers", + }, + { + manifest: []byte(` apiVersion: apps/v1 kind: Deployment metadata: @@ -290,7 +280,11 @@ spec: configMap: name: config `), - []byte(` + expected: nil, + description: "deployment without containers", + }, + { + manifest: []byte(` apiVersion: apps/v1 kind: Deployment metadata: @@ -305,7 +299,11 @@ spec: - name: text-service command: ["echo", "hello"] `), - []byte(` + expected: nil, + description: "deployment with container without image", + }, + { + manifest: []byte(` apiVersion: v1 kind: Pod metadata: @@ -316,17 +314,20 @@ spec: containers: - name: no-image-container command: ["echo", "hello"] -`, - ), +`), + expected: nil, + description: "pod with container without image", + }, } - for _, manifest := range manifests { - resource := unstructured.Unstructured{} - err := yaml.Unmarshal([]byte(manifest), &resource) - require.NoError(t, err) - - images := GetResourceImages(&resource) - require.Empty(t, images) + for _, tc := range testCases { + t.Run(tc.description, func(t *testing.T) { + resource := unstructured.Unstructured{} + err := yaml.Unmarshal(tc.manifest, &resource) + require.NoError(t, err) + images := GetResourceImages(&resource) + require.Equal(t, tc.expected, images) + }) } } From a989b44dca2ad817f769ab374b4cf5bb81989fdb Mon Sep 17 00:00:00 2001 From: Aaron Hoffman <31711338+Aaron-9900@users.noreply.github.com> Date: Wed, 27 Nov 2024 14:17:32 +0100 Subject: [PATCH 8/8] Refactor test Signed-off-by: Aaron Hoffman <31711338+Aaron-9900@users.noreply.github.com> --- pkg/utils/kube/kube_test.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/pkg/utils/kube/kube_test.go b/pkg/utils/kube/kube_test.go index 37350f35d..661ed1fba 100644 --- a/pkg/utils/kube/kube_test.go +++ b/pkg/utils/kube/kube_test.go @@ -167,13 +167,11 @@ spec: } func TestGetResourceImages(t *testing.T) { - type testCase struct { + testCases := []struct { manifest []byte expected []string description string - } - - testCases := []testCase{ + }{ { manifest: []byte(` apiVersion: extensions/v1beta2