/* Copyright 2020 The Flux CD contributors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package controllers import ( "context" "fmt" "io/ioutil" "os" "os/exec" "path" "time" "github.com/go-logr/logr" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1alpha1" "github.com/fluxcd/kustomize-controller/internal/lockedfile" sourcev1 "github.com/fluxcd/source-controller/api/v1alpha1" ) // KustomizationReconciler reconciles a Kustomization object type KustomizationReconciler struct { client.Client Log logr.Logger Scheme *runtime.Scheme } // +kubebuilder:rbac:groups=kustomize.fluxcd.io,resources=kustomizations,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=kustomize.fluxcd.io,resources=kustomizations/status,verbs=get;update;patch func (r *KustomizationReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) defer cancel() var kustomization kustomizev1.Kustomization if err := r.Get(ctx, req.NamespacedName, &kustomization); err != nil { return ctrl.Result{}, client.IgnoreNotFound(err) } log := r.Log.WithValues(kustomization.Kind, req.NamespacedName) var source sourcev1.Source // get artifact source from Git repository if kustomization.Spec.SourceRef.Kind == "GitRepository" { var repository sourcev1.GitRepository repositoryName := types.NamespacedName{ Namespace: kustomization.GetNamespace(), Name: kustomization.Spec.SourceRef.Name, } err := r.Client.Get(ctx, repositoryName, &repository) if err != nil { log.Error(err, "GitRepository not found", "gitrepository", repositoryName) return ctrl.Result{Requeue: true}, err } source = &repository } if source == nil { err := fmt.Errorf("source `%s` kind '%s' not supported", kustomization.Spec.SourceRef.Name, kustomization.Spec.SourceRef.Kind) return ctrl.Result{}, err } // try git sync syncedKustomization, err := r.sync(ctx, *kustomization.DeepCopy(), source) if err != nil { log.Error(err, "Kustomization apply failed") } // update status if err := r.Status().Update(ctx, &syncedKustomization); err != nil { log.Error(err, "unable to update Kustomization status") return ctrl.Result{Requeue: true}, err } log.Info("Kustomization sync finished", "msg", kustomizev1.KustomizationReadyMessage(syncedKustomization)) // requeue kustomization return ctrl.Result{RequeueAfter: kustomization.Spec.Interval.Duration}, nil } func (r *KustomizationReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&kustomizev1.Kustomization{}). WithEventFilter(KustomizationGarbageCollectPredicate{Log: r.Log}). WithEventFilter(KustomizationSyncAtPredicate{}). Complete(r) } func (r *KustomizationReconciler) sync( ctx context.Context, kustomization kustomizev1.Kustomization, source sourcev1.Source) (kustomizev1.Kustomization, error) { if source.GetArtifact() == nil || source.GetArtifact().URL == "" { err := fmt.Errorf("artifact not found in %s", kustomization.Spec.SourceRef.Name) return kustomizev1.KustomizationNotReady(kustomization, kustomizev1.ArtifactFailedReason, err.Error()), err } unlock, err := r.lock(fmt.Sprintf("%s-%s", kustomization.GetName(), kustomization.GetNamespace())) if err != nil { err = fmt.Errorf("tmp dir error: %w", err) return kustomizev1.KustomizationNotReady(kustomization, sourcev1.StorageOperationFailedReason, err.Error()), err } defer unlock() // create tmp dir tmpDir, err := ioutil.TempDir("", kustomization.Name) if err != nil { err = fmt.Errorf("tmp dir error: %w", err) return kustomizev1.KustomizationNotReady(kustomization, sourcev1.StorageOperationFailedReason, err.Error()), err } defer os.RemoveAll(tmpDir) // download artifact and extract files url := source.GetArtifact().URL cmd := fmt.Sprintf("cd %s && curl -sL %s | tar -xz --strip-components=1 -C .", tmpDir, url) command := exec.CommandContext(ctx, "/bin/sh", "-c", cmd) output, err := command.CombinedOutput() if err != nil { err = fmt.Errorf("artifact acquisition failed: %w", err) return kustomizev1.KustomizationNotReady( kustomization, kustomizev1.ArtifactFailedReason, err.Error(), ), fmt.Errorf("artifact download `%s` error: %s", url, string(output)) } // check build path exists buildDir := kustomization.Spec.Path if _, err := os.Stat(path.Join(tmpDir, buildDir)); err != nil { err = fmt.Errorf("kustomization path not found: %w", err) return kustomizev1.KustomizationNotReady( kustomization, kustomizev1.ArtifactFailedReason, err.Error(), ), err } // kustomize build cmd = fmt.Sprintf("cd %s && kustomize build %s > %s.yaml", tmpDir, buildDir, kustomization.GetName()) command = exec.CommandContext(ctx, "/bin/sh", "-c", cmd) output, err = command.CombinedOutput() if err != nil { err = fmt.Errorf("kustomize build error: %w", err) fmt.Println(string(output)) return kustomizev1.KustomizationNotReady( kustomization, kustomizev1.BuildFailedReason, err.Error(), ), fmt.Errorf("kustomize build error: %s", string(output)) } // set apply timeout timeout := kustomization.Spec.Interval.Duration + (time.Second * 1) ctxApply, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() // run apply with timeout cmd = fmt.Sprintf("cd %s && kubectl apply -f %s.yaml --timeout=%s", tmpDir, kustomization.GetName(), kustomization.Spec.Interval.Duration.String()) if kustomization.Spec.Prune != "" { cmd = fmt.Sprintf("%s --prune -l %s", cmd, kustomization.Spec.Prune) } command = exec.CommandContext(ctxApply, "/bin/sh", "-c", cmd) output, err = command.CombinedOutput() if err != nil { err = fmt.Errorf("kubectl apply error: %w", err) return kustomizev1.KustomizationNotReady( kustomization, kustomizev1.ApplyFailedReason, err.Error(), ), fmt.Errorf("kubectl apply: %s", string(output)) } // log apply output fmt.Println(string(output)) return kustomizev1.KustomizationReady( kustomization, kustomizev1.ApplySucceedReason, "kustomization was successfully applied", ), nil } func (r *KustomizationReconciler) lock(name string) (unlock func(), err error) { lockFile := path.Join(os.TempDir(), name+".lock") mutex := lockedfile.MutexAt(lockFile) return mutex.Lock() }