Merge pull request #408 from jackgr/server-charts

Add chart configuration
pull/417/head
Michelle Noorali 9 years ago
commit ee61c3318d

@ -377,12 +377,21 @@ func (c *Chart) loadDirectory(dir string) ([]*ChartMember, error) {
members := []*ChartMember{}
for _, file := range files {
filename := filepath.Join(dir, file.Name())
member, err := c.loadMember(filename)
if !file.IsDir() {
addition, err := c.loadMember(filename)
if err != nil {
return nil, err
}
members = append(members, member)
members = append(members, addition)
} else {
additions, err := c.loadDirectory(filename)
if err != nil {
return nil, err
}
members = append(members, additions...)
}
}
return members, nil
@ -416,3 +425,22 @@ func (c *Chart) loadMember(filename string) (*ChartMember, error) {
return result, nil
}
type ChartContent struct {
Chartfile *Chartfile `json:"chartfile"`
Members []*ChartMember `json:"members"`
}
func (c *Chart) LoadContent() (*ChartContent, error) {
ms, err := c.loadDirectory(c.Dir())
if err != nil {
return nil, err
}
cc := &ChartContent{
Chartfile: c.Chartfile(),
Members: ms,
}
return cc, nil
}

@ -34,6 +34,8 @@ var ErrRemote = errors.New("cannot use remote Locator as local")
const (
SchemeHTTP = "http"
SchemeHTTPS = "https"
SchemeGS = "gs"
SchemeS3 = "s3"
SchemeHelm = "helm"
SchemeFile = "file"
)
@ -85,6 +87,7 @@ func Parse(path string) (*Locator, error) {
if len(parts) < 3 {
return nil, fmt.Errorf("both bucket and chart name are required in %s: %s", path, u.Path)
}
// Need to parse opaque data into bucket and chart.
return &Locator{
Scheme: u.Scheme,
@ -115,6 +118,26 @@ func Parse(path string) (*Locator, error) {
Version: version,
original: path,
}, nil
case SchemeGS, SchemeS3:
// Long name
parts := strings.SplitN(u.Path, "/", 2)
if len(parts) < 2 {
return nil, fmt.Errorf("chart name is required in %s", path)
}
name, version, err := parseTarName(parts[1])
if err != nil {
return nil, err
}
return &Locator{
Scheme: u.Scheme,
Host: u.Scheme,
Bucket: u.Host,
Name: name,
Version: version,
original: path,
}, nil
case SchemeFile:
return &Locator{
LocalRef: u.Path,
@ -153,6 +176,7 @@ func (u *Locator) Short() (string, error) {
if u.IsLocal() {
return "", ErrLocal
}
fname := fmt.Sprintf("%s/%s/%s", u.Host, u.Bucket, u.Name)
return (&url.URL{
Scheme: SchemeHelm,
@ -171,18 +195,30 @@ func (u *Locator) Long(secure bool) (string, error) {
return "", ErrLocal
}
scheme := SchemeHTTPS
scheme := u.Scheme
host := u.Host
switch scheme {
case SchemeGS, SchemeS3:
host = ""
case SchemeHTTP, SchemeHTTPS, SchemeHelm:
switch host {
case SchemeGS, SchemeS3:
scheme = host
host = ""
default:
scheme = SchemeHTTPS
if !secure {
scheme = SchemeHTTP
}
fname := fmt.Sprintf("%s/%s-%s.tgz", u.Bucket, u.Name, u.Version)
}
}
fname := fmt.Sprintf("%s/%s-%s.tgz", u.Bucket, u.Name, u.Version)
return (&url.URL{
Scheme: scheme,
Host: u.Host,
Host: host,
Path: fname,
}).String(), nil
}
// parseTarName parses a long-form tarfile name.

@ -64,8 +64,12 @@ func TestShort(t *testing.T) {
tests := map[string]string{
"https://example.com/foo/bar-1.2.3.tgz": "helm:example.com/foo/bar#1.2.3",
"http://example.com/foo/bar-1.2.3.tgz": "helm:example.com/foo/bar#1.2.3",
"gs://foo/bar-1.2.3.tgz": "helm:gs/foo/bar#1.2.3",
"s3://foo/bar-1.2.3.tgz": "helm:s3/foo/bar#1.2.3",
"helm:example.com/foo/bar#1.2.3": "helm:example.com/foo/bar#1.2.3",
"helm:example.com/foo/bar#>1.2.3": "helm:example.com/foo/bar#%3E1.2.3",
"helm:gs/foo/bar#1.2.3": "helm:gs/foo/bar#1.2.3",
"helm:s3/foo/bar#>1.2.3": "helm:s3/foo/bar#%3E1.2.3",
}
for start, expect := range tests {
@ -74,6 +78,9 @@ func TestShort(t *testing.T) {
t.Errorf("Failed to parse: %s", err)
continue
}
t.Logf("Parsed reference %s into locator %#v", start, u)
short, err := u.Short()
if err != nil {
t.Errorf("Failed to generate short: %s", err)
@ -103,8 +110,12 @@ func TestLong(t *testing.T) {
tests := map[string]string{
"https://example.com/foo/bar-1.2.3.tgz": "https://example.com/foo/bar-1.2.3.tgz",
"http://example.com/foo/bar-1.2.3.tgz": "https://example.com/foo/bar-1.2.3.tgz",
"gs://foo/bar-1.2.3.tgz": "gs://foo/bar-1.2.3.tgz",
"s3://foo/bar-1.2.3.tgz": "s3://foo/bar-1.2.3.tgz",
"helm:example.com/foo/bar#1.2.3": "https://example.com/foo/bar-1.2.3.tgz",
"helm:example.com/foo/bar#>1.2.3": "https://example.com/foo/bar-%3E1.2.3.tgz",
"helm:gs/foo/bar#1.2.3": "gs://foo/bar-1.2.3.tgz",
"helm:s3/foo/bar#>1.2.3": "s3://foo/bar-%3E1.2.3.tgz",
}
for start, expect := range tests {
@ -114,6 +125,9 @@ func TestLong(t *testing.T) {
t.Errorf("Failed to parse: %s", err)
continue
}
t.Logf("Parsed reference %s into locator %#v", start, u)
long, err := u.Long(true)
if err != nil {
t.Errorf("Failed to generate long: %s", err)

@ -174,7 +174,7 @@ type KubernetesSecret struct {
Data map[string]string `json:"data,omitempty"`
}
// Repository related types
// TODO: Remove the remainder of this file when the refactoring of pkg/registry is complete.
// BasicAuthCredential holds a username and password.
type BasicAuthCredential struct {
@ -196,7 +196,6 @@ type RegistryCredential struct {
}
// Registry describes a template registry
// TODO(jackr): Fix ambiguity re: whether or not URL has a scheme.
type Registry struct {
Name string `json:"name,omitempty"` // Friendly name for the registry
Type RegistryType `json:"type,omitempty"` // Technology implementing the registry
@ -234,34 +233,6 @@ const (
OneLevelRegistry RegistryFormat = "onelevel"
)
// RepoType defines the technology that implements a repository.
type RepoType string
// Constants that identify the supported repository types.
const (
GCSRepoType RepoType = "gcs"
)
// RepoFormat is a semi-colon delimited string that describes the format
// of a repository.
type RepoFormat string
const (
// Versioning.
// VersionedRepo identifies a versioned repository, where types appear under versions.
VersionedRepo RepoFormat = "versioned"
// UnversionedRepo identifies an unversioned repository, where types appear under their names.
UnversionedRepo RepoFormat = "unversioned"
// Organization.
// CollectionRepo identfies a collection repository, where types are grouped into collections.
CollectionRepo RepoFormat = "collection"
// OneLevelRepo identifies a one level repository, where all types appear at the top level.
OneLevelRepo RepoFormat = "onelevel"
)
// RegistryService maintains a set of registries that defines the scope of all
// registry based operations, such as search and type resolution.
type RegistryService interface {

@ -0,0 +1,82 @@
/*
Copyright 2015 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 repo
import (
"github.com/ghodss/yaml"
"fmt"
"io/ioutil"
"log"
)
// FilebasedCredentialProvider provides credentials for registries.
type FilebasedCredentialProvider struct {
// Actual backing store
backingCredentialProvider CredentialProvider
}
// NamedRepoCredential associates a name with a RepoCredential.
type NamedRepoCredential struct {
Name string `json:"name,omitempty"`
RepoCredential
}
// NewFilebasedCredentialProvider creates a file based credential provider.
func NewFilebasedCredentialProvider(filename string) (CredentialProvider, error) {
icp := NewInmemCredentialProvider()
log.Printf("Using credentials file %s", filename)
c, err := readCredentialsFile(filename)
if err != nil {
return nil, err
}
for _, nc := range c {
log.Printf("Loading credential named %s", nc.Name)
icp.SetCredential(nc.Name, &nc.RepoCredential)
}
return &FilebasedCredentialProvider{icp}, nil
}
func readCredentialsFile(filename string) ([]NamedRepoCredential, error) {
bytes, err := ioutil.ReadFile(filename)
if err != nil {
return nil, err
}
return parseCredentials(bytes)
}
func parseCredentials(bytes []byte) ([]NamedRepoCredential, error) {
r := []NamedRepoCredential{}
if err := yaml.Unmarshal(bytes, &r); err != nil {
return nil, fmt.Errorf("cannot unmarshal credentials file (%#v)", err)
}
return r, nil
}
// GetCredential returns a credential by name.
func (fcp *FilebasedCredentialProvider) GetCredential(name string) (*RepoCredential, error) {
return fcp.backingCredentialProvider.GetCredential(name)
}
// SetCredential sets a credential by name.
func (fcp *FilebasedCredentialProvider) SetCredential(name string, credential *RepoCredential) error {
return fmt.Errorf("SetCredential operation not supported with FilebasedCredentialProvider")
}

@ -0,0 +1,57 @@
/*
Copyright 2015 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 repo
import (
"testing"
)
var filename = "./testdata/test_credentials_file.yaml"
type filebasedTestCase struct {
name string
exp *RepoCredential
expErr error
}
func TestNotExistFilebased(t *testing.T) {
cp := getProvider(t)
tc := &testCase{"nonexistent", nil, createMissingError("nonexistent")}
testGetCredential(t, cp, tc)
}
func TestGetApiTokenFilebased(t *testing.T) {
cp := getProvider(t)
tc := &testCase{"test1", &RepoCredential{APIToken: "token"}, nil}
testGetCredential(t, cp, tc)
}
func TestSetAndGetBasicAuthFilebased(t *testing.T) {
cp := getProvider(t)
ba := BasicAuthCredential{Username: "user", Password: "password"}
tc := &testCase{"test2", &RepoCredential{BasicAuth: ba}, nil}
testGetCredential(t, cp, tc)
}
func getProvider(t *testing.T) CredentialProvider {
cp, err := NewFilebasedCredentialProvider(filename)
if err != nil {
t.Fatalf("cannot create a new provider from file %s: %s", filename, err)
}
return cp
}

@ -18,7 +18,6 @@ package repo
import (
"github.com/kubernetes/helm/pkg/chart"
"github.com/kubernetes/helm/pkg/common"
"github.com/kubernetes/helm/pkg/util"
storage "google.golang.org/api/storage/v1"
@ -27,34 +26,64 @@ import (
"net/http"
"net/url"
"regexp"
"strings"
)
// GCSRepo implements the ObjectStorageRepo interface
// for Google Cloud Storage.
//
// A GCSRepo root must be a directory that contains all the available charts.
type GCSRepo struct {
chartRepo // A GCSRepo is a chartRepo
// GCSRepoURLMatcher matches the GCS repository URL format (gs://<bucket>).
var GCSRepoURLMatcher = regexp.MustCompile("gs://(.*)")
// GCSChartURLMatcher matches the GCS chart URL format (gs://<bucket>/<name>-<version>.tgz).
var GCSChartURLMatcher = regexp.MustCompile("gs://(.*)/(.*)-(.*).tgz")
const (
// GCSRepoType identifies the GCS repository type.
GCSRepoType = RepoType("gcs")
// GCSRepoFormat identifies the GCS repository format.
// In a GCS repository all charts appear at the top level.
GCSRepoFormat = FlatRepoFormat
// GCSPublicRepoName is the name of the public GCS repository.
GCSPublicRepoName = "kubernetes-charts"
// GCSPublicRepoName is the URL for the public GCS repository.
GCSPublicRepoURL = "gs://" + GCSPublicRepoName
// GCSPublicRepoBucket is the name of the public GCS repository bucket.
GCSPublicRepoBucket = GCSPublicRepoName
)
// gcsRepo implements the ObjectStorageRepo interface for Google Cloud Storage.
type gcsRepo struct {
repo
bucket string
credentialName string
httpClient *http.Client
service *storage.Service
}
// URLFormatMatcher matches the GCS URL format (gs:).
var URLFormatMatcher = regexp.MustCompile("gs://(.*)")
// NewPublicGCSRepo creates a new an ObjectStorageRepo for the public GCS repository.
func NewPublicGCSRepo(httpClient *http.Client) (ObjectStorageRepo, error) {
return NewGCSRepo(GCSPublicRepoName, GCSPublicRepoURL, "", nil)
}
var GCSRepoFormat = common.RepoFormat(fmt.Sprintf("%s;%s", common.UnversionedRepo, common.OneLevelRepo))
// NewGCSRepo creates a new ObjectStorageRepo for a given GCS repository.
func NewGCSRepo(name, URL, credentialName string, httpClient *http.Client) (ObjectStorageRepo, error) {
r, err := newRepo(name, URL, credentialName, GCSRepoFormat, GCSRepoType)
if err != nil {
return nil, err
}
// NewGCSRepo creates a GCS repository.
func NewGCSRepo(name, URL string, httpClient *http.Client) (*GCSRepo, error) {
m := URLFormatMatcher.FindStringSubmatch(URL)
return newGCSRepo(r, httpClient)
}
func newGCSRepo(r *repo, httpClient *http.Client) (*gcsRepo, error) {
URL := r.GetURL()
m := GCSRepoURLMatcher.FindStringSubmatch(URL)
if len(m) != 2 {
return nil, fmt.Errorf("URL must be of the form gs://<bucket>, was %s", URL)
}
cr, err := newRepo(name, URL, string(GCSRepoFormat), string(common.GCSRepoType))
if err != nil {
if err := validateRepoType(r.GetType()); err != nil {
return nil, err
}
@ -62,30 +91,33 @@ func NewGCSRepo(name, URL string, httpClient *http.Client) (*GCSRepo, error) {
httpClient = http.DefaultClient
}
gs, err := storage.New(httpClient)
gcs, err := storage.New(httpClient)
if err != nil {
return nil, fmt.Errorf("cannot create storage service for %s: %s", URL, err)
}
result := &GCSRepo{
chartRepo: *cr,
gcsr := &gcsRepo{
repo: *r,
httpClient: httpClient,
service: gs,
service: gcs,
bucket: m[1],
}
return result, nil
return gcsr, nil
}
// GetBucket returns the repository bucket.
func (g *GCSRepo) GetBucket() string {
return g.bucket
func validateRepoType(repoType RepoType) error {
switch repoType {
case GCSRepoType:
return nil
}
return fmt.Errorf("unknown repository type: %s", repoType)
}
// ListCharts lists charts in this chart repository whose string values conform to the
// supplied regular expression, or all charts, if the regular expression is nil.
func (g *GCSRepo) ListCharts(regex *regexp.Regexp) ([]string, error) {
// List all files in the bucket/prefix that contain the
func (g *gcsRepo) ListCharts(regex *regexp.Regexp) ([]string, error) {
charts := []string{}
// List all objects in a bucket using pagination
@ -96,12 +128,14 @@ func (g *GCSRepo) ListCharts(regex *regexp.Regexp) ([]string, error) {
if pageToken != "" {
call = call.PageToken(pageToken)
}
res, err := call.Do()
if err != nil {
return nil, err
}
for _, object := range res.Items {
// Charts should be named bucket/chart-X.Y.Z.tgz, so tease apart the version here
// Charts should be named bucket/chart-X.Y.Z.tgz, so tease apart the name
m := ChartNameMatcher.FindStringSubmatch(object.Name)
if len(m) != 3 {
continue
@ -121,8 +155,8 @@ func (g *GCSRepo) ListCharts(regex *regexp.Regexp) ([]string, error) {
}
// GetChart retrieves, unpacks and returns a chart by name.
func (g *GCSRepo) GetChart(name string) (*chart.Chart, error) {
// Charts should be named bucket/chart-X.Y.Z.tgz, so tease apart the version here
func (g *gcsRepo) GetChart(name string) (*chart.Chart, error) {
// Charts should be named bucket/chart-X.Y.Z.tgz, so check that the name matches
if !ChartNameMatcher.MatchString(name) {
return nil, fmt.Errorf("name must be of the form <name>-<version>.tgz, was %s", name)
}
@ -130,33 +164,31 @@ func (g *GCSRepo) GetChart(name string) (*chart.Chart, error) {
call := g.service.Objects.Get(g.bucket, name)
object, err := call.Do()
if err != nil {
return nil, err
return nil, fmt.Errorf("cannot get storage object named %s/%s: %s", g.bucket, name, err)
}
u, err := url.Parse(object.MediaLink)
if err != nil {
return nil, fmt.Errorf("Cannot parse URL %s for chart %s/%s: %s",
return nil, fmt.Errorf("cannot parse URL %s for chart %s/%s: %s",
object.MediaLink, object.Bucket, object.Name, err)
}
getter := util.NewHTTPClient(3, g.httpClient, util.NewSleeper())
body, code, err := getter.Get(u.String())
if err != nil {
return nil, fmt.Errorf("Cannot fetch URL %s for chart %s/%s: %d %s",
return nil, fmt.Errorf("cannot fetch URL %s for chart %s/%s: %d %s",
object.MediaLink, object.Bucket, object.Name, code, err)
}
return chart.Load(body)
return chart.LoadDataFromReader(strings.NewReader(body))
}
// Do performs an HTTP operation on the receiver's httpClient.
func (g *GCSRepo) Do(req *http.Request) (resp *http.Response, err error) {
return g.httpClient.Do(req)
// GetBucket returns the repository bucket.
func (g *gcsRepo) GetBucket() string {
return g.bucket
}
// TODO: Remove GetShortURL when no longer needed.
// GetShortURL returns the URL without the scheme.
func (g GCSRepo) GetShortURL() string {
return util.TrimURLScheme(g.URL)
// Do performs an HTTP operation on the receiver's httpClient.
func (g *gcsRepo) Do(req *http.Request) (resp *http.Response, err error) {
return g.httpClient.Do(req)
}

@ -18,7 +18,6 @@ package repo
import (
"github.com/kubernetes/helm/pkg/chart"
"github.com/kubernetes/helm/pkg/common"
"os"
"reflect"
@ -27,72 +26,57 @@ import (
)
var (
TestArchiveBucket = os.Getenv("TEST_ARCHIVE_BUCKET")
TestArchiveName = "frobnitz-0.0.1.tgz"
TestArchiveURL = os.Getenv("TEST_ARCHIVE_URL")
TestChartName = "frobnitz"
TestChartVersion = "0.0.1"
TestArchiveName = TestChartName + "-" + TestChartVersion + ".tgz"
TestChartFile = "testdata/frobnitz/Chart.yaml"
TestShouldFindRegex = regexp.MustCompile(TestArchiveName)
TestShouldNotFindRegex = regexp.MustCompile("foobar")
)
func TestValidGSURL(t *testing.T) {
var validURL = "gs://bucket"
tr, err := NewGCSRepo("testName", validURL, nil)
tr := getTestRepo(t)
err := validateRepo(tr, TestRepoName, TestRepoURL, TestRepoCredentialName, TestRepoFormat, TestRepoType)
if err != nil {
t.Fatal(err)
}
wantType := common.GCSRepoType
haveType := tr.GetRepoType()
if haveType != wantType {
t.Fatalf("unexpected repo type; want: %s, have %s.", wantType, haveType)
wantBucket := TestRepoBucket
haveBucket := tr.GetBucket()
if haveBucket != wantBucket {
t.Fatalf("unexpected bucket; want: %s, have %s.", wantBucket, haveBucket)
}
wantFormat := GCSRepoFormat
haveFormat := tr.GetRepoFormat()
if haveFormat != wantFormat {
t.Fatalf("unexpected repo format; want: %s, have %s.", wantFormat, haveFormat)
}
}
func TestInvalidGSURL(t *testing.T) {
var invalidURL = "https://bucket"
_, err := NewGCSRepo("testName", invalidURL, nil)
var invalidGSURL = "https://valid.url/wrong/scheme"
_, err := NewGCSRepo(TestRepoName, invalidGSURL, TestRepoCredentialName, nil)
if err == nil {
t.Fatalf("expected error did not occur for invalid URL")
t.Fatalf("expected error did not occur for invalid GS URL")
}
}
func TestListCharts(t *testing.T) {
if TestArchiveBucket != "" {
tr, err := NewGCSRepo("testName", TestArchiveBucket, nil)
if err != nil {
t.Fatal(err)
}
tr := getTestRepo(t)
charts, err := tr.ListCharts(nil)
if err != nil {
t.Fatal(err)
}
if len(charts) != 1 {
t.Fatalf("expected one chart in test bucket, got %d", len(charts))
t.Fatalf("expected one chart in list, got %d", len(charts))
}
name := charts[0]
if name != TestArchiveName {
t.Fatalf("expected chart named %s in test bucket, got %s", TestArchiveName, name)
}
haveName := charts[0]
wantName := TestArchiveName
if haveName != wantName {
t.Fatalf("expected chart named %s, got %s", wantName, haveName)
}
}
func TestListChartsWithShouldFindRegex(t *testing.T) {
if TestArchiveBucket != "" {
tr, err := NewGCSRepo("testName", TestArchiveBucket, nil)
if err != nil {
t.Fatal(err)
}
tr := getTestRepo(t)
charts, err := tr.ListCharts(TestShouldFindRegex)
if err != nil {
t.Fatal(err)
@ -101,16 +85,10 @@ func TestListChartsWithShouldFindRegex(t *testing.T) {
if len(charts) != 1 {
t.Fatalf("expected one chart to match regex, got %d", len(charts))
}
}
}
func TestListChartsWithShouldNotFindRegex(t *testing.T) {
if TestArchiveBucket != "" {
tr, err := NewGCSRepo("testName", TestArchiveBucket, nil)
if err != nil {
t.Fatal(err)
}
tr := getTestRepo(t)
charts, err := tr.ListCharts(TestShouldNotFindRegex)
if err != nil {
t.Fatal(err)
@ -119,37 +97,39 @@ func TestListChartsWithShouldNotFindRegex(t *testing.T) {
if len(charts) != 0 {
t.Fatalf("expected zero charts to match regex, got %d", len(charts))
}
}
}
func TestGetChart(t *testing.T) {
if TestArchiveBucket != "" {
tr, err := NewGCSRepo("testName", TestArchiveBucket, nil)
if err != nil {
t.Fatal(err)
}
tr := getTestRepo(t)
tc, err := tr.GetChart(TestArchiveName)
if err != nil {
t.Fatal(err)
}
have := tc.Chartfile()
want, err := chart.LoadChartfile(TestChartFile)
haveFile := tc.Chartfile()
wantFile, err := chart.LoadChartfile(TestChartFile)
if err != nil {
t.Fatal(err)
}
if reflect.DeepEqual(want, have) {
t.Fatalf("retrieved an invalid chart\nwant:%#v\nhave:\n%#v\n", want, have)
}
if reflect.DeepEqual(wantFile, haveFile) {
t.Fatalf("retrieved invalid chart\nwant:%#v\nhave:\n%#v\n", wantFile, haveFile)
}
}
func TestGetChartWithInvalidName(t *testing.T) {
var invalidURL = "https://bucket"
_, err := NewGCSRepo("testName", invalidURL, nil)
tr := getTestRepo(t)
_, err := tr.GetChart("NotAValidArchiveName")
if err == nil {
t.Fatalf("expected error did not occur for invalid URL")
t.Fatalf("found chart using invalid archive name")
}
}
func getTestRepo(t *testing.T) ObjectStorageRepo {
tr, err := NewGCSRepo(TestRepoName, TestRepoURL, TestRepoCredentialName, nil)
if err != nil {
t.Fatal(err)
}
return tr
}

@ -0,0 +1,45 @@
/*
Copyright 2015 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 repo
import (
"fmt"
)
// InmemCredentialProvider is a memory based credential provider.
type InmemCredentialProvider struct {
credentials map[string]*RepoCredential
}
// NewInmemCredentialProvider creates a new memory based credential provider.
func NewInmemCredentialProvider() CredentialProvider {
return &InmemCredentialProvider{credentials: make(map[string]*RepoCredential)}
}
// GetCredential returns a credential by name.
func (fcp *InmemCredentialProvider) GetCredential(name string) (*RepoCredential, error) {
if val, ok := fcp.credentials[name]; ok {
return val, nil
}
return nil, fmt.Errorf("no such credential: %s", name)
}
// SetCredential sets a credential by name.
func (fcp *InmemCredentialProvider) SetCredential(name string, credential *RepoCredential) error {
fcp.credentials[name] = &RepoCredential{APIToken: credential.APIToken, BasicAuth: credential.BasicAuth, ServiceAccount: credential.ServiceAccount}
return nil
}

@ -0,0 +1,72 @@
/*
Copyright 2015 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 repo
import (
"fmt"
"reflect"
"testing"
)
type testCase struct {
name string
exp *RepoCredential
expErr error
}
func createMissingError(name string) error {
return fmt.Errorf("no such credential: %s", name)
}
func testGetCredential(t *testing.T, cp CredentialProvider, tc *testCase) {
actual, actualErr := cp.GetCredential(tc.name)
if !reflect.DeepEqual(actual, tc.exp) {
t.Fatalf("test case %s failed: want: %#v, have: %#v", tc.name, tc.exp, actual)
}
if !reflect.DeepEqual(actualErr, tc.expErr) {
t.Fatalf("test case %s failed: want: %s, have: %s", tc.name, tc.expErr, actualErr)
}
}
func verifySetAndGetCredential(t *testing.T, cp CredentialProvider, tc *testCase) {
err := cp.SetCredential(tc.name, tc.exp)
if err != nil {
t.Fatalf("test case %s failed: cannot set credential: %v", tc.name, err)
}
testGetCredential(t, cp, tc)
}
func TestNotExist(t *testing.T) {
cp := NewInmemCredentialProvider()
tc := &testCase{"nonexistent", nil, createMissingError("nonexistent")}
testGetCredential(t, cp, tc)
}
func TestSetAndGetApiToken(t *testing.T) {
cp := NewInmemCredentialProvider()
tc := &testCase{"testcredential", &RepoCredential{APIToken: "some token here"}, nil}
verifySetAndGetCredential(t, cp, tc)
}
func TestSetAndGetBasicAuth(t *testing.T) {
cp := NewInmemCredentialProvider()
ba := BasicAuthCredential{Username: "user", Password: "pass"}
tc := &testCase{"testcredential", &RepoCredential{BasicAuth: ba}, nil}
verifySetAndGetCredential(t, cp, tc)
}

@ -0,0 +1,96 @@
/*
Copyright 2015 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 repo
import (
"fmt"
"strings"
)
type inmemRepoService struct {
repositories map[string]Repo
}
// NewInmemRepoService returns a new memory based repository service.
func NewInmemRepoService() RepoService {
rs := &inmemRepoService{
repositories: make(map[string]Repo),
}
r, err := NewPublicGCSRepo(nil)
if err == nil {
rs.Create(r)
}
return rs
}
// List returns the list of all known chart repositories
func (rs *inmemRepoService) List() ([]Repo, error) {
ret := []Repo{}
for _, r := range rs.repositories {
ret = append(ret, r)
}
return ret, nil
}
// Create adds a known repository to the list
func (rs *inmemRepoService) Create(repository Repo) error {
rs.repositories[repository.GetName()] = repository
return nil
}
// Get returns the repository with the given name
func (rs *inmemRepoService) Get(name string) (Repo, error) {
r, ok := rs.repositories[name]
if !ok {
return nil, fmt.Errorf("Failed to find repository named %s", name)
}
return r, nil
}
// GetByURL returns the repository that backs the given URL
func (rs *inmemRepoService) GetByURL(URL string) (Repo, error) {
var found Repo
for _, r := range rs.repositories {
rURL := r.GetURL()
if strings.HasPrefix(URL, rURL) {
if found == nil || len(found.GetURL()) < len(rURL) {
found = r
}
}
}
if found == nil {
return nil, fmt.Errorf("Failed to find repository for url: %s", URL)
}
return found, nil
}
// Delete removes a known repository from the list
func (rs *inmemRepoService) Delete(name string) error {
_, ok := rs.repositories[name]
if !ok {
return fmt.Errorf("Failed to find repository named %s", name)
}
delete(rs.repositories, name)
return nil
}

@ -0,0 +1,92 @@
/*
Copyright 2016 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 repo
import (
"reflect"
"testing"
)
func TestService(t *testing.T) {
rs := NewInmemRepoService()
repos, err := rs.List()
if err != nil {
t.Fatal(err)
}
if len(repos) != 1 {
t.Fatalf("unexpected repo count; want: %d, have %d.", 1, len(repos))
}
tr := repos[0]
if err := validateRepo(tr, GCSPublicRepoName, GCSPublicRepoURL, "", GCSRepoFormat, GCSRepoType); err != nil {
t.Fatal(err)
}
r1, err := rs.Get(GCSPublicRepoName)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(r1, tr) {
t.Fatalf("invalid repo returned; want: %#v, have %#v.", tr, r1)
}
r2, err := rs.GetByURL(GCSPublicRepoURL)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(r2, tr) {
t.Fatalf("invalid repo returned; want: %#v, have %#v.", tr, r2)
}
if err := rs.Delete(GCSPublicRepoName); err != nil {
t.Fatal(err)
}
if _, err := rs.Get(GCSPublicRepoName); err == nil {
t.Fatalf("deleted repo named %s returned", GCSPublicRepoName)
}
}
func TestGetRepoWithInvalidName(t *testing.T) {
invalidName := "InvalidRepoName"
rs := NewInmemRepoService()
_, err := rs.Get(invalidName)
if err == nil {
t.Fatalf("found repo with invalid name: %s", invalidName)
}
}
func TestGetRepoWithInvalidURL(t *testing.T) {
invalidURL := "https://not.a.valid/url"
rs := NewInmemRepoService()
_, err := rs.GetByURL(invalidURL)
if err == nil {
t.Fatalf("found repo with invalid URL: %s", invalidURL)
}
}
func TestDeleteRepoWithInvalidName(t *testing.T) {
invalidName := "InvalidRepoName"
rs := NewInmemRepoService()
err := rs.Delete(invalidName)
if err == nil {
t.Fatalf("deleted repo with invalid name: %s", invalidName)
}
}

@ -17,83 +17,116 @@ limitations under the License.
package repo
import (
"github.com/kubernetes/helm/pkg/chart"
"github.com/kubernetes/helm/pkg/common"
"fmt"
"net/url"
"regexp"
)
// ChartRepo abstracts a place that holds charts, which can be
// used in a Deployment Manager configuration. There can be multiple
// ChartRepo implementations.
type ChartRepo interface {
// GetRepoName returns the name of this ChartRepo.
GetRepoName() string
// GetRepoType returns the type of this repo.
GetRepoType() common.RepoType
// GetRepoURL returns the URL to the root of this ChartRepo.
GetRepoURL() string
// GetRepoFormat returns the format of this ChartRepo.
GetRepoFormat() common.RepoFormat
// ListCharts lists charts in this repository whose string values
// conform to the supplied regular expression or all charts if regex is nil
ListCharts(regex *regexp.Regexp) ([]string, error)
// GetChart retrieves, unpacks and returns a chart by name.
GetChart(name string) (*chart.Chart, error)
}
// ObjectStorageRepo abstracts a repository that resides in an Object Storage, for
// example Google Cloud Storage or AWS S3, etc.
type ObjectStorageRepo interface {
ChartRepo // An ObjectStorageRepo is a ChartRepo
GetBucket() string
// repo describes a repository
type repo struct {
Name string `json:"name"` // Friendly name for this repository
URL string `json:"url"` // URL to the root of this repository
CredentialName string `json:"credentialname"` // Credential name used to access this repository
Format RepoFormat `json:"format"` // Format of this repository
Type RepoType `json:"type"` // Technology implementing this repository
}
type chartRepo struct {
Name string `json:"name,omitempty"` // The name of this ChartRepo
URL string `json:"url,omitempty"` // The URL to the root of this ChartRepo
Format common.RepoFormat `json:"format,omitempty"` // The format of this ChartRepo
Type common.RepoType `json:"type,omitempty"` // The type of this ChartRepo
func NewRepo(name, URL, credentialName, repoFormat, repoType string) (Repo, error) {
return newRepo(name, URL, credentialName, RepoFormat(repoFormat), RepoType(repoType))
}
// ChartNameMatcher matches the chart name format
var ChartNameMatcher = regexp.MustCompile("(.*)-(.*).tgz")
func newRepo(name, URL, credentialName string, repoFormat RepoFormat, repoType RepoType) (*repo, error) {
if name == "" {
return nil, fmt.Errorf("name must not be empty")
}
func newRepo(name, URL, format, t string) (*chartRepo, error) {
_, err := url.Parse(URL)
if err != nil {
return nil, fmt.Errorf("invalid URL (%s): %s", URL, err)
}
result := &chartRepo{
if credentialName == "" {
credentialName = "default"
}
if err := validateRepoFormat(repoFormat); err != nil {
return nil, err
}
r := &repo{
Name: name,
Type: repoType,
URL: URL,
Format: common.RepoFormat(format),
Type: common.RepoType(t),
Format: repoFormat,
CredentialName: credentialName,
}
return result, nil
return r, nil
}
// Currently, only flat repositories are supported.
func validateRepoFormat(repoFormat RepoFormat) error {
switch repoFormat {
case FlatRepoFormat:
return nil
}
return fmt.Errorf("unknown repository format: %s", repoFormat)
}
// GetName returns the friendly name of this repository.
func (r *repo) GetName() string {
return r.Name
}
// GetRepoName returns the name of this ChartRepo.
func (cr *chartRepo) GetRepoName() string {
return cr.Name
// GetType returns the technology implementing this repository.
func (r *repo) GetType() RepoType {
return r.Type
}
// GetRepoType returns the type of this repo.
func (cr *chartRepo) GetRepoType() common.RepoType {
return cr.Type
// GetURL returns the URL to the root of this repository.
func (r *repo) GetURL() string {
return r.URL
}
// GetRepoURL returns the URL to the root of this ChartRepo.
func (cr *chartRepo) GetRepoURL() string {
return cr.URL
// GetFormat returns the format of this repository.
func (r *repo) GetFormat() RepoFormat {
return r.Format
}
// GetRepoFormat returns the format of this ChartRepo.
func (cr *chartRepo) GetRepoFormat() common.RepoFormat {
return cr.Format
// GetCredentialName returns the credential name used to access this repository.
func (r *repo) GetCredentialName() string {
return r.CredentialName
}
func validateRepo(tr Repo, wantName, wantURL, wantCredentialName string, wantFormat RepoFormat, wantType RepoType) error {
haveName := tr.GetName()
if haveName != wantName {
return fmt.Errorf("unexpected repo name; want: %s, have %s.", wantName, haveName)
}
haveURL := tr.GetURL()
if haveURL != wantURL {
return fmt.Errorf("unexpected repo url; want: %s, have %s.", wantURL, haveURL)
}
haveCredentialName := tr.GetCredentialName()
if wantCredentialName == "" {
wantCredentialName = "default"
}
if haveCredentialName != wantCredentialName {
return fmt.Errorf("unexpected repo credential name; want: %s, have %s.", wantCredentialName, haveCredentialName)
}
haveFormat := tr.GetFormat()
if haveFormat != wantFormat {
return fmt.Errorf("unexpected repo format; want: %s, have %s.", wantFormat, haveFormat)
}
haveType := tr.GetType()
if haveType != wantType {
return fmt.Errorf("unexpected repo type; want: %s, have %s.", wantType, haveType)
}
return nil
}

@ -20,41 +20,56 @@ import (
"testing"
)
func TestValidURL(t *testing.T) {
var wantName = "wantName"
var wantType = "wantType"
var validURL = "http://valid/url"
var wantFormat = "wantFormat"
var (
TestRepoName = "kubernetes-charts-testing"
TestRepoBucket = TestRepoName
TestRepoURL = "gs://" + TestRepoBucket
TestRepoType = GCSRepoType
TestRepoFormat = GCSRepoFormat
TestRepoCredentialName = "default"
)
tr, err := newRepo(wantName, validURL, wantFormat, wantType)
func TestValidRepoURL(t *testing.T) {
tr, err := NewRepo(TestRepoName, TestRepoURL, TestRepoCredentialName, string(TestRepoFormat), string(TestRepoType))
if err != nil {
t.Fatal(err)
}
haveName := tr.GetRepoName()
if haveName != wantName {
t.Fatalf("unexpected repo name; want: %s, have %s.", wantName, haveName)
if err := validateRepo(tr, TestRepoName, TestRepoURL, TestRepoCredentialName, TestRepoFormat, TestRepoType); err != nil {
t.Fatal(err)
}
}
func TestInvalidRepoName(t *testing.T) {
_, err := newRepo("", TestRepoURL, TestRepoCredentialName, TestRepoFormat, TestRepoType)
if err == nil {
t.Fatalf("expected error did not occur for invalid name")
}
}
haveType := string(tr.GetRepoType())
if haveType != wantType {
t.Fatalf("unexpected repo type; want: %s, have %s.", wantType, haveType)
func TestInvalidRepoURL(t *testing.T) {
_, err := newRepo(TestRepoName, "%:invalid&url:%", TestRepoCredentialName, TestRepoFormat, TestRepoType)
if err == nil {
t.Fatalf("expected error did not occur for invalid URL")
}
}
haveURL := tr.GetRepoURL()
if haveURL != validURL {
t.Fatalf("unexpected repo url; want: %s, have %s.", validURL, haveURL)
func TestDefaultCredentialName(t *testing.T) {
tr, err := newRepo(TestRepoName, TestRepoURL, "", TestRepoFormat, TestRepoType)
if err != nil {
t.Fatalf("cannot create repo using default credential name")
}
haveFormat := string(tr.GetRepoFormat())
if haveFormat != wantFormat {
t.Fatalf("unexpected repo format; want: %s, have %s.", wantFormat, haveFormat)
TestRepoCredentialName := "default"
haveCredentialName := tr.GetCredentialName()
if haveCredentialName != TestRepoCredentialName {
t.Fatalf("unexpected credential name; want: %s, have %s.", TestRepoCredentialName, haveCredentialName)
}
}
func TestInvalidURL(t *testing.T) {
_, err := newRepo("testName", "%:invalid&url:%", "testFormat", "testType")
func TestInvalidRepoFormat(t *testing.T) {
_, err := newRepo(TestRepoName, TestRepoURL, TestRepoCredentialName, "", TestRepoType)
if err == nil {
t.Fatalf("expected error did not occur for invalid URL")
t.Fatalf("expected error did not occur for invalid format")
}
}

@ -0,0 +1,257 @@
/*
Copyright 2015 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 repo
import (
"github.com/kubernetes/helm/pkg/chart"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
storage "google.golang.org/api/storage/v1"
"fmt"
"log"
"net/http"
"strings"
"sync"
)
// RepoProvider is a factory for ChartRepo instances.
type RepoProvider interface {
GetRepoByURL(URL string) (ChartRepo, error)
GetRepoByName(repoName string) (ChartRepo, error)
GetChartByReference(reference string) (*chart.Chart, error)
}
type repoProvider struct {
sync.RWMutex
rs RepoService
cp CredentialProvider
gcsrp GCSRepoProvider
repos map[string]ChartRepo
}
// NewRepoProvider creates a new repository provider.
func NewRepoProvider(rs RepoService, gcsrp GCSRepoProvider, cp CredentialProvider) RepoProvider {
return newRepoProvider(rs, gcsrp, cp)
}
// newRepoProvider creates a new repository provider.
func newRepoProvider(rs RepoService, gcsrp GCSRepoProvider, cp CredentialProvider) *repoProvider {
if rs == nil {
rs = NewInmemRepoService()
}
if cp == nil {
cp = NewInmemCredentialProvider()
}
if gcsrp == nil {
gcsrp = NewGCSRepoProvider(cp)
}
repos := make(map[string]ChartRepo)
rp := &repoProvider{rs: rs, gcsrp: gcsrp, cp: cp, repos: repos}
return rp
}
// GetRepoService returns the repository service used by this repository provider.
func (rp *repoProvider) GetRepoService() RepoService {
return rp.rs
}
// GetCredentialProvider returns the credential provider used by this repository provider.
func (rp *repoProvider) GetCredentialProvider() CredentialProvider {
return rp.cp
}
// GetGCSRepoProvider returns the GCS repository provider used by this repository provider.
func (rp *repoProvider) GetGCSRepoProvider() GCSRepoProvider {
return rp.gcsrp
}
// GetRepoByName returns the repository with the given name.
func (rp *repoProvider) GetRepoByName(repoName string) (ChartRepo, error) {
rp.Lock()
defer rp.Unlock()
if r, ok := rp.repos[repoName]; ok {
return r, nil
}
cr, err := rp.rs.Get(repoName)
if err != nil {
return nil, err
}
return rp.createRepoByType(cr)
}
func (rp *repoProvider) createRepoByType(r Repo) (ChartRepo, error) {
switch r.GetType() {
case GCSRepoType:
cr, err := rp.gcsrp.GetGCSRepo(r)
if err != nil {
return nil, err
}
return rp.createRepo(cr)
}
return nil, fmt.Errorf("unknown repository type: %s", r.GetType())
}
func (rp *repoProvider) createRepo(cr ChartRepo) (ChartRepo, error) {
name := cr.GetName()
if _, ok := rp.repos[name]; ok {
return nil, fmt.Errorf("respository named %s already exists", name)
}
rp.repos[name] = cr
return cr, nil
}
// GetRepoByURL returns the repository whose URL is a prefix of the given URL.
func (rp *repoProvider) GetRepoByURL(URL string) (ChartRepo, error) {
rp.Lock()
defer rp.Unlock()
if r := rp.findRepoByURL(URL); r != nil {
return r, nil
}
cr, err := rp.rs.GetByURL(URL)
if err != nil {
return nil, err
}
return rp.createRepoByType(cr)
}
func (rp *repoProvider) findRepoByURL(URL string) ChartRepo {
var found ChartRepo
for _, r := range rp.repos {
rURL := r.GetURL()
if strings.HasPrefix(URL, rURL) {
if found == nil || len(found.GetURL()) < len(rURL) {
found = r
}
}
}
return found
}
// GetChartByReference maps the supplied chart reference into a fully qualified
// URL, uses the URL to find the repository it references, queries the repository
// for the chart by URL, and returns the result.
func (rp *repoProvider) GetChartByReference(reference string) (*chart.Chart, error) {
l, err := ParseGCSChartReference(reference)
if err != nil {
return nil, err
}
URL, err := l.Long(true)
if err != nil {
return nil, fmt.Errorf("invalid reference %s: %s", reference, err)
}
r, err := rp.GetRepoByURL(URL)
if err != nil {
return nil, err
}
name := fmt.Sprintf("%s-%s.tgz", l.Name, l.Version)
return r.GetChart(name)
}
// GCSRepoProvider is a factory for GCS Repo instances.
type GCSRepoProvider interface {
GetGCSRepo(r Repo) (ObjectStorageRepo, error)
}
type gcsRepoProvider struct {
cp CredentialProvider
}
// NewGCSRepoProvider creates a GCSRepoProvider.
func NewGCSRepoProvider(cp CredentialProvider) GCSRepoProvider {
if cp == nil {
cp = NewInmemCredentialProvider()
}
return gcsRepoProvider{cp: cp}
}
// GetGCSRepo returns a new Google Cloud Storage repository. If a credential is specified, it will try to
// fetch it and use it, and if the credential isn't found, it will fall back to an unauthenticated client.
func (gcsrp gcsRepoProvider) GetGCSRepo(r Repo) (ObjectStorageRepo, error) {
client, err := gcsrp.createGCSClient(r.GetCredentialName())
if err != nil {
return nil, err
}
return NewGCSRepo(r.GetName(), r.GetURL(), r.GetCredentialName(), client)
}
func (gcsrp gcsRepoProvider) createGCSClient(credentialName string) (*http.Client, error) {
if credentialName == "" {
return http.DefaultClient, nil
}
c, err := gcsrp.cp.GetCredential(credentialName)
if err != nil {
log.Printf("credential named %s not found: %s", credentialName, err)
log.Print("falling back to the default client")
return http.DefaultClient, nil
}
config, err := google.JWTConfigFromJSON([]byte(c.ServiceAccount), storage.DevstorageReadOnlyScope)
if err != nil {
log.Fatalf("cannot parse client secret file: %s", err)
}
return config.Client(oauth2.NoContext), nil
}
// IsGCSChartReference returns true if the supplied string is a reference to a chart in a GCS repository
func IsGCSChartReference(r string) bool {
if _, err := ParseGCSChartReference(r); err != nil {
return false
}
return true
}
// ParseGCSChartReference parses a reference to a chart in a GCS repository and returns the URL for the chart
func ParseGCSChartReference(r string) (*chart.Locator, error) {
l, err := chart.Parse(r)
if err != nil {
return nil, fmt.Errorf("cannot parse chart reference %s: %s", r, err)
}
URL, err := l.Long(true)
if err != nil {
return nil, fmt.Errorf("chart reference %s does not resolve to a URL: %s", r, err)
}
m := GCSChartURLMatcher.FindStringSubmatch(URL)
if len(m) != 4 {
return nil, fmt.Errorf("chart reference %s resolve to invalid URL: %s", r, URL)
}
return l, nil
}

@ -0,0 +1,170 @@
/*
Copyright 2015 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 repo
import (
"github.com/kubernetes/helm/pkg/chart"
"reflect"
"testing"
)
var (
TestShortReference = "helm:gs/" + TestRepoBucket + "/" + TestChartName + "#" + TestChartVersion
TestLongReference = TestRepoURL + "/" + TestArchiveName
)
var ValidChartReferences = []string{
TestShortReference,
TestLongReference,
}
var InvalidChartReferences = []string{
"gs://missing-chart-segment",
"https://not-a-gcs-url",
"file://local-chart-reference",
}
func TestRepoProvider(t *testing.T) {
rp := NewRepoProvider(nil, nil, nil)
haveRepo, err := rp.GetRepoByName(GCSPublicRepoName)
if err != nil {
t.Fatal(err)
}
if err := validateRepo(haveRepo, GCSPublicRepoName, GCSPublicRepoURL, "", GCSRepoFormat, GCSRepoType); err != nil {
t.Fatal(err)
}
castRepo, ok := haveRepo.(ObjectStorageRepo)
if !ok {
t.Fatalf("invalid repo type, want: ObjectStorageRepo, have: %T.", haveRepo)
}
wantBucket := GCSPublicRepoBucket
haveBucket := castRepo.GetBucket()
if haveBucket != wantBucket {
t.Fatalf("unexpected bucket; want: %s, have %s.", wantBucket, haveBucket)
}
wantRepo := haveRepo
haveRepo, err = rp.GetRepoByURL(GCSPublicRepoURL)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(wantRepo, haveRepo) {
t.Fatalf("retrieved invalid repo; want: %#v, have %#v.", haveRepo, wantRepo)
}
}
func TestGetRepoByNameWithInvalidName(t *testing.T) {
var invalidName = "InvalidRepoName"
rp := NewRepoProvider(nil, nil, nil)
_, err := rp.GetRepoByName(invalidName)
if err == nil {
t.Fatalf("found repo using invalid name: %s", invalidName)
}
}
func TestGetRepoByURLWithInvalidURL(t *testing.T) {
var invalidURL = "https://valid.url/wrong/scheme"
rp := NewRepoProvider(nil, nil, nil)
_, err := rp.GetRepoByURL(invalidURL)
if err == nil {
t.Fatalf("found repo using invalid URL: %s", invalidURL)
}
}
func TestGetChartByReferenceWithValidReferences(t *testing.T) {
rp := getTestRepoProvider(t)
wantFile, err := chart.LoadChartfile(TestChartFile)
if err != nil {
t.Fatal(err)
}
for _, vcr := range ValidChartReferences {
t.Logf("getting chart by reference: %s", vcr)
tc, err := rp.GetChartByReference(vcr)
if err != nil {
t.Error(err)
continue
}
haveFile := tc.Chartfile()
if reflect.DeepEqual(wantFile, haveFile) {
t.Fatalf("retrieved invalid chart\nwant:%#v\nhave:\n%#v\n", wantFile, haveFile)
}
}
}
func getTestRepoProvider(t *testing.T) RepoProvider {
rp := newRepoProvider(nil, nil, nil)
rs := rp.GetRepoService()
tr, err := newRepo(TestRepoName, TestRepoURL, TestRepoCredentialName, TestRepoFormat, TestRepoType)
if err != nil {
t.Fatalf("cannot create test repository: %s", err)
}
if err := rs.Create(tr); err != nil {
t.Fatalf("cannot initialize repository service: %s", err)
}
return rp
}
func TestGetChartByReferenceWithInvalidReferences(t *testing.T) {
rp := NewRepoProvider(nil, nil, nil)
for _, icr := range InvalidChartReferences {
_, err := rp.GetChartByReference(icr)
if err == nil {
t.Fatalf("found chart using invalid reference: %s", icr)
}
}
}
func TestIsGCSChartReferenceWithValidReferences(t *testing.T) {
for _, vcr := range ValidChartReferences {
if !IsGCSChartReference(vcr) {
t.Fatalf("valid chart reference %s not accepted", vcr)
}
}
}
func TestIsGCSChartReferenceWithInvalidReferences(t *testing.T) {
for _, icr := range InvalidChartReferences {
if IsGCSChartReference(icr) {
t.Fatalf("invalid chart reference %s accepted", icr)
}
}
}
func TestParseGCSChartReferences(t *testing.T) {
for _, vcr := range ValidChartReferences {
if _, err := ParseGCSChartReference(vcr); err != nil {
t.Fatal(err)
}
}
}
func TestParseGCSChartReferenceWithInvalidReferences(t *testing.T) {
for _, icr := range InvalidChartReferences {
if _, err := ParseGCSChartReference(icr); err == nil {
t.Fatalf("invalid chart reference %s parsed correctly", icr)
}
}
}

@ -0,0 +1,134 @@
/*
Copyright 2015 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 repo
import (
"encoding/base64"
"encoding/json"
"flag"
"fmt"
"log"
"github.com/ghodss/yaml"
"github.com/kubernetes/helm/pkg/common"
"github.com/kubernetes/helm/pkg/util"
)
var (
kubePath = flag.String("kubectl", "./kubectl", "The path to the kubectl binary.")
kubeService = flag.String("service", "", "The DNS name of the kubernetes service.")
kubeServer = flag.String("server", "", "The IP address and optional port of the kubernetes master.")
kubeInsecure = flag.Bool("insecure-skip-tls-verify", false, "Do not check the server's certificate for validity.")
kubeConfig = flag.String("config", "", "Path to a kubeconfig file.")
kubeCertAuth = flag.String("certificate-authority", "", "Path to a file for the certificate authority.")
kubeClientCert = flag.String("client-certificate", "", "Path to a client certificate file.")
kubeClientKey = flag.String("client-key", "", "Path to a client key file.")
kubeToken = flag.String("token", "", "A service account token.")
kubeUsername = flag.String("username", "", "The username to use for basic auth.")
kubePassword = flag.String("password", "", "The password to use for basic auth.")
)
var kubernetesConfig *util.KubernetesConfig
const secretType = "Secret"
// SecretsCredentialProvider provides credentials for registries from Kubernertes secrets.
type SecretsCredentialProvider struct {
// Actual object that talks to secrets service.
k util.Kubernetes
}
// NewSecretsCredentialProvider creates a new secrets credential provider.
func NewSecretsCredentialProvider() CredentialProvider {
kubernetesConfig := &util.KubernetesConfig{
KubePath: *kubePath,
KubeService: *kubeService,
KubeServer: *kubeServer,
KubeInsecure: *kubeInsecure,
KubeConfig: *kubeConfig,
KubeCertAuth: *kubeCertAuth,
KubeClientCert: *kubeClientCert,
KubeClientKey: *kubeClientKey,
KubeToken: *kubeToken,
KubeUsername: *kubeUsername,
KubePassword: *kubePassword,
}
return &SecretsCredentialProvider{util.NewKubernetesKubectl(kubernetesConfig)}
}
func parseCredential(credential string) (*RepoCredential, error) {
var c common.KubernetesSecret
if err := json.Unmarshal([]byte(credential), &c); err != nil {
return nil, fmt.Errorf("cannot unmarshal credential (%s): %s", credential, err)
}
d, err := base64.StdEncoding.DecodeString(c.Data["credential"])
if err != nil {
return nil, fmt.Errorf("cannot unmarshal credential (%s): %s", c, err)
}
// And then finally unmarshal it from yaml to RepoCredential
r := &RepoCredential{}
if err := yaml.Unmarshal(d, &r); err != nil {
return nil, fmt.Errorf("cannot unmarshal credential %s (%#v)", c, err)
}
return r, nil
}
// GetCredential returns a credential by name.
func (scp *SecretsCredentialProvider) GetCredential(name string) (*RepoCredential, error) {
o, err := scp.k.Get(name, secretType)
if err != nil {
return nil, err
}
return parseCredential(o)
}
// SetCredential sets a credential by name.
func (scp *SecretsCredentialProvider) SetCredential(name string, credential *RepoCredential) error {
// Marshal the credential & base64 encode it.
b, err := yaml.Marshal(credential)
if err != nil {
log.Printf("yaml marshal failed for credential named %s: %s", name, err)
return err
}
enc := base64.StdEncoding.EncodeToString(b)
// Then create a kubernetes object out of it
metadata := make(map[string]string)
metadata["name"] = name
data := make(map[string]string)
data["credential"] = enc
obj := &common.KubernetesSecret{
Kind: secretType,
APIVersion: "v1",
Metadata: metadata,
Data: data,
}
ko, err := yaml.Marshal(obj)
if err != nil {
log.Printf("yaml marshal failed for kubernetes object named %s: %s", name, err)
return err
}
_, err = scp.k.Create(string(ko))
return err
}

@ -0,0 +1,6 @@
- name: test1
apitoken: token
- name: test2
basicauth:
username: user
password: password

@ -0,0 +1,121 @@
/*
Copyright 2015 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 repo
import (
"github.com/kubernetes/helm/pkg/chart"
"regexp"
)
// ChartNameMatcher matches the chart name format
var ChartNameMatcher = regexp.MustCompile("(.*)-(.*).tgz")
// BasicAuthCredential holds a username and password.
type BasicAuthCredential struct {
Username string `json:"username"`
Password string `json:"password"`
}
// APITokenCredential defines an API token.
type APITokenCredential string
// JWTTokenCredential defines a JWT token.
type JWTTokenCredential string
// RepoCredential holds a credential used to access a repository.
type RepoCredential struct {
APIToken APITokenCredential `json:"apitoken,omitempty"`
BasicAuth BasicAuthCredential `json:"basicauth,omitempty"`
ServiceAccount JWTTokenCredential `json:"serviceaccount,omitempty"`
}
// CredentialProvider provides credentials for chart repositories.
type CredentialProvider interface {
// SetCredential sets the credential for a repository.
// May not be supported by some repository services.
SetCredential(name string, credential *RepoCredential) error
// GetCredential returns the specified credential or nil if there's no credential.
// Error is non-nil if fetching the credential failed.
GetCredential(name string) (*RepoCredential, error)
}
// RepoType defines the technology that implements a repository.
type RepoType string
// RepoFormat is a semi-colon delimited string that describes the format of a repository.
type RepoFormat string
const (
// PathRepo identfies a repository where charts are organized hierarchically.
PathRepoFormat = RepoFormat("path")
// FlatRepo identifies a repository where all charts appear at the top level.
FlatRepoFormat = RepoFormat("flat")
)
// Repo abstracts a repository.
type Repo interface {
// GetName returns the friendly name of this repository.
GetName() string
// GetURL returns the URL to the root of this repository.
GetURL() string
// GetCredentialName returns the credential name used to access this repository.
GetCredentialName() string
// GetFormat returns the format of this repository.
GetFormat() RepoFormat
// GetType returns the technology implementing this repository.
GetType() RepoType
}
// ChartRepo abstracts a place that holds charts.
type ChartRepo interface {
// A ChartRepo is a Repo
Repo
// ListCharts lists charts in this repository whose string values
// conform to the supplied regular expression, or all charts if regex is nil
ListCharts(regex *regexp.Regexp) ([]string, error)
// GetChart retrieves, unpacks and returns a chart by name.
GetChart(name string) (*chart.Chart, error)
}
// ObjectStorageRepo abstracts a repository that resides in Object Storage,
// such as Google Cloud Storage, AWS S3, etc.
type ObjectStorageRepo interface {
// An ObjectStorageRepo is a ChartRepo
ChartRepo
// GetBucket returns the name of the bucket that contains this repository.
GetBucket() string
}
// RepoService maintains a list of chart repositories that defines the scope of all
// repository based operations, such as search and chart reference resolution.
type RepoService interface {
// List returns the list of all known chart repositories
List() ([]Repo, error)
// Create adds a known repository to the list
Create(repository Repo) error
// Get returns the repository with the given name
Get(name string) (Repo, error)
// GetByURL returns the repository that backs the given URL
GetByURL(URL string) (Repo, error)
// Delete removes a known repository from the list
Delete(name string) error
}
Loading…
Cancel
Save