From a568c6a464ec33fae74421e335974eeb2bcb0419 Mon Sep 17 00:00:00 2001 From: Sergiy Kulanov Date: Sat, 21 Mar 2026 10:12:57 +0200 Subject: [PATCH] EPMDEDP-16587: feat: populate image digest in APPLICATIONS_PAYLOAD Extend CodebaseTag with an optional Digest field so CDStageDeploy carries the image SHA256 digest from CBIS through the auto-deploy flow. The autodeploy strategy manager now includes imageDigest in the APPLICATIONS_PAYLOAD JSON. For the Auto (latest) strategy, digest is read directly from the CBIS latest tag. For the AutoStable strategy, the current triggered app gets digest from CDStageDeploy, stable apps get digest backfilled from CBIS during the InputDockerStreams iteration, and remaining apps use the latest tag digest. When digest is unavailable (pre-upgrade CBIS or old CDStageDeploy), the field is omitted via omitempty for full backward compatibility. Relates-to: EPMDEDP-16587 Signed-off-by: Sergiy Kulanov --- api/v1/cdstagedeploy_types.go | 1 + .../v2.edp.epam.com_cdstagedeployments.yaml | 4 + .../chain/put_cd_stage_deploy.go | 1 + .../v2.edp.epam.com_cdstagedeployments.yaml | 4 + docs/api.md | 14 + pkg/autodeploy/autodeploy.go | 43 +- pkg/autodeploy/autodeploy_test.go | 721 +++++++++++++++++- 7 files changed, 763 insertions(+), 25 deletions(-) diff --git a/api/v1/cdstagedeploy_types.go b/api/v1/cdstagedeploy_types.go index e76a96ef..fea1f719 100644 --- a/api/v1/cdstagedeploy_types.go +++ b/api/v1/cdstagedeploy_types.go @@ -39,6 +39,7 @@ type CDStageDeploySpec struct { type CodebaseTag struct { Codebase string `json:"codebase"` Tag string `json:"tag"` + Digest string `json:"digest,omitempty"` } // CDStageDeployStatus defines the observed state of CDStageDeploy. diff --git a/config/crd/bases/v2.edp.epam.com_cdstagedeployments.yaml b/config/crd/bases/v2.edp.epam.com_cdstagedeployments.yaml index 24da9dc1..991cbd96 100644 --- a/config/crd/bases/v2.edp.epam.com_cdstagedeployments.yaml +++ b/config/crd/bases/v2.edp.epam.com_cdstagedeployments.yaml @@ -69,6 +69,8 @@ spec: properties: codebase: type: string + digest: + type: string tag: type: string required: @@ -83,6 +85,8 @@ spec: properties: codebase: type: string + digest: + type: string tag: type: string required: diff --git a/controllers/codebaseimagestream/chain/put_cd_stage_deploy.go b/controllers/codebaseimagestream/chain/put_cd_stage_deploy.go index fc8cc704..4603dce2 100644 --- a/controllers/codebaseimagestream/chain/put_cd_stage_deploy.go +++ b/controllers/codebaseimagestream/chain/put_cd_stage_deploy.go @@ -211,6 +211,7 @@ func getCreateCommand( Tag: codebaseApi.CodebaseTag{ Codebase: codebase, Tag: lastTag.Name, + Digest: lastTag.Digest, }, }, nil } diff --git a/deploy-templates/crds/v2.edp.epam.com_cdstagedeployments.yaml b/deploy-templates/crds/v2.edp.epam.com_cdstagedeployments.yaml index 24da9dc1..991cbd96 100644 --- a/deploy-templates/crds/v2.edp.epam.com_cdstagedeployments.yaml +++ b/deploy-templates/crds/v2.edp.epam.com_cdstagedeployments.yaml @@ -69,6 +69,8 @@ spec: properties: codebase: type: string + digest: + type: string tag: type: string required: @@ -83,6 +85,8 @@ spec: properties: codebase: type: string + digest: + type: string tag: type: string required: diff --git a/docs/api.md b/docs/api.md index 8fcf45b6..848e5831 100644 --- a/docs/api.md +++ b/docs/api.md @@ -172,6 +172,13 @@ Specifies a latest available tag
true + + digest + string + +
+ + false @@ -206,6 +213,13 @@ Specifies a latest available tag
true + + digest + string + +
+ + false diff --git a/pkg/autodeploy/autodeploy.go b/pkg/autodeploy/autodeploy.go index cdb9ead0..a6c8d797 100644 --- a/pkg/autodeploy/autodeploy.go +++ b/pkg/autodeploy/autodeploy.go @@ -33,7 +33,8 @@ type StrategyManager struct { } type ApplicationPayload struct { - ImageTag string `json:"imageTag"` + ImageTag string `json:"imageTag"` + ImageDigest string `json:"imageDigest,omitempty"` } func NewStrategyManager(k8sClient client.Client) *StrategyManager { @@ -63,7 +64,8 @@ func (h *StrategyManager) GetAppPayloadForAllLatestStrategy( } appPayload[codebase] = ApplicationPayload{ - ImageTag: tag, + ImageTag: tag.Name, + ImageDigest: tag.Digest, } } @@ -83,6 +85,7 @@ func (h *StrategyManager) GetAppPayloadForCurrentWithStableStrategy( ) (json.RawMessage, error) { appPayload := make(map[string]ApplicationPayload, len(pipeline.Spec.InputDockerStreams)) + // Stable apps: tag from annotation (digest will be backfilled in step 3). for _, app := range pipeline.Spec.Applications { t, ok := stage.GetAnnotations()[fmt.Sprintf("app.edp.epam.com/%s", app)] if ok { @@ -92,10 +95,13 @@ func (h *StrategyManager) GetAppPayloadForCurrentWithStableStrategy( } } + // Current triggered app: tag and digest from CDStageDeploy. appPayload[current.Codebase] = ApplicationPayload{ - ImageTag: current.Tag, + ImageTag: current.Tag, + ImageDigest: current.Digest, } + // Remaining apps + backfill digest for stable apps from step 1. for _, stream := range pipeline.Spec.InputDockerStreams { imageStream, err := codebaseimagestream.GetCodebaseImageStreamByCodebaseBaseBranchName( ctx, @@ -112,10 +118,15 @@ func (h *StrategyManager) GetAppPayloadForCurrentWithStableStrategy( return nil, err } - if _, ok := appPayload[codebase]; !ok { + existing, exists := appPayload[codebase] + if !exists { appPayload[codebase] = ApplicationPayload{ - ImageTag: tag, + ImageTag: tag.Name, + ImageDigest: tag.Digest, } + } else if existing.ImageDigest == "" { + existing.ImageDigest = findDigestByTagName(imageStream, existing.ImageTag) + appPayload[codebase] = existing } } @@ -130,11 +141,27 @@ func (h *StrategyManager) GetAppPayloadForCurrentWithStableStrategy( func (*StrategyManager) getLatestTag( ctx context.Context, imageStream *codebaseApi.CodebaseImageStream, -) (codebase, tag string, e error) { +) (string, codebaseApi.Tag, error) { t, err := codebaseimagestream.GetLastTag(imageStream.Spec.Tags, ctrl.LoggerFrom(ctx)) if err != nil { - return "", "", ErrLasTagNotFound + return "", codebaseApi.Tag{}, ErrLasTagNotFound } - return imageStream.Spec.Codebase, t.Name, nil + return imageStream.Spec.Codebase, t, nil +} + +// findDigestByTagName looks up a digest for a given tag name in a CodebaseImageStream. +// Returns an empty string if the CBIS is nil, the tag is not found, or the tag has no digest. +func findDigestByTagName(cbis *codebaseApi.CodebaseImageStream, tagName string) string { + if cbis == nil { + return "" + } + + for _, tag := range cbis.Spec.Tags { + if tag.Name == tagName { + return tag.Digest + } + } + + return "" } diff --git a/pkg/autodeploy/autodeploy_test.go b/pkg/autodeploy/autodeploy_test.go index 4045031a..bfbc22d9 100644 --- a/pkg/autodeploy/autodeploy_test.go +++ b/pkg/autodeploy/autodeploy_test.go @@ -127,6 +127,134 @@ func TestStrategyManager_GetAppPayloadForAllLatestStrategy(t *testing.T) { require.Contains(t, err.Error(), "last tag not found") }, }, + { + name: "payload includes imageDigest when CBIS tag has digest", + pipeline: &pipelineAPi.CDPipeline{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pipeline", + Namespace: "default", + }, + Spec: pipelineAPi.CDPipelineSpec{ + InputDockerStreams: []string{"app1-main"}, + }, + }, + k8sClient: func(t *testing.T) client.Client { + return fake.NewClientBuilder().WithScheme(scheme).WithObjects( + &codebaseApi.CodebaseImageStream{ + ObjectMeta: metav1.ObjectMeta{ + Name: "app1-main", + Namespace: "default", + Labels: map[string]string{ + codebaseApi.CodebaseBranchLabel: "app1-main", + }, + }, + Spec: codebaseApi.CodebaseImageStreamSpec{ + Codebase: "app1", + Tags: []codebaseApi.Tag{ + { + Name: "1.0", + Created: time.Now().Format(time.RFC3339), + Digest: "sha256:aaa111", + }, + }, + }, + }, + ).Build() + }, + want: `{"app1":{"imageTag":"1.0","imageDigest":"sha256:aaa111"}}`, + wantErr: require.NoError, + }, + { + name: "payload omits imageDigest when CBIS tag has no digest", + pipeline: &pipelineAPi.CDPipeline{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pipeline", + Namespace: "default", + }, + Spec: pipelineAPi.CDPipelineSpec{ + InputDockerStreams: []string{"app1-main"}, + }, + }, + k8sClient: func(t *testing.T) client.Client { + return fake.NewClientBuilder().WithScheme(scheme).WithObjects( + &codebaseApi.CodebaseImageStream{ + ObjectMeta: metav1.ObjectMeta{ + Name: "app1-main", + Namespace: "default", + Labels: map[string]string{ + codebaseApi.CodebaseBranchLabel: "app1-main", + }, + }, + Spec: codebaseApi.CodebaseImageStreamSpec{ + Codebase: "app1", + Tags: []codebaseApi.Tag{ + { + Name: "1.0", + Created: time.Now().Format(time.RFC3339), + }, + }, + }, + }, + ).Build() + }, + want: `{"app1":{"imageTag":"1.0"}}`, + wantErr: require.NoError, + }, + { + name: "mixed digest: some tags with digest, some without", + pipeline: &pipelineAPi.CDPipeline{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pipeline", + Namespace: "default", + }, + Spec: pipelineAPi.CDPipelineSpec{ + InputDockerStreams: []string{"app1-main", "app2-main"}, + }, + }, + k8sClient: func(t *testing.T) client.Client { + return fake.NewClientBuilder().WithScheme(scheme).WithObjects( + &codebaseApi.CodebaseImageStream{ + ObjectMeta: metav1.ObjectMeta{ + Name: "app1-main", + Namespace: "default", + Labels: map[string]string{ + codebaseApi.CodebaseBranchLabel: "app1-main", + }, + }, + Spec: codebaseApi.CodebaseImageStreamSpec{ + Codebase: "app1", + Tags: []codebaseApi.Tag{ + { + Name: "2.0", + Created: time.Now().Format(time.RFC3339), + Digest: "sha256:abc123", + }, + }, + }, + }, + &codebaseApi.CodebaseImageStream{ + ObjectMeta: metav1.ObjectMeta{ + Name: "app2-main", + Namespace: "default", + Labels: map[string]string{ + codebaseApi.CodebaseBranchLabel: "app2-main", + }, + }, + Spec: codebaseApi.CodebaseImageStreamSpec{ + Codebase: "app2", + Tags: []codebaseApi.Tag{ + { + Name: "3.0", + Created: time.Now().Format(time.RFC3339), + }, + }, + }, + }, + ).Build() + }, + want: `{"app1":{"imageTag":"2.0","imageDigest":"sha256:abc123"},"app2":{"imageTag":"3.0"}}`, + wantErr: require.NoError, + }, } for _, tt := range tests { @@ -293,23 +421,582 @@ func TestStrategyManager_GetAppPayloadForCurrentWithStableStrategy(t *testing.T) require.Contains(t, err.Error(), "failed to get CodebaseImageStream") }, }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - h := NewStrategyManager(tt.k8sClient(t)) - got, err := h.GetAppPayloadForCurrentWithStableStrategy( - ctrl.LoggerInto(context.Background(), logr.Discard()), - tt.current, - tt.pipeline, - tt.stage, - ) - - tt.wantErr(t, err) - - if tt.want != "" { - assert.JSONEq(t, tt.want, string(got)) - } + { + name: "current app with digest from CDStageDeploy", + current: codebaseApi.CodebaseTag{ + Codebase: "app1", + Tag: "2.0", + Digest: "sha256:current111", + }, + pipeline: &pipelineAPi.CDPipeline{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pipeline", + Namespace: "default", + }, + Spec: pipelineAPi.CDPipelineSpec{ + InputDockerStreams: []string{"app1-main"}, + Applications: []string{"app1"}, + }, + }, + stage: &pipelineAPi.Stage{}, + k8sClient: func(t *testing.T) client.Client { + return fake.NewClientBuilder().WithScheme(scheme).WithObjects( + &codebaseApi.CodebaseImageStream{ + ObjectMeta: metav1.ObjectMeta{ + Name: "app1-main", + Namespace: "default", + Labels: map[string]string{ + codebaseApi.CodebaseBranchLabel: "app1-main", + }, + }, + Spec: codebaseApi.CodebaseImageStreamSpec{ + Codebase: "app1", + Tags: []codebaseApi.Tag{ + { + Name: "2.0", + Created: time.Now().Format(time.RFC3339), + Digest: "sha256:current111", + }, + }, + }, + }, + ).Build() + }, + want: `{"app1":{"imageTag":"2.0","imageDigest":"sha256:current111"}}`, + wantErr: require.NoError, + }, + { + name: "current app without digest in CBIS", + current: codebaseApi.CodebaseTag{ + Codebase: "app1", + Tag: "2.0", + }, + pipeline: &pipelineAPi.CDPipeline{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pipeline", + Namespace: "default", + }, + Spec: pipelineAPi.CDPipelineSpec{ + InputDockerStreams: []string{"app1-main"}, + Applications: []string{"app1"}, + }, + }, + stage: &pipelineAPi.Stage{}, + k8sClient: func(t *testing.T) client.Client { + return fake.NewClientBuilder().WithScheme(scheme).WithObjects( + &codebaseApi.CodebaseImageStream{ + ObjectMeta: metav1.ObjectMeta{ + Name: "app1-main", + Namespace: "default", + Labels: map[string]string{ + codebaseApi.CodebaseBranchLabel: "app1-main", + }, + }, + Spec: codebaseApi.CodebaseImageStreamSpec{ + Codebase: "app1", + Tags: []codebaseApi.Tag{ + { + Name: "2.0", + Created: time.Now().Format(time.RFC3339), + }, + }, + }, + }, + ).Build() + }, + want: `{"app1":{"imageTag":"2.0"}}`, + wantErr: require.NoError, + }, + { + name: "current app without digest in CDStageDeploy, backfilled from CBIS", + current: codebaseApi.CodebaseTag{ + Codebase: "app1", + Tag: "2.0", + }, + pipeline: &pipelineAPi.CDPipeline{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pipeline", + Namespace: "default", + }, + Spec: pipelineAPi.CDPipelineSpec{ + InputDockerStreams: []string{"app1-main"}, + Applications: []string{"app1"}, + }, + }, + stage: &pipelineAPi.Stage{}, + k8sClient: func(t *testing.T) client.Client { + return fake.NewClientBuilder().WithScheme(scheme).WithObjects( + &codebaseApi.CodebaseImageStream{ + ObjectMeta: metav1.ObjectMeta{ + Name: "app1-main", + Namespace: "default", + Labels: map[string]string{ + codebaseApi.CodebaseBranchLabel: "app1-main", + }, + }, + Spec: codebaseApi.CodebaseImageStreamSpec{ + Codebase: "app1", + Tags: []codebaseApi.Tag{ + { + Name: "2.0", + Created: time.Now().Format(time.RFC3339), + Digest: "sha256:backfilled", + }, + }, + }, + }, + ).Build() + }, + want: `{"app1":{"imageTag":"2.0","imageDigest":"sha256:backfilled"}}`, + wantErr: require.NoError, + }, + { + name: "stable app from annotation with digest from CBIS", + current: codebaseApi.CodebaseTag{ + Codebase: "app1", + Tag: "1.0", + }, + pipeline: &pipelineAPi.CDPipeline{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pipeline", + Namespace: "default", + }, + Spec: pipelineAPi.CDPipelineSpec{ + InputDockerStreams: []string{"app1-main", "app2-main"}, + Applications: []string{"app1", "app2"}, + }, + }, + stage: &pipelineAPi.Stage{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "app.edp.epam.com/app2": "3.0", + }, + }, + }, + k8sClient: func(t *testing.T) client.Client { + return fake.NewClientBuilder().WithScheme(scheme).WithObjects( + &codebaseApi.CodebaseImageStream{ + ObjectMeta: metav1.ObjectMeta{ + Name: "app1-main", + Namespace: "default", + Labels: map[string]string{ + codebaseApi.CodebaseBranchLabel: "app1-main", + }, + }, + Spec: codebaseApi.CodebaseImageStreamSpec{ + Codebase: "app1", + Tags: []codebaseApi.Tag{ + { + Name: "1.0", + Created: time.Now().Format(time.RFC3339), + Digest: "sha256:app1digest", + }, + }, + }, + }, + &codebaseApi.CodebaseImageStream{ + ObjectMeta: metav1.ObjectMeta{ + Name: "app2-main", + Namespace: "default", + Labels: map[string]string{ + codebaseApi.CodebaseBranchLabel: "app2-main", + }, + }, + Spec: codebaseApi.CodebaseImageStreamSpec{ + Codebase: "app2", + Tags: []codebaseApi.Tag{ + { + Name: "3.0", + Created: time.Now().Format(time.RFC3339), + Digest: "sha256:app2digest", + }, + { + Name: "4.0", + Created: time.Now().Add(time.Hour).Format(time.RFC3339), + Digest: "sha256:app2latest", + }, + }, + }, + }, + ).Build() + }, + want: `{"app1":{"imageTag":"1.0","imageDigest":"sha256:app1digest"},"app2":{"imageTag":"3.0","imageDigest":"sha256:app2digest"}}`, + wantErr: require.NoError, + }, + { + name: "stable app annotation tag not found in CBIS", + current: codebaseApi.CodebaseTag{ + Codebase: "app1", + Tag: "1.0", + }, + pipeline: &pipelineAPi.CDPipeline{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pipeline", + Namespace: "default", + }, + Spec: pipelineAPi.CDPipelineSpec{ + InputDockerStreams: []string{"app1-main", "app2-main"}, + Applications: []string{"app1", "app2"}, + }, + }, + stage: &pipelineAPi.Stage{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "app.edp.epam.com/app2": "old-tag", + }, + }, + }, + k8sClient: func(t *testing.T) client.Client { + return fake.NewClientBuilder().WithScheme(scheme).WithObjects( + &codebaseApi.CodebaseImageStream{ + ObjectMeta: metav1.ObjectMeta{ + Name: "app1-main", + Namespace: "default", + Labels: map[string]string{ + codebaseApi.CodebaseBranchLabel: "app1-main", + }, + }, + Spec: codebaseApi.CodebaseImageStreamSpec{ + Codebase: "app1", + Tags: []codebaseApi.Tag{ + { + Name: "1.0", + Created: time.Now().Format(time.RFC3339), + Digest: "sha256:app1digest", + }, + }, + }, + }, + &codebaseApi.CodebaseImageStream{ + ObjectMeta: metav1.ObjectMeta{ + Name: "app2-main", + Namespace: "default", + Labels: map[string]string{ + codebaseApi.CodebaseBranchLabel: "app2-main", + }, + }, + Spec: codebaseApi.CodebaseImageStreamSpec{ + Codebase: "app2", + Tags: []codebaseApi.Tag{ + { + Name: "3.0", + Created: time.Now().Format(time.RFC3339), + Digest: "sha256:app2digest", + }, + }, + }, + }, + ).Build() + }, + want: `{"app1":{"imageTag":"1.0","imageDigest":"sha256:app1digest"},"app2":{"imageTag":"old-tag"}}`, + wantErr: require.NoError, + }, + { + name: "latest fallback app with digest", + current: codebaseApi.CodebaseTag{ + Codebase: "app1", + Tag: "1.0", + }, + pipeline: &pipelineAPi.CDPipeline{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pipeline", + Namespace: "default", + }, + Spec: pipelineAPi.CDPipelineSpec{ + InputDockerStreams: []string{"app1-main", "app2-main"}, + Applications: []string{"app1", "app2"}, + }, + }, + stage: &pipelineAPi.Stage{}, + k8sClient: func(t *testing.T) client.Client { + return fake.NewClientBuilder().WithScheme(scheme).WithObjects( + &codebaseApi.CodebaseImageStream{ + ObjectMeta: metav1.ObjectMeta{ + Name: "app1-main", + Namespace: "default", + Labels: map[string]string{ + codebaseApi.CodebaseBranchLabel: "app1-main", + }, + }, + Spec: codebaseApi.CodebaseImageStreamSpec{ + Codebase: "app1", + Tags: []codebaseApi.Tag{ + { + Name: "1.0", + Created: time.Now().Format(time.RFC3339), + Digest: "sha256:app1digest", + }, + }, + }, + }, + &codebaseApi.CodebaseImageStream{ + ObjectMeta: metav1.ObjectMeta{ + Name: "app2-main", + Namespace: "default", + Labels: map[string]string{ + codebaseApi.CodebaseBranchLabel: "app2-main", + }, + }, + Spec: codebaseApi.CodebaseImageStreamSpec{ + Codebase: "app2", + Tags: []codebaseApi.Tag{ + { + Name: "5.0", + Created: time.Now().Format(time.RFC3339), + Digest: "sha256:app2latest", + }, + }, + }, + }, + ).Build() + }, + want: `{"app1":{"imageTag":"1.0","imageDigest":"sha256:app1digest"},"app2":{"imageTag":"5.0","imageDigest":"sha256:app2latest"}}`, + wantErr: require.NoError, + }, + { + name: "mixed: current with digest, stable without, latest with", + current: codebaseApi.CodebaseTag{ + Codebase: "app1", + Tag: "1.0", + Digest: "sha256:currentdigest", + }, + pipeline: &pipelineAPi.CDPipeline{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pipeline", + Namespace: "default", + }, + Spec: pipelineAPi.CDPipelineSpec{ + InputDockerStreams: []string{"app1-main", "app2-main", "app3-main"}, + Applications: []string{"app1", "app2", "app3"}, + }, + }, + stage: &pipelineAPi.Stage{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "app.edp.epam.com/app2": "2.0", + }, + }, + }, + k8sClient: func(t *testing.T) client.Client { + return fake.NewClientBuilder().WithScheme(scheme).WithObjects( + &codebaseApi.CodebaseImageStream{ + ObjectMeta: metav1.ObjectMeta{ + Name: "app1-main", + Namespace: "default", + Labels: map[string]string{ + codebaseApi.CodebaseBranchLabel: "app1-main", + }, + }, + Spec: codebaseApi.CodebaseImageStreamSpec{ + Codebase: "app1", + Tags: []codebaseApi.Tag{ + { + Name: "1.0", + Created: time.Now().Format(time.RFC3339), + Digest: "sha256:currentdigest", + }, + }, + }, + }, + &codebaseApi.CodebaseImageStream{ + ObjectMeta: metav1.ObjectMeta{ + Name: "app2-main", + Namespace: "default", + Labels: map[string]string{ + codebaseApi.CodebaseBranchLabel: "app2-main", + }, + }, + Spec: codebaseApi.CodebaseImageStreamSpec{ + Codebase: "app2", + Tags: []codebaseApi.Tag{ + { + Name: "2.0", + Created: time.Now().Format(time.RFC3339), + }, + }, + }, + }, + &codebaseApi.CodebaseImageStream{ + ObjectMeta: metav1.ObjectMeta{ + Name: "app3-main", + Namespace: "default", + Labels: map[string]string{ + codebaseApi.CodebaseBranchLabel: "app3-main", + }, + }, + Spec: codebaseApi.CodebaseImageStreamSpec{ + Codebase: "app3", + Tags: []codebaseApi.Tag{ + { + Name: "7.0", + Created: time.Now().Format(time.RFC3339), + Digest: "sha256:latestdigest", + }, + }, + }, + }, + ).Build() + }, + want: `{"app1":{"imageTag":"1.0","imageDigest":"sha256:currentdigest"},"app2":{"imageTag":"2.0"},"app3":{"imageTag":"7.0","imageDigest":"sha256:latestdigest"}}`, + wantErr: require.NoError, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + h := NewStrategyManager(tt.k8sClient(t)) + got, err := h.GetAppPayloadForCurrentWithStableStrategy( + ctrl.LoggerInto(context.Background(), logr.Discard()), + tt.current, + tt.pipeline, + tt.stage, + ) + + tt.wantErr(t, err) + + if tt.want != "" { + assert.JSONEq(t, tt.want, string(got)) + } + }) + } +} + +func TestStrategyManager_getLatestTag(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + imageStream *codebaseApi.CodebaseImageStream + wantCb string + wantTag codebaseApi.Tag + wantErr require.ErrorAssertionFunc + }{ + { + name: "returns full Tag struct with digest", + imageStream: &codebaseApi.CodebaseImageStream{ + Spec: codebaseApi.CodebaseImageStreamSpec{ + Codebase: "myapp", + Tags: []codebaseApi.Tag{ + { + Name: "1.0", + Created: time.Now().Format(time.RFC3339), + Digest: "sha256:abc123", + }, + { + Name: "2.0", + Created: time.Now().Add(time.Hour).Format(time.RFC3339), + Digest: "sha256:def456", + }, + }, + }, + }, + wantCb: "myapp", + wantTag: codebaseApi.Tag{ + Name: "2.0", + Digest: "sha256:def456", + }, + wantErr: require.NoError, + }, + { + name: "returns error when no tags", + imageStream: &codebaseApi.CodebaseImageStream{ + Spec: codebaseApi.CodebaseImageStreamSpec{ + Codebase: "myapp", + }, + }, + wantCb: "", + wantTag: codebaseApi.Tag{}, + wantErr: func(t require.TestingT, err error, i ...interface{}) { + require.Error(t, err) + require.Contains(t, err.Error(), "last tag not found") + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + h := &StrategyManager{} + codebase, tag, err := h.getLatestTag( + ctrl.LoggerInto(context.Background(), logr.Discard()), + tt.imageStream, + ) + + tt.wantErr(t, err) + + if err == nil { + assert.Equal(t, tt.wantCb, codebase) + assert.Equal(t, tt.wantTag.Name, tag.Name) + assert.Equal(t, tt.wantTag.Digest, tag.Digest) + } + }) + } +} + +func Test_findDigestByTagName(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + cbis *codebaseApi.CodebaseImageStream + tagName string + want string + }{ + { + name: "nil CBIS returns empty string", + cbis: nil, + tagName: "1.0", + want: "", + }, + { + name: "tag found with digest", + cbis: &codebaseApi.CodebaseImageStream{ + Spec: codebaseApi.CodebaseImageStreamSpec{ + Tags: []codebaseApi.Tag{ + { + Name: "1.0", + Digest: "sha256:found", + }, + }, + }, + }, + tagName: "1.0", + want: "sha256:found", + }, + { + name: "tag found without digest", + cbis: &codebaseApi.CodebaseImageStream{ + Spec: codebaseApi.CodebaseImageStreamSpec{ + Tags: []codebaseApi.Tag{ + { + Name: "1.0", + }, + }, + }, + }, + tagName: "1.0", + want: "", + }, + { + name: "tag not found", + cbis: &codebaseApi.CodebaseImageStream{ + Spec: codebaseApi.CodebaseImageStreamSpec{ + Tags: []codebaseApi.Tag{ + { + Name: "2.0", + Digest: "sha256:other", + }, + }, + }, + }, + tagName: "1.0", + want: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := findDigestByTagName(tt.cbis, tt.tagName) + assert.Equal(t, tt.want, got) }) } }