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..2d8128cf0 100644 --- a/pkg/utils/kube/kube.go +++ b/pkg/utils/kube/kube.go @@ -404,6 +404,47 @@ func GetDeploymentReplicas(u *unstructured.Unstructured) *int64 { return &val } +func GetResourceImages(u *unstructured.Unstructured) []string { + var containers []interface{} + var found bool + var err error + var images []string + + 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 { + continue + } + 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..661ed1fba 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,169 @@ spec: assert.Nil(t, GetDeploymentReplicas(&noDeployment)) } +func TestGetResourceImages(t *testing.T) { + testCases := []struct { + manifest []byte + expected []string + description string + }{ + { + 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"}, + description: "deployment with two containers", + }, + { + 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"}, + 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", + }, + { + manifest: []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 +`), + expected: nil, + description: "configmap without containers", + }, + { + manifest: []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 +`), + expected: nil, + description: "deployment without containers", + }, + { + manifest: []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"] +`), + expected: nil, + description: "deployment with container without image", + }, + { + manifest: []byte(` +apiVersion: v1 +kind: Pod +metadata: + name: example-pod + labels: + app: my-app +spec: + containers: + - name: no-image-container + command: ["echo", "hello"] +`), + expected: nil, + description: "pod with container without image", + }, + } + + 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 TestSplitYAML_SingleObject(t *testing.T) { objs, err := SplitYAML([]byte(depWithLabel)) require.NoError(t, err)