Skip to content
This repository has been archived by the owner on Jul 22, 2019. It is now read-only.

Commit

Permalink
Add release name to CRD spec (#29)
Browse files Browse the repository at this point in the history
* Codegen update

* Add optional release name to the spec

* Add finalizer to delete chart in Tiller

* Apply review

* Fix typo
  • Loading branch information
andresmgot authored May 29, 2018
1 parent 8a1b29f commit c6c583c
Show file tree
Hide file tree
Showing 20 changed files with 221 additions and 53 deletions.
130 changes: 106 additions & 24 deletions cmd/controller/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,17 +34,19 @@ import (
const (
defaultNamespace = metav1.NamespaceSystem
defaultRepoURL = "https://kubernetes-charts.storage.googleapis.com"
releaseFinalizer = "helm.bitnami.com/helmrelease"
defaultTimeoutSeconds = 180
maxRetries = 5
)

// Controller is a cache.Controller for acting on Helm CRD objects
type Controller struct {
queue workqueue.RateLimitingInterface
informer cache.SharedIndexInformer
kubeClient kubernetes.Interface
helmClient *helm.Client
netClient *http.Client
queue workqueue.RateLimitingInterface
informer cache.SharedIndexInformer
kubeClient kubernetes.Interface
helmReleaseClient helmClientset.Interface
helmClient *helm.Client
netClient *http.Client
}

type httpClient interface {
Expand Down Expand Up @@ -88,10 +90,11 @@ func NewController(clientset helmClientset.Interface, kubeClient kubernetes.Inte
log.Printf("Using tiller host: %s", settings.TillerHost)

return &Controller{
informer: informer,
queue: queue,
kubeClient: kubeClient,
helmClient: helm.NewClient(helm.Host(settings.TillerHost)),
informer: informer,
queue: queue,
kubeClient: kubeClient,
helmReleaseClient: clientset,
helmClient: helm.NewClient(helm.Host(settings.TillerHost)),
netClient: &http.Client{
Timeout: time.Second * defaultTimeoutSeconds,
},
Expand Down Expand Up @@ -252,17 +255,12 @@ func fetchChart(netClient httpClient, chartURL, authHeader string) (*chart.Chart
return chartutil.LoadArchive(bytes.NewReader(body))
}

func releaseName(ns, name string) string {
return fmt.Sprintf("%s-%s", ns, name)
}

func isNotFound(err error) bool {
// Ideally this would be `grpc.Code(err) == codes.NotFound`,
// but it seems helm doesn't return grpc codes
return strings.Contains(grpc.ErrorDesc(err), "not found")
}

//
func resolveChartURL(index, chart string) (string, error) {
indexURL, err := url.Parse(strings.TrimSpace(index))
if err != nil {
Expand All @@ -275,30 +273,115 @@ func resolveChartURL(index, chart string) (string, error) {
return chartURL.String(), nil
}

func getReleaseName(r *helmCrdV1.HelmRelease) string {
rname := r.Spec.ReleaseName
if rname == "" {
rname = fmt.Sprintf("%s-%s", r.Namespace, r.Name)
}
return rname
}

func findIndex(target string, s []string) int {
for i := range s {
if s[i] == target {
return i
}
}
return -1
}

func removeIndex(i int, s []string) []string {
lastIdx := len(s) - 1
if i != lastIdx {
s[i] = s[lastIdx]
}
s[lastIdx] = "" // drop reference to string contents
return s[:lastIdx]
}

// remove item from slice without keeping order
func remove(item string, s []string) ([]string, error) {
index := findIndex(item, s)
if index == -1 {
return []string{}, fmt.Errorf("%s not present in %v", item, s)
}
return removeIndex(index, s), nil
}
func hasFinalizer(h *helmCrdV1.HelmRelease) bool {
currentFinalizers := h.ObjectMeta.Finalizers
for _, f := range currentFinalizers {
if f == releaseFinalizer {
return true
}
}
return false
}

func removeFinalizer(helmObj *helmCrdV1.HelmRelease) *helmCrdV1.HelmRelease {
helmObjClone := helmObj.DeepCopy()
newSlice, _ := remove(releaseFinalizer, helmObj.ObjectMeta.Finalizers)
if len(newSlice) == 0 {
newSlice = nil
}
helmObjClone.ObjectMeta.Finalizers = newSlice
return helmObjClone
}

func addFinalizer(helmObj *helmCrdV1.HelmRelease) *helmCrdV1.HelmRelease {
helmObjClone := helmObj.DeepCopy()
helmObjClone.ObjectMeta.Finalizers = append(helmObjClone.ObjectMeta.Finalizers, releaseFinalizer)
return helmObjClone
}

func updateHelmRelease(helmReleaseClient helmClientset.Interface, helmObj *helmCrdV1.HelmRelease) error {
_, err := helmReleaseClient.HelmV1().HelmReleases(helmObj.Namespace).Update(helmObj)
return err
}

func (c *Controller) updateRelease(key string) error {
obj, exists, err := c.informer.GetIndexer().GetByKey(key)
if err != nil {
return fmt.Errorf("error fetching object with key %s from store: %v", key, err)
}

// this is an update when Function API object is actually deleted, we dont need to process anything here
if !exists {
log.Printf("HelmRelease %s has gone, uninstalling chart", key)
ns, name, err := cache.SplitMetaNamespaceKey(key)
log.Printf("HelmRelease object %s not found in the cache, ignoring the deletion update", key)
return nil
}

helmObj := obj.(*helmCrdV1.HelmRelease)

if helmObj.ObjectMeta.DeletionTimestamp != nil {
log.Printf("HelmRelease %s marked to be deleted, uninstalling chart", key)
// If finalizer is removed, then we already processed the delete update, so just return
if !hasFinalizer(helmObj) {
return nil
}
_, err = c.helmClient.DeleteRelease(getReleaseName(helmObj), helm.DeletePurge(true))
if err != nil {
return err
}
_, err = c.helmClient.DeleteRelease(
releaseName(ns, name),
helm.DeletePurge(true),
)

// remove finalizer from the function object, so that we dont have to process any further and object can be deleted
helmObjCopy := removeFinalizer(helmObj)
err = updateHelmRelease(c.helmReleaseClient, helmObjCopy)
if err != nil {
// fixme: ignore "not found" or similar
log.Printf("Failed to remove finalizer for obj: %s object due to: %v: ", key, err)
return err
}
log.Printf("Release %s has been successfully processed and marked for deletion", key)
return nil
}

helmObj := obj.(*helmCrdV1.HelmRelease)
if !hasFinalizer(helmObj) {
helmObjCopy := addFinalizer(helmObj)
err = updateHelmRelease(c.helmReleaseClient, helmObjCopy)
if err != nil {
log.Printf("Error adding finalizer to %s due to: %v: ", key, err)
return err
}
}

repoURL := helmObj.Spec.RepoURL
if repoURL == "" {
Expand Down Expand Up @@ -343,8 +426,7 @@ func (c *Controller) updateRelease(key string) error {
return err
}

rlsName := releaseName(helmObj.Namespace, helmObj.Name)

rlsName := getReleaseName(helmObj)
var rel *release.Release

_, err = c.helmClient.ReleaseHistory(rlsName, helm.WithMaxHistory(1))
Expand Down
35 changes: 35 additions & 0 deletions cmd/controller/controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import (
"testing"

"github.com/arschles/assert"
helmCrdV1 "github.com/bitnami-labs/helm-crd/pkg/apis/helm.bitnami.com/v1"
apiequality "k8s.io/apimachinery/pkg/api/equality"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// TODO: add more tests
Expand Down Expand Up @@ -48,3 +51,35 @@ func Test_resolveChartURL(t *testing.T) {
})
}
}

func TestAddFinalizer(t *testing.T) {
tests := []struct {
src *helmCrdV1.HelmRelease
expectedFinalizer []string
}{
{&helmCrdV1.HelmRelease{}, []string{"helm.bitnami.com/helmrelease"}},
{&helmCrdV1.HelmRelease{ObjectMeta: metav1.ObjectMeta{Finalizers: []string{"foo"}}}, []string{"foo", "helm.bitnami.com/helmrelease"}},
}
for _, tt := range tests {
res := addFinalizer(tt.src)
if !apiequality.Semantic.DeepEqual(res.ObjectMeta.Finalizers, tt.expectedFinalizer) {
t.Errorf("Expecting %v received %v", tt.expectedFinalizer, res.ObjectMeta.Finalizers)
}
}
}

func TestRemoveFinalizer(t *testing.T) {
tests := []struct {
src *helmCrdV1.HelmRelease
expectedFinalizer []string
}{
{&helmCrdV1.HelmRelease{ObjectMeta: metav1.ObjectMeta{Finalizers: []string{"helm.bitnami.com/helmrelease"}}}, []string{}},
{&helmCrdV1.HelmRelease{ObjectMeta: metav1.ObjectMeta{Finalizers: []string{"foo", "helm.bitnami.com/helmrelease", "bar"}}}, []string{"foo", "bar"}},
}
for _, tt := range tests {
res := removeFinalizer(tt.src)
if !apiequality.Semantic.DeepEqual(res.ObjectMeta.Finalizers, tt.expectedFinalizer) {
t.Errorf("Expecting %v received %v", tt.expectedFinalizer, res.ObjectMeta.Finalizers)
}
}
}
File renamed without changes.
15 changes: 11 additions & 4 deletions hack/update-codegen.sh
100644 → 100755
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
#!/bin/sh
#!/bin/bash

set -e
set -o errexit
set -o nounset
set -o pipefail

mydir=${0#*/}
SCRIPT_ROOT=$(dirname ${BASH_SOURCE})/..
CODEGEN_PKG=${CODEGEN_PKG:-$(cd ${SCRIPT_ROOT}; ls -d -1 ./vendor/k8s.io/code-generator 2>/dev/null || echo ../code-generator)}

vendor/k8s.io/code-generator/generate-groups.sh \
### Workaround for issue: https://github.com/kubernetes/code-generator/issues/6
mkdir -p ${GOPATH}/src/k8s.io/kubernetes/hack/boilerplate
cp ${SCRIPT_ROOT}/hack/boilerplate.go.txt ${GOPATH}/src/k8s.io/kubernetes/hack/boilerplate/

${CODEGEN_PKG}/generate-groups.sh \
all \
github.com/bitnami-labs/helm-crd/pkg/client \
github.com/bitnami-labs/helm-crd/pkg/apis \
Expand Down
2 changes: 2 additions & 0 deletions pkg/apis/helm.bitnami.com/v1/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ type HelmReleaseSpec struct {
RepoURL string `json:"repoUrl,omitempty"`
// ChartName is the name of the chart within the repo
ChartName string `json:"chartName,omitempty"`
// ReleaseName is the Name of the release given to Tiller. Defaults to namespace-name. Must not be changed after initial object creation.
ReleaseName string `json:"releaseName,omitempty"`
// Version is the chart version
Version string `json:"version,omitempty"`
// Auth is the authentication
Expand Down
55 changes: 53 additions & 2 deletions pkg/apis/helm.bitnami.com/v1/zz_generated.deepcopy.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// +build !ignore_autogenerated

/*
Copyright 2018 The Kubernetes Authors.
Copyright 2018 The helm-crd-controller Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -40,6 +40,14 @@ func RegisterDeepCopies(scheme *runtime.Scheme) error {
in.(*HelmRelease).DeepCopyInto(out.(*HelmRelease))
return nil
}, InType: reflect.TypeOf(&HelmRelease{})},
conversion.GeneratedDeepCopyFunc{Fn: func(in interface{}, out interface{}, c *conversion.Cloner) error {
in.(*HelmReleaseAuth).DeepCopyInto(out.(*HelmReleaseAuth))
return nil
}, InType: reflect.TypeOf(&HelmReleaseAuth{})},
conversion.GeneratedDeepCopyFunc{Fn: func(in interface{}, out interface{}, c *conversion.Cloner) error {
in.(*HelmReleaseAuthHeader).DeepCopyInto(out.(*HelmReleaseAuthHeader))
return nil
}, InType: reflect.TypeOf(&HelmReleaseAuthHeader{})},
conversion.GeneratedDeepCopyFunc{Fn: func(in interface{}, out interface{}, c *conversion.Cloner) error {
in.(*HelmReleaseList).DeepCopyInto(out.(*HelmReleaseList))
return nil
Expand All @@ -56,7 +64,7 @@ func (in *HelmRelease) DeepCopyInto(out *HelmRelease) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
out.Spec = in.Spec
in.Spec.DeepCopyInto(&out.Spec)
return
}

Expand All @@ -79,6 +87,48 @@ func (in *HelmRelease) DeepCopyObject() runtime.Object {
}
}

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *HelmReleaseAuth) DeepCopyInto(out *HelmReleaseAuth) {
*out = *in
if in.Header != nil {
in, out := &in.Header, &out.Header
if *in == nil {
*out = nil
} else {
*out = new(HelmReleaseAuthHeader)
(*in).DeepCopyInto(*out)
}
}
return
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HelmReleaseAuth.
func (in *HelmReleaseAuth) DeepCopy() *HelmReleaseAuth {
if in == nil {
return nil
}
out := new(HelmReleaseAuth)
in.DeepCopyInto(out)
return out
}

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *HelmReleaseAuthHeader) DeepCopyInto(out *HelmReleaseAuthHeader) {
*out = *in
in.SecretKeyRef.DeepCopyInto(&out.SecretKeyRef)
return
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HelmReleaseAuthHeader.
func (in *HelmReleaseAuthHeader) DeepCopy() *HelmReleaseAuthHeader {
if in == nil {
return nil
}
out := new(HelmReleaseAuthHeader)
in.DeepCopyInto(out)
return out
}

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *HelmReleaseList) DeepCopyInto(out *HelmReleaseList) {
*out = *in
Expand Down Expand Up @@ -116,6 +166,7 @@ func (in *HelmReleaseList) DeepCopyObject() runtime.Object {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *HelmReleaseSpec) DeepCopyInto(out *HelmReleaseSpec) {
*out = *in
in.Auth.DeepCopyInto(&out.Auth)
return
}

Expand Down
3 changes: 1 addition & 2 deletions pkg/client/clientset/versioned/clientset.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
Copyright 2018 The Kubernetes Authors.
Copyright 2018 The helm-crd-controller Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand All @@ -13,7 +13,6 @@ 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 versioned

import (
Expand Down
2 changes: 1 addition & 1 deletion pkg/client/clientset/versioned/doc.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
Copyright 2018 The Kubernetes Authors.
Copyright 2018 The helm-crd-controller Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand Down
3 changes: 1 addition & 2 deletions pkg/client/clientset/versioned/fake/clientset_generated.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
Copyright 2018 The Kubernetes Authors.
Copyright 2018 The helm-crd-controller Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand All @@ -13,7 +13,6 @@ 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 fake

import (
Expand Down
Loading

0 comments on commit c6c583c

Please sign in to comment.