Commit 55142528 authored by Matt Butcher's avatar Matt Butcher

Merge pull request #11 from technosophos/feat/chart-pkg

feat(chart): add chart package
parents ffa38d16 47991c9c
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: []
......@@ -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
This diff is collapsed.
/*
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
}
/*
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)
}
/*
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)
}
}
/*
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
/*
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
}
/*
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
}
This directory houses charts used in testing.
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"
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
This is an install document. The client may display this.
# Frobnitz
This is an example chart.
## Usage
This is an example. It has no usage.
## Development
For developer info, see the top-level repository.
This is a placeholder for documentation.
<?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>
# A values file contains configuration.
name = "Some Name"
[section]
name = "Name in a section"
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
# A values file contains configuration.
name = "Some Name"
[section]
name = "Name in a section"
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment