From 57474fb27480d3829747e1850ccb9698f42b68a5 Mon Sep 17 00:00:00 2001 From: stefanprodan Date: Tue, 28 Apr 2020 10:22:39 +0300 Subject: [PATCH] Implement create kustomization --- cmd/tk/create_kustomization.go | 240 +++++++++++++++++++++++++++++++++ 1 file changed, 240 insertions(+) create mode 100644 cmd/tk/create_kustomization.go diff --git a/cmd/tk/create_kustomization.go b/cmd/tk/create_kustomization.go new file mode 100644 index 00000000..c296b3c3 --- /dev/null +++ b/cmd/tk/create_kustomization.go @@ -0,0 +1,240 @@ +package main + +import ( + "context" + "fmt" + kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1alpha1" + "strings" + "time" + + sourcev1 "github.com/fluxcd/source-controller/api/v1alpha1" + "github.com/spf13/cobra" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/wait" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +var createKsCmd = &cobra.Command{ + Use: "kustomization [name]", + Short: "Create or update a kustomization resource", + Long: ` +The kustomization source command generates a kustomization.kustomize.fluxcd.io resource for a given GitRepository source. +API spec: https://github.com/fluxcd/kustomize-controller/tree/master/docs/spec/v1alpha1`, + Example: ` # Create a kustomization from a source at a given path + create kustomization backend \ + --source=webapp \ + --path="./overlays/backend/" \ + --prune="app=backend" \ + --interval=10m \ + --validate=client \ + --health-check="StatefulSet/backend.test" \ + --health-check-timeout=3m + + # Create a kustomization that depends on another + create kustomization frontend \ + --depends-on=backend \ + --source=webapp \ + --path="./overlays/frontend/" \ + --prune="app=frontend" \ + --interval=5m \ + --validate=client \ + --health-check="Deployment/frontend.test" \ + --health-check-timeout=2m +`, + RunE: createKsCmdRun, +} + +var ( + ksSource string + ksPath string + ksPrune string + ksDependsOn []string + ksValidate string + ksHealthCheck []string + ksHealthTimeout time.Duration + ksGenerate bool +) + +func init() { + createKsCmd.Flags().StringVar(&ksSource, "source", "", "GitRepository name") + createKsCmd.Flags().StringVar(&ksPath, "path", "./", "path to the directory containing the kustomization file") + createKsCmd.Flags().StringVar(&ksPrune, "prune", "", "label selector used for garbage collection") + createKsCmd.Flags().StringArrayVar(&ksHealthCheck, "health-check", nil, "workload to be included in the health assessment, in the format '/.'") + createKsCmd.Flags().DurationVar(&ksHealthTimeout, "health-check-timeout", 2*time.Minute, "timeout of health checking operations") + createKsCmd.Flags().StringVar(&ksValidate, "validate", "", "validate the manifests before applying them on the cluster, can be 'client' or 'server'") + createKsCmd.Flags().BoolVar(&ksGenerate, "generate", false, "generate the kustomization.yaml for all the Kubernetes manifests in the specified path and sub-directories") + createKsCmd.Flags().StringArrayVar(&ksDependsOn, "depends-on", nil, "kustomization that must be ready before this kustomization can be applied") + + createCmd.AddCommand(createKsCmd) +} + +func createKsCmdRun(cmd *cobra.Command, args []string) error { + if len(args) < 1 { + return fmt.Errorf("kustomization name is required") + } + name := args[0] + + if ksSource == "" { + return fmt.Errorf("source is required") + } + if ksPath == "" { + return fmt.Errorf("path is required") + } + if !strings.HasPrefix(ksPath, "./") { + return fmt.Errorf("path must begin with ./") + } + + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + kubeClient, err := utils.kubeClient(kubeconfig) + if err != nil { + return err + } + + logAction("generating %s kustomization", name) + + emptyAPIGroup := "" + kustomization := kustomizev1.Kustomization{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Spec: kustomizev1.KustomizationSpec{ + DependsOn: ksDependsOn, + Generate: ksGenerate, + Interval: metav1.Duration{ + Duration: interval, + }, + Path: ksPath, + Prune: ksPrune, + SourceRef: corev1.TypedLocalObjectReference{ + APIGroup: &emptyAPIGroup, + Kind: "GitRepository", + Name: ksSource, + }, + Suspend: false, + Validation: ksValidate, + }, + } + + if len(ksHealthCheck) > 0 { + healthChecks := make([]kustomizev1.WorkloadReference, 0) + for _, w := range ksHealthCheck { + kindObj := strings.Split(w, "/") + if len(kindObj) != 2 { + return fmt.Errorf("invalid health check '%s' must be in the format 'kind/name.namespace' %v", w, kindObj) + } + kind := kindObj[0] + kinds := map[string]bool{ + "Deployment": true, + "DaemonSet": true, + "StatefulSet": true, + } + if !kinds[kind] { + return fmt.Errorf("invalid health check kind '%s' can be Deployment, DaemonSet or StatefulSet", kind) + } + nameNs := strings.Split(kindObj[1], ".") + if len(nameNs) != 2 { + return fmt.Errorf("invalid health check '%s' must be in the format 'kind/name.namespace'", w) + } + + healthChecks = append(healthChecks, kustomizev1.WorkloadReference{ + Kind: kind, + Name: nameNs[0], + Namespace: nameNs[1], + }) + } + kustomization.Spec.HealthChecks = healthChecks + kustomization.Spec.Timeout = &metav1.Duration{ + Duration: ksHealthTimeout, + } + } + + if err := upsertKustomization(ctx, kubeClient, kustomization); err != nil { + return err + } + + logAction("waiting for kustomization sync") + if err := wait.PollImmediate(2*time.Second, timeout, + isKustomizationReady(ctx, kubeClient, name, namespace)); err != nil { + return err + } + + logSuccess("kustomization %s is ready", name) + + namespacedName := types.NamespacedName{ + Namespace: namespace, + Name: name, + } + err = kubeClient.Get(ctx, namespacedName, &kustomization) + if err != nil { + return fmt.Errorf("kustomization sync failed: %w", err) + } + + if kustomization.Status.LastAppliedRevision != "" { + logSuccess("applied revision %s", kustomization.Status.LastAppliedRevision) + } else { + return fmt.Errorf("kustomization sync failed") + } + + return nil +} + +func upsertKustomization(ctx context.Context, kubeClient client.Client, kustomization kustomizev1.Kustomization) error { + namespacedName := types.NamespacedName{ + Namespace: kustomization.GetNamespace(), + Name: kustomization.GetName(), + } + + var existing kustomizev1.Kustomization + err := kubeClient.Get(ctx, namespacedName, &existing) + if err != nil { + if errors.IsNotFound(err) { + if err := kubeClient.Create(ctx, &kustomization); err != nil { + return err + } else { + logSuccess("kustomization created") + return nil + } + } + return err + } + + existing.Spec = kustomization.Spec + if err := kubeClient.Update(ctx, &existing); err != nil { + return err + } + + logSuccess("kustomization updated") + return nil +} + +func isKustomizationReady(ctx context.Context, kubeClient client.Client, name, namespace string) wait.ConditionFunc { + return func() (bool, error) { + var kustomization kustomizev1.Kustomization + namespacedName := types.NamespacedName{ + Namespace: namespace, + Name: name, + } + + err := kubeClient.Get(ctx, namespacedName, &kustomization) + if err != nil { + return false, err + } + + for _, condition := range kustomization.Status.Conditions { + if condition.Type == sourcev1.ReadyCondition { + if condition.Status == corev1.ConditionTrue { + return true, nil + } else if condition.Status == corev1.ConditionFalse { + return false, fmt.Errorf(condition.Message) + } + } + } + return false, nil + } +}