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{} members := []*ChartMember{}
for _, file := range files { for _, file := range files {
filename := filepath.Join(dir, file.Name()) filename := filepath.Join(dir, file.Name())
member, err := c.loadMember(filename) if !file.IsDir() {
addition, err := c.loadMember(filename)
if err != nil { if err != nil {
return nil, err 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 return members, nil
@ -416,3 +425,22 @@ func (c *Chart) loadMember(filename string) (*ChartMember, error) {
return result, nil 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 ( const (
SchemeHTTP = "http" SchemeHTTP = "http"
SchemeHTTPS = "https" SchemeHTTPS = "https"
SchemeGS = "gs"
SchemeS3 = "s3"
SchemeHelm = "helm" SchemeHelm = "helm"
SchemeFile = "file" SchemeFile = "file"
) )
@ -85,6 +87,7 @@ func Parse(path string) (*Locator, error) {
if len(parts) < 3 { if len(parts) < 3 {
return nil, fmt.Errorf("both bucket and chart name are required in %s: %s", path, u.Path) 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. // Need to parse opaque data into bucket and chart.
return &Locator{ return &Locator{
Scheme: u.Scheme, Scheme: u.Scheme,
@ -115,6 +118,26 @@ func Parse(path string) (*Locator, error) {
Version: version, Version: version,
original: path, original: path,
}, nil }, 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: case SchemeFile:
return &Locator{ return &Locator{
LocalRef: u.Path, LocalRef: u.Path,
@ -153,6 +176,7 @@ func (u *Locator) Short() (string, error) {
if u.IsLocal() { if u.IsLocal() {
return "", ErrLocal return "", ErrLocal
} }
fname := fmt.Sprintf("%s/%s/%s", u.Host, u.Bucket, u.Name) fname := fmt.Sprintf("%s/%s/%s", u.Host, u.Bucket, u.Name)
return (&url.URL{ return (&url.URL{
Scheme: SchemeHelm, Scheme: SchemeHelm,
@ -171,18 +195,30 @@ func (u *Locator) Long(secure bool) (string, error) {
return "", ErrLocal 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 { if !secure {
scheme = SchemeHTTP 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{ return (&url.URL{
Scheme: scheme, Scheme: scheme,
Host: u.Host, Host: host,
Path: fname, Path: fname,
}).String(), nil }).String(), nil
} }
// parseTarName parses a long-form tarfile name. // parseTarName parses a long-form tarfile name.

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

@ -174,7 +174,7 @@ type KubernetesSecret struct {
Data map[string]string `json:"data,omitempty"` 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. // BasicAuthCredential holds a username and password.
type BasicAuthCredential struct { type BasicAuthCredential struct {
@ -196,7 +196,6 @@ type RegistryCredential struct {
} }
// Registry describes a template registry // Registry describes a template registry
// TODO(jackr): Fix ambiguity re: whether or not URL has a scheme.
type Registry struct { type Registry struct {
Name string `json:"name,omitempty"` // Friendly name for the registry Name string `json:"name,omitempty"` // Friendly name for the registry
Type RegistryType `json:"type,omitempty"` // Technology implementing the registry Type RegistryType `json:"type,omitempty"` // Technology implementing the registry
@ -234,34 +233,6 @@ const (
OneLevelRegistry RegistryFormat = "onelevel" 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 // RegistryService maintains a set of registries that defines the scope of all
// registry based operations, such as search and type resolution. // registry based operations, such as search and type resolution.
type RegistryService interface { 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 ( import (
"github.com/kubernetes/helm/pkg/chart" "github.com/kubernetes/helm/pkg/chart"
"github.com/kubernetes/helm/pkg/common"
"github.com/kubernetes/helm/pkg/util" "github.com/kubernetes/helm/pkg/util"
storage "google.golang.org/api/storage/v1" storage "google.golang.org/api/storage/v1"
@ -27,34 +26,64 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"regexp" "regexp"
"strings"
) )
// GCSRepo implements the ObjectStorageRepo interface // GCSRepoURLMatcher matches the GCS repository URL format (gs://<bucket>).
// for Google Cloud Storage. var GCSRepoURLMatcher = regexp.MustCompile("gs://(.*)")
//
// A GCSRepo root must be a directory that contains all the available charts. // GCSChartURLMatcher matches the GCS chart URL format (gs://<bucket>/<name>-<version>.tgz).
type GCSRepo struct { var GCSChartURLMatcher = regexp.MustCompile("gs://(.*)/(.*)-(.*).tgz")
chartRepo // A GCSRepo is a chartRepo
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 bucket string
credentialName string
httpClient *http.Client httpClient *http.Client
service *storage.Service service *storage.Service
} }
// URLFormatMatcher matches the GCS URL format (gs:). // NewPublicGCSRepo creates a new an ObjectStorageRepo for the public GCS repository.
var URLFormatMatcher = regexp.MustCompile("gs://(.*)") 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. return newGCSRepo(r, httpClient)
func NewGCSRepo(name, URL string, httpClient *http.Client) (*GCSRepo, error) { }
m := URLFormatMatcher.FindStringSubmatch(URL)
func newGCSRepo(r *repo, httpClient *http.Client) (*gcsRepo, error) {
URL := r.GetURL()
m := GCSRepoURLMatcher.FindStringSubmatch(URL)
if len(m) != 2 { if len(m) != 2 {
return nil, fmt.Errorf("URL must be of the form gs://<bucket>, was %s", URL) 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 := validateRepoType(r.GetType()); err != nil {
if err != nil {
return nil, err return nil, err
} }
@ -62,30 +91,33 @@ func NewGCSRepo(name, URL string, httpClient *http.Client) (*GCSRepo, error) {
httpClient = http.DefaultClient httpClient = http.DefaultClient
} }
gs, err := storage.New(httpClient) gcs, err := storage.New(httpClient)
if err != nil { if err != nil {
return nil, fmt.Errorf("cannot create storage service for %s: %s", URL, err) return nil, fmt.Errorf("cannot create storage service for %s: %s", URL, err)
} }
result := &GCSRepo{ gcsr := &gcsRepo{
chartRepo: *cr, repo: *r,
httpClient: httpClient, httpClient: httpClient,
service: gs, service: gcs,
bucket: m[1], bucket: m[1],
} }
return result, nil return gcsr, nil
} }
// GetBucket returns the repository bucket. func validateRepoType(repoType RepoType) error {
func (g *GCSRepo) GetBucket() string { switch repoType {
return g.bucket 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 // 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. // supplied regular expression, or all charts, if the regular expression is nil.
func (g *GCSRepo) ListCharts(regex *regexp.Regexp) ([]string, error) { func (g *gcsRepo) ListCharts(regex *regexp.Regexp) ([]string, error) {
// List all files in the bucket/prefix that contain the
charts := []string{} charts := []string{}
// List all objects in a bucket using pagination // List all objects in a bucket using pagination
@ -96,12 +128,14 @@ func (g *GCSRepo) ListCharts(regex *regexp.Regexp) ([]string, error) {
if pageToken != "" { if pageToken != "" {
call = call.PageToken(pageToken) call = call.PageToken(pageToken)
} }
res, err := call.Do() res, err := call.Do()
if err != nil { if err != nil {
return nil, err return nil, err
} }
for _, object := range res.Items { 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) m := ChartNameMatcher.FindStringSubmatch(object.Name)
if len(m) != 3 { if len(m) != 3 {
continue continue
@ -121,8 +155,8 @@ func (g *GCSRepo) ListCharts(regex *regexp.Regexp) ([]string, error) {
} }
// GetChart retrieves, unpacks and returns a chart by name. // GetChart retrieves, unpacks and returns a chart by name.
func (g *GCSRepo) GetChart(name string) (*chart.Chart, error) { 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 // Charts should be named bucket/chart-X.Y.Z.tgz, so check that the name matches
if !ChartNameMatcher.MatchString(name) { if !ChartNameMatcher.MatchString(name) {
return nil, fmt.Errorf("name must be of the form <name>-<version>.tgz, was %s", 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) call := g.service.Objects.Get(g.bucket, name)
object, err := call.Do() object, err := call.Do()
if err != nil { 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) u, err := url.Parse(object.MediaLink)
if err != nil { 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) object.MediaLink, object.Bucket, object.Name, err)
} }
getter := util.NewHTTPClient(3, g.httpClient, util.NewSleeper()) getter := util.NewHTTPClient(3, g.httpClient, util.NewSleeper())
body, code, err := getter.Get(u.String()) body, code, err := getter.Get(u.String())
if err != nil { 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) 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. // GetBucket returns the repository bucket.
func (g *GCSRepo) Do(req *http.Request) (resp *http.Response, err error) { func (g *gcsRepo) GetBucket() string {
return g.httpClient.Do(req) return g.bucket
} }
// TODO: Remove GetShortURL when no longer needed. // Do performs an HTTP operation on the receiver's httpClient.
func (g *gcsRepo) Do(req *http.Request) (resp *http.Response, err error) {
// GetShortURL returns the URL without the scheme. return g.httpClient.Do(req)
func (g GCSRepo) GetShortURL() string {
return util.TrimURLScheme(g.URL)
} }

@ -18,7 +18,6 @@ package repo
import ( import (
"github.com/kubernetes/helm/pkg/chart" "github.com/kubernetes/helm/pkg/chart"
"github.com/kubernetes/helm/pkg/common"
"os" "os"
"reflect" "reflect"
@ -27,72 +26,57 @@ import (
) )
var ( var (
TestArchiveBucket = os.Getenv("TEST_ARCHIVE_BUCKET") TestArchiveURL = os.Getenv("TEST_ARCHIVE_URL")
TestArchiveName = "frobnitz-0.0.1.tgz" TestChartName = "frobnitz"
TestChartVersion = "0.0.1"
TestArchiveName = TestChartName + "-" + TestChartVersion + ".tgz"
TestChartFile = "testdata/frobnitz/Chart.yaml" TestChartFile = "testdata/frobnitz/Chart.yaml"
TestShouldFindRegex = regexp.MustCompile(TestArchiveName) TestShouldFindRegex = regexp.MustCompile(TestArchiveName)
TestShouldNotFindRegex = regexp.MustCompile("foobar") TestShouldNotFindRegex = regexp.MustCompile("foobar")
) )
func TestValidGSURL(t *testing.T) { func TestValidGSURL(t *testing.T) {
var validURL = "gs://bucket" tr := getTestRepo(t)
tr, err := NewGCSRepo("testName", validURL, nil) err := validateRepo(tr, TestRepoName, TestRepoURL, TestRepoCredentialName, TestRepoFormat, TestRepoType)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
wantType := common.GCSRepoType wantBucket := TestRepoBucket
haveType := tr.GetRepoType() haveBucket := tr.GetBucket()
if haveType != wantType { if haveBucket != wantBucket {
t.Fatalf("unexpected repo type; want: %s, have %s.", wantType, haveType) 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) { func TestInvalidGSURL(t *testing.T) {
var invalidURL = "https://bucket" var invalidGSURL = "https://valid.url/wrong/scheme"
_, err := NewGCSRepo("testName", invalidURL, nil) _, err := NewGCSRepo(TestRepoName, invalidGSURL, TestRepoCredentialName, nil)
if err == 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) { func TestListCharts(t *testing.T) {
if TestArchiveBucket != "" { tr := getTestRepo(t)
tr, err := NewGCSRepo("testName", TestArchiveBucket, nil)
if err != nil {
t.Fatal(err)
}
charts, err := tr.ListCharts(nil) charts, err := tr.ListCharts(nil)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if len(charts) != 1 { 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] haveName := charts[0]
if name != TestArchiveName { wantName := TestArchiveName
t.Fatalf("expected chart named %s in test bucket, got %s", TestArchiveName, name) if haveName != wantName {
} t.Fatalf("expected chart named %s, got %s", wantName, haveName)
} }
} }
func TestListChartsWithShouldFindRegex(t *testing.T) { func TestListChartsWithShouldFindRegex(t *testing.T) {
if TestArchiveBucket != "" { tr := getTestRepo(t)
tr, err := NewGCSRepo("testName", TestArchiveBucket, nil)
if err != nil {
t.Fatal(err)
}
charts, err := tr.ListCharts(TestShouldFindRegex) charts, err := tr.ListCharts(TestShouldFindRegex)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -101,16 +85,10 @@ func TestListChartsWithShouldFindRegex(t *testing.T) {
if len(charts) != 1 { if len(charts) != 1 {
t.Fatalf("expected one chart to match regex, got %d", len(charts)) t.Fatalf("expected one chart to match regex, got %d", len(charts))
} }
}
} }
func TestListChartsWithShouldNotFindRegex(t *testing.T) { func TestListChartsWithShouldNotFindRegex(t *testing.T) {
if TestArchiveBucket != "" { tr := getTestRepo(t)
tr, err := NewGCSRepo("testName", TestArchiveBucket, nil)
if err != nil {
t.Fatal(err)
}
charts, err := tr.ListCharts(TestShouldNotFindRegex) charts, err := tr.ListCharts(TestShouldNotFindRegex)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -119,37 +97,39 @@ func TestListChartsWithShouldNotFindRegex(t *testing.T) {
if len(charts) != 0 { if len(charts) != 0 {
t.Fatalf("expected zero charts to match regex, got %d", len(charts)) t.Fatalf("expected zero charts to match regex, got %d", len(charts))
} }
}
} }
func TestGetChart(t *testing.T) { func TestGetChart(t *testing.T) {
if TestArchiveBucket != "" { tr := getTestRepo(t)
tr, err := NewGCSRepo("testName", TestArchiveBucket, nil)
if err != nil {
t.Fatal(err)
}
tc, err := tr.GetChart(TestArchiveName) tc, err := tr.GetChart(TestArchiveName)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
have := tc.Chartfile() haveFile := tc.Chartfile()
want, err := chart.LoadChartfile(TestChartFile) wantFile, err := chart.LoadChartfile(TestChartFile)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if reflect.DeepEqual(want, have) { if reflect.DeepEqual(wantFile, haveFile) {
t.Fatalf("retrieved an invalid chart\nwant:%#v\nhave:\n%#v\n", want, have) t.Fatalf("retrieved invalid chart\nwant:%#v\nhave:\n%#v\n", wantFile, haveFile)
}
} }
} }
func TestGetChartWithInvalidName(t *testing.T) { func TestGetChartWithInvalidName(t *testing.T) {
var invalidURL = "https://bucket" tr := getTestRepo(t)
_, err := NewGCSRepo("testName", invalidURL, nil) _, err := tr.GetChart("NotAValidArchiveName")
if err == nil { 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 package repo
import ( import (
"github.com/kubernetes/helm/pkg/chart"
"github.com/kubernetes/helm/pkg/common"
"fmt" "fmt"
"net/url" "net/url"
"regexp"
) )
// ChartRepo abstracts a place that holds charts, which can be // repo describes a repository
// used in a Deployment Manager configuration. There can be multiple type repo struct {
// ChartRepo implementations. Name string `json:"name"` // Friendly name for this repository
type ChartRepo interface { URL string `json:"url"` // URL to the root of this repository
// GetRepoName returns the name of this ChartRepo. CredentialName string `json:"credentialname"` // Credential name used to access this repository
GetRepoName() string Format RepoFormat `json:"format"` // Format of this repository
// GetRepoType returns the type of this repo. Type RepoType `json:"type"` // Technology implementing this repository
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
} }
type chartRepo struct { func NewRepo(name, URL, credentialName, repoFormat, repoType string) (Repo, error) {
Name string `json:"name,omitempty"` // The name of this ChartRepo return newRepo(name, URL, credentialName, RepoFormat(repoFormat), RepoType(repoType))
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
} }
// ChartNameMatcher matches the chart name format func newRepo(name, URL, credentialName string, repoFormat RepoFormat, repoType RepoType) (*repo, error) {
var ChartNameMatcher = regexp.MustCompile("(.*)-(.*).tgz") if name == "" {
return nil, fmt.Errorf("name must not be empty")
}
func newRepo(name, URL, format, t string) (*chartRepo, error) {
_, err := url.Parse(URL) _, err := url.Parse(URL)
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid URL (%s): %s", URL, err) 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, Name: name,
Type: repoType,
URL: URL, URL: URL,
Format: common.RepoFormat(format), Format: repoFormat,
Type: common.RepoType(t), 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. // GetType returns the technology implementing this repository.
func (cr *chartRepo) GetRepoName() string { func (r *repo) GetType() RepoType {
return cr.Name return r.Type
} }
// GetRepoType returns the type of this repo. // GetURL returns the URL to the root of this repository.
func (cr *chartRepo) GetRepoType() common.RepoType { func (r *repo) GetURL() string {
return cr.Type return r.URL
} }
// GetRepoURL returns the URL to the root of this ChartRepo. // GetFormat returns the format of this repository.
func (cr *chartRepo) GetRepoURL() string { func (r *repo) GetFormat() RepoFormat {
return cr.URL return r.Format
} }
// GetRepoFormat returns the format of this ChartRepo. // GetCredentialName returns the credential name used to access this repository.
func (cr *chartRepo) GetRepoFormat() common.RepoFormat { func (r *repo) GetCredentialName() string {
return cr.Format 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" "testing"
) )
func TestValidURL(t *testing.T) { var (
var wantName = "wantName" TestRepoName = "kubernetes-charts-testing"
var wantType = "wantType" TestRepoBucket = TestRepoName
var validURL = "http://valid/url" TestRepoURL = "gs://" + TestRepoBucket
var wantFormat = "wantFormat" 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 { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
haveName := tr.GetRepoName() if err := validateRepo(tr, TestRepoName, TestRepoURL, TestRepoCredentialName, TestRepoFormat, TestRepoType); err != nil {
if haveName != wantName { t.Fatal(err)
t.Fatalf("unexpected repo name; want: %s, have %s.", wantName, haveName) }
}
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()) func TestInvalidRepoURL(t *testing.T) {
if haveType != wantType { _, err := newRepo(TestRepoName, "%:invalid&url:%", TestRepoCredentialName, TestRepoFormat, TestRepoType)
t.Fatalf("unexpected repo type; want: %s, have %s.", wantType, haveType) if err == nil {
t.Fatalf("expected error did not occur for invalid URL")
} }
}
haveURL := tr.GetRepoURL() func TestDefaultCredentialName(t *testing.T) {
if haveURL != validURL { tr, err := newRepo(TestRepoName, TestRepoURL, "", TestRepoFormat, TestRepoType)
t.Fatalf("unexpected repo url; want: %s, have %s.", validURL, haveURL) if err != nil {
t.Fatalf("cannot create repo using default credential name")
} }
haveFormat := string(tr.GetRepoFormat()) TestRepoCredentialName := "default"
if haveFormat != wantFormat { haveCredentialName := tr.GetCredentialName()
t.Fatalf("unexpected repo format; want: %s, have %s.", wantFormat, haveFormat) if haveCredentialName != TestRepoCredentialName {
t.Fatalf("unexpected credential name; want: %s, have %s.", TestRepoCredentialName, haveCredentialName)
} }
} }
func TestInvalidURL(t *testing.T) { func TestInvalidRepoFormat(t *testing.T) {
_, err := newRepo("testName", "%:invalid&url:%", "testFormat", "testType") _, err := newRepo(TestRepoName, TestRepoURL, TestRepoCredentialName, "", TestRepoType)
if err == nil { 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