diff --git a/pkg/repo/inmem_credential_provider.go b/pkg/repo/inmem_credential_provider.go new file mode 100644 index 0000000000000000000000000000000000000000..93d36fbc9b2d36a944fa95c940c046c4117bf419 --- /dev/null +++ b/pkg/repo/inmem_credential_provider.go @@ -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 +} diff --git a/pkg/repo/inmem_credential_provider_test.go b/pkg/repo/inmem_credential_provider_test.go new file mode 100644 index 0000000000000000000000000000000000000000..2c7d9e6397e6f5b5c6b46b459f837d430170e6de --- /dev/null +++ b/pkg/repo/inmem_credential_provider_test.go @@ -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) +} diff --git a/pkg/repo/inmem_repo_service.go b/pkg/repo/inmem_repo_service.go new file mode 100644 index 0000000000000000000000000000000000000000..887345958163287210faab7978bbade2fa9a5a7d --- /dev/null +++ b/pkg/repo/inmem_repo_service.go @@ -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 +} diff --git a/pkg/repo/inmem_repo_service_test.go b/pkg/repo/inmem_repo_service_test.go new file mode 100644 index 0000000000000000000000000000000000000000..14686777a5cc9b827732c9efbb9b4849c234cfd5 --- /dev/null +++ b/pkg/repo/inmem_repo_service_test.go @@ -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) + } +} diff --git a/pkg/repo/repoprovider.go b/pkg/repo/repoprovider.go new file mode 100644 index 0000000000000000000000000000000000000000..abaa965bd4f8ec35e1333f128127b926d2450038 --- /dev/null +++ b/pkg/repo/repoprovider.go @@ -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 +} diff --git a/pkg/repo/repoprovider_test.go b/pkg/repo/repoprovider_test.go new file mode 100644 index 0000000000000000000000000000000000000000..d0891266edcbc37092844a6e36e92496f05c03c4 --- /dev/null +++ b/pkg/repo/repoprovider_test.go @@ -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) + } + } +}