Add chart file support.

Also added a log abstraction.
pull/195/head
Matt Butcher 9 years ago
parent f3b0fb3cae
commit 84e61c0391

@ -0,0 +1,107 @@
package chart
import (
"fmt"
"os"
"path/filepath"
"github.com/helm/helm/manifest"
)
// Chart represents a complete chart.
//
// A chart consists of the following parts:
//
// - Chart.yaml: In code, we refer to this as the Chartfile
// - manifests/*.yaml: The Kubernetes manifests
//
// On the Chart object, the manifests are sorted by type into a handful of
// recognized Kubernetes API v1 objects.
//
// TODO: Investigate treating these as unversioned.
type Chart struct {
Chartfile *Chartfile
// Kind is a map of Kind to an array of manifests.
//
// For example, Kind["Pod"] has an array of Pod manifests.
Kind map[string][]*manifest.Manifest
// Manifests is an array of Manifest objects.
Manifests []*manifest.Manifest
}
// Load loads an entire chart.
//
// This includes the Chart.yaml (*Chartfile) and all of the manifests.
//
// If you are just reading the Chart.yaml file, it is substantially more
// performant to use LoadChartfile.
func Load(chart string) (*Chart, error) {
if fi, err := os.Stat(chart); err != nil {
return nil, err
} else if !fi.IsDir() {
return nil, fmt.Errorf("Chart %s is not a directory.", chart)
}
cf, err := LoadChartfile(filepath.Join(chart, "Chart.yaml"))
if err != nil {
return nil, err
}
c := &Chart{
Chartfile: cf,
Kind: map[string][]*manifest.Manifest{},
}
ms, err := manifest.ParseDir(chart)
if err != nil {
return c, err
}
c.attachManifests(ms)
return c, nil
}
const (
// AnnFile is the annotation key for a file's origin.
AnnFile = "chart.helm.sh/file"
// AnnChartVersion is the annotation key for a chart's version.
AnnChartVersion = "chart.helm.sh/version"
// AnnChartDesc is the annotation key for a chart's description.
AnnChartDesc = "chart.helm.sh/description"
// AnnChartName is the annotation key for a chart name.
AnnChartName = "chart.helm.sh/name"
)
// attachManifests sorts manifests into their respective categories, adding to the Chart.
func (c *Chart) attachManifests(manifests []*manifest.Manifest) {
c.Manifests = manifests
for _, m := range manifests {
c.Kind[m.Kind] = append(c.Kind[m.Kind], m)
}
}
// UnknownKinds returns a list of kinds that this chart contains, but which were not in the passed in array.
//
// A Chart will store all kinds that are given to it. This makes it possible to get a list of kinds that are not
// known beforehand.
func (c *Chart) UnknownKinds(known []string) []string {
lookup := make(map[string]bool, len(known))
for _, k := range known {
lookup[k] = true
}
u := []string{}
for n := range c.Kind {
if _, ok := lookup[n]; !ok {
u = append(u, n)
}
}
return u
}

@ -0,0 +1,117 @@
package chart
import (
"testing"
)
const testfile = "../testdata/test-Chart.yaml"
const testchart = "../testdata/charts/kitchensink"
func TestLoad(t *testing.T) {
c, err := Load(testchart)
if err != nil {
t.Errorf("Failed to load chart: %s", err)
}
if c.Chartfile.Name != "kitchensink" {
t.Errorf("Expected chart name to be 'kitchensink'. Got '%s'.", c.Chartfile.Name)
}
if c.Chartfile.Dependencies[0].Version != "~10.21" {
d := c.Chartfile.Dependencies[0].Version
t.Errorf("Expected dependency 0 to have version '~10.21'. Got '%s'.", d)
}
if len(c.Kind["Pod"]) != 3 {
t.Errorf("Expected 3 pods, got %d", len(c.Kind["Pod"]))
}
if len(c.Kind["ReplicationController"]) == 0 {
t.Error("No RCs found")
}
if len(c.Kind["Namespace"]) == 0 {
t.Errorf("No namespaces found")
}
if len(c.Kind["Secret"]) == 0 {
t.Error("Is it secret? Is it safe? NO!")
}
if len(c.Kind["PersistentVolume"]) == 0 {
t.Errorf("No volumes.")
}
if len(c.Kind["Service"]) == 0 {
t.Error("No service. Just like [insert mobile provider name here]")
}
}
func TestLoadChart(t *testing.T) {
f, err := LoadChartfile(testfile)
if err != nil {
t.Errorf("Error loading %s: %s", testfile, err)
}
if f.Name != "alpine-pod" {
t.Errorf("Expected alpine-pod, got %s", f.Name)
}
if len(f.Maintainers) != 2 {
t.Errorf("Expected 2 maintainers, got %d", len(f.Maintainers))
}
if len(f.Dependencies) != 2 {
t.Errorf("Expected 2 dependencies, got %d", len(f.Dependencies))
}
if f.Dependencies[1].Name != "bar" {
t.Errorf("Expected second dependency to be bar: %q", f.Dependencies[1].Name)
}
if f.PreInstall["mykeys"] != "generate-keypair foo" {
t.Errorf("Expected map value for mykeys.")
}
if f.Source[0] != "https://example.com/helm" {
t.Errorf("Expected https://example.com/helm, got %s", f.Source)
}
}
func TestVersionOK(t *testing.T) {
f, err := LoadChartfile(testfile)
if err != nil {
t.Errorf("Error loading %s: %s", testfile, err)
}
// These are canaries. The SemVer package exhuastively tests the
// various permutations. This will alert us if we wired it up
// incorrectly.
d := f.Dependencies[1]
if d.VersionOK("1.0.0") {
t.Errorf("1.0.0 should have been marked out of range")
}
if !d.VersionOK("1.2.3") {
t.Errorf("Version 1.2.3 should have been marked in-range")
}
}
func TestUnknownKinds(t *testing.T) {
known := []string{"Pod"}
c, err := Load(testchart)
if err != nil {
t.Errorf("Failed to load chart: %s", err)
}
unknown := c.UnknownKinds(known)
if len(unknown) < 5 {
t.Errorf("Expected at least 5 unknown chart types, got %d.", len(unknown))
}
for _, k := range unknown {
if k == "Pod" {
t.Errorf("Pod is not an unknown kind.")
}
}
}

@ -0,0 +1,79 @@
package chart
import (
"io/ioutil"
"github.com/Masterminds/semver"
"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"`
Dependencies []*Dependency `yaml:"dependencies,omitempty"`
Environment []*EnvConstraint `yaml:"environment,omitempty"`
}
// Maintainer describes a chart maintainer.
type Maintainer struct {
Name string `yaml:"name"`
Email string `yaml:"email,omitempty"`
}
// Dependency describes a specific dependency.
type Dependency struct {
Name string `yaml:"name,omitempty"`
Version string `yaml:"version"`
Location string `yaml:"location"`
}
// Specify environmental constraints.
type EnvConstraint struct {
Name string `yaml:"name"`
Version string `yaml:"version"`
Extensions []string `yaml:"extensions,omitempty"`
APIGroups []string `yaml:"apiGroups,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 := yaml.Marshal(c)
if err != nil {
return err
}
return ioutil.WriteFile(filename, b, 0644)
}
// VersionOK returns true if the given version meets the constraints.
//
// It returns false if the version string or constraint is unparsable or if the
// version does not meet the constraint.
func (d *Dependency) VersionOK(version string) bool {
c, err := semver.NewConstraint(d.Version)
if err != nil {
return false
}
v, err := semver.NewVersion(version)
if err != nil {
return false
}
return c.Check(v)
}

@ -0,0 +1,60 @@
package chart
import (
"io/ioutil"
"os"
"testing"
)
var testChart = `#helm:generate foo
name: frobniz
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
dependencies:
- name: thingerbob
location: https://example.com/charts/thingerbob-3.2.1.tgz
version: ^3
environment:
- name: Kubernetes
version: ~1.1
extensions:
- extensions/v1beta1
- extensions/v1beta1/daemonset
apiGroups:
- 3rdParty
`
func TestLoadChartfile(t *testing.T) {
out, err := ioutil.TempFile("", "chartfile-")
if err != nil {
t.Fatal(err)
}
tname := out.Name()
defer func() {
os.Remove(tname)
}()
out.Write([]byte(testChart))
out.Close()
c, err := LoadChartfile(tname)
if err != nil {
t.Errorf("Failed to open %s: %s", tname, err)
return
}
if len(c.Environment[0].Extensions) != 2 {
t.Errorf("Expected two extensions, got %d", len(c.Environment[0].Extensions))
}
}

51
glide.lock generated

@ -0,0 +1,51 @@
hash: db55a031aaa2f352fa5e9e4fda871039afb80e383a57fc77e4b35114d47cca8a
updated: 2016-01-26T17:30:54.243252416-07:00
imports:
- name: github.com/emicklei/go-restful
version: b86acf97a74ed7603ac78d012f5535b4d587b156
- name: github.com/ghodss/yaml
version: 73d445a93680fa1a78ae23a5839bad48f32ba1ee
- name: github.com/golang/glog
version: 23def4e6c14b4da8ac2ed8007337bc5eb5007998
- name: github.com/golang/protobuf
version: 6aaa8d47701fa6cf07e914ec01fde3d4a1fe79c3
- name: github.com/google/go-github
version: b8b4ac742977310ff6e75140a403a38dab109977
subpackages:
- /github
- name: github.com/google/go-querystring
version: 2a60fc2ba6c19de80291203597d752e9ba58e4c0
- name: github.com/gorilla/context
version: 1c83b3eabd45b6d76072b66b746c20815fb2872d
- name: github.com/gorilla/handlers
version: 8f2758070a82adb7a3ad6b223a0b91878f32d400
- name: github.com/gorilla/mux
version: 26a6070f849969ba72b72256e9f14cf519751690
- name: github.com/gorilla/schema
version: 14c555599c2a4f493c1e13fd1ea6fdf721739028
- name: github.com/Masterminds/semver
version: c4f7ef0702f269161a60489ccbbc9f1241ad1265
- name: github.com/mjibson/appstats
version: 0542d5f0e87ea3a8fa4174322b9532f5d04f9fa8
- name: golang.org/x/crypto
version: 1f22c0103821b9390939b6776727195525381532
- name: golang.org/x/net
version: 04b9de9b512f58addf28c9853d50ebef61c3953e
- name: golang.org/x/oauth2
version: 8a57ed94ffd43444c0879fe75701732a38afc985
- name: golang.org/x/text
version: 6d3c22c4525a4da167968fa2479be5524d2e8bd0
- name: google.golang.com/appengine
version: ""
repo: https://google.golang.com/appengine
- name: google.golang.org/api
version: 0caa37974a5f5ae67172acf68b4970f7864f994c
- name: google.golang.org/appengine
version: 6bde959377a90acb53366051d7d587bfd7171354
- name: google.golang.org/cloud
version: fb10e8da373d97f6ba5e648299a10b3b91f14cd5
- name: google.golang.org/grpc
version: e29d659177655e589850ba7d3d83f7ce12ef23dd
- name: gopkg.in/yaml.v2
version: f7716cbe52baa25d2e9b0d0da546fcf909fc16b4
devImports: []

@ -0,0 +1,11 @@
package: github.com/kubernetes/deployment-manager
import:
- package: github.com/emicklei/go-restful
- package: github.com/ghodss/yaml
- package: github.com/google/go-github
subpackages:
- /github
- package: github.com/gorilla/handlers
- package: github.com/gorilla/mux
- package: gopkg.in/yaml.v2
- package: github.com/Masterminds/semver

@ -0,0 +1,51 @@
/* Package log provides simple convenience wrappers for logging.
Following convention, this provides functions for logging warnings, errors, information
and debugging.
*/
package log
import (
"log"
"os"
)
// LogReceiver can receive log messages from this package.
type LogReceiver interface {
Printf(format string, v ...interface{})
}
// Logger is the destination for this package.
//
// The logger that this prints to.
var Logger LogReceiver = log.New(os.Stderr, "", log.LstdFlags)
// IsDebugging controls debugging output.
//
// If this is true, debugging messages will be printed. Expensive debugging
// operations can be wrapped in `if log.IsDebugging {}`.
var IsDebugging bool = false
// Err prints an error of severity ERROR to the log.
func Err(msg string, v ...interface{}) {
Logger.Printf("[ERROR] "+msg+"\n", v...)
}
// Warn prints an error severity WARN to the log.
func Warn(msg string, v ...interface{}) {
Logger.Printf("[WARN] "+msg+"\n", v...)
}
// Info prints an error of severity INFO to the log.
func Info(msg string, v ...interface{}) {
Logger.Printf("[INFO] "+msg+"\n", v...)
}
// Debug prints an error severity DEBUG to the log.
//
// Debug will only print if IsDebugging is true.
func Debug(msg string, v ...interface{}) {
if IsDebugging {
Logger.Printf("[DEBUG] "+msg+"\n", v...)
}
}

@ -0,0 +1,49 @@
package log
import (
"bytes"
"fmt"
"testing"
)
type LoggerMock struct {
b bytes.Buffer
}
func (l *LoggerMock) Printf(m string, v ...interface{}) {
l.b.Write([]byte(fmt.Sprintf(m, v...)))
}
func TestLogger(t *testing.T) {
l := &LoggerMock{}
Logger = l
IsDebugging = true
Err("%s%s%s", "a", "b", "c")
expect := "[ERROR] abc\n"
if l.b.String() != expect {
t.Errorf("Expected %q, got %q", expect, l.b.String())
}
l.b.Reset()
tests := map[string]func(string, ...interface{}){
"[WARN] test\n": Warn,
"[INFO] test\n": Info,
"[DEBUG] test\n": Debug,
}
for expect, f := range tests {
f("test")
if l.b.String() != expect {
t.Errorf("Expected %q, got %q", expect, l.b.String())
}
l.b.Reset()
}
IsDebugging = false
Debug("HELLO")
if l.b.String() != "" {
t.Errorf("Expected debugging to disable. Got %q", l.b.String())
}
l.b.Reset()
}
Loading…
Cancel
Save