diff --git a/pkg/chartutil/chartfile.go b/pkg/chartutil/chartfile.go new file mode 100644 index 000000000..995a496a1 --- /dev/null +++ b/pkg/chartutil/chartfile.go @@ -0,0 +1,27 @@ +package chartutil + +import ( + "io/ioutil" + + "github.com/ghodss/yaml" + "github.com/kubernetes/helm/pkg/proto/hapi/chart" +) + +// UnmarshalChartfile takes raw Chart.yaml data and unmarshals it. +func UnmarshalChartfile(data []byte) (*chart.Metadata, error) { + y := &chart.Metadata{} + err := yaml.Unmarshal(data, y) + if err != nil { + return nil, err + } + return y, nil +} + +// LoadChartfile loads a Chart.yaml file into a *chart.Metadata. +func LoadChartfile(filename string) (*chart.Metadata, error) { + b, err := ioutil.ReadFile(filename) + if err != nil { + return nil, err + } + return UnmarshalChartfile(b) +} diff --git a/pkg/chartutil/chartfile_test.go b/pkg/chartutil/chartfile_test.go new file mode 100644 index 000000000..8df26b058 --- /dev/null +++ b/pkg/chartutil/chartfile_test.go @@ -0,0 +1,73 @@ +package chartutil + +import ( + "testing" + + "github.com/kubernetes/helm/pkg/proto/hapi/chart" +) + +const testfile = "testdata/chartfiletest.yaml" + +func TestLoadChartfile(t *testing.T) { + f, err := LoadChartfile(testfile) + if err != nil { + t.Errorf("Failed to open %s: %s", testfile, err) + return + } + verifyChartfile(t, f) +} + +func verifyChartfile(t *testing.T, f *chart.Metadata) { + + if f == nil { + t.Fatal("Failed verifyChartfile because f is nil") + } + + if f.Name != "frobnitz" { + t.Errorf("Expected frobnitz, got %s", f.Name) + } + + if f.Description != "This is a frobnitz." { + t.Errorf("Unexpected description %q", f.Description) + } + + if f.Version != "1.2.3" { + t.Errorf("Unexpected version %q", f.Version) + } + + if len(f.Maintainers) != 2 { + t.Errorf("Expected 2 maintainers, got %d", len(f.Maintainers)) + } + + if f.Maintainers[0].Name != "The Helm Team" { + t.Errorf("Unexpected maintainer name.") + } + + if f.Maintainers[1].Email != "nobody@example.com" { + t.Errorf("Unexpected maintainer email.") + } + + if len(f.Sources) != 1 { + t.Fatalf("Unexpected number of sources") + } + + if f.Sources[0] != "https://example.com/foo/bar" { + t.Errorf("Expected https://example.com/foo/bar, got %s", f.Sources) + } + + if f.Home != "http://example.com" { + t.Error("Unexpected home.") + } + + if len(f.Keywords) != 3 { + t.Error("Unexpected keywords") + } + + kk := []string{"frobnitz", "sprocket", "dodad"} + for i, k := range f.Keywords { + if kk[i] != k { + t.Errorf("Expected %q, got %q", kk[i], k) + } + } + +} diff --git a/pkg/chartutil/doc.go b/pkg/chartutil/doc.go new file mode 100644 index 000000000..0191c3800 --- /dev/null +++ b/pkg/chartutil/doc.go @@ -0,0 +1,28 @@ +/*Package chartutil contains tools for working with charts. + +Charts are described in the protocol buffer definition (pkg/proto/hapi/charts). +This packe provides utilities for serializing and deserializing charts. + +A chart can be represented on the file system in one of two ways: + + - As a directory that contains a Chart.yaml file and other chart things. + - As a tarred gzipped file containing a directory that then contains a + Chart.yaml file. + +This package provides utilitites for working with those file formats. + +The preferred way of loading a chart is using 'chartutil.Load`: + + chart, err := chartutil.Load(filename) + +This will attempt to discover whether the file at 'filename' is a directory or +a chart archive. It will then load accordingly. + +For accepting raw compressed tar file data from an io.Reader, the +'chartutil.LoadArchive()' will read in the data, uncompress it, and unpack it +into a Chart. + +When creating charts in memory, use the 'github.com/kubernetes/helm/pkg/proto/happy/chart' +package directly. +*/ +package chartutil diff --git a/pkg/chartutil/load.go b/pkg/chartutil/load.go new file mode 100644 index 000000000..0a8757205 --- /dev/null +++ b/pkg/chartutil/load.go @@ -0,0 +1,203 @@ +package chartutil + +import ( + "archive/tar" + "bytes" + "compress/gzip" + "errors" + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + "strings" + + "github.com/golang/protobuf/ptypes/any" + "github.com/kubernetes/helm/pkg/proto/hapi/chart" +) + +// Load takes a string name, tries to resolve it to a file or directory, and then loads it. +// +// This is the preferred way to load a chart. It will discover the chart encoding +// and hand off to the appropriate chart reader. +func Load(name string) (*chart.Chart, error) { + fi, err := os.Stat(name) + if err != nil { + return nil, err + } + if fi.IsDir() { + return LoadDir(name) + } + return LoadFile(name) +} + +// afile represents an archive file buffered for later processing. +type afile struct { + name string + data []byte +} + +// LoadArchive loads from a reader containing a compressed tar archive. +func LoadArchive(in io.Reader) (*chart.Chart, error) { + unzipped, err := gzip.NewReader(in) + if err != nil { + return &chart.Chart{}, err + } + defer unzipped.Close() + + files := []*afile{} + tr := tar.NewReader(unzipped) + for { + b := bytes.NewBuffer(nil) + hd, err := tr.Next() + if err == io.EOF { + break + } + if err != nil { + return &chart.Chart{}, err + } + + if hd.FileInfo().IsDir() { + // Use this instead of hd.Typeflag because we don't have to do any + // inference chasing. + continue + } + + parts := strings.Split(hd.Name, "/") + n := strings.Join(parts[1:], "/") + + if _, err := io.Copy(b, tr); err != nil { + return &chart.Chart{}, err + } + + files = append(files, &afile{name: n, data: b.Bytes()}) + b.Reset() + } + + if len(files) == 0 { + return nil, errors.New("no files in chart archive") + } + + return loadFiles(files) +} + +func loadFiles(files []*afile) (*chart.Chart, error) { + c := &chart.Chart{} + subcharts := map[string][]*afile{} + + for _, f := range files { + if f.name == "Chart.yaml" { + m, err := UnmarshalChartfile(f.data) + if err != nil { + return c, err + } + c.Metadata = m + } else if f.name == "values.toml" || f.name == "values.yaml" { + c.Values = &chart.Config{Raw: string(f.data)} + } else if strings.HasPrefix(f.name, "templates/") { + c.Templates = append(c.Templates, &chart.Template{Name: f.name, Data: f.data}) + } else if strings.HasPrefix(f.name, "charts/") { + cname := strings.TrimPrefix(f.name, "charts/") + parts := strings.SplitN(cname, "/", 2) + scname := parts[0] + subcharts[scname] = append(subcharts[scname], &afile{name: cname, data: f.data}) + } else { + c.Files = append(c.Files, &any.Any{TypeUrl: f.name, Value: f.data}) + } + } + + // Ensure that we got a Chart.yaml file + if c.Metadata == nil || c.Metadata.Name == "" { + return c, errors.New("chart metadata (Chart.yaml) missing") + } + + for n, files := range subcharts { + var sc *chart.Chart + var err error + if filepath.Ext(n) == ".tgz" { + file := files[0] + if file.name != n { + return c, fmt.Errorf("error unpacking tar in %s: expected %s, got %s", c.Metadata.Name, n, file.name) + } + // Untar the chart and add to c.Dependencies + b := bytes.NewBuffer(file.data) + sc, err = LoadArchive(b) + } else { + // We have to trim the prefix off of every file, and ignore any file + // that is in charts/, but isn't actually a chart. + buff := make([]*afile, 0, len(files)) + for _, f := range files { + parts := strings.SplitN(f.name, "/", 2) + if len(parts) < 2 { + continue + } + f.name = parts[1] + buff = append(buff, f) + } + sc, err = loadFiles(buff) + } + + if err != nil { + return c, fmt.Errorf("error unpacking %s in %s: %s", n, c.Metadata.Name, err) + } + + c.Dependencies = append(c.Dependencies, sc) + } + + return c, nil +} + +// LoadFile loads from an archive file. +func LoadFile(name string) (*chart.Chart, error) { + if fi, err := os.Stat(name); err != nil { + return nil, err + } else if fi.IsDir() { + return nil, errors.New("cannot load a directory") + } + + raw, err := os.Open(name) + if err != nil { + return nil, err + } + defer raw.Close() + + return LoadArchive(raw) +} + +// LoadDir loads from a directory. +// +// This loads charts only from directories. +func LoadDir(dir string) (*chart.Chart, error) { + topdir, err := filepath.Abs(dir) + if err != nil { + return nil, err + } + + // Just used for errors. + c := &chart.Chart{} + + files := []*afile{} + topdir += string(filepath.Separator) + err = filepath.Walk(topdir, func(name string, fi os.FileInfo, err error) error { + n := strings.TrimPrefix(name, topdir) + if err != nil { + return err + } + if fi.IsDir() { + return nil + } + + data, err := ioutil.ReadFile(name) + if err != nil { + return fmt.Errorf("error reading %s: %s", n, err) + } + + files = append(files, &afile{name: n, data: data}) + return nil + }) + if err != nil { + return c, err + } + + return loadFiles(files) +} diff --git a/pkg/chartutil/load_test.go b/pkg/chartutil/load_test.go new file mode 100644 index 000000000..2ed052c69 --- /dev/null +++ b/pkg/chartutil/load_test.go @@ -0,0 +1,88 @@ +package chartutil + +import ( + "testing" + + "github.com/kubernetes/helm/pkg/proto/hapi/chart" +) + +func TestLoadDir(t *testing.T) { + c, err := Load("testdata/frobnitz") + if err != nil { + t.Fatalf("Failed to load testdata: %s", err) + } + verifyFrobnitz(t, c) + verifyChart(t, c) +} + +func TestLoadFile(t *testing.T) { + c, err := Load("testdata/frobnitz-1.2.3.tgz") + if err != nil { + t.Fatalf("Failed to load testdata: %s", err) + } + verifyFrobnitz(t, c) + verifyChart(t, c) +} + +func verifyChart(t *testing.T, c *chart.Chart) { + if c.Metadata.Name == "" { + t.Fatalf("No chart metadata found on %v", c) + } + t.Logf("Verifying chart %s", c.Metadata.Name) + if len(c.Templates) != 1 { + t.Errorf("Expected 1 template, got %d", len(c.Templates)) + } + + if len(c.Files) != 5 { + t.Errorf("Expected 5 extra files, got %d", len(c.Files)) + for _, n := range c.Files { + t.Logf("\t%s", n.TypeUrl) + } + } + + if len(c.Dependencies) != 2 { + t.Errorf("Expected 2 dependencies, got %d (%v)", len(c.Dependencies), c.Dependencies) + for _, d := range c.Dependencies { + t.Logf("\tSubchart: %s\n", d.Metadata.Name) + } + } + + expect := map[string]map[string]string{ + "alpine": { + "version": "0.1.0", + }, + "mariner": { + "version": "4.3.2", + }, + } + + for _, dep := range c.Dependencies { + if dep.Metadata == nil { + t.Fatalf("expected metadata on dependency: %v", dep) + } + exp, ok := expect[dep.Metadata.Name] + if !ok { + t.Fatalf("Unknown dependency %s", dep.Metadata.Name) + } + if exp["version"] != dep.Metadata.Version { + t.Errorf("Expected %s version %s, got %s", dep.Metadata.Name, exp["version"], dep.Metadata.Version) + } + } + +} + +func verifyFrobnitz(t *testing.T, c *chart.Chart) { + verifyChartfile(t, c.Metadata) + + if len(c.Templates) != 1 { + t.Fatalf("Expected 1 template, got %d", len(c.Templates)) + } + + if c.Templates[0].Name != "templates/template.tpl" { + t.Errorf("Unexpected template: %s", c.Templates[0].Name) + } + + if len(c.Templates[0].Data) == 0 { + t.Error("No template data.") + } +} diff --git a/pkg/chartutil/testdata/albatross/Chart.yaml b/pkg/chartutil/testdata/albatross/Chart.yaml new file mode 100644 index 000000000..eeef737ff --- /dev/null +++ b/pkg/chartutil/testdata/albatross/Chart.yaml @@ -0,0 +1,4 @@ +name: albatross +description: A Helm chart for Kubernetes +version: 0.1.0 +home: "" diff --git a/pkg/chartutil/testdata/albatross/values.toml b/pkg/chartutil/testdata/albatross/values.toml new file mode 100644 index 000000000..0ef7eb2f9 --- /dev/null +++ b/pkg/chartutil/testdata/albatross/values.toml @@ -0,0 +1 @@ +albatross = "true" diff --git a/pkg/chartutil/testdata/chartfiletest.yaml b/pkg/chartutil/testdata/chartfiletest.yaml new file mode 100644 index 000000000..9f255b9bd --- /dev/null +++ b/pkg/chartutil/testdata/chartfiletest.yaml @@ -0,0 +1,15 @@ +name: frobnitz +description: This is a frobnitz. +version: "1.2.3" +keywords: + - frobnitz + - sprocket + - dodad +maintainers: + - name: The Helm Team + email: helm@example.com + - name: Someone Else + email: nobody@example.com +sources: + - https://example.com/foo/bar +home: http://example.com diff --git a/pkg/chartutil/testdata/frobnitz-1.2.3.tgz b/pkg/chartutil/testdata/frobnitz-1.2.3.tgz new file mode 100644 index 000000000..c908b2264 Binary files /dev/null and b/pkg/chartutil/testdata/frobnitz-1.2.3.tgz differ diff --git a/pkg/chartutil/testdata/frobnitz/Chart.yaml b/pkg/chartutil/testdata/frobnitz/Chart.yaml new file mode 100644 index 000000000..9f255b9bd --- /dev/null +++ b/pkg/chartutil/testdata/frobnitz/Chart.yaml @@ -0,0 +1,15 @@ +name: frobnitz +description: This is a frobnitz. +version: "1.2.3" +keywords: + - frobnitz + - sprocket + - dodad +maintainers: + - name: The Helm Team + email: helm@example.com + - name: Someone Else + email: nobody@example.com +sources: + - https://example.com/foo/bar +home: http://example.com diff --git a/pkg/chartutil/testdata/frobnitz/INSTALL.txt b/pkg/chartutil/testdata/frobnitz/INSTALL.txt new file mode 100644 index 000000000..2010438c2 --- /dev/null +++ b/pkg/chartutil/testdata/frobnitz/INSTALL.txt @@ -0,0 +1 @@ +This is an install document. The client may display this. diff --git a/pkg/chartutil/testdata/frobnitz/LICENSE b/pkg/chartutil/testdata/frobnitz/LICENSE new file mode 100644 index 000000000..6121943b1 --- /dev/null +++ b/pkg/chartutil/testdata/frobnitz/LICENSE @@ -0,0 +1 @@ +LICENSE placeholder. diff --git a/pkg/chartutil/testdata/frobnitz/README.md b/pkg/chartutil/testdata/frobnitz/README.md new file mode 100644 index 000000000..8cf4cc3d7 --- /dev/null +++ b/pkg/chartutil/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/chartutil/testdata/frobnitz/charts/alpine/Chart.yaml b/pkg/chartutil/testdata/frobnitz/charts/alpine/Chart.yaml new file mode 100644 index 000000000..cab858d0a --- /dev/null +++ b/pkg/chartutil/testdata/frobnitz/charts/alpine/Chart.yaml @@ -0,0 +1,4 @@ +name: alpine +description: Deploy a basic Alpine Linux pod +version: 0.1.0 +home: https://github.com/kubernetes/helm diff --git a/pkg/chartutil/testdata/frobnitz/charts/alpine/README.md b/pkg/chartutil/testdata/frobnitz/charts/alpine/README.md new file mode 100644 index 000000000..a7c84fc41 --- /dev/null +++ b/pkg/chartutil/testdata/frobnitz/charts/alpine/README.md @@ -0,0 +1,9 @@ +This example was generated using the command `helm create alpine`. + +The `templates/` directory contains a very simple pod resource with a +couple of parameters. + +The `values.toml` file contains the default values for the +`alpine-pod.yaml` template. + +You can install this example using `helm install docs/examples/alpine`. diff --git a/pkg/chartutil/testdata/frobnitz/charts/alpine/charts/mast1/Chart.yaml b/pkg/chartutil/testdata/frobnitz/charts/alpine/charts/mast1/Chart.yaml new file mode 100644 index 000000000..171e36156 --- /dev/null +++ b/pkg/chartutil/testdata/frobnitz/charts/alpine/charts/mast1/Chart.yaml @@ -0,0 +1,4 @@ +name: mast1 +description: A Helm chart for Kubernetes +version: 0.1.0 +home: "" diff --git a/pkg/chartutil/testdata/frobnitz/charts/alpine/charts/mast1/values.toml b/pkg/chartutil/testdata/frobnitz/charts/alpine/charts/mast1/values.toml new file mode 100644 index 000000000..f0cab9e08 --- /dev/null +++ b/pkg/chartutil/testdata/frobnitz/charts/alpine/charts/mast1/values.toml @@ -0,0 +1,4 @@ +# Default values for mast1. +# This is a TOML-formatted file. https://github.com/toml-lang/toml +# Declare name/value pairs to be passed into your templates. +# name = "value" diff --git a/pkg/chartutil/testdata/frobnitz/charts/alpine/charts/mast2-0.1.0.tgz b/pkg/chartutil/testdata/frobnitz/charts/alpine/charts/mast2-0.1.0.tgz new file mode 100644 index 000000000..232322a26 Binary files /dev/null and b/pkg/chartutil/testdata/frobnitz/charts/alpine/charts/mast2-0.1.0.tgz differ diff --git a/pkg/chartutil/testdata/frobnitz/charts/alpine/templates/alpine-pod.yaml b/pkg/chartutil/testdata/frobnitz/charts/alpine/templates/alpine-pod.yaml new file mode 100644 index 000000000..08cf3c2c1 --- /dev/null +++ b/pkg/chartutil/testdata/frobnitz/charts/alpine/templates/alpine-pod.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Pod +metadata: + name: {{.Release.Name}}-{{.Chart.Name}} + labels: + heritage: {{.Release.Service}} + chartName: {{.Chart.Name}} + chartVersion: {{.Chart.Version | quote}} + annotations: + "helm.sh/created": "{{.Release.Time.Seconds}}" +spec: + restartPolicy: {{default "Never" .restart_policy}} + containers: + - name: waiter + image: "alpine:3.3" + command: ["/bin/sleep","9000"] diff --git a/pkg/chartutil/testdata/frobnitz/charts/alpine/values.toml b/pkg/chartutil/testdata/frobnitz/charts/alpine/values.toml new file mode 100644 index 000000000..504e6e1be --- /dev/null +++ b/pkg/chartutil/testdata/frobnitz/charts/alpine/values.toml @@ -0,0 +1,2 @@ +# The pod name +name = "my-alpine" diff --git a/pkg/chartutil/testdata/frobnitz/charts/mariner-4.3.2.tgz b/pkg/chartutil/testdata/frobnitz/charts/mariner-4.3.2.tgz new file mode 100644 index 000000000..d4f19e624 Binary files /dev/null and b/pkg/chartutil/testdata/frobnitz/charts/mariner-4.3.2.tgz differ diff --git a/pkg/chartutil/testdata/frobnitz/docs/README.md b/pkg/chartutil/testdata/frobnitz/docs/README.md new file mode 100644 index 000000000..d40747caf --- /dev/null +++ b/pkg/chartutil/testdata/frobnitz/docs/README.md @@ -0,0 +1 @@ +This is a placeholder for documentation. diff --git a/pkg/chartutil/testdata/frobnitz/icon.svg b/pkg/chartutil/testdata/frobnitz/icon.svg new file mode 100644 index 000000000..892130606 --- /dev/null +++ b/pkg/chartutil/testdata/frobnitz/icon.svg @@ -0,0 +1,8 @@ + + + Example icon + + + diff --git a/pkg/chartutil/testdata/frobnitz/templates/template.tpl b/pkg/chartutil/testdata/frobnitz/templates/template.tpl new file mode 100644 index 000000000..c651ee6a0 --- /dev/null +++ b/pkg/chartutil/testdata/frobnitz/templates/template.tpl @@ -0,0 +1 @@ +Hello {{.Name | default "world"}} diff --git a/pkg/chartutil/testdata/frobnitz/values.toml b/pkg/chartutil/testdata/frobnitz/values.toml new file mode 100644 index 000000000..6fc24051f --- /dev/null +++ b/pkg/chartutil/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/chartutil/testdata/genfrob.sh b/pkg/chartutil/testdata/genfrob.sh new file mode 100755 index 000000000..38fc1b22c --- /dev/null +++ b/pkg/chartutil/testdata/genfrob.sh @@ -0,0 +1,12 @@ +#!/bin/sh + +# Pack the albatross chart into the mariner chart. +echo "Packing albatross into mariner" +tar -zcvf mariner/charts/albatross-0.1.0.tgz albatross + +echo "Packing mariner into frobnitz" +tar -zcvf frobnitz/charts/mariner-4.3.2.tgz mariner + +# Pack the frobnitz chart. +echo "Packing frobnitz" +tar -zcvf frobnitz-1.2.3.tgz frobnitz diff --git a/pkg/chartutil/testdata/mariner/Chart.yaml b/pkg/chartutil/testdata/mariner/Chart.yaml new file mode 100644 index 000000000..4d52794c6 --- /dev/null +++ b/pkg/chartutil/testdata/mariner/Chart.yaml @@ -0,0 +1,4 @@ +name: mariner +description: A Helm chart for Kubernetes +version: 4.3.2 +home: "" diff --git a/pkg/chartutil/testdata/mariner/charts/albatross-0.1.0.tgz b/pkg/chartutil/testdata/mariner/charts/albatross-0.1.0.tgz new file mode 100644 index 000000000..ed3c1aee9 Binary files /dev/null and b/pkg/chartutil/testdata/mariner/charts/albatross-0.1.0.tgz differ diff --git a/pkg/chartutil/testdata/mariner/templates/placeholder.tpl b/pkg/chartutil/testdata/mariner/templates/placeholder.tpl new file mode 100644 index 000000000..29c11843a --- /dev/null +++ b/pkg/chartutil/testdata/mariner/templates/placeholder.tpl @@ -0,0 +1 @@ +# This is a placeholder. diff --git a/pkg/chartutil/testdata/mariner/values.toml b/pkg/chartutil/testdata/mariner/values.toml new file mode 100644 index 000000000..4a7bbf8e4 --- /dev/null +++ b/pkg/chartutil/testdata/mariner/values.toml @@ -0,0 +1,4 @@ +# Default values for mariner. +# This is a TOML-formatted file. https://github.com/toml-lang/toml +# Declare name/value pairs to be passed into your templates. +# name = "value"