diff --git a/pkg/chart/chart.go b/pkg/chart/chart.go index 21df43408755d1d9b689e15c490c9cf9fcf4102c..8857bf441ea68c89846fa11dc169ae05cff1d10e 100644 --- a/pkg/chart/chart.go +++ b/pkg/chart/chart.go @@ -20,6 +20,7 @@ import ( "archive/tar" "bytes" "compress/gzip" + "encoding/base64" "errors" "fmt" "io" @@ -243,25 +244,7 @@ func LoadDir(chart string) (*Chart, error) { // LoadData loads a chart from data, where data is a []byte containing a gzipped tar file. func LoadData(data []byte) (*Chart, error) { - b := bytes.NewBuffer(data) - unzipped, err := gzip.NewReader(b) - if err != nil { - return nil, err - } - defer unzipped.Close() - - untarred := tar.NewReader(unzipped) - c, err := loadTar(untarred) - if err != nil { - return nil, err - } - - cf, err := LoadChartfile(filepath.Join(c.tmpDir, ChartfileName)) - if err != nil { - return nil, err - } - c.chartyaml = cf - return &Chart{loader: c}, nil + return LoadDataFromReader(bytes.NewBuffer(data)) } // Load loads a chart from a chart archive. @@ -281,7 +264,11 @@ func Load(archive string) (*Chart, error) { } defer raw.Close() - unzipped, err := gzip.NewReader(raw) + return LoadDataFromReader(raw) +} + +func LoadDataFromReader(r io.Reader) (*Chart, error) { + unzipped, err := gzip.NewReader(r) if err != nil { return nil, err } @@ -367,3 +354,65 @@ func loadTar(r *tar.Reader) (*tarChart, error) { return c, nil } + +// ChartMember is a file in a chart. +type ChartMember struct { + Path string `json:"path"` // Path from the root of the chart. + Content []byte `json:"content"` // Base64 encoded content. +} + +// LoadTemplates loads the members of TemplatesDir(). +func (c *Chart) LoadTemplates() ([]*ChartMember, error) { + dir := c.TemplatesDir() + return c.loadDirectory(dir) +} + +// loadDirectory loads the members of a directory. +func (c *Chart) loadDirectory(dir string) ([]*ChartMember, error) { + files, err := ioutil.ReadDir(dir) + if err != nil { + return nil, err + } + + members := []*ChartMember{} + for _, file := range files { + filename := filepath.Join(dir, file.Name()) + member, err := c.loadMember(filename) + if err != nil { + return nil, err + } + + members = append(members, member) + } + + return members, nil +} + +// path is from the root of the chart. +func (c *Chart) LoadMember(path string) (*ChartMember, error) { + filename := filepath.Join(c.loader.dir(), path) + return c.loadMember(filename) +} + +// loadMember loads and base 64 encodes a file. +func (c *Chart) loadMember(filename string) (*ChartMember, error) { + dir := c.Dir() + if !strings.HasPrefix(filename, dir) { + err := fmt.Errorf("File %s is outside chart directory %s", filename, dir) + return nil, err + } + + b, err := ioutil.ReadFile(filename) + if err != nil { + return nil, err + } + + path := strings.TrimPrefix(filename, dir) + content := base64.StdEncoding.EncodeToString(b) + result := &ChartMember{ + Path: path, + Content: []byte(content), + } + + return result, nil +} diff --git a/pkg/chart/chart_test.go b/pkg/chart/chart_test.go index 36ac856d20df1fe89fde3059e9d9968940140e9b..5d1c46f6706edbc6e9fe9c08ec51e95fb027bdb6 100644 --- a/pkg/chart/chart_test.go +++ b/pkg/chart/chart_test.go @@ -17,6 +17,8 @@ limitations under the License. package chart import ( + "encoding/base64" + "fmt" "io/ioutil" "path/filepath" "testing" @@ -30,6 +32,7 @@ const ( testarchive = "testdata/frobnitz-0.0.1.tgz" testill = "testdata/ill-1.2.3.tgz" testnochart = "testdata/nochart.tgz" + testmember = "templates/wordpress.jinja" ) // Type canaries. If these fail, they will fail at compile time. @@ -160,3 +163,91 @@ func TestChart(t *testing.T) { t.Errorf("Unexpectedly, icon is in %s", i) } } + +func TestLoadTemplates(t *testing.T) { + c, err := LoadDir(testdir) + if err != nil { + t.Errorf("Failed to load chart: %s", err) + } + + members, err := c.LoadTemplates() + if members == nil { + t.Fatalf("Cannot load templates: unknown error") + } + + if err != nil { + t.Fatalf("Cannot load templates: %s", err) + } + + dir := c.TemplatesDir() + files, err := ioutil.ReadDir(dir) + if err != nil { + t.Fatalf("Cannot read template directory: %s", err) + } + + if len(members) != len(files) { + t.Fatalf("Expected %s templates, got %d", len(files), len(members)) + } + + root := c.loader.dir() + for _, file := range files { + path := filepath.Join(preTemplates, file.Name()) + if err := findMember(root, path, members); err != nil { + t.Fatal(err) + } + } +} + +func findMember(root, path string, members []*ChartMember) error { + for _, member := range members { + if member.Path == path { + filename := filepath.Join(root, path) + if err := compareContent(filename, string(member.Content)); err != nil { + return err + } + + return nil + } + } + + return fmt.Errorf("Template not found: %s", path) +} + +func TestLoadMember(t *testing.T) { + c, err := LoadDir(testdir) + if err != nil { + t.Errorf("Failed to load chart: %s", err) + } + + member, err := c.LoadMember(testmember) + if member == nil { + t.Fatalf("Cannot load member %s: unknown error", testmember) + } + + if err != nil { + t.Fatalf("Cannot load member %s: %s", testmember, err) + } + + if member.Path != testmember { + t.Errorf("Expected member path %s, got %s", testmember, member.Path) + } + + filename := filepath.Join(c.loader.dir(), testmember) + if err := compareContent(filename, string(member.Content)); err != nil { + t.Fatal(err) + } +} + +func compareContent(filename, content string) error { + b, err := ioutil.ReadFile(filename) + if err != nil { + return fmt.Errorf("Cannot read test file %s: %s", filename, err) + } + + compare := base64.StdEncoding.EncodeToString(b) + if content != compare { + return fmt.Errorf("Expected member content\n%v\ngot\n%v", []byte(compare), []byte(content)) + } + + return nil +} diff --git a/pkg/chart/chartfile.go b/pkg/chart/chartfile.go index b694a373465d5a57eb257c3008f2f6016a885ab7..1332ef805ba6d3bd2c1fb4056c42a7cd0d5f3126 100644 --- a/pkg/chart/chartfile.go +++ b/pkg/chart/chartfile.go @@ -34,6 +34,8 @@ type Chartfile struct { Home string `yaml:"home"` Dependencies []*Dependency `yaml:"dependencies,omitempty"` Environment []*EnvConstraint `yaml:"environment,omitempty"` + Expander *Expander `yaml:"expander,omitempty"` + Schema string `yaml:"schema,omitempty"` } // Maintainer describes a chart maintainer. @@ -57,6 +59,14 @@ type EnvConstraint struct { APIGroups []string `yaml:"apiGroups,omitempty"` } +// Expander controls how template/ is evaluated. +type Expander struct { + // Currently just Expandybird or GoTemplate + Name string `json:"name"` + // During evaluation, which file to start from. + Entrypoint string `json:"entrypoint"` +} + // LoadChartfile loads a Chart.yaml file into a *Chart. func LoadChartfile(filename string) (*Chartfile, error) { b, err := ioutil.ReadFile(filename) diff --git a/pkg/chart/chartfile_test.go b/pkg/chart/chartfile_test.go index b5e46dfa6f345bd6200106f9690052abc42ef87f..3c4042db7a4bca7f207bc5c2858fdb016ff25a3b 100644 --- a/pkg/chart/chartfile_test.go +++ b/pkg/chart/chartfile_test.go @@ -50,6 +50,23 @@ func TestLoadChartfile(t *testing.T) { if f.Source[0] != "https://example.com/foo/bar" { t.Errorf("Expected https://example.com/foo/bar, got %s", f.Source) } + + expander := f.Expander + if expander == nil { + t.Errorf("No expander found in %s", testfile) + } else { + if expander.Name != "Expandybird" { + t.Errorf("Expected expander name Expandybird, got %s", expander.Name) + } + + if expander.Entrypoint != "templates/wordpress.jinja" { + t.Errorf("Expected expander entrypoint templates/wordpress.jinja, got %s", expander.Entrypoint) + } + } + + if f.Schema != "wordpress.jinja.schema" { + t.Errorf("Expected schema wordpress.jinja.schema, got %s", f.Schema) + } } func TestVersionOK(t *testing.T) { diff --git a/pkg/chart/testdata/frobnitz/Chart.yaml b/pkg/chart/testdata/frobnitz/Chart.yaml index 9572f010c1826cadced546702a14627e0c0fb6d9..b1e67a038f8cfca9bf6158d35e85db44068ddae4 100644 --- a/pkg/chart/testdata/frobnitz/Chart.yaml +++ b/pkg/chart/testdata/frobnitz/Chart.yaml @@ -26,3 +26,8 @@ environment: - extensions/v1beta1/daemonset apiGroups: - 3rdParty +expander: + name: Expandybird + entrypoint: templates/wordpress.jinja +schema: wordpress.jinja.schema + \ No newline at end of file diff --git a/pkg/common/types.go b/pkg/common/types.go index b7dbe6fc03948bba2ad765b164836984055c69de..df030da8773f290461a5e5eb5d77012da2a16814 100644 --- a/pkg/common/types.go +++ b/pkg/common/types.go @@ -97,28 +97,6 @@ type Manifest struct { Layout *Layout `json:"layout,omitempty"` } -// Expander controls how template/ is evaluated. -type Expander struct { - // Currently just Expandybird or GoTemplate - Name string `json:"name"` - // During evaluation, which file to start from. - Entrypoint string `json:"entry_point"` -} - -// ChartFile is a file in a chart that is not chart.yaml. -type ChartFile struct { - Path string `json:"path"` // Path from the root of the chart. - Content string `json:"content"` // Base64 encoded file content. -} - -// Chart is our internal representation of the chart.yaml (in structured form) + all supporting files. -type Chart struct { - Name string `json:"name"` - Expander *Expander `json:"expander"` - Schema interface{} `json:"schema"` - Files []*ChartFile `json:"files"` -} - // Template describes a set of resources to be deployed. // Manager expands a Template into a Configuration, which // describes the set in a form that can be instantiated. @@ -227,10 +205,10 @@ type Registry struct { CredentialName string `json:"credentialname,omitempty"` // Name of the credential to use } -// RegistryType defines the technology that implements the registry +// RegistryType defines the technology that implements a registry. type RegistryType string -// Constants that identify the supported registry layouts. +// Constants that identify the supported registry types. const ( GithubRegistryType RegistryType = "github" GCSRegistryType RegistryType = "gcs" @@ -256,6 +234,34 @@ const ( OneLevelRegistry RegistryFormat = "onelevel" ) +// RepoType defines the technology that implements a repository. +type RepoType string + +// Constants that identify the supported repository types. +const ( + GCSRepoType RepoType = "gcs" +) + +// RepoFormat is a semi-colon delimited string that describes the format +// of a repository. +type RepoFormat string + +const ( + // Versioning. + + // VersionedRepo identifies a versioned repository, where types appear under versions. + VersionedRepo RepoFormat = "versioned" + // UnversionedRepo identifies an unversioned repository, where types appear under their names. + UnversionedRepo RepoFormat = "unversioned" + + // Organization. + + // CollectionRepo identfies a collection repository, where types are grouped into collections. + CollectionRepo RepoFormat = "collection" + // OneLevelRepo identifies a one level repository, where all types appear at the top level. + OneLevelRepo RepoFormat = "onelevel" +) + // RegistryService maintains a set of registries that defines the scope of all // registry based operations, such as search and type resolution. type RegistryService interface { diff --git a/pkg/repo/gcs_repo.go b/pkg/repo/gcs_repo.go new file mode 100644 index 0000000000000000000000000000000000000000..0feb7793461087c94c95a61ae15c88624e69d754 --- /dev/null +++ b/pkg/repo/gcs_repo.go @@ -0,0 +1,162 @@ +/* +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" + "github.com/kubernetes/helm/pkg/common" + "github.com/kubernetes/helm/pkg/util" + + storage "google.golang.org/api/storage/v1" + + "fmt" + "net/http" + "net/url" + "regexp" +) + +// GCSRepo implements the ObjectStorageRepo interface +// for Google Cloud Storage. +// +// A GCSRepo root must be a directory that contains all the available charts. +type GCSRepo struct { + chartRepo // A GCSRepo is a chartRepo + bucket string + credentialName string + httpClient *http.Client + service *storage.Service +} + +// URLFormatMatcher matches the GCS URL format (gs:). +var URLFormatMatcher = regexp.MustCompile("gs://(.*)") + +var GCSRepoFormat = common.RepoFormat(fmt.Sprintf("%s;%s", common.UnversionedRepo, common.OneLevelRepo)) + +// NewGCSRepo creates a GCS repository. +func NewGCSRepo(name, URL string, httpClient *http.Client) (*GCSRepo, error) { + m := URLFormatMatcher.FindStringSubmatch(URL) + if len(m) != 2 { + return nil, fmt.Errorf("URL must be of the form gs://<bucket>, was %s", URL) + } + + cr, err := newRepo(name, URL, string(GCSRepoFormat), string(common.GCSRepoType)) + if err != nil { + return nil, err + } + + if httpClient == nil { + httpClient = http.DefaultClient + } + + gs, err := storage.New(httpClient) + if err != nil { + return nil, fmt.Errorf("cannot create storage service for %s: %s", URL, err) + } + + result := &GCSRepo{ + chartRepo: *cr, + httpClient: httpClient, + service: gs, + bucket: m[1], + } + + return result, nil +} + +// GetBucket returns the repository bucket. +func (g *GCSRepo) GetBucket() string { + return g.bucket +} + +// ListCharts lists charts in this chart repository whose string values conform to the +// supplied regular expression, or all charts, if the regular expression is nil. +func (g *GCSRepo) ListCharts(regex *regexp.Regexp) ([]string, error) { + // List all files in the bucket/prefix that contain the + charts := []string{} + + // List all objects in a bucket using pagination + pageToken := "" + for { + call := g.service.Objects.List(g.bucket) + call.Delimiter("/") + if pageToken != "" { + call = call.PageToken(pageToken) + } + res, err := call.Do() + if err != nil { + return nil, err + } + for _, object := range res.Items { + // Charts should be named bucket/chart-X.Y.Z.tgz, so tease apart the version here + m := ChartNameMatcher.FindStringSubmatch(object.Name) + if len(m) != 3 { + continue + } + + if regex == nil || regex.MatchString(object.Name) { + charts = append(charts, object.Name) + } + } + + if pageToken = res.NextPageToken; pageToken == "" { + break + } + } + + return charts, nil +} + +// GetChart retrieves, unpacks and returns a chart by name. +func (g *GCSRepo) GetChart(name string) (*chart.Chart, error) { + // Charts should be named bucket/chart-X.Y.Z.tgz, so tease apart the version here + if !ChartNameMatcher.MatchString(name) { + return nil, fmt.Errorf("name must be of the form <name>-<version>.tgz, was %s", name) + } + + call := g.service.Objects.Get(g.bucket, name) + object, err := call.Do() + if err != nil { + return nil, err + } + + u, err := url.Parse(object.MediaLink) + if err != nil { + return nil, fmt.Errorf("Cannot parse URL %s for chart %s/%s: %s", + object.MediaLink, object.Bucket, object.Name, err) + } + + getter := util.NewHTTPClient(3, g.httpClient, util.NewSleeper()) + body, code, err := getter.Get(u.String()) + if err != nil { + return nil, fmt.Errorf("Cannot fetch URL %s for chart %s/%s: %d %s", + object.MediaLink, object.Bucket, object.Name, code, err) + } + + return chart.Load(body) +} + +// Do performs an HTTP operation on the receiver's httpClient. +func (g *GCSRepo) Do(req *http.Request) (resp *http.Response, err error) { + return g.httpClient.Do(req) +} + +// TODO: Remove GetShortURL when no longer needed. + +// GetShortURL returns the URL without the scheme. +func (g GCSRepo) GetShortURL() string { + return util.TrimURLScheme(g.URL) +} diff --git a/pkg/repo/gcs_repo_test.go b/pkg/repo/gcs_repo_test.go new file mode 100644 index 0000000000000000000000000000000000000000..52cf5863e2c86d40d97de9a573ed327d4f7085aa --- /dev/null +++ b/pkg/repo/gcs_repo_test.go @@ -0,0 +1,155 @@ +/* +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 ( + "github.com/kubernetes/helm/pkg/chart" + "github.com/kubernetes/helm/pkg/common" + + "os" + "reflect" + "regexp" + "testing" +) + +var ( + TestArchiveBucket = os.Getenv("TEST_ARCHIVE_BUCKET") + TestArchiveName = "frobnitz-0.0.1.tgz" + TestChartFile = "testdata/frobnitz/Chart.yaml" + TestShouldFindRegex = regexp.MustCompile(TestArchiveName) + TestShouldNotFindRegex = regexp.MustCompile("foobar") +) + +func TestValidGSURL(t *testing.T) { + var validURL = "gs://bucket" + tr, err := NewGCSRepo("testName", validURL, nil) + if err != nil { + t.Fatal(err) + } + + wantType := common.GCSRepoType + haveType := tr.GetRepoType() + if haveType != wantType { + t.Fatalf("unexpected repo type; want: %s, have %s.", wantType, haveType) + } + + wantFormat := GCSRepoFormat + haveFormat := tr.GetRepoFormat() + if haveFormat != wantFormat { + t.Fatalf("unexpected repo format; want: %s, have %s.", wantFormat, haveFormat) + } + +} + +func TestInvalidGSURL(t *testing.T) { + var invalidURL = "https://bucket" + _, err := NewGCSRepo("testName", invalidURL, nil) + if err == nil { + t.Fatalf("expected error did not occur for invalid URL") + } +} + +func TestListCharts(t *testing.T) { + if TestArchiveBucket != "" { + tr, err := NewGCSRepo("testName", TestArchiveBucket, nil) + if err != nil { + t.Fatal(err) + } + + charts, err := tr.ListCharts(nil) + if err != nil { + t.Fatal(err) + } + + if len(charts) != 1 { + t.Fatalf("expected one chart in test bucket, got %d", len(charts)) + } + + name := charts[0] + if name != TestArchiveName { + t.Fatalf("expected chart named %s in test bucket, got %s", TestArchiveName, name) + } + } +} + +func TestListChartsWithShouldFindRegex(t *testing.T) { + if TestArchiveBucket != "" { + tr, err := NewGCSRepo("testName", TestArchiveBucket, nil) + if err != nil { + t.Fatal(err) + } + + charts, err := tr.ListCharts(TestShouldFindRegex) + if err != nil { + t.Fatal(err) + } + + if len(charts) != 1 { + t.Fatalf("expected one chart to match regex, got %d", len(charts)) + } + } +} + +func TestListChartsWithShouldNotFindRegex(t *testing.T) { + if TestArchiveBucket != "" { + tr, err := NewGCSRepo("testName", TestArchiveBucket, nil) + if err != nil { + t.Fatal(err) + } + + charts, err := tr.ListCharts(TestShouldNotFindRegex) + if err != nil { + t.Fatal(err) + } + + if len(charts) != 0 { + t.Fatalf("expected zero charts to match regex, got %d", len(charts)) + } + } +} + +func TestGetChart(t *testing.T) { + if TestArchiveBucket != "" { + tr, err := NewGCSRepo("testName", TestArchiveBucket, nil) + if err != nil { + t.Fatal(err) + } + + tc, err := tr.GetChart(TestArchiveName) + if err != nil { + t.Fatal(err) + } + + have := tc.Chartfile() + want, err := chart.LoadChartfile(TestChartFile) + if err != nil { + t.Fatal(err) + } + + if reflect.DeepEqual(want, have) { + t.Fatalf("retrieved an invalid chart\nwant:%#v\nhave:\n%#v\n", want, have) + } + } +} + +func TestGetChartWithInvalidName(t *testing.T) { + var invalidURL = "https://bucket" + _, err := NewGCSRepo("testName", invalidURL, nil) + if err == nil { + t.Fatalf("expected error did not occur for invalid URL") + } +} diff --git a/pkg/repo/repo.go b/pkg/repo/repo.go new file mode 100644 index 0000000000000000000000000000000000000000..d9cee38a651c10f166d6297670e3fe6d9f63b8cc --- /dev/null +++ b/pkg/repo/repo.go @@ -0,0 +1,99 @@ +/* +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" + "github.com/kubernetes/helm/pkg/common" + + "fmt" + "net/url" + "regexp" +) + +// ChartRepo abstracts a place that holds charts, which can be +// used in a Deployment Manager configuration. There can be multiple +// ChartRepo implementations. +type ChartRepo interface { + // GetRepoName returns the name of this ChartRepo. + GetRepoName() string + // GetRepoType returns the type of this repo. + GetRepoType() common.RepoType + // GetRepoURL returns the URL to the root of this ChartRepo. + GetRepoURL() string + // GetRepoFormat returns the format of this ChartRepo. + GetRepoFormat() common.RepoFormat + + // ListCharts lists charts in this repository whose string values + // conform to the supplied regular expression or all charts if regex is nil + ListCharts(regex *regexp.Regexp) ([]string, error) + // GetChart retrieves, unpacks and returns a chart by name. + GetChart(name string) (*chart.Chart, error) +} + +// ObjectStorageRepo abstracts a repository that resides in an Object Storage, for +// example Google Cloud Storage or AWS S3, etc. +type ObjectStorageRepo interface { + ChartRepo // An ObjectStorageRepo is a ChartRepo + GetBucket() string +} + +type chartRepo struct { + Name string `json:"name,omitempty"` // The name of this ChartRepo + URL string `json:"url,omitempty"` // The URL to the root of this ChartRepo + Format common.RepoFormat `json:"format,omitempty"` // The format of this ChartRepo + Type common.RepoType `json:"type,omitempty"` // The type of this ChartRepo +} + +// ChartNameMatcher matches the chart name format +var ChartNameMatcher = regexp.MustCompile("(.*)-(.*).tgz") + +func newRepo(name, URL, format, t string) (*chartRepo, error) { + _, err := url.Parse(URL) + if err != nil { + return nil, fmt.Errorf("invalid URL (%s): %s", URL, err) + } + + result := &chartRepo{ + Name: name, + URL: URL, + Format: common.RepoFormat(format), + Type: common.RepoType(t), + } + + return result, nil +} + +// GetRepoName returns the name of this ChartRepo. +func (cr *chartRepo) GetRepoName() string { + return cr.Name +} + +// GetRepoType returns the type of this repo. +func (cr *chartRepo) GetRepoType() common.RepoType { + return cr.Type +} + +// GetRepoURL returns the URL to the root of this ChartRepo. +func (cr *chartRepo) GetRepoURL() string { + return cr.URL +} + +// GetRepoFormat returns the format of this ChartRepo. +func (cr *chartRepo) GetRepoFormat() common.RepoFormat { + return cr.Format +} diff --git a/pkg/repo/repo_test.go b/pkg/repo/repo_test.go new file mode 100644 index 0000000000000000000000000000000000000000..f4bc608864cc2f1707346a6db60673821438acab --- /dev/null +++ b/pkg/repo/repo_test.go @@ -0,0 +1,60 @@ +/* +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 ( + "testing" +) + +func TestValidURL(t *testing.T) { + var wantName = "wantName" + var wantType = "wantType" + var validURL = "http://valid/url" + var wantFormat = "wantFormat" + + tr, err := newRepo(wantName, validURL, wantFormat, wantType) + if err != nil { + t.Fatal(err) + } + + haveName := tr.GetRepoName() + if haveName != wantName { + t.Fatalf("unexpected repo name; want: %s, have %s.", wantName, haveName) + } + + haveType := string(tr.GetRepoType()) + if haveType != wantType { + t.Fatalf("unexpected repo type; want: %s, have %s.", wantType, haveType) + } + + haveURL := tr.GetRepoURL() + if haveURL != validURL { + t.Fatalf("unexpected repo url; want: %s, have %s.", validURL, haveURL) + } + + haveFormat := string(tr.GetRepoFormat()) + if haveFormat != wantFormat { + t.Fatalf("unexpected repo format; want: %s, have %s.", wantFormat, haveFormat) + } +} + +func TestInvalidURL(t *testing.T) { + _, err := newRepo("testName", "%:invalid&url:%", "testFormat", "testType") + if err == nil { + t.Fatalf("expected error did not occur for invalid URL") + } +} diff --git a/pkg/repo/testdata/README.md b/pkg/repo/testdata/README.md new file mode 100644 index 0000000000000000000000000000000000000000..2f58d9774544c0c597acc9af5cf0e22e3883282f --- /dev/null +++ b/pkg/repo/testdata/README.md @@ -0,0 +1,6 @@ +The testdata directory here holds charts that match the specification. + +The `fromnitz/` directory contains a chart that matches the chart +specification. + +The `frobnitz-0.0.1.tgz` file is an archive of the `frobnitz` directory. diff --git a/pkg/repo/testdata/frobnitz-0.0.1.tgz b/pkg/repo/testdata/frobnitz-0.0.1.tgz new file mode 100644 index 0000000000000000000000000000000000000000..322f463969025c897ec52b7f94fc44f98d37093d Binary files /dev/null and b/pkg/repo/testdata/frobnitz-0.0.1.tgz differ diff --git a/pkg/repo/testdata/frobnitz/Chart.yaml b/pkg/repo/testdata/frobnitz/Chart.yaml new file mode 100644 index 0000000000000000000000000000000000000000..b1e67a038f8cfca9bf6158d35e85db44068ddae4 --- /dev/null +++ b/pkg/repo/testdata/frobnitz/Chart.yaml @@ -0,0 +1,33 @@ +#helm:generate foo +name: frobnitz +description: This is a frobniz. +version: "1.2.3-alpha.1+12345" +keywords: + - frobnitz + - sprocket + - dodad +maintainers: + - name: The Helm Team + email: helm@example.com + - name: Someone Else + email: nobody@example.com +source: + - https://example.com/foo/bar +home: http://example.com +dependencies: + - name: thingerbob + location: https://example.com/charts/thingerbob-3.2.1.tgz + version: ^3 +environment: + - name: Kubernetes + version: ~1.1 + extensions: + - extensions/v1beta1 + - extensions/v1beta1/daemonset + apiGroups: + - 3rdParty +expander: + name: Expandybird + entrypoint: templates/wordpress.jinja +schema: wordpress.jinja.schema + \ No newline at end of file diff --git a/pkg/repo/testdata/frobnitz/LICENSE b/pkg/repo/testdata/frobnitz/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..6121943b10a68319b2c1f66e659b8a69a4df7ed0 --- /dev/null +++ b/pkg/repo/testdata/frobnitz/LICENSE @@ -0,0 +1 @@ +LICENSE placeholder. diff --git a/pkg/repo/testdata/frobnitz/README.md b/pkg/repo/testdata/frobnitz/README.md new file mode 100644 index 0000000000000000000000000000000000000000..8cf4cc3d7c0f1d3c418e7ba4eab1335b606f063d --- /dev/null +++ b/pkg/repo/testdata/frobnitz/README.md @@ -0,0 +1,11 @@ +# Frobnitz + +This is an example chart. + +## Usage + +This is an example. It has no usage. + +## Development + +For developer info, see the top-level repository. diff --git a/pkg/repo/testdata/frobnitz/docs/README.md b/pkg/repo/testdata/frobnitz/docs/README.md new file mode 100644 index 0000000000000000000000000000000000000000..d40747cafd28860f06babbf27e85d56203e53e35 --- /dev/null +++ b/pkg/repo/testdata/frobnitz/docs/README.md @@ -0,0 +1 @@ +This is a placeholder for documentation. diff --git a/pkg/repo/testdata/frobnitz/hooks/pre-install.py b/pkg/repo/testdata/frobnitz/hooks/pre-install.py new file mode 100644 index 0000000000000000000000000000000000000000..c9b0d0a9211271c95e4c4632e9780ded4f477fc4 --- /dev/null +++ b/pkg/repo/testdata/frobnitz/hooks/pre-install.py @@ -0,0 +1 @@ +# Placeholder. diff --git a/pkg/repo/testdata/frobnitz/icon.svg b/pkg/repo/testdata/frobnitz/icon.svg new file mode 100644 index 0000000000000000000000000000000000000000..8921306066db91867ee2fd152c83ce75785c7803 --- /dev/null +++ b/pkg/repo/testdata/frobnitz/icon.svg @@ -0,0 +1,8 @@ +<?xml version="1.0"?> +<svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink" + version="1.0" width="256" height="256" id="test"> + <desc>Example icon</desc> + <rect id="first" x="2" y="2" width="40" height="60" fill="navy"/> + <rect id="second" x="15" y="4" width="40" height="60" fill="red"/> +</svg> diff --git a/pkg/repo/testdata/frobnitz/templates/wordpress-resources.yaml b/pkg/repo/testdata/frobnitz/templates/wordpress-resources.yaml new file mode 100644 index 0000000000000000000000000000000000000000..00f709de04d5aabc6706573f4b0ba604edfe7ec9 --- /dev/null +++ b/pkg/repo/testdata/frobnitz/templates/wordpress-resources.yaml @@ -0,0 +1,12 @@ +# Google Cloud Deployment Manager template +resources: +- name: nfs-disk + type: compute.v1.disk + properties: + zone: us-central1-b + sizeGb: 200 +- name: mysql-disk + type: compute.v1.disk + properties: + zone: us-central1-b + sizeGb: 200 diff --git a/pkg/repo/testdata/frobnitz/templates/wordpress.jinja b/pkg/repo/testdata/frobnitz/templates/wordpress.jinja new file mode 100644 index 0000000000000000000000000000000000000000..f34e4fec91b2a69ba6d8d0de0541df5c89f4e444 --- /dev/null +++ b/pkg/repo/testdata/frobnitz/templates/wordpress.jinja @@ -0,0 +1,72 @@ +#helm:generate dm_template +{% set PROPERTIES = properties or {} %} +{% set PROJECT = PROPERTIES['project'] or 'dm-k8s-testing' %} +{% set NFS_SERVER = PROPERTIES['nfs-server'] or {} %} +{% set NFS_SERVER_IP = NFS_SERVER['ip'] or '10.0.253.247' %} +{% set NFS_SERVER_PORT = NFS_SERVER['port'] or 2049 %} +{% set NFS_SERVER_DISK = NFS_SERVER['disk'] or 'nfs-disk' %} +{% set NFS_SERVER_DISK_FSTYPE = NFS_SERVER['fstype'] or 'ext4' %} +{% set NGINX = PROPERTIES['nginx'] or {} %} +{% set NGINX_PORT = 80 %} +{% set NGINX_REPLICAS = NGINX['replicas'] or 2 %} +{% set WORDPRESS_PHP = PROPERTIES['wordpress-php'] or {} %} +{% set WORDPRESS_PHP_REPLICAS = WORDPRESS_PHP['replicas'] or 2 %} +{% set WORDPRESS_PHP_PORT = WORDPRESS_PHP['port'] or 9000 %} +{% set MYSQL = PROPERTIES['mysql'] or {} %} +{% set MYSQL_PORT = MYSQL['port'] or 3306 %} +{% set MYSQL_PASSWORD = MYSQL['password'] or 'mysql-password' %} +{% set MYSQL_DISK = MYSQL['disk'] or 'mysql-disk' %} +{% set MYSQL_DISK_FSTYPE = MYSQL['fstype'] or 'ext4' %} + +resources: +- name: nfs + type: github.com/kubernetes/application-dm-templates/storage/nfs:v1 + properties: + ip: {{ NFS_SERVER_IP }} + port: {{ NFS_SERVER_PORT }} + disk: {{ NFS_SERVER_DISK }} + fstype: {{NFS_SERVER_DISK_FSTYPE }} +- name: nginx + type: github.com/kubernetes/application-dm-templates/common/replicatedservice:v2 + properties: + service_port: {{ NGINX_PORT }} + container_port: {{ NGINX_PORT }} + replicas: {{ NGINX_REPLICAS }} + external_service: true + image: gcr.io/{{ PROJECT }}/nginx:latest + volumes: + - mount_path: /var/www/html + persistentVolumeClaim: + claimName: nfs +- name: mysql + type: github.com/kubernetes/application-dm-templates/common/replicatedservice:v2 + properties: + service_port: {{ MYSQL_PORT }} + container_port: {{ MYSQL_PORT }} + replicas: 1 + image: mysql:5.6 + env: + - name: MYSQL_ROOT_PASSWORD + value: {{ MYSQL_PASSWORD }} + volumes: + - mount_path: /var/lib/mysql + gcePersistentDisk: + pdName: {{ MYSQL_DISK }} + fsType: {{ MYSQL_DISK_FSTYPE }} +- name: wordpress-php + type: github.com/kubernetes/application-dm-templates/common/replicatedservice:v2 + properties: + service_name: wordpress-php + service_port: {{ WORDPRESS_PHP_PORT }} + container_port: {{ WORDPRESS_PHP_PORT }} + replicas: 2 + image: wordpress:fpm + env: + - name: WORDPRESS_DB_PASSWORD + value: {{ MYSQL_PASSWORD }} + - name: WORDPRESS_DB_HOST + value: mysql-service + volumes: + - mount_path: /var/www/html + persistentVolumeClaim: + claimName: nfs diff --git a/pkg/repo/testdata/frobnitz/templates/wordpress.jinja.schema b/pkg/repo/testdata/frobnitz/templates/wordpress.jinja.schema new file mode 100644 index 0000000000000000000000000000000000000000..215b47e1e397c106915cba6dc4685e69830425e6 --- /dev/null +++ b/pkg/repo/testdata/frobnitz/templates/wordpress.jinja.schema @@ -0,0 +1,69 @@ +info: + title: Wordpress + description: | + Defines a Wordpress website by defining four replicated services: an NFS service, an nginx service, a wordpress-php service, and a MySQL service. + + The nginx service and the Wordpress-php service both use NFS to share files. + +properties: + project: + type: string + default: dm-k8s-testing + description: Project location to load the images from. + nfs-service: + type: object + properties: + ip: + type: string + default: 10.0.253.247 + description: The IP of the NFS service. + port: + type: int + default: 2049 + description: The port of the NFS service. + disk: + type: string + default: nfs-disk + description: The name of the persistent disk the NFS service uses. + fstype: + type: string + default: ext4 + description: The filesystem the disk of the NFS service uses. + nginx: + type: object + properties: + replicas: + type: int + default: 2 + description: The number of replicas for the nginx service. + wordpress-php: + type: object + properties: + replicas: + type: int + default: 2 + description: The number of replicas for the wordpress-php service. + port: + type: int + default: 9000 + description: The port the wordpress-php service runs on. + mysql: + type: object + properties: + port: + type: int + default: 3306 + description: The port the MySQL service runs on. + password: + type: string + default: mysql-password + description: The root password of the MySQL service. + disk: + type: string + default: mysql-disk + description: The name of the persistent disk the MySQL service uses. + fstype: + type: string + default: ext4 + description: The filesystem the disk of the MySQL service uses. + diff --git a/pkg/repo/testdata/frobnitz/templates/wordpress.yaml b/pkg/repo/testdata/frobnitz/templates/wordpress.yaml new file mode 100644 index 0000000000000000000000000000000000000000..b401897abbc3934e2436360681b01ac028125146 --- /dev/null +++ b/pkg/repo/testdata/frobnitz/templates/wordpress.yaml @@ -0,0 +1,6 @@ +imports: +- path: wordpress.jinja + +resources: +- name: wordpress + type: wordpress.jinja