mirror of https://github.com/helm/helm
parent
b928088a8a
commit
b0e7a43b5b
@ -0,0 +1,168 @@
|
||||
/*
|
||||
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 "k8s.io/helm/pkg/repo"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/ghodss/yaml"
|
||||
|
||||
"k8s.io/helm/pkg/chartutil"
|
||||
"k8s.io/helm/pkg/provenance"
|
||||
"k8s.io/helm/pkg/tlsutil"
|
||||
)
|
||||
|
||||
// ChartRepositoryConfig represents a collection of parameters for chart repository
|
||||
type ChartRepositoryConfig struct {
|
||||
Name string `json:"name"`
|
||||
Cache string `json:"cache"`
|
||||
URL string `json:"url"`
|
||||
CertFile string `json:"certFile"`
|
||||
KeyFile string `json:"keyFile"`
|
||||
CAFile string `json:"caFile"`
|
||||
}
|
||||
|
||||
// ChartRepository represents a chart repository
|
||||
type ChartRepository struct {
|
||||
Config *ChartRepositoryConfig
|
||||
ChartPaths []string
|
||||
IndexFile *ChartRepositoryIndex
|
||||
Client *http.Client
|
||||
}
|
||||
|
||||
// NewChartRepository constructs ChartRepository
|
||||
func NewChartRepository(cfg *ChartRepositoryConfig) (*ChartRepository, error) {
|
||||
var client *http.Client
|
||||
if cfg.CertFile != "" && cfg.KeyFile != "" && cfg.CAFile != "" {
|
||||
tlsConf, err := tlsutil.NewClientTLS(cfg.CertFile, cfg.KeyFile, cfg.CAFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't create TLS config for client: %s", err.Error())
|
||||
}
|
||||
tlsConf.BuildNameToCertificate()
|
||||
client = &http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: tlsConf,
|
||||
},
|
||||
}
|
||||
} else {
|
||||
client = http.DefaultClient
|
||||
}
|
||||
|
||||
return &ChartRepository{
|
||||
Config: cfg,
|
||||
IndexFile: NewChartRepositoryIndex(),
|
||||
Client: client,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Load loads a directory of charts as if it were a repository.
|
||||
//
|
||||
// It requires the presence of an index.yaml file in the directory.
|
||||
func (r *ChartRepository) Load() error {
|
||||
dirInfo, err := os.Stat(r.Config.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !dirInfo.IsDir() {
|
||||
return fmt.Errorf("%q is not a directory", r.Config.Name)
|
||||
}
|
||||
|
||||
// FIXME: Why are we recursively walking directories?
|
||||
// FIXME: Why are we not reading the repositories.yaml to figure out
|
||||
// what repos to use?
|
||||
filepath.Walk(r.Config.Name, func(path string, f os.FileInfo, err error) error {
|
||||
if !f.IsDir() {
|
||||
if strings.Contains(f.Name(), "-index.yaml") {
|
||||
i, err := NewChartRepositoryIndexFromFile(path)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
r.IndexFile = i
|
||||
} else if strings.HasSuffix(f.Name(), ".tgz") {
|
||||
r.ChartPaths = append(r.ChartPaths, path)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// DownloadIndexFile fetches the index from a repository.
|
||||
func (r *ChartRepository) DownloadIndexFile() error {
|
||||
var indexURL string
|
||||
|
||||
indexURL = strings.TrimSuffix(r.Config.URL, "/") + "/index.yaml"
|
||||
resp, err := r.Client.Get(indexURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
index, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := loadIndex(index); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return ioutil.WriteFile(r.Config.Cache, index, 0644)
|
||||
}
|
||||
|
||||
// Index generates an index for the chart repository and writes an index.yaml file.
|
||||
func (r *ChartRepository) Index() error {
|
||||
err := r.generateIndex()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return r.saveIndexFile()
|
||||
}
|
||||
|
||||
func (r *ChartRepository) saveIndexFile() error {
|
||||
index, err := yaml.Marshal(r.IndexFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ioutil.WriteFile(filepath.Join(r.Config.Name, indexPath), index, 0644)
|
||||
}
|
||||
|
||||
func (r *ChartRepository) generateIndex() error {
|
||||
for _, path := range r.ChartPaths {
|
||||
ch, err := chartutil.Load(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
digest, err := provenance.DigestFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !r.IndexFile.Has(ch.Metadata.Name, ch.Metadata.Version) {
|
||||
r.IndexFile.Add(ch.Metadata, path, r.Config.URL, digest)
|
||||
}
|
||||
// TODO: If a chart exists, but has a different Digest, should we error?
|
||||
}
|
||||
r.IndexFile.SortEntries()
|
||||
return nil
|
||||
}
|
@ -0,0 +1,185 @@
|
||||
/*
|
||||
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 (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"k8s.io/helm/pkg/proto/hapi/chart"
|
||||
)
|
||||
|
||||
const (
|
||||
testRepository = "testdata/repository"
|
||||
testURL = "http://example-charts.com"
|
||||
)
|
||||
|
||||
func TestLoadChartRepository(t *testing.T) {
|
||||
r, err := NewChartRepository(&ChartRepositoryConfig{
|
||||
Name: testRepository,
|
||||
URL: testURL,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("Problem creating chart repository from %s: %v", testRepository, err)
|
||||
}
|
||||
|
||||
if err := r.Load(); err != nil {
|
||||
t.Errorf("Problem loading chart repository from %s: %v", testRepository, err)
|
||||
}
|
||||
|
||||
paths := []string{
|
||||
filepath.Join(testRepository, "frobnitz-1.2.3.tgz"),
|
||||
filepath.Join(testRepository, "sprocket-1.1.0.tgz"),
|
||||
filepath.Join(testRepository, "sprocket-1.2.0.tgz"),
|
||||
}
|
||||
|
||||
if r.Config.Name != testRepository {
|
||||
t.Errorf("Expected %s as Name but got %s", testRepository, r.Config.Name)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(r.ChartPaths, paths) {
|
||||
t.Errorf("Expected %#v but got %#v\n", paths, r.ChartPaths)
|
||||
}
|
||||
|
||||
if r.Config.URL != testURL {
|
||||
t.Errorf("Expected url for chart repository to be %s but got %s", testURL, r.Config.URL)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIndex(t *testing.T) {
|
||||
r, err := NewChartRepository(&ChartRepositoryConfig{
|
||||
Name: testRepository,
|
||||
URL: testURL,
|
||||
})
|
||||
if err != nil {
|
||||
t.Errorf("Problem creating chart repository from %s: %v", testRepository, err)
|
||||
}
|
||||
|
||||
if err := r.Load(); err != nil {
|
||||
t.Errorf("Problem loading chart repository from %s: %v", testRepository, err)
|
||||
}
|
||||
|
||||
err = r.Index()
|
||||
if err != nil {
|
||||
t.Errorf("Error performing index: %v\n", err)
|
||||
}
|
||||
|
||||
tempIndexPath := filepath.Join(testRepository, indexPath)
|
||||
actual, err := NewChartRepositoryIndexFromFile(tempIndexPath)
|
||||
defer os.Remove(tempIndexPath) // clean up
|
||||
if err != nil {
|
||||
t.Errorf("Error loading index file %v", err)
|
||||
}
|
||||
verifyIndex(t, actual)
|
||||
|
||||
// Re-index and test again.
|
||||
err = r.Index()
|
||||
if err != nil {
|
||||
t.Errorf("Error performing re-index: %s\n", err)
|
||||
}
|
||||
second, err := NewChartRepositoryIndexFromFile(tempIndexPath)
|
||||
if err != nil {
|
||||
t.Errorf("Error re-loading index file %v", err)
|
||||
}
|
||||
verifyIndex(t, second)
|
||||
}
|
||||
|
||||
func verifyIndex(t *testing.T, actual *ChartRepositoryIndex) {
|
||||
var empty time.Time
|
||||
if actual.Generated == empty {
|
||||
t.Errorf("Generated should be greater than 0: %s", actual.Generated)
|
||||
}
|
||||
|
||||
if actual.APIVersion != APIVersionV1 {
|
||||
t.Error("Expected v1 API")
|
||||
}
|
||||
|
||||
entries := actual.Entries
|
||||
if numEntries := len(entries); numEntries != 2 {
|
||||
t.Errorf("Expected 2 charts to be listed in index file but got %v", numEntries)
|
||||
}
|
||||
|
||||
expects := map[string]ChartVersions{
|
||||
"frobnitz": {
|
||||
{
|
||||
Metadata: &chart.Metadata{
|
||||
Name: "frobnitz",
|
||||
Version: "1.2.3",
|
||||
},
|
||||
},
|
||||
},
|
||||
"sprocket": {
|
||||
{
|
||||
Metadata: &chart.Metadata{
|
||||
Name: "sprocket",
|
||||
Version: "1.2.0",
|
||||
},
|
||||
},
|
||||
{
|
||||
Metadata: &chart.Metadata{
|
||||
Name: "sprocket",
|
||||
Version: "1.1.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, versions := range expects {
|
||||
got, ok := entries[name]
|
||||
if !ok {
|
||||
t.Errorf("Could not find %q entry", name)
|
||||
continue
|
||||
}
|
||||
if len(versions) != len(got) {
|
||||
t.Errorf("Expected %d versions, got %d", len(versions), len(got))
|
||||
continue
|
||||
}
|
||||
for i, e := range versions {
|
||||
g := got[i]
|
||||
if e.Name != g.Name {
|
||||
t.Errorf("Expected %q, got %q", e.Name, g.Name)
|
||||
}
|
||||
if e.Version != g.Version {
|
||||
t.Errorf("Expected %q, got %q", e.Version, g.Version)
|
||||
}
|
||||
if len(g.Keywords) != 3 {
|
||||
t.Error("Expected 3 keyrwords.")
|
||||
}
|
||||
if len(g.Maintainers) != 2 {
|
||||
t.Error("Expected 2 maintainers.")
|
||||
}
|
||||
if g.Created == empty {
|
||||
t.Error("Expected created to be non-empty")
|
||||
}
|
||||
if g.Description == "" {
|
||||
t.Error("Expected description to be non-empty")
|
||||
}
|
||||
if g.Home == "" {
|
||||
t.Error("Expected home to be non-empty")
|
||||
}
|
||||
if g.Digest == "" {
|
||||
t.Error("Expected digest to be non-empty")
|
||||
}
|
||||
if len(g.URLs) != 1 {
|
||||
t.Error("Expected exactly 1 URL")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
/*
|
||||
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 tlsutil
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
)
|
||||
|
||||
// NewClientTLS returns tls.Config appropriate for client auth.
|
||||
func NewClientTLS(certFile, keyFile, caFile string) (*tls.Config, error) {
|
||||
cert, err := CertFromFilePair(certFile, keyFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cp, err := CertPoolFromFile(caFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &tls.Config{
|
||||
Certificates: []tls.Certificate{*cert},
|
||||
RootCAs: cp,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CertPoolFromFile returns an x509.CertPool containing the certificates
|
||||
// in the given PEM-encoded file.
|
||||
// Returns an error if the file could not be read, a certificate could not
|
||||
// be parsed, or if the file does not contain any certificates
|
||||
func CertPoolFromFile(filename string) (*x509.CertPool, error) {
|
||||
b, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't read CA file: %v", filename)
|
||||
}
|
||||
cp := x509.NewCertPool()
|
||||
if !cp.AppendCertsFromPEM(b) {
|
||||
return nil, fmt.Errorf("failed to append certificates from file: %s", filename)
|
||||
}
|
||||
return cp, nil
|
||||
}
|
||||
|
||||
// CertFromFilePair returns an tls.Certificate containing the
|
||||
// certificates public/private key pair from a pair of given PEM-encoded files.
|
||||
// Returns an error if the file could not be read, a certificate could not
|
||||
// be parsed, or if the file does not contain any certificates
|
||||
func CertFromFilePair(certFile, keyFile string) (*tls.Certificate, error) {
|
||||
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't load key pair from cert %s and key %s", certFile, keyFile)
|
||||
}
|
||||
return &cert, err
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
/*
|
||||
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 urlutil
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"path"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// URLJoin joins a base URL to one or more path components.
|
||||
//
|
||||
// It's like filepath.Join for URLs. If the baseURL is pathish, this will still
|
||||
// perform a join.
|
||||
//
|
||||
// If the URL is unparsable, this returns an error.
|
||||
func URLJoin(baseURL string, paths ...string) (string, error) {
|
||||
u, err := url.Parse(baseURL)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
// We want path instead of filepath because path always uses /.
|
||||
all := []string{u.Path}
|
||||
all = append(all, paths...)
|
||||
u.Path = path.Join(all...)
|
||||
return u.String(), nil
|
||||
}
|
||||
|
||||
// URLAreEqual normalizes two URLs and then compares for equality.
|
||||
//
|
||||
// TODO: This and the urlJoin functions should really be moved to a 'urlutil' package.
|
||||
func URLAreEqual(a, b string) bool {
|
||||
au, err := url.Parse(a)
|
||||
if err != nil {
|
||||
a = filepath.Clean(a)
|
||||
b = filepath.Clean(b)
|
||||
// If urls are paths, return true only if they are an exact match
|
||||
return a == b
|
||||
}
|
||||
bu, err := url.Parse(b)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, u := range []*url.URL{au, bu} {
|
||||
if u.Path == "" {
|
||||
u.Path = "/"
|
||||
}
|
||||
u.Path = filepath.Clean(u.Path)
|
||||
}
|
||||
return au.String() == bu.String()
|
||||
}
|
@ -0,0 +1,64 @@
|
||||
/*
|
||||
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 urlutil
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestUrlJoin(t *testing.T) {
|
||||
tests := []struct {
|
||||
name, url, expect string
|
||||
paths []string
|
||||
}{
|
||||
{name: "URL, one path", url: "http://example.com", paths: []string{"hello"}, expect: "http://example.com/hello"},
|
||||
{name: "Long URL, one path", url: "http://example.com/but/first", paths: []string{"slurm"}, expect: "http://example.com/but/first/slurm"},
|
||||
{name: "URL, two paths", url: "http://example.com", paths: []string{"hello", "world"}, expect: "http://example.com/hello/world"},
|
||||
{name: "URL, no paths", url: "http://example.com", paths: []string{}, expect: "http://example.com"},
|
||||
{name: "basepath, two paths", url: "../example.com", paths: []string{"hello", "world"}, expect: "../example.com/hello/world"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
if got, err := URLJoin(tt.url, tt.paths...); err != nil {
|
||||
t.Errorf("%s: error %q", tt.name, err)
|
||||
} else if got != tt.expect {
|
||||
t.Errorf("%s: expected %q, got %q", tt.name, tt.expect, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUrlAreEqual(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
a, b string
|
||||
match bool
|
||||
}{
|
||||
{"http://example.com", "http://example.com", true},
|
||||
{"http://example.com", "http://another.example.com", false},
|
||||
{"https://example.com", "https://example.com", true},
|
||||
{"http://example.com/", "http://example.com", true},
|
||||
{"https://example.com", "http://example.com", false},
|
||||
{"http://example.com/foo", "http://example.com/foo/", true},
|
||||
{"http://example.com/foo//", "http://example.com/foo/", true},
|
||||
{"http://example.com/./foo/", "http://example.com/foo/", true},
|
||||
{"http://example.com/bar/../foo/", "http://example.com/foo/", true},
|
||||
{"/foo", "/foo", true},
|
||||
{"/foo", "/foo/", true},
|
||||
{"/foo/.", "/foo/", true},
|
||||
} {
|
||||
if tt.match != URLAreEqual(tt.a, tt.b) {
|
||||
t.Errorf("Expected %q==%q to be %t", tt.a, tt.b, tt.match)
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in new issue