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
14 changes: 14 additions & 0 deletions api/v1/annotations.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,18 @@ const (
// GitLabCITemplateAnnotation is an annotation on a Codebase CR that specifies the ConfigMap
// name to use as the GitLab CI template. When absent, the operator falls back to "gitlab-ci-default".
GitLabCITemplateAnnotation = "app.edp.epam.com/gitlab-ci-template"

// BranchCleanupStrategyAnnotation is an annotation on a Codebase CR that defines what the
// operator does with CodebaseBranch resources whose branch no longer exists in git.
// Supported values: "mark" (default) - only mark the branch as stale;
// "auto" - delete the stale branch when it is not referenced by any CDPipeline/Stage.
BranchCleanupStrategyAnnotation = "app.edp.epam.com/branch-cleanup-strategy"
)

const (
// BranchCleanupStrategyMark marks stale branches for manual cleanup.
BranchCleanupStrategyMark = "mark"

// BranchCleanupStrategyAuto deletes stale branches that are not used by any CDPipeline/Stage.
BranchCleanupStrategyAuto = "auto"
)
19 changes: 19 additions & 0 deletions api/v1/codebasebranch_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ import (

const (
CodebaseBranchGitStatusBranchCreated = "branch-created"

// ConditionStale is a condition type indicating whether the branch
// no longer exists in the git repository ("True" means the branch is missing).
ConditionStale = "Stale"

ReasonBranchNotFoundInGit = "BranchNotFoundInGit"
ReasonBranchFoundInGit = "BranchFoundInGit"
)

// CodebaseBranchSpec defines the desired state of CodebaseBranch.
Expand Down Expand Up @@ -83,6 +90,17 @@ type CodebaseBranchStatus struct {

// Specifies a status of action for git.
Git string `json:"git,omitempty"`

// Conditions represent the latest available observations of an object's state.
// Each condition type is an independent observation (e.g. Stale); new types can be
// added over time without schema changes.
// +optional
// +nullable
// +listType=map
// +listMapKey=type
// +patchStrategy=merge
// +patchMergeKey=type
Conditions []metaV1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"`
}

// +kubebuilder:object:root=true
Expand All @@ -94,6 +112,7 @@ type CodebaseBranchStatus struct {
// +kubebuilder:printcolumn:name="Codebase Name",type="string",JSONPath=".spec.codebaseName",description="Owner of object"
// +kubebuilder:printcolumn:name="Release",type="boolean",JSONPath=".spec.release",description="Is a release branch"
// +kubebuilder:printcolumn:name="Branch",type="string",JSONPath=".spec.branchName",description="Name of branch"
// +kubebuilder:printcolumn:name="Stale",type="string",JSONPath=`.status.conditions[?(@.type=="Stale")].status`,description="Branch is missing in git"

// CodebaseBranch is the Schema for the CodebaseBranches API.
type CodebaseBranch struct {
Expand Down
6 changes: 6 additions & 0 deletions api/v1/labels.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,10 @@ const (

// GitServerLabel is a label used to store the name of the GitServer in related resources.
GitServerLabel = "app.edp.epam.com/gitServer"

// StaleLabel marks a CodebaseBranch whose branch no longer exists in the git repository.
// It mirrors the Stale status condition so that resources can be filtered with a label
// selector; the condition remains the source of truth and the label is re-asserted by
// the operator on every staleness check.
StaleLabel = "app.edp.epam.com/stale"
)
8 changes: 8 additions & 0 deletions api/v1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

44 changes: 44 additions & 0 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,13 @@ import (
"github.com/epam/edp-codebase-operator/v2/controllers/cdstagedeploy/chain"
"github.com/epam/edp-codebase-operator/v2/controllers/codebase"
"github.com/epam/edp-codebase-operator/v2/controllers/codebasebranch"
"github.com/epam/edp-codebase-operator/v2/controllers/codebasebranch/stalecheck"
"github.com/epam/edp-codebase-operator/v2/controllers/codebaseimagestream"
"github.com/epam/edp-codebase-operator/v2/controllers/gitserver"
"github.com/epam/edp-codebase-operator/v2/controllers/integrationsecret"
"github.com/epam/edp-codebase-operator/v2/controllers/jiraissuemetadata"
"github.com/epam/edp-codebase-operator/v2/controllers/jiraserver"
gitproviderv2 "github.com/epam/edp-codebase-operator/v2/pkg/git"
"github.com/epam/edp-codebase-operator/v2/pkg/telemetry"
"github.com/epam/edp-codebase-operator/v2/pkg/util"
"github.com/epam/edp-codebase-operator/v2/pkg/webhook"
Expand All @@ -59,6 +61,8 @@ const (
telemetryDefaultDelay = time.Hour
telemetrySendEvery = time.Hour * 24
telemetryUrl = "https://telemetry.kuberocketci.io"
branchStaleCheckIntervalEnv = "BRANCH_STALE_CHECK_INTERVAL"
branchStaleCheckDefaultInterval = time.Hour * 24
)

func main() {
Expand Down Expand Up @@ -322,6 +326,27 @@ func main() {
os.Exit(1)
}

if interval := getBranchStaleCheckInterval(); interval > 0 {
staleRecorder := mgr.GetEventRecorderFor("stale-branch-checker")

markAction := stalecheck.NewMarkAction(mgr.GetClient(), staleRecorder)

checker := stalecheck.NewChecker(
mgr.GetClient(),
ns,
interval,
gitproviderv2.DefaultGitProviderFactory,
markAction,
stalecheck.NewCleanupAction(mgr.GetClient(), staleRecorder, markAction),
)
if err := mgr.Add(checker); err != nil {
setupLog.Error(err, "failed to add stale branch checker to manager")
os.Exit(1)
}
} else {
setupLog.Info("Stale branch checker is disabled", "env", branchStaleCheckIntervalEnv)
}

setupLog.Info("starting manager")

ctx := ctrl.SetupSignalHandler()
Expand Down Expand Up @@ -351,6 +376,25 @@ func getMaxConcurrentReconciles(envVar string) int {
return int(n)
}

// getBranchStaleCheckInterval accepts Go duration strings (e.g. "24h", "30m");
// a zero or negative duration disables the check.
func getBranchStaleCheckInterval() time.Duration {
val, exists := os.LookupEnv(branchStaleCheckIntervalEnv)
if !exists {
return branchStaleCheckDefaultInterval
}

d, err := time.ParseDuration(val)
if err != nil {
setupLog.Error(err, "Invalid stale branch check interval, using default",
"env", branchStaleCheckIntervalEnv, "value", val, "default", branchStaleCheckDefaultInterval)

return branchStaleCheckDefaultInterval
}

return d
}

func getTelemetryDelay() time.Duration {
val, exists := os.LookupEnv("TELEMETRY_DELAY")
if !exists {
Expand Down
68 changes: 68 additions & 0 deletions config/crd/bases/v2.edp.epam.com_codebasebranches.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ spec:
jsonPath: .spec.branchName
name: Branch
type: string
- description: Branch is missing in git
jsonPath: .status.conditions[?(@.type=="Stale")].status
name: Stale
type: string
name: v1
schema:
openAPIV3Schema:
Expand Down Expand Up @@ -109,6 +113,70 @@ spec:
build:
nullable: true
type: string
conditions:
description: |-
Conditions represent the latest available observations of an object's state.
Each condition type is an independent observation (e.g. Stale); new types can be
added over time without schema changes.
items:
description: Condition contains details for one aspect of the current
state of this API Resource.
properties:
lastTransitionTime:
description: |-
lastTransitionTime is the last time the condition transitioned from one status to another.
This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
format: date-time
type: string
message:
description: |-
message is a human readable message indicating details about the transition.
This may be an empty string.
maxLength: 32768
type: string
observedGeneration:
description: |-
observedGeneration represents the .metadata.generation that the condition was set based upon.
For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
with respect to the current state of the instance.
format: int64
minimum: 0
type: integer
reason:
description: |-
reason contains a programmatic identifier indicating the reason for the condition's last transition.
Producers of specific condition types may define expected values and meanings for this field,
and whether the values are considered a guaranteed API.
The value should be a CamelCase string.
This field may not be empty.
maxLength: 1024
minLength: 1
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
type: string
status:
description: status of the condition, one of True, False, Unknown.
enum:
- "True"
- "False"
- Unknown
type: string
type:
description: type of condition in CamelCase or in foo.example.com/CamelCase.
maxLength: 316
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
type: string
required:
- lastTransitionTime
- message
- reason
- status
- type
type: object
nullable: true
type: array
x-kubernetes-list-map-keys:
- type
x-kubernetes-list-type: map
detailedMessage:
description: |-
Detailed information regarding action result
Expand Down
1 change: 1 addition & 0 deletions controllers/codebasebranch/chain/check_reference.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,5 +134,6 @@ func (CheckReferenceExists) setFailedFields(
Build: codebaseBranch.Status.Build,
FailureCount: codebaseBranch.Status.FailureCount,
Git: codebaseBranch.Status.Git,
Conditions: codebaseBranch.Status.Conditions,
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,5 +52,6 @@ func setFailedFields(cb *codebaseApi.CodebaseBranch, a codebaseApi.ActionType, m
Git: cb.Status.Git,
VersionHistory: cb.Status.VersionHistory,
Build: cb.Status.Build,
Conditions: cb.Status.Conditions,
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ func (h PutBranchInGit) setIntermediateSuccessFields(
Build: cb.Status.Build,
FailureCount: cb.Status.FailureCount,
Git: cb.Status.Git,
Conditions: cb.Status.Conditions,
}

err := h.Client.Status().Update(ctx, cb)
Expand All @@ -185,6 +186,7 @@ func putGitBranchSetFailedFields(cb *codebaseApi.CodebaseBranch, message string)
Build: cb.Status.Build,
FailureCount: cb.Status.FailureCount,
Git: cb.Status.Git,
Conditions: cb.Status.Conditions,
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ func setFailedFields(cb *codebaseApi.CodebaseBranch, a codebaseApi.ActionType, m
Git: cb.Status.Git,
VersionHistory: cb.Status.VersionHistory,
Build: cb.Status.Build,
Conditions: cb.Status.Conditions,
}
}

Expand All @@ -230,6 +231,7 @@ func (h PutCodebaseImageStream) setIntermediateSuccessFields(
LastSuccessfulBuild: cb.Status.LastSuccessfulBuild,
Build: cb.Status.Build,
Git: cb.Status.Git,
Conditions: cb.Status.Conditions,
}

err := h.Client.Status().Update(ctx, cb)
Expand Down
21 changes: 21 additions & 0 deletions controllers/codebasebranch/codebasebranch_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,22 @@ func (r *ReconcileCodebaseBranch) Reconcile(ctx context.Context, request reconci

c := &codebaseApi.Codebase{}
if err := r.client.Get(ctx, types.NamespacedName{Name: cb.Spec.CodebaseName, Namespace: cb.Namespace}, c); err != nil {
// When the owning Codebase is already gone the branch is being garbage-collected
// as part of a cascade deletion; the only work left is to run the deletion flow
// and release the finalizer, otherwise the branch stays terminating forever.
if k8sErrors.IsNotFound(err) && cb.DeletionTimestamp != nil {
result, delErr := r.tryToDeleteCodebaseBranch(ctx, cb, factory.GetDeletionChain())
if delErr != nil {
return reconcile.Result{}, fmt.Errorf("failed to remove codebasebranch %v: %w", cb.Name, delErr)
}

if result != nil {
return *result, nil
}

return reconcile.Result{}, nil
}

return reconcile.Result{}, fmt.Errorf("failed to get Codebase: %w", err)
}

Expand Down Expand Up @@ -210,6 +226,7 @@ func (r *ReconcileCodebaseBranch) setSuccessStatus(
LastSuccessfulBuild: cb.Status.LastSuccessfulBuild,
Build: cb.Status.Build,
Git: cb.Status.Git,
Conditions: cb.Status.Conditions,
}

return r.updateStatus(ctx, cb)
Expand All @@ -223,7 +240,11 @@ func (r *ReconcileCodebaseBranch) updateStatus(ctx context.Context, cb *codebase
return fmt.Errorf("failed to get CodebaseBranch: %w", err)
}

// Conditions are owned by the stale branch checker, which may have patched them
// after cb was read; the freshly fetched value is always the authoritative one.
conditions := cbbranch.Status.Conditions
cbbranch.Status = cb.Status
cbbranch.Status.Conditions = conditions

if err := r.client.Status().Update(ctx, cbbranch); err != nil {
return fmt.Errorf("failed to update CodebaseBranch status: %w", err)
Expand Down
Loading
Loading