mirror of https://github.com/helm/helm
parent
d4ccef7ba7
commit
9af1018bd3
@ -0,0 +1,258 @@
|
||||
/*
|
||||
Copyright 2017 The Kubernetes Authors All rights reserved.
|
||||
|
||||
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 "k8s.io/helm/pkg/storage/driver"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
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"
|
||||
"k8s.io/kubernetes/pkg/api"
|
||||
"k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/typed/core/internalversion"
|
||||
|
||||
rspb "k8s.io/helm/pkg/proto/hapi/release"
|
||||
)
|
||||
|
||||
var _ Driver = (*Secrets)(nil)
|
||||
|
||||
// SecretsDriverName is the string name of the driver.
|
||||
const SecretsDriverName = "Secret"
|
||||
|
||||
// Secrets is a wrapper around an implementation of a kubernetes
|
||||
// SecretsInterface.
|
||||
type Secrets struct {
|
||||
impl internalversion.SecretInterface
|
||||
Log func(string, ...interface{})
|
||||
}
|
||||
|
||||
// NewSecrets initializes a new Secrets wrapping an implmenetation of
|
||||
// the kubernetes SecretsInterface.
|
||||
func NewSecrets(impl internalversion.SecretInterface) *Secrets {
|
||||
return &Secrets{
|
||||
impl: impl,
|
||||
Log: func(_ string, _ ...interface{}) {},
|
||||
}
|
||||
}
|
||||
|
||||
// Name returns the name of the driver.
|
||||
func (secrets *Secrets) Name() string {
|
||||
return SecretsDriverName
|
||||
}
|
||||
|
||||
// Get fetches the release named by key. The corresponding release is returned
|
||||
// or error if not found.
|
||||
func (secrets *Secrets) Get(key string) (*rspb.Release, error) {
|
||||
// fetch the secret holding the release named by key
|
||||
obj, err := secrets.impl.Get(key, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
if apierrors.IsNotFound(err) {
|
||||
return nil, 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 *Secrets) List(filter func(*rspb.Release) bool) ([]*rspb.Release, error) {
|
||||
lsel := kblabels.Set{"OWNER": "TILLER"}.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 *Secrets) Query(labels map[string]string) ([]*rspb.Release, error) {
|
||||
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, 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 *Secrets) 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 := newSecretsObject(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 ErrReleaseExists(rls.Name)
|
||||
}
|
||||
|
||||
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 *Secrets) 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 := newSecretsObject(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 *Secrets) Delete(key string) (rls *rspb.Release, err error) {
|
||||
// fetch the release to check existence
|
||||
if rls, err = secrets.Get(key); err != nil {
|
||||
if apierrors.IsNotFound(err) {
|
||||
return nil, 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(key, &metav1.DeleteOptions{}); err != nil {
|
||||
return rls, err
|
||||
}
|
||||
return rls, nil
|
||||
}
|
||||
|
||||
// newSecretsObject 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, currently "TILLER".
|
||||
// "NAME" - name of the release.
|
||||
//
|
||||
func newSecretsObject(key string, rls *rspb.Release, lbs labels) (*api.Secret, error) {
|
||||
const owner = "TILLER"
|
||||
|
||||
// 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", owner)
|
||||
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 &api.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: key,
|
||||
Labels: lbs.toMap(),
|
||||
},
|
||||
Data: map[string][]byte{"release": []byte(s)},
|
||||
}, nil
|
||||
}
|
@ -0,0 +1,186 @@
|
||||
/*
|
||||
Copyright 2017 The Kubernetes Authors All rights reserved.
|
||||
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 (
|
||||
"encoding/base64"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/gogo/protobuf/proto"
|
||||
"k8s.io/kubernetes/pkg/api"
|
||||
|
||||
rspb "k8s.io/helm/pkg/proto/hapi/release"
|
||||
)
|
||||
|
||||
func TestSecretName(t *testing.T) {
|
||||
c := newTestFixtureSecrets(t)
|
||||
if c.Name() != SecretsDriverName {
|
||||
t.Errorf("Expected name to be %q, got %q", SecretsDriverName, c.Name())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSecretGet(t *testing.T) {
|
||||
vers := int32(1)
|
||||
name := "smug-pigeon"
|
||||
namespace := "default"
|
||||
key := testKey(name, vers)
|
||||
rel := releaseStub(name, vers, namespace, rspb.Status_DEPLOYED)
|
||||
|
||||
secrets := newTestFixtureSecrets(t, []*rspb.Release{rel}...)
|
||||
|
||||
// get release with key
|
||||
got, err := secrets.Get(key)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get release: %s", err)
|
||||
}
|
||||
// compare fetched release with original
|
||||
if !reflect.DeepEqual(rel, got) {
|
||||
t.Errorf("Expected {%q}, got {%q}", rel, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUNcompressedSecretGet(t *testing.T) {
|
||||
vers := int32(1)
|
||||
name := "smug-pigeon"
|
||||
namespace := "default"
|
||||
key := testKey(name, vers)
|
||||
rel := releaseStub(name, vers, namespace, rspb.Status_DEPLOYED)
|
||||
|
||||
// Create a test fixture which contains an uncompressed release
|
||||
secret, err := newSecretsObject(key, rel, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create secret: %s", err)
|
||||
}
|
||||
b, err := proto.Marshal(rel)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to marshal release: %s", err)
|
||||
}
|
||||
secret.Data["release"] = []byte(base64.StdEncoding.EncodeToString(b))
|
||||
var mock MockSecretsInterface
|
||||
mock.objects = map[string]*api.Secret{key: secret}
|
||||
secrets := NewSecrets(&mock)
|
||||
|
||||
// get release with key
|
||||
got, err := secrets.Get(key)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get release: %s", err)
|
||||
}
|
||||
// compare fetched release with original
|
||||
if !reflect.DeepEqual(rel, got) {
|
||||
t.Errorf("Expected {%q}, got {%q}", rel, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSecretList(t *testing.T) {
|
||||
secrets := newTestFixtureSecrets(t, []*rspb.Release{
|
||||
releaseStub("key-1", 1, "default", rspb.Status_DELETED),
|
||||
releaseStub("key-2", 1, "default", rspb.Status_DELETED),
|
||||
releaseStub("key-3", 1, "default", rspb.Status_DEPLOYED),
|
||||
releaseStub("key-4", 1, "default", rspb.Status_DEPLOYED),
|
||||
releaseStub("key-5", 1, "default", rspb.Status_SUPERSEDED),
|
||||
releaseStub("key-6", 1, "default", rspb.Status_SUPERSEDED),
|
||||
}...)
|
||||
|
||||
// list all deleted releases
|
||||
del, err := secrets.List(func(rel *rspb.Release) bool {
|
||||
return rel.Info.Status.Code == rspb.Status_DELETED
|
||||
})
|
||||
// check
|
||||
if err != nil {
|
||||
t.Errorf("Failed to list deleted: %s", err)
|
||||
}
|
||||
if len(del) != 2 {
|
||||
t.Errorf("Expected 2 deleted, got %d:\n%v\n", len(del), del)
|
||||
}
|
||||
|
||||
// list all deployed releases
|
||||
dpl, err := secrets.List(func(rel *rspb.Release) bool {
|
||||
return rel.Info.Status.Code == rspb.Status_DEPLOYED
|
||||
})
|
||||
// check
|
||||
if err != nil {
|
||||
t.Errorf("Failed to list deployed: %s", err)
|
||||
}
|
||||
if len(dpl) != 2 {
|
||||
t.Errorf("Expected 2 deployed, got %d", len(dpl))
|
||||
}
|
||||
|
||||
// list all superseded releases
|
||||
ssd, err := secrets.List(func(rel *rspb.Release) bool {
|
||||
return rel.Info.Status.Code == rspb.Status_SUPERSEDED
|
||||
})
|
||||
// check
|
||||
if err != nil {
|
||||
t.Errorf("Failed to list superseded: %s", err)
|
||||
}
|
||||
if len(ssd) != 2 {
|
||||
t.Errorf("Expected 2 superseded, got %d", len(ssd))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSecretCreate(t *testing.T) {
|
||||
secrets := newTestFixtureSecrets(t)
|
||||
|
||||
vers := int32(1)
|
||||
name := "smug-pigeon"
|
||||
namespace := "default"
|
||||
key := testKey(name, vers)
|
||||
rel := releaseStub(name, vers, namespace, rspb.Status_DEPLOYED)
|
||||
|
||||
// store the release in a secret
|
||||
if err := secrets.Create(key, rel); err != nil {
|
||||
t.Fatalf("Failed to create release with key %q: %s", key, err)
|
||||
}
|
||||
|
||||
// get the release back
|
||||
got, err := secrets.Get(key)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get release with key %q: %s", key, err)
|
||||
}
|
||||
|
||||
// compare created release with original
|
||||
if !reflect.DeepEqual(rel, got) {
|
||||
t.Errorf("Expected {%q}, got {%q}", rel, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSecretUpdate(t *testing.T) {
|
||||
vers := int32(1)
|
||||
name := "smug-pigeon"
|
||||
namespace := "default"
|
||||
key := testKey(name, vers)
|
||||
rel := releaseStub(name, vers, namespace, rspb.Status_DEPLOYED)
|
||||
|
||||
secrets := newTestFixtureSecrets(t, []*rspb.Release{rel}...)
|
||||
|
||||
// modify release status code
|
||||
rel.Info.Status.Code = rspb.Status_SUPERSEDED
|
||||
|
||||
// perform the update
|
||||
if err := secrets.Update(key, rel); err != nil {
|
||||
t.Fatalf("Failed to update release: %s", err)
|
||||
}
|
||||
|
||||
// fetch the updated release
|
||||
got, err := secrets.Get(key)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get release with key %q: %s", key, err)
|
||||
}
|
||||
|
||||
// check release has actually been updated by comparing modified fields
|
||||
if rel.Info.Status.Code != got.Info.Status.Code {
|
||||
t.Errorf("Expected status %s, got status %s", rel.Info.Status.Code, got.Info.Status.Code)
|
||||
}
|
||||
}
|
@ -0,0 +1,85 @@
|
||||
/*
|
||||
Copyright 2017 The Kubernetes Authors All rights reserved.
|
||||
|
||||
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 "k8s.io/helm/pkg/storage/driver"
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"encoding/base64"
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/golang/protobuf/proto"
|
||||
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
|
||||
}
|
Loading…
Reference in new issue