diff --git a/pkg/chartutil/save.go b/pkg/chartutil/save.go new file mode 100644 index 000000000..11a2b190e --- /dev/null +++ b/pkg/chartutil/save.go @@ -0,0 +1,133 @@ +package chartutil + +import ( + "archive/tar" + "compress/gzip" + "errors" + "fmt" + "os" + "path/filepath" + + "github.com/ghodss/yaml" + "github.com/kubernetes/helm/pkg/proto/hapi/chart" +) + +var headerBytes = []byte("+aHR0cHM6Ly95b3V0dS5iZS96OVV6MWljandyTQo=") + +// 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.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) + } + + if c.Metadata == nil { + return "", errors.New("no Chart.yaml data") + } + + cfile := c.Metadata + if cfile.Name == "" { + return "", errors.New("no chart name specified (Chart.yaml)") + } else if cfile.Version == "" { + return "", errors.New("no chart version specified (Chart.yaml)") + } + + filename := fmt.Sprintf("%s-%s.tgz", cfile.Name, cfile.Version) + filename = filepath.Join(outDir, filename) + 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) + } + }() + + if err := writeTarContents(twriter, c, ""); err != nil { + rollback = true + } + return filename, err +} + +func writeTarContents(out *tar.Writer, c *chart.Chart, prefix string) error { + base := filepath.Join(prefix, c.Metadata.Name) + + // Save Chart.yaml + cdata, err := yaml.Marshal(c.Metadata) + if err != nil { + return err + } + if err := writeToTar(out, base+"/Chart.yaml", cdata); err != nil { + return err + } + + // Save values.yaml + if c.Values != nil && len(c.Values.Raw) > 0 { + if err := writeToTar(out, base+"/values.yaml", []byte(c.Values.Raw)); err != nil { + return err + } + } + + // Save templates + for _, f := range c.Templates { + n := filepath.Join(base, f.Name) + if err := writeToTar(out, n, f.Data); err != nil { + return err + } + } + + // Save files + for _, f := range c.Files { + n := filepath.Join(base, f.TypeUrl) + if err := writeToTar(out, n, f.Value); err != nil { + return err + } + } + + // Save dependencies + for _, dep := range c.Dependencies { + if err := writeTarContents(out, dep, base+"/charts"); err != nil { + return err + } + } + return nil +} + +// writeToTar writes a single file to a tar archive. +func writeToTar(out *tar.Writer, name string, body []byte) error { + // TODO: Do we need to create dummy parent directory names if none exist? + h := &tar.Header{ + Name: name, + Mode: 0755, + Size: int64(len(body)), + } + if err := out.WriteHeader(h); err != nil { + return err + } + if _, err := out.Write(body); err != nil { + return err + } + return nil +} diff --git a/pkg/chartutil/save_test.go b/pkg/chartutil/save_test.go new file mode 100644 index 000000000..451d15583 --- /dev/null +++ b/pkg/chartutil/save_test.go @@ -0,0 +1,51 @@ +package chartutil + +import ( + "io/ioutil" + "os" + "strings" + "testing" + + "github.com/kubernetes/helm/pkg/proto/hapi/chart" +) + +func TestSave(t *testing.T) { + tmp, err := ioutil.TempDir("", "helm-") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmp) + + c := &chart.Chart{ + Metadata: &chart.Metadata{ + Name: "ahab", + Version: "1.2.3.4", + }, + Values: &chart.Config{ + Raw: "ship: Pequod", + }, + } + + where, err := Save(c, tmp) + if err != nil { + t.Fatalf("Failed to save: %s", err) + } + if !strings.HasPrefix(where, tmp) { + t.Fatalf("Expected %q to start with %q", where, tmp) + } + if !strings.HasSuffix(where, ".tgz") { + t.Fatalf("Expected %q to end with .tgz", where) + } + + c2, err := LoadFile(where) + if err != nil { + t.Fatal(err) + } + + if c2.Metadata.Name != c.Metadata.Name { + t.Fatalf("Expected chart archive to have %q, got %q", c.Metadata.Name, c2.Metadata.Name) + } + if c2.Values.Raw != c.Values.Raw { + t.Fatal("Values data did not match") + } +}