mirror of https://github.com/helm/helm
parent
87429c66e9
commit
25053e6ada
@ -0,0 +1,26 @@
|
||||
package chartutil
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/ghodss/yaml"
|
||||
"github.com/kubernetes/helm/pkg/proto/hapi/chart"
|
||||
)
|
||||
|
||||
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,69 @@
|
||||
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.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,274 @@
|
||||
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)
|
||||
}
|
||||
|
||||
// subchart is an intermediate representation of a dependency.
|
||||
//
|
||||
// It is used to temporarily store a dependency while we process the outer
|
||||
// file.
|
||||
type subchart []*afile
|
||||
|
||||
func newSubchart() subchart {
|
||||
return []*afile{}
|
||||
}
|
||||
|
||||
func (s subchart) add(name string, data []byte, arch bool) subchart {
|
||||
s = append(s, &afile{name, data, arch})
|
||||
return s
|
||||
}
|
||||
|
||||
// afile represents an archive file buffered for later processing.
|
||||
type afile struct {
|
||||
name string
|
||||
data []byte
|
||||
archive bool
|
||||
}
|
||||
|
||||
// LoadArchive loads from a reader containing a compressed tar archive.
|
||||
func LoadArchive(in io.Reader) (*chart.Chart, error) {
|
||||
sc := map[string]subchart{}
|
||||
unzipped, err := gzip.NewReader(in)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer unzipped.Close()
|
||||
|
||||
c := &chart.Chart{}
|
||||
b := bytes.NewBuffer(nil)
|
||||
|
||||
tr := tar.NewReader(unzipped)
|
||||
for {
|
||||
hd, err := tr.Next()
|
||||
if err == io.EOF {
|
||||
// We're done with the reader. Now add subcharts and exit.
|
||||
e := addSubcharts(c, sc)
|
||||
return c, e
|
||||
}
|
||||
if err != nil {
|
||||
return c, 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 c, err
|
||||
}
|
||||
|
||||
if strings.HasPrefix(n, "charts/") {
|
||||
// If there are subcharts, we put those into a temporary holding
|
||||
// array for later processing.
|
||||
fmt.Printf("Appending %s to chart %s:\n%s\n", n, c.Metadata.Name, b.String())
|
||||
appendSubchart(sc, n, b.Bytes())
|
||||
b.Reset()
|
||||
continue
|
||||
}
|
||||
|
||||
addToChart(c, n, b.Bytes())
|
||||
b.Reset()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
topdir += string(filepath.Separator)
|
||||
sc := map[string]subchart{}
|
||||
c := &chart.Chart{}
|
||||
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)
|
||||
}
|
||||
|
||||
if strings.HasPrefix(n, "charts/") {
|
||||
appendSubchart(sc, n, data)
|
||||
return nil
|
||||
}
|
||||
|
||||
return addToChart(c, n, data)
|
||||
})
|
||||
if err != nil {
|
||||
return c, err
|
||||
}
|
||||
|
||||
// Ensure that we had a Chart.yaml file
|
||||
if c.Metadata == nil || c.Metadata.Name == "" {
|
||||
return c, errors.New("chart metadata (Chart.yaml) missing")
|
||||
}
|
||||
|
||||
err = addSubcharts(c, sc)
|
||||
return c, err
|
||||
}
|
||||
|
||||
func addToChart(c *chart.Chart, n string, data []byte) error {
|
||||
fmt.Printf("--> Scanning %s\n", n)
|
||||
if n == "Chart.yaml" {
|
||||
md, err := UnmarshalChartfile(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if md.Name == "" {
|
||||
fmt.Printf("Chart:\n%s\n", string(data))
|
||||
}
|
||||
fmt.Printf("--> Adding %s as Chart.yaml\n", md.Name)
|
||||
c.Metadata = md
|
||||
} else if n == "values.toml" {
|
||||
c.Values = &chart.Config{Raw: string(data)}
|
||||
fmt.Printf("--> Adding to values:\n%s\n", string(data))
|
||||
} else if strings.HasPrefix(n, "charts/") {
|
||||
// SKIP THESE. These are handled elsewhere, because they need context
|
||||
// to process.
|
||||
return nil
|
||||
} else if strings.HasPrefix(n, "templates/") {
|
||||
c.Templates = append(c.Templates, &chart.Template{Name: n, Data: data})
|
||||
} else {
|
||||
c.Files = append(c.Files, &any.Any{TypeUrl: n, Value: data})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func addSubcharts(c *chart.Chart, s map[string]subchart) error {
|
||||
for n, sc := range s {
|
||||
fmt.Printf("===> Unpacking %s\n", n)
|
||||
if err := addSubchart(c, sc); err != nil {
|
||||
return fmt.Errorf("error adding %q: %s", n, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// addSubchart transforms a subchart to a new chart, and then embeds it into the given chart.
|
||||
func addSubchart(c *chart.Chart, sc subchart) error {
|
||||
nc := &chart.Chart{}
|
||||
deps := map[string]subchart{}
|
||||
|
||||
// The sc paths are all relative to the sc itself.
|
||||
for _, sub := range sc {
|
||||
if sub.archive {
|
||||
b := bytes.NewBuffer(sub.data)
|
||||
var err error
|
||||
nc, err = LoadArchive(b)
|
||||
if err != nil {
|
||||
fmt.Printf("Bad data in %s: %q", sub.name, string(sub.data))
|
||||
return err
|
||||
}
|
||||
break
|
||||
} else if strings.HasPrefix(sub.name, "charts/") {
|
||||
appendSubchart(deps, sub.name, sub.data)
|
||||
} else {
|
||||
fmt.Printf("Adding %s to subchart in %s\n", sub.name, c.Metadata.Name)
|
||||
addToChart(nc, sub.name, sub.data)
|
||||
}
|
||||
}
|
||||
|
||||
if nc.Metadata == nil || nc.Metadata.Name == "" {
|
||||
return errors.New("embedded chart is not well-formed")
|
||||
}
|
||||
|
||||
fmt.Printf("Added dependency: %q\n", nc.Metadata.Name)
|
||||
c.Dependencies = append(c.Dependencies, nc)
|
||||
return nil
|
||||
}
|
||||
|
||||
func appendSubchart(sc map[string]subchart, n string, b []byte) {
|
||||
fmt.Printf("Append subchart %s\n", n)
|
||||
// TODO: Do we need to filter out 0 byte files?
|
||||
// TODO: If this finds a dependency that is a tarball, we need to untar it,
|
||||
// and express it as a subchart.
|
||||
parts := strings.SplitN(n, "/", 3)
|
||||
lp := len(parts)
|
||||
switch lp {
|
||||
case 2:
|
||||
if filepath.Ext(parts[1]) == ".tgz" {
|
||||
fmt.Printf("--> Adding archive %s\n", n)
|
||||
// Basically, we delay expanding tar files until the last minute,
|
||||
// which helps (a little) keep memory usage down.
|
||||
bn := strings.TrimSuffix(parts[1], ".tgz")
|
||||
cc := newSubchart()
|
||||
sc[bn] = cc.add(parts[1], b, true)
|
||||
return
|
||||
} else {
|
||||
// Skip directory entries and non-charts.
|
||||
return
|
||||
}
|
||||
case 3:
|
||||
if _, ok := sc[parts[1]]; !ok {
|
||||
sc[parts[1]] = newSubchart()
|
||||
}
|
||||
//fmt.Printf("Adding file %q to %s\n", parts[2], parts[1])
|
||||
sc[parts[1]] = sc[parts[1]].add(parts[2], b, false)
|
||||
return
|
||||
default:
|
||||
// Skip 1 or 0.
|
||||
return
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,81 @@
|
||||
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 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": map[string]string{
|
||||
"version": "0.1.0",
|
||||
},
|
||||
"mariner": map[string]string{
|
||||
"version": "4.3.2",
|
||||
},
|
||||
}
|
||||
|
||||
for _, dep := range c.Dependencies {
|
||||
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"
|
Binary file not shown.
@ -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"
|
Binary file not shown.
@ -0,0 +1 @@
|
||||
This is a placeholder for documentation.
|
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: ""
|
Binary file not shown.
@ -0,0 +1 @@
|
||||
# This is a placeholder.
|
@ -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…
Reference in new issue