From 5f0b95dc592b76f2971502e4170f431dd78eb168 Mon Sep 17 00:00:00 2001 From: stefanprodan Date: Tue, 21 Jul 2020 12:20:41 +0300 Subject: [PATCH] Implement create/delete for Helm releases --- cmd/tk/create_helmrelease.go | 272 ++++++++++++++++++++++++++++++ cmd/tk/delete_helmrelease.go | 91 ++++++++++ cmd/tk/utils.go | 7 +- docs/cmd/tk_create.md | 1 + docs/cmd/tk_create_helmrelease.md | 69 ++++++++ docs/cmd/tk_delete.md | 1 + docs/cmd/tk_delete_helmrelease.md | 40 +++++ go.mod | 1 + mkdocs.yml | 2 + 9 files changed, 482 insertions(+), 2 deletions(-) create mode 100644 cmd/tk/create_helmrelease.go create mode 100644 cmd/tk/delete_helmrelease.go create mode 100644 docs/cmd/tk_create_helmrelease.md create mode 100644 docs/cmd/tk_delete_helmrelease.md diff --git a/cmd/tk/create_helmrelease.go b/cmd/tk/create_helmrelease.go new file mode 100644 index 00000000..fd10e2cd --- /dev/null +++ b/cmd/tk/create_helmrelease.go @@ -0,0 +1,272 @@ +/* +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 main + +import ( + "context" + "fmt" + "io/ioutil" + + "github.com/spf13/cobra" + corev1 "k8s.io/api/core/v1" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/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" + "sigs.k8s.io/yaml" + + helmv2 "github.com/fluxcd/helm-controller/api/v2alpha1" + sourcev1 "github.com/fluxcd/source-controller/api/v1alpha1" +) + +var createHelmReleaseCmd = &cobra.Command{ + Use: "helmrelease [name]", + Aliases: []string{"hr"}, + Short: "Create or update a HelmRelease resource", + Long: "The helmrelease create command generates a HelmRelease resource for a given HelmRepository source.", + Example: ` # Create a HelmRelease from a source + tk create hr podinfo \ + --interval=10m \ + --target-namespace=default \ + --source=podinfo \ + --chart-name=podinfo \ + --chart-version=">4.0.0" + + # Create a HelmRelease with values for a local YAML file + tk create hr podinfo \ + --target-namespace=default \ + --source=podinfo \ + --chart-name=podinfo \ + --chart-version=4.0.5 \ + --values=./my-values.yaml + + # Create a HelmRelease definition on disk without applying it on the cluster + tk create hr podinfo \ + --target-namespace=default \ + --source=podinfo \ + --chart-name=podinfo \ + --chart-version=4.0.5 \ + --values=./values.yaml \ + --export > podinfo-release.yaml +`, + RunE: createHelmReleaseCmdRun, +} + +var ( + hrSource string + hrDependsOn []string + hrChartName string + hrChartVersion string + hrTargetNamespace string + hrValuesFile string +) + +func init() { + createHelmReleaseCmd.Flags().StringVar(&hrSource, "source", "", "HelmRepository name") + createHelmReleaseCmd.Flags().StringVar(&hrChartName, "chart-name", "", "Helm chart name") + createHelmReleaseCmd.Flags().StringVar(&hrChartVersion, "chart-version", "", "Helm chart version, accepts semver range") + createHelmReleaseCmd.Flags().StringArrayVar(&hrDependsOn, "depends-on", nil, "HelmReleases that must be ready before this release can be installed") + createHelmReleaseCmd.Flags().StringVar(&hrTargetNamespace, "target-namespace", "", "namespace to install this release, defaults to the HelmRelease namespace") + createHelmReleaseCmd.Flags().StringVar(&hrValuesFile, "values", "", "local path to the values.yaml file") + createCmd.AddCommand(createHelmReleaseCmd) +} + +func createHelmReleaseCmdRun(cmd *cobra.Command, args []string) error { + if len(args) < 1 { + return fmt.Errorf("release name is required") + } + name := args[0] + + if hrSource == "" { + return fmt.Errorf("source is required") + } + if hrChartName == "" { + return fmt.Errorf("chart name is required") + } + if hrChartVersion == "" { + return fmt.Errorf("chart version is required") + } + + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + kubeClient, err := utils.kubeClient(kubeconfig) + if err != nil { + return err + } + + if !export { + logger.Generatef("generating release") + } + + helmRelease := helmv2.HelmRelease{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Spec: helmv2.HelmReleaseSpec{ + DependsOn: hrDependsOn, + Interval: metav1.Duration{ + Duration: interval, + }, + TargetNamespace: hrTargetNamespace, + Chart: helmv2.HelmChartTemplate{ + Name: hrChartName, + Version: hrChartVersion, + SourceRef: helmv2.CrossNamespaceObjectReference{ + Kind: sourcev1.HelmRepositoryKind, + Name: hrSource, + }, + }, + Suspend: false, + }, + } + + if hrValuesFile != "" { + data, err := ioutil.ReadFile(hrValuesFile) + if err != nil { + return fmt.Errorf("reading values from %s failed: %w", hrValuesFile, err) + } + + json, err := yaml.YAMLToJSON(data) + if err != nil { + return fmt.Errorf("converting values to JSON from %s failed: %w", hrValuesFile, err) + } + + helmRelease.Spec.Values = apiextensionsv1.JSON{Raw: json} + } + + if export { + return exportHelmRelease(helmRelease) + } + + logger.Actionf("applying release") + if err := upsertHelmRelease(ctx, kubeClient, helmRelease); err != nil { + return err + } + + logger.Waitingf("waiting for reconciliation") + chartName := fmt.Sprintf("%s-%s", namespace, name) + if err := wait.PollImmediate(pollInterval, timeout, + isHelmChartReady(ctx, kubeClient, chartName, namespace)); err != nil { + return err + } + if err := wait.PollImmediate(pollInterval, timeout, + isHelmReleaseReady(ctx, kubeClient, name, namespace)); err != nil { + return err + } + + logger.Successf("release %s is ready", name) + + namespacedName := types.NamespacedName{ + Namespace: namespace, + Name: name, + } + err = kubeClient.Get(ctx, namespacedName, &helmRelease) + if err != nil { + return fmt.Errorf("release failed: %w", err) + } + + if helmRelease.Status.LastAppliedRevision != "" { + logger.Successf("applied revision %s", helmRelease.Status.LastAppliedRevision) + } else { + return fmt.Errorf("reconciliation failed") + } + + return nil +} + +func upsertHelmRelease(ctx context.Context, kubeClient client.Client, helmRelease helmv2.HelmRelease) error { + namespacedName := types.NamespacedName{ + Namespace: helmRelease.GetNamespace(), + Name: helmRelease.GetName(), + } + + var existing helmv2.HelmRelease + err := kubeClient.Get(ctx, namespacedName, &existing) + if err != nil { + if errors.IsNotFound(err) { + if err := kubeClient.Create(ctx, &helmRelease); err != nil { + return err + } else { + logger.Successf("release created") + return nil + } + } + return err + } + + existing.Spec = helmRelease.Spec + if err := kubeClient.Update(ctx, &existing); err != nil { + return err + } + + logger.Successf("release updated") + return nil +} + +func exportHelmRelease(helmRelease helmv2.HelmRelease) error { + gvk := helmv2.GroupVersion.WithKind(helmv2.HelmReleaseKind) + export := helmv2.HelmRelease{ + TypeMeta: metav1.TypeMeta{ + Kind: gvk.Kind, + APIVersion: gvk.GroupVersion().String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Name: helmRelease.Name, + Namespace: helmRelease.Namespace, + }, + Spec: helmRelease.Spec, + } + + data, err := yaml.Marshal(export) + if err != nil { + return err + } + + fmt.Println("---") + fmt.Println(string(data)) + return nil +} + +func isHelmChartReady(ctx context.Context, kubeClient client.Client, name, namespace string) wait.ConditionFunc { + return func() (bool, error) { + var helmChart sourcev1.HelmChart + namespacedName := types.NamespacedName{ + Namespace: namespace, + Name: name, + } + + err := kubeClient.Get(ctx, namespacedName, &helmChart) + if err != nil { + return false, err + } + + for _, condition := range helmChart.Status.Conditions { + if condition.Type == helmv2.ReadyCondition { + if condition.Status == corev1.ConditionTrue { + return true, nil + } else if condition.Status == corev1.ConditionFalse { + return false, fmt.Errorf(condition.Message) + } + } + } + return false, nil + } +} diff --git a/cmd/tk/delete_helmrelease.go b/cmd/tk/delete_helmrelease.go new file mode 100644 index 00000000..367fc139 --- /dev/null +++ b/cmd/tk/delete_helmrelease.go @@ -0,0 +1,91 @@ +/* +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 main + +import ( + "context" + "fmt" + + "github.com/manifoldco/promptui" + "github.com/spf13/cobra" + "k8s.io/apimachinery/pkg/types" + + helmv2 "github.com/fluxcd/helm-controller/api/v2alpha1" +) + +var deleteHelmReleaseCmd = &cobra.Command{ + Use: "helmrelease [name]", + Aliases: []string{"hr"}, + Short: "Delete a HelmRelease resource", + Long: "The delete helmrelease command removes the given HelmRelease from the cluster.", + Example: ` # Delete a Helm release and the Kubernetes resources created by it + tk delete hr podinfo +`, + RunE: deleteHelmReleaseCmdRun, +} + +func init() { + deleteCmd.AddCommand(deleteHelmReleaseCmd) +} + +func deleteHelmReleaseCmdRun(cmd *cobra.Command, args []string) error { + if len(args) < 1 { + return fmt.Errorf("release name is required") + } + name := args[0] + + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + kubeClient, err := utils.kubeClient(kubeconfig) + if err != nil { + return err + } + + namespacedName := types.NamespacedName{ + Namespace: namespace, + Name: name, + } + + var helmRelease helmv2.HelmRelease + err = kubeClient.Get(ctx, namespacedName, &helmRelease) + if err != nil { + return err + } + + if !deleteSilent { + if !helmRelease.Spec.Suspend { + logger.Waitingf("This action will remove the Kubernetes objects previously applied by the %s Helm release!", name) + } + prompt := promptui.Prompt{ + Label: "Are you sure you want to delete this Helm release", + IsConfirm: true, + } + if _, err := prompt.Run(); err != nil { + return fmt.Errorf("aborting") + } + } + + logger.Actionf("deleting release %s in %s namespace", name, namespace) + err = kubeClient.Delete(ctx, &helmRelease) + if err != nil { + return err + } + logger.Successf("release deleted") + + return nil +} diff --git a/cmd/tk/utils.go b/cmd/tk/utils.go index 81012877..d2441106 100644 --- a/cmd/tk/utils.go +++ b/cmd/tk/utils.go @@ -26,12 +26,14 @@ import ( "os/exec" "text/template" - kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1alpha1" - sourcev1 "github.com/fluxcd/source-controller/api/v1alpha1" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/tools/clientcmd" "sigs.k8s.io/controller-runtime/pkg/client" + + helmv2 "github.com/fluxcd/helm-controller/api/v2alpha1" + kustomizev1 "github.com/fluxcd/kustomize-controller/api/v1alpha1" + sourcev1 "github.com/fluxcd/source-controller/api/v1alpha1" ) type Utils struct { @@ -118,6 +120,7 @@ func (*Utils) kubeClient(config string) (client.Client, error) { _ = corev1.AddToScheme(scheme) _ = sourcev1.AddToScheme(scheme) _ = kustomizev1.AddToScheme(scheme) + _ = helmv2.AddToScheme(scheme) kubeClient, err := client.New(cfg, client.Options{ Scheme: scheme, diff --git a/docs/cmd/tk_create.md b/docs/cmd/tk_create.md index 3cfc07d1..c326e11a 100644 --- a/docs/cmd/tk_create.md +++ b/docs/cmd/tk_create.md @@ -26,6 +26,7 @@ The create sub-commands generate sources and resources. ### SEE ALSO * [tk](tk.md) - Command line utility for assembling Kubernetes CD pipelines +* [tk create helmrelease](tk_create_helmrelease.md) - Create or update a HelmRelease resource * [tk create kustomization](tk_create_kustomization.md) - Create or update a Kustomization resource * [tk create source](tk_create_source.md) - Create or update sources diff --git a/docs/cmd/tk_create_helmrelease.md b/docs/cmd/tk_create_helmrelease.md new file mode 100644 index 00000000..9c295f60 --- /dev/null +++ b/docs/cmd/tk_create_helmrelease.md @@ -0,0 +1,69 @@ +## tk create helmrelease + +Create or update a HelmRelease resource + +### Synopsis + +The helmrelease create command generates a HelmRelease resource for a given HelmRepository source. + +``` +tk create helmrelease [name] [flags] +``` + +### Examples + +``` + # Create a HelmRelease from a source + tk create hr podinfo \ + --interval=10m \ + --target-namespace=default \ + --source=podinfo \ + --chart-name=podinfo \ + --chart-version=">4.0.0" + + # Create a HelmRelease with values for a local YAML file + tk create hr podinfo \ + --target-namespace=default \ + --source=podinfo \ + --chart-name=podinfo \ + --chart-version=4.0.5 \ + --values=./my-values.yaml + + # Create a HelmRelease definition on disk without applying it on the cluster + tk create hr podinfo \ + --target-namespace=default \ + --source=podinfo \ + --chart-name=podinfo \ + --chart-version=4.0.5 \ + --values=./values.yaml \ + --export > podinfo-release.yaml + +``` + +### Options + +``` + --chart-name string Helm chart name + --chart-version string Helm chart version, accepts semver range + --depends-on stringArray HelmReleases that must be ready before this release can be installed + -h, --help help for helmrelease + --source string HelmRepository name + --target-namespace string namespace to install this release, defaults to the HelmRelease namespace + --values string local path to the values.yaml file +``` + +### Options inherited from parent commands + +``` + --export export in YAML format to stdout + --interval duration source sync interval (default 1m0s) + --kubeconfig string path to the kubeconfig file (default "~/.kube/config") + --namespace string the namespace scope for this operation (default "gitops-system") + --timeout duration timeout for this operation (default 5m0s) + --verbose print generated objects +``` + +### SEE ALSO + +* [tk create](tk_create.md) - Create or update sources and resources + diff --git a/docs/cmd/tk_delete.md b/docs/cmd/tk_delete.md index cc1c6e96..b61bfac3 100644 --- a/docs/cmd/tk_delete.md +++ b/docs/cmd/tk_delete.md @@ -25,6 +25,7 @@ The delete sub-commands delete sources and resources. ### SEE ALSO * [tk](tk.md) - Command line utility for assembling Kubernetes CD pipelines +* [tk delete helmrelease](tk_delete_helmrelease.md) - Delete a HelmRelease resource * [tk delete kustomization](tk_delete_kustomization.md) - Delete a Kustomization resource * [tk delete source](tk_delete_source.md) - Delete sources diff --git a/docs/cmd/tk_delete_helmrelease.md b/docs/cmd/tk_delete_helmrelease.md new file mode 100644 index 00000000..be400153 --- /dev/null +++ b/docs/cmd/tk_delete_helmrelease.md @@ -0,0 +1,40 @@ +## tk delete helmrelease + +Delete a HelmRelease resource + +### Synopsis + +The delete helmrelease command removes the given HelmRelease from the cluster. + +``` +tk delete helmrelease [name] [flags] +``` + +### Examples + +``` + # Delete a Helm release and the Kubernetes resources created by it + tk delete hr podinfo + +``` + +### Options + +``` + -h, --help help for helmrelease +``` + +### Options inherited from parent commands + +``` + --kubeconfig string path to the kubeconfig file (default "~/.kube/config") + --namespace string the namespace scope for this operation (default "gitops-system") + -s, --silent delete resource without asking for confirmation + --timeout duration timeout for this operation (default 5m0s) + --verbose print generated objects +``` + +### SEE ALSO + +* [tk delete](tk_delete.md) - Delete sources and resources + diff --git a/go.mod b/go.mod index 9bb52591..c932fecf 100644 --- a/go.mod +++ b/go.mod @@ -16,6 +16,7 @@ require ( google.golang.org/appengine v1.6.6 // indirect google.golang.org/protobuf v1.24.0 // indirect k8s.io/api v0.18.4 + k8s.io/apiextensions-apiserver v0.18.4 k8s.io/apimachinery v0.18.4 k8s.io/client-go v0.18.4 sigs.k8s.io/controller-runtime v0.6.1 diff --git a/mkdocs.yml b/mkdocs.yml index dbee6cce..4eedc776 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -73,11 +73,13 @@ nav: - Check: cmd/tk_check.md - Create: cmd/tk_create.md - Create kustomization: cmd/tk_create_kustomization.md + - Create helmrelease: cmd/tk_create_helmrelease.md - Create source: cmd/tk_create_source.md - Create source git: cmd/tk_create_source_git.md - Create source helm: cmd/tk_create_source_helm.md - Delete: cmd/tk_delete.md - Delete kustomization: cmd/tk_delete_kustomization.md + - Delete helmrelease: cmd/tk_delete_helmrelease.md - Delete source: cmd/tk_delete_source.md - Delete source git: cmd/tk_delete_source_git.md - Delete source helm: cmd/tk_delete_source_helm.md