Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions controllers/codebase/service/chain/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,34 @@ func setIntermediateSuccessFields(ctx context.Context, c client.Client, cb *code

return nil
}

// updateGitStatusWithPatch updates the codebase Git status using Patch instead of Update.
// If a conflict occurs, the function returns an error, causing the reconciliation
// to requeue automatically via the controller-runtime framework.
func updateGitStatusWithPatch(
ctx context.Context,
c client.Client,
codebase *codebaseApi.Codebase,
action codebaseApi.ActionType,
gitStatus string,
) error {
// Skip update if status already matches (idempotency check)
if codebase.Status.Git == gitStatus {
return nil
}

// Create patch based on current object state
patch := client.MergeFrom(codebase.DeepCopy())

// Modify the status field
codebase.Status.Git = gitStatus

// Apply patch to status subresource
if err := c.Status().Patch(ctx, codebase, patch); err != nil {
setFailedFields(codebase, action, err.Error())
return fmt.Errorf("failed to patch git status to %s for codebase %s: %w",
gitStatus, codebase.Name, err)
}

return nil
}
250 changes: 250 additions & 0 deletions controllers/codebase/service/chain/common_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
package chain

import (
"context"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client/fake"

codebaseApi "github.com/epam/edp-codebase-operator/v2/api/v1"
"github.com/epam/edp-codebase-operator/v2/pkg/util"
)

func TestUpdateGitStatusWithPatch_Success(t *testing.T) {
scheme := runtime.NewScheme()
require.NoError(t, codebaseApi.AddToScheme(scheme))

codebase := &codebaseApi.Codebase{
ObjectMeta: metav1.ObjectMeta{
Name: fakeName,
Namespace: fakeNamespace,
},
Status: codebaseApi.CodebaseStatus{
Git: "",
},
}

fakeClient := fake.NewClientBuilder().
WithScheme(scheme).
WithObjects(codebase).
WithStatusSubresource(codebase).
Build()

err := updateGitStatusWithPatch(
context.Background(),
fakeClient,
codebase,
codebaseApi.RepositoryProvisioning,
util.ProjectPushedStatus,
)

assert.NoError(t, err)
assert.Equal(t, util.ProjectPushedStatus, codebase.Status.Git)

// Verify status was actually patched in the cluster
updated := &codebaseApi.Codebase{}
err = fakeClient.Get(context.Background(), types.NamespacedName{
Name: fakeName,
Namespace: fakeNamespace,
}, updated)
assert.NoError(t, err)
assert.Equal(t, util.ProjectPushedStatus, updated.Status.Git)
}

func TestUpdateGitStatusWithPatch_Idempotent(t *testing.T) {
scheme := runtime.NewScheme()
require.NoError(t, codebaseApi.AddToScheme(scheme))

codebase := &codebaseApi.Codebase{
ObjectMeta: metav1.ObjectMeta{
Name: fakeName,
Namespace: fakeNamespace,
},
Status: codebaseApi.CodebaseStatus{
Git: util.ProjectPushedStatus,
},
}

fakeClient := fake.NewClientBuilder().
WithScheme(scheme).
WithObjects(codebase).
WithStatusSubresource(codebase).
Build()

err := updateGitStatusWithPatch(
context.Background(),
fakeClient,
codebase,
codebaseApi.RepositoryProvisioning,
util.ProjectPushedStatus,
)

assert.NoError(t, err)
assert.Equal(t, util.ProjectPushedStatus, codebase.Status.Git)
}

func TestUpdateGitStatusWithPatch_StatusTransition(t *testing.T) {
// Setup
scheme := runtime.NewScheme()
require.NoError(t, codebaseApi.AddToScheme(scheme))

codebase := &codebaseApi.Codebase{
ObjectMeta: metav1.ObjectMeta{
Name: fakeName,
Namespace: fakeNamespace,
},
Status: codebaseApi.CodebaseStatus{
Git: util.ProjectPushedStatus,
},
}

fakeClient := fake.NewClientBuilder().
WithScheme(scheme).
WithObjects(codebase).
WithStatusSubresource(codebase).
Build()

// Execute - transition from ProjectPushedStatus to ProjectGitLabCIPushedStatus
err := updateGitStatusWithPatch(
context.Background(),
fakeClient,
codebase,
codebaseApi.RepositoryProvisioning,
util.ProjectGitLabCIPushedStatus,
)

// Verify
assert.NoError(t, err)
assert.Equal(t, util.ProjectGitLabCIPushedStatus, codebase.Status.Git)

// Verify in cluster
updated := &codebaseApi.Codebase{}
err = fakeClient.Get(context.Background(), types.NamespacedName{
Name: fakeName,
Namespace: fakeNamespace,
}, updated)
assert.NoError(t, err)
assert.Equal(t, util.ProjectGitLabCIPushedStatus, updated.Status.Git)
}

func TestUpdateGitStatusWithPatch_PreservesOtherStatusFields(t *testing.T) {
// Setup
scheme := runtime.NewScheme()
require.NoError(t, codebaseApi.AddToScheme(scheme))

codebase := &codebaseApi.Codebase{
ObjectMeta: metav1.ObjectMeta{
Name: fakeName,
Namespace: fakeNamespace,
},
Status: codebaseApi.CodebaseStatus{
Git: "",
WebHookRef: "webhook-123",
GitWebUrl: "https://git.example.com/repo",
},
}

fakeClient := fake.NewClientBuilder().
WithScheme(scheme).
WithObjects(codebase).
WithStatusSubresource(codebase).
Build()

// Execute
err := updateGitStatusWithPatch(
context.Background(),
fakeClient,
codebase,
codebaseApi.RepositoryProvisioning,
util.ProjectPushedStatus,
)

// Verify - only Git field changed, others preserved
assert.NoError(t, err)
assert.Equal(t, util.ProjectPushedStatus, codebase.Status.Git)
assert.Equal(t, "webhook-123", codebase.Status.WebHookRef)
assert.Equal(t, "https://git.example.com/repo", codebase.Status.GitWebUrl)

// Verify in cluster
updated := &codebaseApi.Codebase{}
err = fakeClient.Get(context.Background(), types.NamespacedName{
Name: fakeName,
Namespace: fakeNamespace,
}, updated)
assert.NoError(t, err)
assert.Equal(t, util.ProjectPushedStatus, updated.Status.Git)
assert.Equal(t, "webhook-123", updated.Status.WebHookRef)
assert.Equal(t, "https://git.example.com/repo", updated.Status.GitWebUrl)
}

func TestUpdateGitStatusWithPatch_SequentialUpdates(t *testing.T) {
// Setup - This test simulates the chain handler scenario
scheme := runtime.NewScheme()
require.NoError(t, codebaseApi.AddToScheme(scheme))

codebase := &codebaseApi.Codebase{
ObjectMeta: metav1.ObjectMeta{
Name: fakeName,
Namespace: fakeNamespace,
},
Status: codebaseApi.CodebaseStatus{
Git: "",
},
}

fakeClient := fake.NewClientBuilder().
WithScheme(scheme).
WithObjects(codebase).
WithStatusSubresource(codebase).
Build()

ctx := context.Background()

// Simulate sequential updates as in chain handlers
// 1. PutProject sets ProjectPushedStatus
err := updateGitStatusWithPatch(
ctx,
fakeClient,
codebase,
codebaseApi.RepositoryProvisioning,
util.ProjectPushedStatus,
)
assert.NoError(t, err)
assert.Equal(t, util.ProjectPushedStatus, codebase.Status.Git)

// 2. PutGitLabCIConfig sets ProjectGitLabCIPushedStatus
err = updateGitStatusWithPatch(
ctx,
fakeClient,
codebase,
codebaseApi.RepositoryProvisioning,
util.ProjectGitLabCIPushedStatus,
)
assert.NoError(t, err)
assert.Equal(t, util.ProjectGitLabCIPushedStatus, codebase.Status.Git)

// 3. PutDeployConfigs sets ProjectTemplatesPushedStatus
err = updateGitStatusWithPatch(
ctx,
fakeClient,
codebase,
codebaseApi.SetupDeploymentTemplates,
util.ProjectTemplatesPushedStatus,
)
assert.NoError(t, err)
assert.Equal(t, util.ProjectTemplatesPushedStatus, codebase.Status.Git)

// Verify final state in cluster
updated := &codebaseApi.Codebase{}
err = fakeClient.Get(ctx, types.NamespacedName{
Name: fakeName,
Namespace: fakeNamespace,
}, updated)
assert.NoError(t, err)
assert.Equal(t, util.ProjectTemplatesPushedStatus, updated.Status.Git)
}
5 changes: 2 additions & 3 deletions controllers/codebase/service/chain/put_deploy_configs.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,9 +145,8 @@ func (h *PutDeployConfigs) tryToPushConfigs(ctx context.Context, codebase *codeb

log.Info("Changes have been pushed")

codebase.Status.Git = util.ProjectTemplatesPushedStatus
if err = h.client.Status().Update(ctx, codebase); err != nil {
return fmt.Errorf("failed to set git status %s for codebase %s: %w", util.ProjectTemplatesPushedStatus, codebase.Name, err)
if err = updateGitStatusWithPatch(ctx, h.client, codebase, codebaseApi.SetupDeploymentTemplates, util.ProjectTemplatesPushedStatus); err != nil {
return err
}

log.Info("Config has been pushed")
Expand Down
7 changes: 2 additions & 5 deletions controllers/codebase/service/chain/put_gitlab_ci_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,8 @@ func (h *PutGitLabCIConfig) ServeRequest(ctx context.Context, codebase *codebase
log.Info("End pushing GitLab CI config")

// Set status to mark this stage complete
codebase.Status.Git = util.ProjectGitLabCIPushedStatus
if err := h.client.Status().Update(ctx, codebase); err != nil {
setFailedFields(codebase, codebaseApi.RepositoryProvisioning, err.Error())
return fmt.Errorf("failed to set git status %s for codebase %s: %w",
util.ProjectGitLabCIPushedStatus, codebase.Name, err)
if err := updateGitStatusWithPatch(ctx, h.client, codebase, codebaseApi.RepositoryProvisioning, util.ProjectGitLabCIPushedStatus); err != nil {
return err
}

log.Info("GitLab CI status has been set successfully")
Expand Down
6 changes: 2 additions & 4 deletions controllers/codebase/service/chain/put_project.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,10 +92,8 @@ func (h *PutProject) ServeRequest(ctx context.Context, codebase *codebaseApi.Cod
return fmt.Errorf("failed to create project: %w", err)
}

codebase.Status.Git = util.ProjectPushedStatus
if err = h.client.Status().Update(ctx, codebase); err != nil {
setFailedFields(codebase, codebaseApi.RepositoryProvisioning, err.Error())
return fmt.Errorf("failed to set git status %s for codebase %s: %w", util.ProjectPushedStatus, codebase.Name, err)
if err = updateGitStatusWithPatch(ctx, h.client, codebase, codebaseApi.RepositoryProvisioning, util.ProjectPushedStatus); err != nil {
return err
}

log.Info("Finish putting project")
Expand Down