diff --git a/glide.lock b/glide.lock index 0aaa8649f..795c132ac 100644 --- a/glide.lock +++ b/glide.lock @@ -1,14 +1,16 @@ -hash: 1dcbbc192182125021b40497fcf9d52bc643455a3bb03d6cd3458819fcb03dbb -updated: 2016-04-12T13:27:50.987288211-06:00 +hash: 7f9a27ad54a10edaa7c57521246676477d0f84ef4246524bae75f8df9d049983 +updated: 2016-04-14T12:24:49.130995956-06:00 imports: - name: github.com/aokoli/goutils version: 9c37978a95bd5c709a15883b6242714ea6709e64 - name: github.com/codegangsta/cli version: 71f57d300dd6a780ac1856c005c4b518cfd498ec - name: github.com/golang/protobuf - version: dda510ac0fd43b39770f22ac6260eb91d377bce3 + version: f0a097ddac24fb00e07d2ac17f8671423f3ea47c subpackages: - proto +- name: github.com/Masterminds/semver + version: 808ed7761c233af2de3f9729a041d68c62527f3a - name: github.com/Masterminds/sprig version: 679bb747f11c6ffc3373965988fea8877c40b47b - name: github.com/spf13/cobra @@ -16,9 +18,9 @@ imports: subpackages: - cobra - name: github.com/spf13/pflag - version: 1f296710f879815ad9e6d39d947c828c3e4b4c3d + version: 8f6a28b0916586e7f22fe931ae2fcfc380b1c0e6 - name: golang.org/x/net - version: 589fda73dd0faec3dc59e7d7dab5b069e3fce0f9 + version: fb93926129b8ec0056f2f458b1f519654814edf0 subpackages: - context - http2 @@ -26,7 +28,7 @@ imports: - http2/hpack - internal/timeseries - name: google.golang.org/grpc - version: d07d0562ffca36dd7ee333b5d236209f98fe9ba0 + version: 9ac074585f926c8506b6351bfdc396d2b19b1cb1 subpackages: - codes - credentials @@ -36,4 +38,6 @@ imports: - naming - transport - peer +- name: gopkg.in/yaml.v2 + version: a83829b6f1293c91addabc89d0571c246397bbf4 devImports: [] diff --git a/glide.yaml b/glide.yaml index dab2d29c8..a47f5a981 100644 --- a/glide.yaml +++ b/glide.yaml @@ -8,3 +8,7 @@ import: subpackages: - cobra - package: github.com/Masterminds/sprig + version: ^2.1 +- package: gopkg.in/yaml.v2 +- package: github.com/Masterminds/semver + version: 1.1.0 diff --git a/pkg/chart/chart.go b/pkg/chart/chart.go new file mode 100644 index 000000000..6edad5bc8 --- /dev/null +++ b/pkg/chart/chart.go @@ -0,0 +1,444 @@ +/* +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 chart + +import ( + "archive/tar" + "bytes" + "compress/gzip" + "errors" + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + "strings" +) + +// ChartfileName is the default Chart file name. +const ChartfileName string = "Chart.yaml" + +const ( + preTemplates string = "templates/" + preHooks string = "hooks/" + preDocs string = "docs/" + preIcon string = "icon.svg" +) + +var headerBytes = []byte("+aHR0cHM6Ly95b3V0dS5iZS96OVV6MWljandyTQo=") + +// Chart represents a complete chart. +// +// A chart consists of the following parts: +// +// - Chart.yaml: In code, we refer to this as the Chartfile +// - templates/*: The template directory +// - README.md: Optional README file +// - LICENSE: Optional license file +// - hooks/: Optional hooks registry +// - docs/: Optional docs directory +// +// Packed charts are stored in gzipped tar archives (.tgz). Unpackaged charts +// are directories where the directory name is the Chartfile.Name. +// +// Optionally, a chart might also locate a provenance (.prov) file that it +// can use for cryptographic signing. +type Chart struct { + loader chartLoader +} + +// Close the chart. +// +// Charts should always be closed when no longer needed. +func (c *Chart) Close() error { + return c.loader.close() +} + +// Chartfile gets the Chartfile (Chart.yaml) for this chart. +func (c *Chart) Chartfile() *Chartfile { + return c.loader.chartfile() +} + +// Dir returns the directory where the charts are located. +func (c *Chart) Dir() string { + return c.loader.dir() +} + +// DocsDir returns the directory where the chart's documentation is stored. +func (c *Chart) DocsDir() string { + return filepath.Join(c.loader.dir(), preDocs) +} + +// HooksDir returns the directory where the hooks are stored. +func (c *Chart) HooksDir() string { + return filepath.Join(c.loader.dir(), preHooks) +} + +// TemplatesDir returns the directory where the templates are stored. +func (c *Chart) TemplatesDir() string { + return filepath.Join(c.loader.dir(), preTemplates) +} + +// Icon returns the path to the icon.svg file. +// +// If an icon is not found in the chart, this will return an error. +func (c *Chart) Icon() (string, error) { + i := filepath.Join(c.Dir(), preIcon) + _, err := os.Stat(i) + return i, err +} + +// chartLoader provides load, close, and save implementations for a chart. +type chartLoader interface { + // Chartfile resturns a *Chartfile for this chart. + chartfile() *Chartfile + // Dir returns a directory where the chart can be accessed. + dir() string + + // Close cleans up a chart. + close() error +} + +type dirChart struct { + chartyaml *Chartfile + chartdir string +} + +func (d *dirChart) chartfile() *Chartfile { + return d.chartyaml +} + +func (d *dirChart) dir() string { + return d.chartdir +} + +func (d *dirChart) close() error { + return nil +} + +type tarChart struct { + chartyaml *Chartfile + tmpDir string +} + +func (t *tarChart) chartfile() *Chartfile { + return t.chartyaml +} + +func (t *tarChart) dir() string { + return t.tmpDir +} + +func (t *tarChart) close() error { + // Remove the temp directory. + return os.RemoveAll(t.tmpDir) +} + +// Create creates a new chart in a directory. +// +// Inside of dir, this will create a directory based on the name of +// chartfile.Name. It will then write the Chart.yaml into this directory and +// create the (empty) appropriate directories. +// +// The returned *Chart will point to the newly created directory. +// +// If dir does not exist, this will return an error. +// If Chart.yaml or any directories cannot be created, this will return an +// error. In such a case, this will attempt to clean up by removing the +// new chart directory. +func Create(chartfile *Chartfile, dir string) (*Chart, error) { + path, err := filepath.Abs(dir) + if err != nil { + return nil, err + } + + if fi, err := os.Stat(path); err != nil { + return nil, err + } else if !fi.IsDir() { + return nil, fmt.Errorf("no such directory %s", path) + } + + n := fname(chartfile.Name) + cdir := filepath.Join(path, n) + if _, err := os.Stat(cdir); err == nil { + return nil, fmt.Errorf("directory already exists: %s", cdir) + } + if err := os.MkdirAll(cdir, 0755); err != nil { + return nil, err + } + + rollback := func() { + // TODO: Should we log failures here? + os.RemoveAll(cdir) + } + + if err := chartfile.Save(filepath.Join(cdir, ChartfileName)); err != nil { + rollback() + return nil, err + } + + for _, d := range []string{preHooks, preDocs, preTemplates} { + if err := os.MkdirAll(filepath.Join(cdir, d), 0755); err != nil { + rollback() + return nil, err + } + } + + return &Chart{ + loader: &dirChart{chartyaml: chartfile, chartdir: cdir}, + }, nil +} + +// fname prepares names for the filesystem +func fname(name string) string { + // Right now, we don't do anything. Do we need to encode any particular + // characters? What characters are legal in a chart name, but not in file + // names on Windows, Linux, or OSX. + return name +} + +// LoadDir loads an entire chart from a directory. +// +// This includes the Chart.yaml (*Chartfile) and all of the manifests. +// +// If you are just reading the Chart.yaml file, it is substantially more +// performant to use LoadChartfile. +func LoadDir(chart string) (*Chart, error) { + dir, err := filepath.Abs(chart) + if err != nil { + return nil, fmt.Errorf("%s is not a valid path", chart) + } + + if fi, err := os.Stat(dir); err != nil { + return nil, err + } else if !fi.IsDir() { + return nil, fmt.Errorf("%s is not a directory", chart) + } + + cf, err := LoadChartfile(filepath.Join(dir, "Chart.yaml")) + if err != nil { + return nil, err + } + + cl := &dirChart{ + chartyaml: cf, + chartdir: dir, + } + + return &Chart{ + loader: cl, + }, nil +} + +// LoadData loads a chart from data, where data is a []byte containing a gzipped tar file. +func LoadData(data []byte) (*Chart, error) { + return LoadDataFromReader(bytes.NewBuffer(data)) +} + +// Load loads a chart from a chart archive. +// +// A chart archive is a gzipped tar archive that follows the Chart format +// specification. +func Load(archive string) (*Chart, error) { + if fi, err := os.Stat(archive); err != nil { + return nil, err + } else if fi.IsDir() { + return nil, errors.New("cannot load a directory with chart.Load()") + } + + raw, err := os.Open(archive) + if err != nil { + return nil, err + } + defer raw.Close() + + return LoadDataFromReader(raw) +} + +// LoadDataFromReader loads a chart from a reader +func LoadDataFromReader(r io.Reader) (*Chart, error) { + unzipped, err := gzip.NewReader(r) + 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 +} + +func loadTar(r *tar.Reader) (*tarChart, error) { + td, err := ioutil.TempDir("", "chart-") + if err != nil { + return nil, err + } + + // ioutil.TempDir uses Getenv("TMPDIR"), so there are no guarantees + dir, err := filepath.Abs(td) + if err != nil { + return nil, fmt.Errorf("%s is not a valid path", td) + } + + c := &tarChart{ + chartyaml: &Chartfile{}, + tmpDir: dir, + } + + firstDir := "" + + hdr, err := r.Next() + for err == nil { + // This is to prevent malformed tar attacks. + hdr.Name = filepath.Clean(hdr.Name) + + if firstDir == "" { + fi := hdr.FileInfo() + if fi.IsDir() { + firstDir = hdr.Name + } + } else if strings.HasPrefix(hdr.Name, firstDir) { + // We know this has the prefix, so we know there won't be an error. + rel, _ := filepath.Rel(firstDir, hdr.Name) + + // If tar record is a directory, create one in the tmpdir and return. + if hdr.FileInfo().IsDir() { + os.MkdirAll(filepath.Join(c.tmpDir, rel), 0755) + hdr, err = r.Next() + continue + } + + //dest := filepath.Join(c.tmpDir, rel) + f, err := os.Create(filepath.Join(c.tmpDir, rel)) + if err != nil { + hdr, err = r.Next() + continue + } + if _, err := io.Copy(f, r); err != nil { + } + f.Close() + } + hdr, err = r.Next() + } + + if err != nil && err != io.EOF { + c.close() + return c, err + } + return c, nil +} + +// Member is a file in a chart. +type Member 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() ([]*Member, error) { + dir := c.TemplatesDir() + return c.loadDirectory(dir) +} + +// loadDirectory loads the members of a directory. +func (c *Chart) loadDirectory(dir string) ([]*Member, error) { + files, err := ioutil.ReadDir(dir) + if err != nil { + return nil, err + } + + members := []*Member{} + for _, file := range files { + filename := filepath.Join(dir, file.Name()) + if !file.IsDir() { + addition, err := c.loadMember(filename) + if err != nil { + return nil, err + } + + members = append(members, addition) + } else { + additions, err := c.loadDirectory(filename) + if err != nil { + return nil, err + } + + members = append(members, additions...) + } + } + + return members, nil +} + +// LoadMember loads a chart member from a given path where path is the root of the chart. +func (c *Chart) LoadMember(path string) (*Member, 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) (*Member, error) { + dir := c.Dir() + if !strings.HasPrefix(filename, dir) { + err := fmt.Errorf("File %s is outside chart directory %s", filename, dir) + return nil, err + } + + content, err := ioutil.ReadFile(filename) + if err != nil { + return nil, err + } + + path := strings.TrimPrefix(filename, dir) + path = strings.TrimLeft(path, "/") + result := &Member{ + Path: path, + Content: content, + } + + return result, nil +} + +// Content is abstraction for the contents of a chart. +type Content struct { + Chartfile *Chartfile `json:"chartfile"` + Members []*Member `json:"members"` +} + +// LoadContent loads contents of a chart directory into Content +func (c *Chart) LoadContent() (*Content, error) { + ms, err := c.loadDirectory(c.Dir()) + if err != nil { + return nil, err + } + + cc := &Content{ + Chartfile: c.Chartfile(), + Members: ms, + } + + return cc, nil +} diff --git a/pkg/chart/chart_test.go b/pkg/chart/chart_test.go new file mode 100644 index 000000000..dd114fe67 --- /dev/null +++ b/pkg/chart/chart_test.go @@ -0,0 +1,233 @@ +/* +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 chart + +import ( + "fmt" + "io/ioutil" + "path/filepath" + "reflect" + "testing" +) + +const ( + testfile = "testdata/frobnitz/Chart.yaml" + testdir = "testdata/frobnitz/" + testarchive = "testdata/frobnitz-0.0.1.tgz" + testmember = "templates/template.tpl" +) + +// Type canaries. If these fail, they will fail at compile time. +var _ chartLoader = &dirChart{} +var _ chartLoader = &tarChart{} + +func TestLoadDir(t *testing.T) { + + c, err := LoadDir(testdir) + if err != nil { + t.Errorf("Failed to load chart: %s", err) + } + + if c.Chartfile().Name != "frobnitz" { + t.Errorf("Expected chart name to be 'frobnitz'. Got '%s'.", c.Chartfile().Name) + } +} + +func TestLoad(t *testing.T) { + c, err := Load(testarchive) + if err != nil { + t.Errorf("Failed to load chart: %s", err) + return + } + defer c.Close() + + if c.Chartfile() == nil { + t.Error("No chartfile was loaded.") + return + } + + if c.Chartfile().Name != "frobnitz" { + t.Errorf("Expected name to be frobnitz, got %q", c.Chartfile().Name) + } +} + +func TestLoadData(t *testing.T) { + data, err := ioutil.ReadFile(testarchive) + if err != nil { + t.Errorf("Failed to read testarchive file: %s", err) + return + } + c, err := LoadData(data) + if err != nil { + t.Errorf("Failed to load chart: %s", err) + return + } + if c.Chartfile() == nil { + t.Error("No chartfile was loaded.") + return + } + + if c.Chartfile().Name != "frobnitz" { + t.Errorf("Expected name to be frobnitz, got %q", c.Chartfile().Name) + } +} + +func TestChart(t *testing.T) { + c, err := LoadDir(testdir) + if err != nil { + t.Errorf("Failed to load chart: %s", err) + } + defer c.Close() + + if c.Dir() != c.loader.dir() { + t.Errorf("Unexpected location for directory: %s", c.Dir()) + } + + if c.Chartfile().Name != c.loader.chartfile().Name { + t.Errorf("Unexpected chart file name: %s", c.Chartfile().Name) + } + + dir := c.Dir() + d := c.DocsDir() + if d != filepath.Join(dir, preDocs) { + t.Errorf("Unexpectedly, docs are in %s", d) + } + + d = c.TemplatesDir() + if d != filepath.Join(dir, preTemplates) { + t.Errorf("Unexpectedly, templates are in %s", d) + } +} + +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 %d 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 []*Member) error { + for _, member := range members { + if member.Path == path { + filename := filepath.Join(root, path) + if err := compareContent(filename, 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, member.Content); err != nil { + t.Fatal(err) + } +} + +func TestLoadContent(t *testing.T) { + c, err := LoadDir(testdir) + if err != nil { + t.Errorf("Failed to load chart: %s", err) + } + + content, err := c.LoadContent() + if err != nil { + t.Errorf("Failed to load chart content: %s", err) + } + + want := c.Chartfile() + have := content.Chartfile + if !reflect.DeepEqual(want, have) { + t.Errorf("Unexpected chart file\nwant:\n%v\nhave:\n%v\n", want, have) + } + + for _, member := range content.Members { + have := member.Content + wantMember, err := c.LoadMember(member.Path) + if err != nil { + t.Errorf("Failed to load chart member: %s", err) + } + + t.Logf("%s:\n%s\n\n", member.Path, member.Content) + want := wantMember.Content + if !reflect.DeepEqual(want, have) { + t.Errorf("Unexpected chart member %s\nwant:\n%v\nhave:\n%v\n", member.Path, want, have) + } + } +} + +func compareContent(filename string, content []byte) error { + compare, err := ioutil.ReadFile(filename) + if err != nil { + return fmt.Errorf("Cannot read test file %s: %s", filename, err) + } + + if !reflect.DeepEqual(compare, content) { + return fmt.Errorf("Expected member content\n%v\ngot\n%v", compare, content) + } + + return nil +} diff --git a/pkg/chart/chartfile.go b/pkg/chart/chartfile.go new file mode 100644 index 000000000..23d74c035 --- /dev/null +++ b/pkg/chart/chartfile.go @@ -0,0 +1,65 @@ +/* +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 chart + +import ( + "io/ioutil" + + "gopkg.in/yaml.v2" +) + +// Chartfile describes a Helm Chart (e.g. Chart.yaml) +type Chartfile struct { + Name string `yaml:"name"` + Description string `yaml:"description"` + Version string `yaml:"version"` + Keywords []string `yaml:"keywords,omitempty"` + Maintainers []*Maintainer `yaml:"maintainers,omitempty"` + Source []string `yaml:"source,omitempty"` + Home string `yaml:"home"` +} + +// Maintainer describes a chart maintainer. +type Maintainer struct { + Name string `yaml:"name"` + Email string `yaml:"email,omitempty"` +} + +// LoadChartfile loads a Chart.yaml file into a *Chart. +func LoadChartfile(filename string) (*Chartfile, error) { + b, err := ioutil.ReadFile(filename) + if err != nil { + return nil, err + } + var y Chartfile + return &y, yaml.Unmarshal(b, &y) +} + +// Save saves a Chart.yaml file +func (c *Chartfile) Save(filename string) error { + b, err := c.Marshal() + if err != nil { + return err + } + + return ioutil.WriteFile(filename, b, 0644) +} + +// Marshal encodes the chart file into YAML. +func (c *Chartfile) Marshal() ([]byte, error) { + return yaml.Marshal(c) +} diff --git a/pkg/chart/chartfile_test.go b/pkg/chart/chartfile_test.go new file mode 100644 index 000000000..975871d03 --- /dev/null +++ b/pkg/chart/chartfile_test.go @@ -0,0 +1,41 @@ +/* +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 chart + +import ( + "testing" +) + +func TestLoadChartfile(t *testing.T) { + f, err := LoadChartfile(testfile) + if err != nil { + t.Errorf("Failed to open %s: %s", testfile, err) + return + } + + if f.Name != "frobnitz" { + t.Errorf("Expected frobnitz, got %s", f.Name) + } + + if len(f.Maintainers) != 2 { + t.Errorf("Expected 2 maintainers, got %d", len(f.Maintainers)) + } + + if f.Source[0] != "https://example.com/foo/bar" { + t.Errorf("Expected https://example.com/foo/bar, got %s", f.Source) + } +} diff --git a/pkg/chart/doc.go b/pkg/chart/doc.go new file mode 100644 index 000000000..ec0627506 --- /dev/null +++ b/pkg/chart/doc.go @@ -0,0 +1,23 @@ +/* +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 chart implements the Chart format. + +This package provides tools for working with the Chart format, including the +Chartfile (chart.yaml) and compressed chart archives. +*/ +package chart diff --git a/pkg/chart/save.go b/pkg/chart/save.go new file mode 100644 index 000000000..d6fc5600e --- /dev/null +++ b/pkg/chart/save.go @@ -0,0 +1,117 @@ +/* +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 chart + +import ( + "archive/tar" + "compress/gzip" + "fmt" + "io" + "os" + "path/filepath" +) + +// Save creates an archived chart to the given directory. +// +// This takes an existing chart and a destination directory. +// +// If the directory is /foo, and the chart is named bar, with version 1.0.0, this +// will generate /foo/bar-1.0.0.tgz. +// +// This returns the absolute path to the chart archive file. +func Save(c *Chart, outDir string) (string, error) { + // Create archive + if fi, err := os.Stat(outDir); err != nil { + return "", err + } else if !fi.IsDir() { + return "", fmt.Errorf("location %s is not a directory", outDir) + } + + cfile := c.Chartfile() + dir := c.Dir() + pdir := filepath.Dir(dir) + filename := fmt.Sprintf("%s-%s.tgz", fname(cfile.Name), cfile.Version) + filename = filepath.Join(outDir, filename) + + // Fail early if the YAML is borked. + if err := cfile.Save(filepath.Join(dir, ChartfileName)); err != nil { + return "", err + } + + // Create file. + f, err := os.Create(filename) + if err != nil { + return "", err + } + + // Wrap in gzip writer + zipper := gzip.NewWriter(f) + zipper.Header.Extra = headerBytes + zipper.Header.Comment = "Helm" + + // Wrap in tar writer + twriter := tar.NewWriter(zipper) + rollback := false + defer func() { + twriter.Close() + zipper.Close() + f.Close() + if rollback { + os.Remove(filename) + } + }() + + err = filepath.Walk(dir, func(path string, fi os.FileInfo, err error) error { + if err != nil { + return err + } + hdr, err := tar.FileInfoHeader(fi, ".") + if err != nil { + return err + } + + relpath, err := filepath.Rel(pdir, path) + if err != nil { + return err + } + hdr.Name = relpath + + twriter.WriteHeader(hdr) + + // Skip directories. + if fi.IsDir() { + return nil + } + + in, err := os.Open(path) + if err != nil { + return err + } + _, err = io.Copy(twriter, in) + in.Close() + if err != nil { + return err + } + + return nil + }) + if err != nil { + rollback = true + return filename, err + } + return filename, nil +} diff --git a/pkg/chart/save_test.go b/pkg/chart/save_test.go new file mode 100644 index 000000000..5722e987f --- /dev/null +++ b/pkg/chart/save_test.go @@ -0,0 +1,120 @@ +/* +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 chart + +import ( + "archive/tar" + "compress/gzip" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "sort" + "testing" +) + +const sprocketdir = "testdata/sprocket" + +func TestSave(t *testing.T) { + + tmpdir, err := ioutil.TempDir("", "helm-") + if err != nil { + t.Fatal("Could not create temp directory") + } + t.Logf("Temp: %s", tmpdir) + // Because of the defer, don't call t.Fatal in the remainder of this + // function. + defer os.RemoveAll(tmpdir) + + c, err := LoadDir(sprocketdir) + if err != nil { + t.Errorf("Failed to load %s: %s", sprocketdir, err) + return + } + + tfile, err := Save(c, tmpdir) + if err != nil { + t.Errorf("Failed to save %s to %s: %s", c.Chartfile().Name, tmpdir, err) + return + } + + b := filepath.Base(tfile) + expectname := "sprocket-1.2.3-alpha.1+12345.tgz" + if b != expectname { + t.Errorf("Expected %q, got %q", expectname, b) + } + + files, err := getAllFiles(tfile) + if err != nil { + t.Errorf("Could not extract files: %s", err) + } + + // Files should come back in order. + expect := []string{ + "sprocket", + "sprocket/Chart.yaml", + "sprocket/values.toml", + "sprocket/templates", + "sprocket/templates/template.tpl", + } + if len(expect) != len(files) { + t.Errorf("Expected %d files, found %d", len(expect), len(files)) + return + } + sort.Strings(files) + sort.Strings(expect) + for i := 0; i < len(expect); i++ { + if expect[i] != files[i] { + t.Errorf("Expected file %q, got %q", expect[i], files[i]) + } + } +} + +func getAllFiles(tfile string) ([]string, error) { + f1, err := os.Open(tfile) + if err != nil { + return []string{}, err + } + f2, err := gzip.NewReader(f1) + if err != nil { + f1.Close() + return []string{}, err + } + + if f2.Header.Comment != "Helm" { + return []string{}, fmt.Errorf("Expected header Helm. Got %s", f2.Header.Comment) + } + if string(f2.Header.Extra) != string(headerBytes) { + return []string{}, fmt.Errorf("Expected header signature. Got %v", f2.Header.Extra) + } + + f3 := tar.NewReader(f2) + + files := []string{} + var e error + var hdr *tar.Header + for e == nil { + hdr, e = f3.Next() + if e == nil { + files = append(files, hdr.Name) + } + } + + f2.Close() + f1.Close() + return files, nil +} diff --git a/pkg/chart/testdata/README.md b/pkg/chart/testdata/README.md new file mode 100644 index 000000000..a3aa71f14 --- /dev/null +++ b/pkg/chart/testdata/README.md @@ -0,0 +1 @@ +This directory houses charts used in testing. diff --git a/pkg/chart/testdata/frobnitz-0.0.1.tgz b/pkg/chart/testdata/frobnitz-0.0.1.tgz new file mode 100644 index 000000000..41f3197a5 Binary files /dev/null and b/pkg/chart/testdata/frobnitz-0.0.1.tgz differ diff --git a/pkg/chart/testdata/frobnitz/Chart.toml b/pkg/chart/testdata/frobnitz/Chart.toml new file mode 100644 index 000000000..0f4c51d57 --- /dev/null +++ b/pkg/chart/testdata/frobnitz/Chart.toml @@ -0,0 +1,15 @@ +name = "frobnitz" +description = "This is a frobniz." +version = "1.2.3-alpha.1+12345" +keywords = ["frobnitz", "sprocket", "dodad"] +home = "http://example.com" +source = [ + "https://example.com/foo/bar", + "https://github.com/example/foo" +] +[[maintainer]] + name = "The Helm Team" + email = "helm@example.com" +[[maintainer]] + name = "Someone Else" + email = "nobody@example.com" diff --git a/pkg/chart/testdata/frobnitz/Chart.yaml b/pkg/chart/testdata/frobnitz/Chart.yaml new file mode 100644 index 000000000..2fa9ac025 --- /dev/null +++ b/pkg/chart/testdata/frobnitz/Chart.yaml @@ -0,0 +1,15 @@ +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 diff --git a/pkg/chart/testdata/frobnitz/INSTALL.txt b/pkg/chart/testdata/frobnitz/INSTALL.txt new file mode 100644 index 000000000..2010438c2 --- /dev/null +++ b/pkg/chart/testdata/frobnitz/INSTALL.txt @@ -0,0 +1 @@ +This is an install document. The client may display this. diff --git a/pkg/chart/testdata/frobnitz/LICENSE b/pkg/chart/testdata/frobnitz/LICENSE new file mode 100644 index 000000000..6121943b1 --- /dev/null +++ b/pkg/chart/testdata/frobnitz/LICENSE @@ -0,0 +1 @@ +LICENSE placeholder. diff --git a/pkg/chart/testdata/frobnitz/README.md b/pkg/chart/testdata/frobnitz/README.md new file mode 100644 index 000000000..8cf4cc3d7 --- /dev/null +++ b/pkg/chart/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/chart/testdata/frobnitz/docs/README.md b/pkg/chart/testdata/frobnitz/docs/README.md new file mode 100644 index 000000000..d40747caf --- /dev/null +++ b/pkg/chart/testdata/frobnitz/docs/README.md @@ -0,0 +1 @@ +This is a placeholder for documentation. diff --git a/pkg/chart/testdata/frobnitz/icon.svg b/pkg/chart/testdata/frobnitz/icon.svg new file mode 100644 index 000000000..892130606 --- /dev/null +++ b/pkg/chart/testdata/frobnitz/icon.svg @@ -0,0 +1,8 @@ + + + Example icon + + + diff --git a/pkg/chart/testdata/frobnitz/templates/template.tpl b/pkg/chart/testdata/frobnitz/templates/template.tpl new file mode 100644 index 000000000..c651ee6a0 --- /dev/null +++ b/pkg/chart/testdata/frobnitz/templates/template.tpl @@ -0,0 +1 @@ +Hello {{.Name | default "world"}} diff --git a/pkg/chart/testdata/frobnitz/values.toml b/pkg/chart/testdata/frobnitz/values.toml new file mode 100644 index 000000000..6fc24051f --- /dev/null +++ b/pkg/chart/testdata/frobnitz/values.toml @@ -0,0 +1,6 @@ +# A values file contains configuration. + +name = "Some Name" + +[section] +name = "Name in a section" diff --git a/pkg/chart/testdata/sprocket/Chart.yaml b/pkg/chart/testdata/sprocket/Chart.yaml new file mode 100644 index 000000000..1a0b759b5 --- /dev/null +++ b/pkg/chart/testdata/sprocket/Chart.yaml @@ -0,0 +1,15 @@ +name: sprocket +description: This is a sprocket" +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 diff --git a/pkg/chart/testdata/sprocket/templates/template.tpl b/pkg/chart/testdata/sprocket/templates/template.tpl new file mode 100644 index 000000000..c651ee6a0 --- /dev/null +++ b/pkg/chart/testdata/sprocket/templates/template.tpl @@ -0,0 +1 @@ +Hello {{.Name | default "world"}} diff --git a/pkg/chart/testdata/sprocket/values.toml b/pkg/chart/testdata/sprocket/values.toml new file mode 100644 index 000000000..6fc24051f --- /dev/null +++ b/pkg/chart/testdata/sprocket/values.toml @@ -0,0 +1,6 @@ +# A values file contains configuration. + +name = "Some Name" + +[section] +name = "Name in a section"