From 02bcf76047076e8568ff73198226ab88ff54983b Mon Sep 17 00:00:00 2001 From: Joe Lanford Date: Wed, 13 Feb 2019 01:05:00 -0500 Subject: [PATCH] pkg/helm: WIP custom storage backend per CR with ownerref --- Gopkg.lock | 3 + pkg/helm/controller/controller.go | 6 + pkg/helm/controller/reconcile.go | 7 +- pkg/helm/release/manager.go | 38 +--- pkg/helm/release/manager_factory.go | 66 ++++-- pkg/helm/run.go | 13 +- pkg/helm/storage/driver/labels.go | 46 ++++ pkg/helm/storage/driver/ownersecrets.go | 278 ++++++++++++++++++++++++ pkg/helm/storage/driver/util.go | 96 ++++++++ 9 files changed, 488 insertions(+), 65 deletions(-) create mode 100644 pkg/helm/storage/driver/labels.go create mode 100644 pkg/helm/storage/driver/ownersecrets.go create mode 100644 pkg/helm/storage/driver/util.go diff --git a/Gopkg.lock b/Gopkg.lock index b8a9359723a..440f8358495 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -1562,6 +1562,7 @@ "github.com/ghodss/yaml", "github.com/go-logr/logr", "github.com/go-logr/zapr", + "github.com/golang/protobuf/proto", "github.com/markbates/inflect", "github.com/martinlindhe/base36", "github.com/mattbaird/jsonpatch", @@ -1608,6 +1609,7 @@ "k8s.io/client-go/discovery/cached", "k8s.io/client-go/kubernetes", "k8s.io/client-go/kubernetes/scheme", + "k8s.io/client-go/kubernetes/typed/core/v1", "k8s.io/client-go/plugin/pkg/client/auth/gcp", "k8s.io/client-go/rest", "k8s.io/client-go/restmapper", @@ -1623,6 +1625,7 @@ "k8s.io/helm/pkg/releaseutil", "k8s.io/helm/pkg/storage", "k8s.io/helm/pkg/storage/driver", + "k8s.io/helm/pkg/storage/errors", "k8s.io/helm/pkg/tiller", "k8s.io/helm/pkg/tiller/environment", "sigs.k8s.io/controller-runtime/pkg/cache", diff --git a/pkg/helm/controller/controller.go b/pkg/helm/controller/controller.go index 3d57b76cad9..aeef8108898 100644 --- a/pkg/helm/controller/controller.go +++ b/pkg/helm/controller/controller.go @@ -24,6 +24,7 @@ import ( "time" yaml "gopkg.in/yaml.v2" + corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -78,6 +79,11 @@ func Add(mgr manager.Manager, options WatchOptions) error { return err } + // Watch release secrets + if err := c.Watch(&source.Kind{Type: &corev1.Secret{}}, &crthandler.EnqueueRequestForOwner{OwnerType: o, IsController: true}); err != nil { + return err + } + if options.WatchDependentResources { watchDependentResources(mgr, r, c) } diff --git a/pkg/helm/controller/reconcile.go b/pkg/helm/controller/reconcile.go index b71337b6804..a13592813c1 100644 --- a/pkg/helm/controller/reconcile.go +++ b/pkg/helm/controller/reconcile.go @@ -75,7 +75,12 @@ func (r HelmOperatorReconciler) Reconcile(request reconcile.Request) (reconcile. return reconcile.Result{}, err } - manager := r.ManagerFactory.NewManager(o) + manager, err := r.ManagerFactory.NewManager(o) + if err != nil { + log.Error(err, "Failed to get release manager") + return reconcile.Result{}, err + } + status := types.StatusFor(o) log = log.WithValues("release", manager.ReleaseName()) diff --git a/pkg/helm/release/manager.go b/pkg/helm/release/manager.go index ff68ea5f39b..7482cebd563 100644 --- a/pkg/helm/release/manager.go +++ b/pkg/helm/release/manager.go @@ -94,10 +94,6 @@ func (m manager) IsUpdateRequired() bool { // Sync ensures the Helm storage backend is in sync with the status of the // custom resource. func (m *manager) Sync(ctx context.Context) error { - if err := m.syncReleaseStatus(*m.status); err != nil { - return fmt.Errorf("failed to sync release status to storage backend: %s", err) - } - // Get release history for this release name releases, err := m.storageBackend.History(m.releaseName) if err != nil && !notFoundErr(err) { @@ -147,31 +143,6 @@ func (m *manager) Sync(ctx context.Context) error { return nil } -func (m manager) syncReleaseStatus(status types.HelmAppStatus) error { - var release *rpb.Release - for _, condition := range status.Conditions { - if condition.Type == types.ConditionDeployed && condition.Status == types.StatusTrue { - release = condition.Release - break - } - } - if release == nil { - return nil - } - - name := release.GetName() - version := release.GetVersion() - _, err := m.storageBackend.Get(name, version) - if err == nil { - return nil - } - - if !notFoundErr(err) { - return err - } - return m.storageBackend.Create(release) -} - func notFoundErr(err error) bool { return strings.Contains(err.Error(), "not found") } @@ -392,12 +363,15 @@ func (m manager) UninstallRelease(ctx context.Context) (*rpb.Release, error) { func uninstallRelease(ctx context.Context, storageBackend *storage.Storage, tiller *tiller.ReleaseServer, releaseName string) (*rpb.Release, error) { // Get history of this release h, err := storageBackend.History(releaseName) - if err != nil { + + // If the error is not nil, only return it if it is something other than + // a not found error. + if err != nil && !notFoundErr(err) { return nil, fmt.Errorf("failed to get release history: %s", err) } - // If there is no history, the release has already been uninstalled, - // so return ErrNotFound. + // If there is no history, the release was never installed or has already + // been uninstalled, so return ErrNotFound. if len(h) == 0 { return nil, ErrNotFound } diff --git a/pkg/helm/release/manager_factory.go b/pkg/helm/release/manager_factory.go index 6f777aa6e1c..46cf3f3287a 100644 --- a/pkg/helm/release/manager_factory.go +++ b/pkg/helm/release/manager_factory.go @@ -20,19 +20,22 @@ import ( "github.com/martinlindhe/base36" "github.com/pborman/uuid" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" apitypes "k8s.io/apimachinery/pkg/types" clientset "k8s.io/client-go/kubernetes" + corev1 "k8s.io/client-go/kubernetes/typed/core/v1" helmengine "k8s.io/helm/pkg/engine" "k8s.io/helm/pkg/kube" "k8s.io/helm/pkg/storage" "k8s.io/helm/pkg/tiller" "k8s.io/helm/pkg/tiller/environment" + crmanager "sigs.k8s.io/controller-runtime/pkg/manager" + "github.com/operator-framework/operator-sdk/pkg/helm/client" "github.com/operator-framework/operator-sdk/pkg/helm/engine" "github.com/operator-framework/operator-sdk/pkg/helm/internal/types" + "github.com/operator-framework/operator-sdk/pkg/helm/storage/driver" ) // ManagerFactory creates Managers that are specific to custom resources. It is @@ -40,42 +43,53 @@ import ( // improves decoupling between reconciliation logic and the Helm backend // components used to manage releases. type ManagerFactory interface { - NewManager(r *unstructured.Unstructured) Manager + NewManager(r *unstructured.Unstructured) (Manager, error) } type managerFactory struct { - storageBackend *storage.Storage - tillerKubeClient *kube.Client - chartDir string + crmanager crmanager.Manager + chartDir string } // NewManagerFactory returns a new Helm manager factory capable of installing and uninstalling releases. -func NewManagerFactory(storageBackend *storage.Storage, tillerKubeClient *kube.Client, chartDir string) ManagerFactory { - return &managerFactory{storageBackend, tillerKubeClient, chartDir} +func NewManagerFactory(crmanager crmanager.Manager, chartDir string) ManagerFactory { + return &managerFactory{crmanager, chartDir} } -func (f managerFactory) NewManager(r *unstructured.Unstructured) Manager { +func (f managerFactory) NewManager(r *unstructured.Unstructured) (Manager, error) { return f.newManagerForCR(r) } -func (f managerFactory) newManagerForCR(r *unstructured.Unstructured) Manager { +func (f managerFactory) newManagerForCR(r *unstructured.Unstructured) (Manager, error) { + storageBackend, err := f.getStorageBackend(r) + if err != nil { + return nil, err + } + tillerKubeClient, err := client.NewFromManager(f.crmanager) + if err != nil { + return nil, err + } + releaseServer, err := tillerRendererForCR(r, tillerKubeClient, storageBackend) + if err != nil { + return nil, err + } return &manager{ - storageBackend: f.storageBackend, - tillerKubeClient: f.tillerKubeClient, + storageBackend: storageBackend, + tillerKubeClient: tillerKubeClient, chartDir: f.chartDir, - tiller: f.tillerRendererForCR(r), + tiller: releaseServer, releaseName: getReleaseName(r), namespace: r.GetNamespace(), spec: r.Object["spec"], status: types.StatusFor(r), - } + }, nil } // tillerRendererForCR creates a ReleaseServer configured with a rendering engine that adds ownerrefs to rendered assets // based on the CR. -func (f managerFactory) tillerRendererForCR(r *unstructured.Unstructured) *tiller.ReleaseServer { +func tillerRendererForCR(r *unstructured.Unstructured, tillerKubeClient *kube.Client, storageBackend *storage.Storage) (*tiller.ReleaseServer, error) { controllerRef := metav1.NewControllerRef(r, r.GroupVersionKind()) ownerRefs := []metav1.OwnerReference{ *controllerRef, @@ -87,13 +101,15 @@ func (f managerFactory) tillerRendererForCR(r *unstructured.Unstructured) *tille } env := &environment.Environment{ EngineYard: ey, - Releases: f.storageBackend, - KubeClient: f.tillerKubeClient, + Releases: storageBackend, + KubeClient: tillerKubeClient, } - kubeconfig, _ := f.tillerKubeClient.ToRESTConfig() - cs := clientset.NewForConfigOrDie(kubeconfig) - - return tiller.NewReleaseServer(env, cs, false) + kubeconfig, _ := tillerKubeClient.ToRESTConfig() + cs, err := clientset.NewForConfig(kubeconfig) + if err != nil { + return nil, err + } + return tiller.NewReleaseServer(env, cs, false), nil } func getReleaseName(r *unstructured.Unstructured) string { @@ -108,3 +124,13 @@ func shortenUID(uid apitypes.UID) string { } return strings.ToLower(base36.EncodeBytes(uidBytes)) } + +func (f managerFactory) getStorageBackend(r *unstructured.Unstructured) (*storage.Storage, error) { + ownerRef := metav1.NewControllerRef(r, r.GroupVersionKind()) + clientv1, err := corev1.NewForConfig(f.crmanager.GetConfig()) + if err != nil { + return nil, err + } + secrets := driver.NewOwnerSecrets(*ownerRef, clientv1.Secrets(r.GetNamespace())) + return storage.Init(secrets), nil +} diff --git a/pkg/helm/run.go b/pkg/helm/run.go index d9cf3fefe92..2f2f36deda1 100644 --- a/pkg/helm/run.go +++ b/pkg/helm/run.go @@ -20,7 +20,6 @@ import ( "os" "runtime" - "github.com/operator-framework/operator-sdk/pkg/helm/client" "github.com/operator-framework/operator-sdk/pkg/helm/controller" hoflags "github.com/operator-framework/operator-sdk/pkg/helm/flags" "github.com/operator-framework/operator-sdk/pkg/helm/release" @@ -30,8 +29,6 @@ import ( sdkVersion "github.com/operator-framework/operator-sdk/version" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/helm/pkg/storage" - "k8s.io/helm/pkg/storage/driver" "sigs.k8s.io/controller-runtime/pkg/client/config" "sigs.k8s.io/controller-runtime/pkg/manager" logf "sigs.k8s.io/controller-runtime/pkg/runtime/log" @@ -73,14 +70,6 @@ func Run(flags *hoflags.HelmOperatorFlags) error { return err } - // Create Tiller's storage backend and kubernetes client - storageBackend := storage.Init(driver.NewMemory()) - tillerKubeClient, err := client.NewFromManager(mgr) - if err != nil { - log.Error(err, "Failed to create new Tiller client.") - return err - } - watches, err := watches.Load(flags.WatchesFile) if err != nil { log.Error(err, "Failed to create new manager factories.") @@ -92,7 +81,7 @@ func Run(flags *hoflags.HelmOperatorFlags) error { err := controller.Add(mgr, controller.WatchOptions{ Namespace: namespace, GVK: w.GroupVersionKind, - ManagerFactory: release.NewManagerFactory(storageBackend, tillerKubeClient, w.ChartDir), + ManagerFactory: release.NewManagerFactory(mgr, w.ChartDir), ReconcilePeriod: flags.ReconcilePeriod, WatchDependentResources: w.WatchDependentResources, }) diff --git a/pkg/helm/storage/driver/labels.go b/pkg/helm/storage/driver/labels.go new file mode 100644 index 00000000000..a95996daf18 --- /dev/null +++ b/pkg/helm/storage/driver/labels.go @@ -0,0 +1,46 @@ +// Copyright 2019 The Operator-SDK Authors +// +// 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 driver + +// labels is a map of key value pairs to be included as metadata in a configmap object. +type labels map[string]string + +func (lbs *labels) init() { *lbs = labels(make(map[string]string)) } +func (lbs labels) get(key string) string { return lbs[key] } +func (lbs labels) set(key, val string) { lbs[key] = val } + +func (lbs labels) keys() (ls []string) { + for key := range lbs { + ls = append(ls, key) + } + return +} + +func (lbs labels) match(set labels) bool { + for _, key := range set.keys() { + if lbs.get(key) != set.get(key) { + return false + } + } + return true +} + +func (lbs labels) toMap() map[string]string { return lbs } + +func (lbs *labels) fromMap(kvs map[string]string) { + for k, v := range kvs { + lbs.set(k, v) + } +} diff --git a/pkg/helm/storage/driver/ownersecrets.go b/pkg/helm/storage/driver/ownersecrets.go new file mode 100644 index 00000000000..01e45266690 --- /dev/null +++ b/pkg/helm/storage/driver/ownersecrets.go @@ -0,0 +1,278 @@ +// Copyright 2019 The Operator-SDK Authors +// +// 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 driver + +import ( + "fmt" + "strconv" + "strings" + "time" + + v1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + kblabels "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/util/validation" + corev1 "k8s.io/client-go/kubernetes/typed/core/v1" + + rspb "k8s.io/helm/pkg/proto/hapi/release" + helmdriver "k8s.io/helm/pkg/storage/driver" + storageerrors "k8s.io/helm/pkg/storage/errors" +) + +var _ helmdriver.Driver = (*OwnerSecrets)(nil) + +// OwnerSecretsDriverName is the string name of the driver. +const OwnerSecretsDriverName = "OwnerSecret" + +// OwnerSecrets is a wrapper around an implementation of a kubernetes +// SecretsInterface. It is intended to be used when a release is owned +// by an in-cluster object. +type OwnerSecrets struct { + impl corev1.SecretInterface + ownerRef metav1.OwnerReference + Log func(string, ...interface{}) +} + +// NewOwnerSecrets initializes a new OwnerSecrets wrapping an implementation of +// the kubernetes SecretsInterface. The provided ownerRef is used in three ways: +// +// First, it is included as an owner reference on each secret created by the +// returned storage driver +// +// Second, it is used to construct the name of release secrets so that they are +// isolated from releases managed by other storage drivers that share the same +// release name. +// +// Third, it is used to override the "OWNER" label that is used in queries and +// included created secrets. +func NewOwnerSecrets(ownerRef metav1.OwnerReference, impl corev1.SecretInterface) *OwnerSecrets { + return &OwnerSecrets{ + impl: impl, + ownerRef: ownerRef, + Log: func(_ string, _ ...interface{}) {}, + } +} + +// Name returns the name of the driver. +func (secrets *OwnerSecrets) Name() string { + return OwnerSecretsDriverName +} + +// Get fetches the release named by key. The corresponding release is returned +// or error if not found. +func (secrets *OwnerSecrets) Get(key string) (*rspb.Release, error) { + secretName := fmt.Sprintf("%s-%s", shortenUID(secrets.ownerRef.UID), key) + + // fetch the secret holding the release named by secretName + obj, err := secrets.impl.Get(secretName, metav1.GetOptions{}) + if err != nil { + if apierrors.IsNotFound(err) { + return nil, storageerrors.ErrReleaseNotFound(key) + } + + secrets.Log("get: failed to get %q: %s", key, err) + return nil, err + } + // found the secret, decode the base64 data string + r, err := decodeRelease(string(obj.Data["release"])) + if err != nil { + secrets.Log("get: failed to decode data %q: %s", key, err) + return nil, err + } + // return the release object + return r, nil +} + +// List fetches all releases and returns the list releases such +// that filter(release) == true. An error is returned if the +// secret fails to retrieve the releases. +func (secrets *OwnerSecrets) List(filter func(*rspb.Release) bool) ([]*rspb.Release, error) { + owner := string(secrets.ownerRef.UID) + lsel := kblabels.Set{"OWNER": owner}.AsSelector() + opts := metav1.ListOptions{LabelSelector: lsel.String()} + + list, err := secrets.impl.List(opts) + if err != nil { + secrets.Log("list: failed to list: %s", err) + return nil, err + } + + var results []*rspb.Release + + // iterate over the secrets object list + // and decode each release + for _, item := range list.Items { + rls, err := decodeRelease(string(item.Data["release"])) + if err != nil { + secrets.Log("list: failed to decode release: %v: %s", item, err) + continue + } + if filter(rls) { + results = append(results, rls) + } + } + return results, nil +} + +// Query fetches all releases that match the provided map of labels. +// An error is returned if the secret fails to retrieve the releases. +func (secrets *OwnerSecrets) Query(labels map[string]string) ([]*rspb.Release, error) { + // Set or override the OWNER label since we know what it should be. + labels["OWNER"] = string(secrets.ownerRef.UID) + + ls := kblabels.Set{} + for k, v := range labels { + if errs := validation.IsValidLabelValue(v); len(errs) != 0 { + return nil, fmt.Errorf("invalid label value: %q: %s", v, strings.Join(errs, "; ")) + } + ls[k] = v + } + + opts := metav1.ListOptions{LabelSelector: ls.AsSelector().String()} + + list, err := secrets.impl.List(opts) + if err != nil { + secrets.Log("query: failed to query with labels: %s", err) + return nil, err + } + + if len(list.Items) == 0 { + return nil, storageerrors.ErrReleaseNotFound(labels["NAME"]) + } + + var results []*rspb.Release + for _, item := range list.Items { + rls, err := decodeRelease(string(item.Data["release"])) + if err != nil { + secrets.Log("query: failed to decode release: %s", err) + continue + } + results = append(results, rls) + } + return results, nil +} + +// Create creates a new Secret holding the release. If the +// Secret already exists, ErrReleaseExists is returned. +func (secrets *OwnerSecrets) Create(key string, rls *rspb.Release) error { + // set labels for secrets object meta data + var lbs labels + + lbs.init() + lbs.set("CREATED_AT", strconv.Itoa(int(time.Now().Unix()))) + + // create a new secret to hold the release + obj, err := newOwnerSecretsObject(secrets.ownerRef, key, rls, lbs) + if err != nil { + secrets.Log("create: failed to encode release %q: %s", rls.Name, err) + return err + } + // push the secret object out into the kubiverse + if _, err := secrets.impl.Create(obj); err != nil { + if apierrors.IsAlreadyExists(err) { + return storageerrors.ErrReleaseExists(key) + } + + secrets.Log("create: failed to create: %s", err) + return err + } + return nil +} + +// Update updates the Secret holding the release. If not found +// the Secret is created to hold the release. +func (secrets *OwnerSecrets) Update(key string, rls *rspb.Release) error { + // set labels for secrets object meta data + var lbs labels + + lbs.init() + lbs.set("MODIFIED_AT", strconv.Itoa(int(time.Now().Unix()))) + + // create a new secret object to hold the release + obj, err := newOwnerSecretsObject(secrets.ownerRef, key, rls, lbs) + if err != nil { + secrets.Log("update: failed to encode release %q: %s", rls.Name, err) + return err + } + // push the secret object out into the kubiverse + _, err = secrets.impl.Update(obj) + if err != nil { + secrets.Log("update: failed to update: %s", err) + return err + } + return nil +} + +// Delete deletes the Secret holding the release named by key. +func (secrets *OwnerSecrets) Delete(key string) (rls *rspb.Release, err error) { + secretName := fmt.Sprintf("%s-%s", shortenUID(secrets.ownerRef.UID), key) + + // fetch the release to check existence + if rls, err = secrets.Get(key); err != nil { + if apierrors.IsNotFound(err) { + return nil, storageerrors.ErrReleaseExists(rls.Name) + } + + secrets.Log("delete: failed to get release %q: %s", key, err) + return nil, err + } + // delete the release + if err = secrets.impl.Delete(secretName, &metav1.DeleteOptions{}); err != nil { + return rls, err + } + return rls, nil +} + +// newOwnerSecretsObject constructs a kubernetes Secret object +// to store a release. Each secret data entry is the base64 +// encoded string of a release's binary protobuf encoding. +// +// The following labels are used within each secret: +// +// "MODIFIED_AT" - timestamp indicating when this secret was last modified. (set in Update) +// "CREATED_AT" - timestamp indicating when this secret was created. (set in Create) +// "VERSION" - version of the release. +// "STATUS" - status of the release (see proto/hapi/release.status.pb.go for variants) +// "OWNER" - owner of the secret, uid of the owner reference. +// "NAME" - name of the release. +// +func newOwnerSecretsObject(ownerRef metav1.OwnerReference, key string, rls *rspb.Release, lbs labels) (*v1.Secret, error) { + // encode the release + s, err := encodeRelease(rls) + if err != nil { + return nil, err + } + + if lbs == nil { + lbs.init() + } + + // apply labels + lbs.set("NAME", rls.Name) + lbs.set("OWNER", string(ownerRef.UID)) + lbs.set("STATUS", rspb.Status_Code_name[int32(rls.Info.Status.Code)]) + lbs.set("VERSION", strconv.Itoa(int(rls.Version))) + + // create and return secret object + return &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("%s-%s", shortenUID(ownerRef.UID), key), + Labels: lbs.toMap(), + OwnerReferences: []metav1.OwnerReference{ownerRef}, + }, + Data: map[string][]byte{"release": []byte(s)}, + }, nil +} diff --git a/pkg/helm/storage/driver/util.go b/pkg/helm/storage/driver/util.go new file mode 100644 index 00000000000..0d9eaabf0c7 --- /dev/null +++ b/pkg/helm/storage/driver/util.go @@ -0,0 +1,96 @@ +// Copyright 2019 The Operator-SDK Authors +// +// 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 driver + +import ( + "bytes" + "compress/gzip" + "encoding/base64" + "io/ioutil" + "strings" + + "github.com/golang/protobuf/proto" + "github.com/martinlindhe/base36" + "github.com/pborman/uuid" + apitypes "k8s.io/apimachinery/pkg/types" + rspb "k8s.io/helm/pkg/proto/hapi/release" +) + +var b64 = base64.StdEncoding + +var magicGzip = []byte{0x1f, 0x8b, 0x08} + +// encodeRelease encodes a release returning a base64 encoded +// gzipped binary protobuf encoding representation, or error. +func encodeRelease(rls *rspb.Release) (string, error) { + b, err := proto.Marshal(rls) + if err != nil { + return "", err + } + var buf bytes.Buffer + w, err := gzip.NewWriterLevel(&buf, gzip.BestCompression) + if err != nil { + return "", err + } + if _, err = w.Write(b); err != nil { + return "", err + } + w.Close() + + return b64.EncodeToString(buf.Bytes()), nil +} + +// decodeRelease decodes the bytes in data into a release +// type. Data must contain a base64 encoded string of a +// valid protobuf encoding of a release, otherwise +// an error is returned. +func decodeRelease(data string) (*rspb.Release, error) { + // base64 decode string + b, err := b64.DecodeString(data) + if err != nil { + return nil, err + } + + // For backwards compatibility with releases that were stored before + // compression was introduced we skip decompression if the + // gzip magic header is not found + if bytes.Equal(b[0:3], magicGzip) { + r, err := gzip.NewReader(bytes.NewReader(b)) + if err != nil { + return nil, err + } + b2, err := ioutil.ReadAll(r) + if err != nil { + return nil, err + } + b = b2 + } + + var rls rspb.Release + // unmarshal protobuf bytes + if err := proto.Unmarshal(b, &rls); err != nil { + return nil, err + } + return &rls, nil +} + +func shortenUID(uid apitypes.UID) string { + u := uuid.Parse(string(uid)) + uidBytes, err := u.MarshalBinary() + if err != nil { + return strings.Replace(string(uid), "-", "", -1) + } + return strings.ToLower(base36.EncodeBytes(uidBytes)) +}