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) }) } }