internal/handler/appdeployment.go (191 lines of code) (raw):

package handler import ( "context" "fmt" "github.com/go-logr/logr" batchv1 "k8s.io/api/batch/v1" apierror "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/tools/record" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "github.com/Azure/operation-cache-controller/api/v1alpha1" "github.com/Azure/operation-cache-controller/internal/log" ctrlutils "github.com/Azure/operation-cache-controller/internal/utils/controller" "github.com/Azure/operation-cache-controller/internal/utils/reconciler" ) type AppdeploymentHandlerContextKey struct{} //go:generate mockgen -destination=./mocks/mock_appdeployment.go -package=mocks github.com/Azure/operation-cache-controller/internal/handler AppDeploymentHandlerInterface type AppDeploymentHandlerInterface interface { EnsureApplicationValid(ctx context.Context) (reconciler.OperationResult, error) EnsureFinalizer(ctx context.Context) (reconciler.OperationResult, error) EnsureFinalizerDeleted(ctx context.Context) (reconciler.OperationResult, error) EnsureDependenciesReady(ctx context.Context) (reconciler.OperationResult, error) EnsureDeployingFinished(ctx context.Context) (reconciler.OperationResult, error) EnsureTeardownFinished(ctx context.Context) (reconciler.OperationResult, error) } type AppDeploymentHandler struct { appDeployment *v1alpha1.AppDeployment logger logr.Logger client client.Client recorder record.EventRecorder apdutil ctrlutils.AppDeploymentHelper } func NewAppDeploymentHandler(ctx context.Context, appDeployment *v1alpha1.AppDeployment, logger logr.Logger, client client.Client, recorder record.EventRecorder) AppDeploymentHandlerInterface { if appdeploymentHandler, ok := ctx.Value(AppdeploymentHandlerContextKey{}).(AppDeploymentHandlerInterface); ok { return appdeploymentHandler } return &AppDeploymentHandler{ appDeployment: appDeployment, logger: logger, recorder: recorder, client: client, apdutil: ctrlutils.NewAppDeploymentHelper(), } } func (a *AppDeploymentHandler) phaseIs(phase ...string) bool { for _, p := range phase { if a.appDeployment.Status.Phase == p { return true } } return false } func (a *AppDeploymentHandler) EnsureApplicationValid(ctx context.Context) (reconciler.OperationResult, error) { a.logger.V(1).Info("Operation EnsureApplicationValid") if err := ctrlutils.Validate(a.appDeployment); err != nil { a.recorder.Event(a.appDeployment, "Error", "InvalidApplication", err.Error()) return reconciler.RequeueWithError(err) } // initialize the appdeployment status if a.phaseIs(v1alpha1.AppDeploymentPhaseEmpty) { a.logger.V(1).Info("Initializing appdeployment status") a.appDeployment.Status.Phase = v1alpha1.AppDeploymentPhasePending a.apdutil.ClearConditions(ctx, a.appDeployment) return reconciler.RequeueOnErrorOrContinue(a.client.Status().Update(ctx, a.appDeployment)) } return reconciler.ContinueProcessing() } func (a *AppDeploymentHandler) EnsureFinalizer(ctx context.Context) (reconciler.OperationResult, error) { a.logger.V(1).Info("Operation EnsureFinalizer") if a.appDeployment.ObjectMeta.DeletionTimestamp.IsZero() && !controllerutil.ContainsFinalizer(a.appDeployment, v1alpha1.AppDeploymentFinalizerName) { controllerutil.AddFinalizer(a.appDeployment, v1alpha1.AppDeploymentFinalizerName) } return reconciler.RequeueOnErrorOrContinue(a.client.Update(ctx, a.appDeployment)) } func (a *AppDeploymentHandler) EnsureFinalizerDeleted(ctx context.Context) (reconciler.OperationResult, error) { a.logger.V(1).Info("Operation EnsureFinalizerDeleted") if !a.appDeployment.ObjectMeta.DeletionTimestamp.IsZero() && controllerutil.ContainsFinalizer(a.appDeployment, v1alpha1.AppDeploymentFinalizerName) { if a.phaseIs(v1alpha1.AppDeploymentPhaseDeleted) { a.logger.V(1).Info("All app deleted removing finalizer") controllerutil.RemoveFinalizer(a.appDeployment, v1alpha1.AppDeploymentFinalizerName) return reconciler.RequeueOnErrorOrContinue(a.client.Update(ctx, a.appDeployment)) } if !a.phaseIs(v1alpha1.AppDeploymentPhaseDeleting) { a.logger.V(1).Info("App is not deleted yet, setting phase to deleting") a.appDeployment.Status.Phase = v1alpha1.AppDeploymentPhaseDeleting return reconciler.RequeueOnErrorOrContinue(a.client.Status().Update(ctx, a.appDeployment)) } } return reconciler.ContinueProcessing() } func (a *AppDeploymentHandler) EnsureDependenciesReady(ctx context.Context) (reconciler.OperationResult, error) { if !a.phaseIs(v1alpha1.AppDeploymentPhasePending) { return reconciler.ContinueProcessing() } a.logger.V(1).Info("Operation EnsureDependenciesReady") // list all dependencies and check if they are ready for _, dep := range a.appDeployment.Spec.Dependencies { // check if dependency is ready appdeployment := &v1alpha1.AppDeployment{} realAppName := ctrlutils.OperationScopedAppDeployment(dep, a.appDeployment.Spec.OpId) if err := a.client.Get(ctx, client.ObjectKey{Namespace: a.appDeployment.Namespace, Name: realAppName}, appdeployment); err != nil { a.logger.V(1).Error(err, "dependency not found", "dependency", realAppName) return reconciler.RequeueWithError(fmt.Errorf("dependency not found: %s ", realAppName)) } if appdeployment.Status.Phase != v1alpha1.AppDeploymentPhaseReady { return reconciler.RequeueWithError(fmt.Errorf("dependency is not ready: %s", realAppName)) } } // all dependencies are ready a.appDeployment.Status.Phase = v1alpha1.AppDeploymentPhaseDeploying return reconciler.RequeueOnErrorOrContinue(a.client.Status().Update(ctx, a.appDeployment)) } var ( errJobNotCompleted = fmt.Errorf("job not completed") ) func (a *AppDeploymentHandler) createJob(ctx context.Context, jobTemplate *batchv1.Job) error { if err := ctrl.SetControllerReference(a.appDeployment, jobTemplate, a.client.Scheme()); err != nil { return fmt.Errorf("failed to set controller reference for job %s: %w", jobTemplate.Name, err) } if err := a.client.Create(ctx, jobTemplate); err != nil { return fmt.Errorf("failed to create job %s: %w", jobTemplate.Name, err) } return nil } func (a *AppDeploymentHandler) initializeJobAndAwaitCompletion(ctx context.Context, jobTemplate *batchv1.Job) error { job := &batchv1.Job{} // check if the job exists if err := a.client.Get(ctx, client.ObjectKey{Namespace: a.appDeployment.Namespace, Name: jobTemplate.Name}, job); err != nil { if !apierror.IsNotFound(err) { return fmt.Errorf("failed to get job %s: %w", jobTemplate.Name, err) } // create a new job if err := a.createJob(ctx, jobTemplate); err != nil { a.recorder.Event(a.appDeployment, "Error", "FailedCreateJob", err.Error()) return fmt.Errorf("failed to create job %s: %w", jobTemplate.Name, err) } return errJobNotCompleted // requeue } // check if the job is running switch ctrlutils.CheckJobStatus(ctx, job) { // if job is failed then delete the job and create a new one case ctrlutils.JobStatusFailed: // delete the failed job if err := a.client.Delete(ctx, job, client.PropagationPolicy(metav1.DeletePropagationBackground)); client.IgnoreNotFound(err) != nil { a.recorder.Event(a.appDeployment, "Error", "FailedDeleteJob", err.Error()) return fmt.Errorf("failed to delete job %s: %w", job.Name, err) } // create a new job if err := ctrl.SetControllerReference(a.appDeployment, jobTemplate, a.client.Scheme()); err != nil { return fmt.Errorf("failed to set controller reference for job %s: %w", job.Name, err) } if err := a.client.Create(ctx, jobTemplate); err != nil { return fmt.Errorf("failed to create job %s: %w", jobTemplate.Name, err) } // if job is succeeded then delete the job case ctrlutils.JobStatusSucceeded: // delete the succeeded job if err := a.client.Delete(ctx, job, client.PropagationPolicy(metav1.DeletePropagationBackground)); client.IgnoreNotFound(err) != nil { return fmt.Errorf("failed to delete succeeded job %s: %w", job.Name, err) } return nil } return errJobNotCompleted } // EnsureDeployingFinished checks if the provision job exists // if not exist then create a new provision job // if job is exist && running then requeue and waiting for the job complete // if job is exist && failed then delete the job and create a new one // if job is exist && succeeded then update the appdeployment status to ready func (a *AppDeploymentHandler) EnsureDeployingFinished(ctx context.Context) (reconciler.OperationResult, error) { a.logger.V(1).Info("Operation EnsureDeployingFinished") if !a.phaseIs(v1alpha1.AppDeploymentPhaseDeploying) { return reconciler.ContinueProcessing() } provisionJob := ctrlutils.ProvisionJobFromAppDeploymentSpec(a.appDeployment) err := a.initializeJobAndAwaitCompletion(ctx, provisionJob) switch err { case nil: // provision job is succeeded move the appdeployment to ready phase a.appDeployment.Status.Phase = v1alpha1.AppDeploymentPhaseReady return reconciler.RequeueOnErrorOrContinue(a.client.Status().Update(ctx, a.appDeployment)) case errJobNotCompleted: a.logger.V(1).WithValues(log.AppDeploymentJobName, provisionJob.Name).Info("provision job is not completed yet") return reconciler.Requeue() default: a.logger.Error(err, "provision job failed %s", provisionJob.Name) return reconciler.RequeueWithError(err) } } func (a *AppDeploymentHandler) EnsureTeardownFinished(ctx context.Context) (reconciler.OperationResult, error) { a.logger.V(1).Info("Operation EnsureTeardownFinished") if !a.phaseIs(v1alpha1.AppDeploymentPhaseDeleting) { return reconciler.ContinueProcessing() } teardownJob := ctrlutils.TeardownJobFromAppDeploymentSpec(a.appDeployment) err := a.initializeJobAndAwaitCompletion(ctx, teardownJob) switch err { case nil: // teardown job is succeeded move the appdeployment to deleted phase a.appDeployment.Status.Phase = v1alpha1.AppDeploymentPhaseDeleted return reconciler.RequeueOnErrorOrContinue(a.client.Status().Update(ctx, a.appDeployment)) case errJobNotCompleted: a.logger.V(1).WithValues(log.AppDeploymentJobName, teardownJob.Name).Info("teardown job is not completed yet") return reconciler.Requeue() default: a.logger.WithValues(log.AppDeploymentJobName, teardownJob.Name).Error(err, "teardown job failed %s") return reconciler.RequeueWithError(err) } }