Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement create/delete for Helm releases #118

Merged
merged 2 commits into from
Jul 21, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .github/workflows/e2e.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,20 @@ jobs:
- name: tk delete source git
run: |
./bin/tk delete source git podinfo --silent
- name: tk create source helm
run: |
./bin/tk create source helm podinfo \
--url https://stefanprodan.github.io/podinfo
- name: tk create hr
run: |
./bin/tk create hr podinfo \
--target-namespace=default \
--source=podinfo \
--chart-name=podinfo \
--chart-version=">4.0.0 <5.0.0"
- name: tk delete hr
run: |
./bin/tk delete hr podinfo --silent
- name: tk check
run: |
./bin/tk check
Expand Down
280 changes: 280 additions & 0 deletions cmd/tk/create_helmrelease.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,280 @@
/*
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"
apierrors "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 \
--release-name=podinfo \
--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 (
hrName string
hrSource string
hrDependsOn []string
hrChartName string
hrChartVersion string
hrTargetNamespace string
hrValuesFile string
)

func init() {
createHelmReleaseCmd.Flags().StringVar(&hrName, "release-name", "", "name used for the Helm release, defaults to a composition of '<target-namespace>-<hr-name>'")
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{
ReleaseName: hrName,
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 {
if apierrors.IsNotFound(err) {
return false, 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
}
}
91 changes: 91 additions & 0 deletions cmd/tk/delete_helmrelease.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading