controllers/codebasebranch/codebasebranch_controller.go (293 lines of code) (raw):
package codebasebranch
import (
"context"
"errors"
"fmt"
"reflect"
"strings"
"time"
"github.com/go-logr/logr"
k8sErrors "k8s.io/apimachinery/pkg/api/errors"
metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/util/retry"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/builder"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"sigs.k8s.io/controller-runtime/pkg/event"
"sigs.k8s.io/controller-runtime/pkg/predicate"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
codebaseApi "github.com/epam/edp-codebase-operator/v2/api/v1"
"github.com/epam/edp-codebase-operator/v2/controllers/codebasebranch/chain/factory"
cbHandler "github.com/epam/edp-codebase-operator/v2/controllers/codebasebranch/chain/handler"
"github.com/epam/edp-codebase-operator/v2/controllers/codebasebranch/service"
"github.com/epam/edp-codebase-operator/v2/pkg/codebasebranch"
"github.com/epam/edp-codebase-operator/v2/pkg/model"
codebasepredicate "github.com/epam/edp-codebase-operator/v2/pkg/predicate"
"github.com/epam/edp-codebase-operator/v2/pkg/util"
)
func NewReconcileCodebaseBranch(c client.Client, scheme *runtime.Scheme, log logr.Logger) *ReconcileCodebaseBranch {
return &ReconcileCodebaseBranch{
client: c,
scheme: scheme,
log: log.WithName("codebase-branch"),
}
}
type ReconcileCodebaseBranch struct {
client client.Client
scheme *runtime.Scheme
log logr.Logger
}
const (
codebaseBranchOperatorFinalizerName = "codebase.branch.operator.finalizer.name"
errorStatus = "error"
)
func (r *ReconcileCodebaseBranch) SetupWithManager(mgr ctrl.Manager, maxConcurrentReconciles int) error {
p := predicate.Funcs{
UpdateFunc: func(e event.UpdateEvent) bool {
oo, ok := e.ObjectOld.(*codebaseApi.CodebaseBranch)
if !ok {
return false
}
no, ok := e.ObjectNew.(*codebaseApi.CodebaseBranch)
if !ok {
return false
}
if codebasepredicate.PauseAnnotationChanged(oo, no) {
return true
}
if !reflect.DeepEqual(oo.Spec, no.Spec) {
return true
}
if no.DeletionTimestamp != nil {
return true
}
return false
},
}
pause := codebasepredicate.NewPause(r.log)
err := ctrl.NewControllerManagedBy(mgr).
For(&codebaseApi.CodebaseBranch{}, builder.WithPredicates(pause, p)).
WithOptions(controller.Options{
MaxConcurrentReconciles: maxConcurrentReconciles,
}).
Complete(r)
if err != nil {
return fmt.Errorf("failed to build CodebaseBranch controller: %w", err)
}
return nil
}
//+kubebuilder:rbac:groups=v2.edp.epam.com,namespace=placeholder,resources=codebasebranches,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=v2.edp.epam.com,namespace=placeholder,resources=codebasebranches/status,verbs=get;update;patch
//+kubebuilder:rbac:groups=v2.edp.epam.com,namespace=placeholder,resources=codebasebranches/finalizers,verbs=update
// Reconcile reads that state of the cluster for a CodebaseBranch object and makes changes based on the state.
func (r *ReconcileCodebaseBranch) Reconcile(ctx context.Context, request reconcile.Request) (reconcile.Result, error) {
log := ctrl.LoggerFrom(ctx)
log.Info("Reconciling CodebaseBranch")
cb := &codebaseApi.CodebaseBranch{}
if err := r.client.Get(ctx, request.NamespacedName, cb); err != nil {
if k8sErrors.IsNotFound(err) {
return reconcile.Result{}, nil
}
return reconcile.Result{}, fmt.Errorf("failed to fetch CodebaseBranch resource %q: %w", request.NamespacedName, err)
}
c := &codebaseApi.Codebase{}
if err := r.client.Get(ctx, types.NamespacedName{Name: cb.Spec.CodebaseName, Namespace: cb.Namespace}, c); err != nil {
return reconcile.Result{}, fmt.Errorf("failed to get Codebase: %w", err)
}
updated, err := r.setDefaultValues(ctx, cb, c)
if err != nil {
return reconcile.Result{}, fmt.Errorf("failed to set default values: %w", err)
}
if updated {
if err = r.client.Update(ctx, cb); err != nil {
return reconcile.Result{}, fmt.Errorf("failed to update CodebaseBranch: %w", err)
}
return reconcile.Result{}, nil
}
if err = r.setOwnerRef(cb, c); err != nil {
setErrorStatus(cb, err.Error())
if err = r.updateStatus(ctx, cb); err != nil {
log.Error(err, "error on codebase branch update status")
}
return reconcile.Result{}, fmt.Errorf("failed to set OwnerRef for codebasebranch %v: %w", cb.Name, err)
}
if err = codebasebranch.AddCodebaseLabel(ctx, r.client, cb, c.Name); err != nil {
log.Error(err, "set labels failed")
}
result, err := r.tryToDeleteCodebaseBranch(ctx, cb, factory.GetDeletionChain())
if err != nil {
return reconcile.Result{}, fmt.Errorf("failed to remove codebasebranch %v: %w", cb.Name, err)
}
if result != nil {
return *result, nil
}
// this is a case where we want to init build number
// a default build number is a "0"
// later will be incremented during CI/CD stages
if c.Spec.Versioning.Type == codebaseApi.VersioningTypeEDP && cb.Status.Build == nil {
buildNumber := "0"
cb.Status.Build = &buildNumber
}
cbChain := factory.GetChain(r.client)
if err := cbChain.ServeRequest(ctx, cb); err != nil {
const defaultPostponeTime = 5 * time.Second
log.Error(err, "an error has occurred while handling codebase branch", "name", cb.Name)
reconcileErr := util.NewCodebaseBranchReconcileError("error")
if errors.As(err, &reconcileErr) {
return reconcile.Result{RequeueAfter: defaultPostponeTime}, nil
}
timeout := r.setFailureCount(cb)
if statErr := r.updateStatus(ctx, cb); statErr != nil {
ctrl.LoggerFrom(ctx).Error(statErr, "failed to update CodebaseBranch status with failure count")
}
return reconcile.Result{RequeueAfter: timeout}, fmt.Errorf("failed to process default chain: %w", err)
}
if err := r.setSuccessStatus(ctx, cb, codebaseApi.CIConfiguration); err != nil {
return reconcile.Result{},
fmt.Errorf("failed to update Codebase %v branch status: %w", cb.Name, err)
}
log.Info("Reconciling CodebaseBranch has been finished")
return reconcile.Result{}, nil
}
func (r *ReconcileCodebaseBranch) setSuccessStatus(ctx context.Context, cb *codebaseApi.CodebaseBranch, action codebaseApi.ActionType) error {
cb.Status = codebaseApi.CodebaseBranchStatus{
LastTimeUpdated: metaV1.Now(),
Username: "system",
Action: action,
Result: codebaseApi.Success,
Value: "active",
Status: model.StatusFinished,
VersionHistory: cb.Status.VersionHistory,
LastSuccessfulBuild: cb.Status.LastSuccessfulBuild,
Build: cb.Status.Build,
Git: cb.Status.Git,
}
return r.updateStatus(ctx, cb)
}
func (r *ReconcileCodebaseBranch) updateStatus(ctx context.Context, cb *codebaseApi.CodebaseBranch) error {
// We need to get the latest version of the CodebaseBranch before updating it,
// because the object might have been updated since it was fetched.
cbbranch := &codebaseApi.CodebaseBranch{}
if err := r.client.Get(ctx, types.NamespacedName{Name: cb.Name, Namespace: cb.Namespace}, cbbranch); err != nil {
return fmt.Errorf("failed to get CodebaseBranch: %w", err)
}
cbbranch.Status = cb.Status
if err := r.client.Status().Update(ctx, cbbranch); err != nil {
return fmt.Errorf("failed to update CodebaseBranch status: %w", err)
}
r.log.V(2).Info("codebase branch status has been updated", "name", cb.Name)
return nil
}
func (r *ReconcileCodebaseBranch) tryToDeleteCodebaseBranch(ctx context.Context, cb *codebaseApi.CodebaseBranch,
deletionChain cbHandler.CodebaseBranchHandler,
) (*reconcile.Result, error) {
if cb.GetDeletionTimestamp().IsZero() {
if controllerutil.AddFinalizer(cb, codebaseBranchOperatorFinalizerName) {
if err := r.client.Update(ctx, cb); err != nil {
return &reconcile.Result{}, fmt.Errorf("failed to add finalizer to %v: %w", cb.Name, err)
}
}
return nil, nil
}
if err := deletionChain.ServeRequest(ctx, cb); err != nil {
if errors.Is(err, service.ErrJobFailed) {
r.log.Error(err, "deletion job failed")
timeout := r.setFailureCount(cb)
if err = r.client.Status().Update(ctx, cb); err != nil {
ctrl.LoggerFrom(ctx).Error(err, "failed to update CodebaseBranch status with failure count")
}
return &reconcile.Result{RequeueAfter: timeout}, nil
}
}
if err := removeDirectoryIfExists(cb.Spec.CodebaseName, cb.Name, cb.Namespace); err != nil {
return &reconcile.Result{}, fmt.Errorf("failed to remove codebase branch directory: %w", err)
}
err := retry.RetryOnConflict(retry.DefaultRetry, func() error {
// Fetch the resource here; we need to refetch it on every try, since
// if we got a conflict on the last update attempt then we need to get
// the current version before making your own changes.
if err := r.client.Get(ctx, types.NamespacedName{Namespace: cb.Namespace, Name: cb.Name}, cb); err != nil {
return err //nolint:wrapcheck // We have to return original error
}
r.log.Info("Trying to remove finalizer from", "codebasebranch_name", cb.Name)
controllerutil.RemoveFinalizer(cb, codebaseBranchOperatorFinalizerName)
err := r.client.Update(ctx, cb)
// We have to return err itself here (not wrapped inside another error)
// so that RetryOnConflict can identify it correctly.
return err //nolint:wrapcheck // We have to return original error
})
if err != nil {
// May be conflict if max retries were hit, or may be something unrelated
// like permissions or a network error
return &reconcile.Result{}, fmt.Errorf("failed to remove finalizer from %v: %w", cb.Name, err)
}
return &reconcile.Result{}, nil
}
func removeDirectoryIfExists(codebaseName, branchName, namespace string) error {
wd := util.GetWorkDir(codebaseName, fmt.Sprintf("%v-%v", namespace, branchName))
if err := util.RemoveDirectory(wd); err != nil {
return fmt.Errorf("failed to remove directory %q: %w", wd, err)
}
return nil
}
// setFailureCount increments failure count and returns delay for next reconciliation.
func (r *ReconcileCodebaseBranch) setFailureCount(c *codebaseApi.CodebaseBranch) time.Duration {
const defaultDuration = 10 * time.Second
timeout := util.GetTimeout(c.Status.FailureCount, defaultDuration)
r.log.V(2).Info("wait for next reconciliation", "next reconciliation in", timeout)
c.Status.FailureCount++
return timeout
}
func (r *ReconcileCodebaseBranch) setOwnerRef(cb *codebaseApi.CodebaseBranch, c *codebaseApi.Codebase) error {
if err := controllerutil.SetControllerReference(c, cb, r.scheme); err != nil {
return fmt.Errorf("failed to set owner ref for CodebaseBranch CR: %w", err)
}
return nil
}
const codebaseTypeShorLen = 3
func (r *ReconcileCodebaseBranch) setDefaultValues(
ctx context.Context,
cb *codebaseApi.CodebaseBranch,
codebase *codebaseApi.Codebase,
) (bool, error) {
if pipelinesIsSet(cb) {
return false, nil
}
gitServer := &codebaseApi.GitServer{}
if err := r.client.Get(ctx, types.NamespacedName{
Name: codebase.Spec.GitServer,
Namespace: codebase.Namespace,
}, gitServer); err != nil {
return false, fmt.Errorf("failed to get GitServer: %w", err)
}
var codebaseType string
if len(codebase.Spec.Type) < codebaseTypeShorLen {
return false, fmt.Errorf("codebase type is invalid: %v", codebase.Spec.Type)
}
codebaseType = codebase.Spec.Type[:codebaseTypeShorLen]
changed := false
if cb.Spec.Pipelines == nil {
cb.Spec.Pipelines = make(map[string]string, 2)
}
if _, ok := cb.Spec.Pipelines["build"]; !ok {
cb.Spec.Pipelines["build"] = fmt.Sprintf(
"%s-%s-%s-%s-build-%s",
gitServer.Spec.GitProvider,
strings.ToLower(codebase.Spec.BuildTool),
strings.ToLower(codebase.Spec.Framework),
codebaseType,
codebase.Spec.Versioning.Type,
)
changed = true
}
if _, ok := cb.Spec.Pipelines["review"]; !ok {
cb.Spec.Pipelines["review"] = fmt.Sprintf(
"%s-%s-%s-%s-review",
gitServer.Spec.GitProvider,
strings.ToLower(codebase.Spec.BuildTool),
strings.ToLower(codebase.Spec.Framework),
codebaseType,
)
changed = true
}
return changed, nil
}
func pipelinesIsSet(cb *codebaseApi.CodebaseBranch) bool {
if cb.Spec.Pipelines == nil {
return false
}
_, hasReview := cb.Spec.Pipelines["review"]
_, hasBuild := cb.Spec.Pipelines["build"]
return hasReview && hasBuild
}
func setErrorStatus(metadata *codebaseApi.CodebaseBranch, msg string) {
metadata.Status.Status = errorStatus
metadata.Status.DetailedMessage = msg
}