Merge pull request #788 from technosophos/feat/chartutil

feat(chartutil): add chartutil package
pull/789/head
Matt Butcher 8 years ago
commit 4bb596d187

@ -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)
}

@ -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)
}
}
}

@ -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

@ -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)
}

@ -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.")
}
}

@ -0,0 +1,4 @@
name: albatross
description: A Helm chart for Kubernetes
version: 0.1.0
home: ""

@ -0,0 +1 @@
albatross = "true"

@ -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

Binary file not shown.

@ -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

@ -0,0 +1 @@
This is an install document. The client may display this.

@ -0,0 +1 @@
LICENSE placeholder.

@ -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.

@ -0,0 +1,4 @@
name: alpine
description: Deploy a basic Alpine Linux pod
version: 0.1.0
home: https://github.com/kubernetes/helm

@ -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`.

@ -0,0 +1,4 @@
name: mast1
description: A Helm chart for Kubernetes
version: 0.1.0
home: ""

@ -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"

@ -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"]

@ -0,0 +1,2 @@
# The pod name
name = "my-alpine"

@ -0,0 +1 @@
This is a placeholder for documentation.

@ -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>

After

Width:  |  Height:  |  Size: 374 B

@ -0,0 +1 @@
Hello {{.Name | default "world"}}

@ -0,0 +1,6 @@
# A values file contains configuration.
name = "Some Name"
[section]
name = "Name in a section"

@ -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

@ -0,0 +1,4 @@
name: mariner
description: A Helm chart for Kubernetes
version: 4.3.2
home: ""

@ -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"
Loading…
Cancel
Save