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