Commit 70b7d9c2 authored by Jack Greenfield's avatar Jack Greenfield

Merge pull request #195 from technosophos/feat/chartfile

feat(chart): Add a chart/ package.
parents b9def76b 4fe37ea6
...@@ -4,3 +4,4 @@ resourcifier/pkg/* ...@@ -4,3 +4,4 @@ resourcifier/pkg/*
resourcifier/bin/* resourcifier/bin/*
manager/pkg/* manager/pkg/*
.project .project
vendor/*
/*
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"
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/kubernetes/deployment-manager/log"
)
const ChartfileName string = "Chart.yaml"
const (
preTemplates string = "templates/"
preHooks string = "hooks/"
preDocs string = "docs/"
preIcon string = "icon.svg"
)
var headerBytes = []byte("+aHR0cHM6Ly95b3V0dS5iZS96OVV6MWljandyTQo=")
// Chart represents a complete chart.
//
// A chart consists of the following parts:
//
// - Chart.yaml: In code, we refer to this as the Chartfile
// - templates/*: The template directory
// - README.md: Optional README file
// - LICENSE: Optional license file
// - hooks/: Optional hooks registry
// - docs/: Optional docs directory
//
// Packed charts are stored in gzipped tar archives (.tgz). Unpackaged charts
// are directories where the directory name is the Chartfile.Name.
//
// Optionally, a chart might also locate a provenance (.prov) file that it
// can use for cryptographic signing.
type Chart struct {
loader chartLoader
}
// Close the chart.
//
// Charts should always be closed when no longer needed.
func (c *Chart) Close() error {
return c.loader.close()
}
// Chartfile gets the Chartfile (Chart.yaml) for this chart.
func (c *Chart) Chartfile() *Chartfile {
return c.loader.chartfile()
}
// Dir() returns the directory where the charts are located.
func (c *Chart) Dir() string {
return c.loader.dir()
}
// DocsDir returns the directory where the chart's documentation is stored.
func (c *Chart) DocsDir() string {
return filepath.Join(c.loader.dir(), preDocs)
}
// HooksDir returns the directory where the hooks are stored.
func (c *Chart) HooksDir() string {
return filepath.Join(c.loader.dir(), preHooks)
}
// TemplatesDir returns the directory where the templates are stored.
func (c *Chart) TemplatesDir() string {
return filepath.Join(c.loader.dir(), preTemplates)
}
// Icon returns the path to the icon.svg file.
//
// If an icon is not found in the chart, this will return an error.
func (c *Chart) Icon() (string, error) {
i := filepath.Join(c.Dir(), preIcon)
_, err := os.Stat(i)
return i, err
}
// chartLoader provides load, close, and save implementations for a chart.
type chartLoader interface {
// Chartfile resturns a *Chartfile for this chart.
chartfile() *Chartfile
// Dir returns a directory where the chart can be accessed.
dir() string
// Close cleans up a chart.
close() error
}
type dirChart struct {
chartyaml *Chartfile
chartdir string
}
func (d *dirChart) chartfile() *Chartfile {
return d.chartyaml
}
func (d *dirChart) dir() string {
return d.chartdir
}
func (d *dirChart) close() error {
return nil
}
type tarChart struct {
chartyaml *Chartfile
tmpDir string
}
func (t *tarChart) chartfile() *Chartfile {
return t.chartyaml
}
func (t *tarChart) dir() string {
return t.tmpDir
}
func (t *tarChart) close() error {
// Remove the temp directory.
return os.RemoveAll(t.tmpDir)
}
// New creates a new chart in a directory.
//
// Inside of dir, this will create a directory based on the name of
// chartfile.Name. It will then write the Chart.yaml into this directory and
// create the (empty) appropriate directories.
//
// The returned *Chart will point to the newly created directory.
//
// If dir does not exist, this will return an error.
// If Chart.yaml or any directories cannot be created, this will return an
// error. In such a case, this will attempt to clean up by removing the
// new chart directory.
func Create(chartfile *Chartfile, dir string) (*Chart, error) {
path, err := filepath.Abs(dir)
if err != nil {
return nil, err
}
if fi, err := os.Stat(path); err != nil {
return nil, err
} else if !fi.IsDir() {
return nil, fmt.Errorf("no such directory %s", path)
}
n := fname(chartfile.Name)
cdir := filepath.Join(path, n)
if _, err := os.Stat(cdir); err == nil {
return nil, fmt.Errorf("directory already exists: %s", cdir)
}
if err := os.MkdirAll(cdir, 0755); err != nil {
return nil, err
}
rollback := func() {
// TODO: Should we log failures here?
os.RemoveAll(cdir)
}
if err := chartfile.Save(filepath.Join(cdir, ChartfileName)); err != nil {
rollback()
return nil, err
}
for _, d := range []string{preHooks, preDocs, preTemplates} {
if err := os.MkdirAll(filepath.Join(cdir, d), 0755); err != nil {
rollback()
return nil, err
}
}
return &Chart{
loader: &dirChart{chartyaml: chartfile, chartdir: cdir},
}, nil
}
// fname prepares names for the filesystem
func fname(name string) string {
// Right now, we don't do anything. Do we need to encode any particular
// characters? What characters are legal in a chart name, but not in file
// names on Windows, Linux, or OSX.
return name
}
// LoadDir loads an entire chart from a directory.
//
// 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 LoadDir(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
}
cl := &dirChart{
chartyaml: cf,
chartdir: chart,
}
return &Chart{
loader: cl,
}, nil
}
// Load loads a chart from a chart archive.
//
// A chart archive is a gzipped tar archive that follows the Chart format
// specification.
func Load(archive string) (*Chart, error) {
if fi, err := os.Stat(archive); err != nil {
return nil, err
} else if fi.IsDir() {
return nil, errors.New("cannot load a directory with chart.Load()")
}
raw, err := os.Open(archive)
if err != nil {
return nil, err
}
defer raw.Close()
unzipped, err := gzip.NewReader(raw)
if err != nil {
return nil, err
}
defer unzipped.Close()
untarred := tar.NewReader(unzipped)
c, err := loadTar(untarred)
if err != nil {
return nil, err
}
cf, err := LoadChartfile(filepath.Join(c.tmpDir, ChartfileName))
if err != nil {
return nil, err
}
c.chartyaml = cf
return &Chart{loader: c}, nil
}
func loadTar(r *tar.Reader) (*tarChart, error) {
td, err := ioutil.TempDir("", "chart-")
if err != nil {
return nil, err
}
c := &tarChart{
chartyaml: &Chartfile{},
tmpDir: td,
}
firstDir := ""
hdr, err := r.Next()
for err == nil {
log.Debug("Reading %s", hdr.Name)
// This is to prevent malformed tar attacks.
hdr.Name = filepath.Clean(hdr.Name)
if firstDir == "" {
fi := hdr.FileInfo()
if fi.IsDir() {
log.Debug("Discovered app named %s", hdr.Name)
firstDir = hdr.Name
} else {
log.Warn("Unexpected file at root of archive: %s", hdr.Name)
}
} else if strings.HasPrefix(hdr.Name, firstDir) {
log.Debug("Extracting %s to %s", hdr.Name, c.tmpDir)
// We know this has the prefix, so we know there won't be an error.
rel, _ := filepath.Rel(firstDir, hdr.Name)
// If tar record is a directory, create one in the tmpdir and return.
if hdr.FileInfo().IsDir() {
os.MkdirAll(filepath.Join(c.tmpDir, rel), 0755)
hdr, err = r.Next()
continue
}
dest := filepath.Join(c.tmpDir, rel)
f, err := os.Create(filepath.Join(c.tmpDir, rel))
if err != nil {
log.Warn("Could not create %s: %s", dest, err)
hdr, err = r.Next()
continue
}
if _, err := io.Copy(f, r); err != nil {
log.Warn("Failed to copy %s: %s", dest, err)
}
f.Close()
} else {
log.Warn("Unexpected file outside of chart: %s", hdr.Name)
}
hdr, err = r.Next()
}
if err != nil && err != io.EOF {
log.Warn("Unexpected error reading tar: %s", err)
c.close()
return c, err
}
log.Info("Reached end of Tar file")
return c, 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 (
"path/filepath"
"testing"
"github.com/kubernetes/deployment-manager/log"
)
const (
testfile = "testdata/frobnitz/Chart.yaml"
testdir = "testdata/frobnitz/"
testarchive = "testdata/frobnitz-0.0.1.tgz"
testill = "testdata/ill-1.2.3.tgz"
testnochart = "testdata/nochart.tgz"
)
// Type canaries. If these fail, they will fail at compile time.
var _ chartLoader = &dirChart{}
var _ chartLoader = &tarChart{}
func init() {
log.IsDebugging = true
}
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)
}
if c.Chartfile().Dependencies[0].Version != "^3" {
d := c.Chartfile().Dependencies[0].Version
t.Errorf("Expected dependency 0 to have version '^3'. Got '%s'.", d)
}
}
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 TestLoadIll(t *testing.T) {
c, err := Load(testill)
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
}
// Ill does not have an icon.
if i, err := c.Icon(); err == nil {
t.Errorf("Expected icon to be missing. Got %s", i)
}
}
func TestLoadNochart(t *testing.T) {
_, err := Load(testnochart)
if err == nil {
t.Error("Nochart should not have loaded at all.")
}
}
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)
}
d := c.DocsDir()
if d != filepath.Join(testdir, preDocs) {
t.Errorf("Unexpectedly, docs are in %s", d)
}
d = c.TemplatesDir()
if d != filepath.Join(testdir, preTemplates) {
t.Errorf("Unexpectedly, templates are in %s", d)
}
d = c.HooksDir()
if d != filepath.Join(testdir, preHooks) {
t.Errorf("Unexpectedly, hooks are in %s", d)
}
i, err := c.Icon()
if err != nil {
t.Errorf("No icon found in test chart: %s", err)
}
if i != filepath.Join(testdir, preIcon) {
t.Errorf("Unexpectedly, icon is in %s", i)
}
}
/*
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"
"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 := 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)
}
// 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)
}
/*
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 len(f.Environment[0].Extensions) != 2 {
t.Errorf("Expected two extensions, got %d", len(f.Environment[0].Extensions))
}
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 len(f.Dependencies) != 1 {
t.Errorf("Expected 2 dependencies, got %d", len(f.Dependencies))
}
if f.Dependencies[0].Name != "thingerbob" {
t.Errorf("Expected second dependency to be thingerbob: %q", f.Dependencies[0].Name)
}
if f.Source[0] != "https://example.com/foo/bar" {
t.Errorf("Expected https://example.com/foo/bar, 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[0]
if d.VersionOK("1.0.0") {
t.Errorf("1.0.0 should have been marked out of range")
}
if !d.VersionOK("3.2.3") {
t.Errorf("Version 3.2.3 should have been marked in-range")
}
}
/*
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 (
"errors"
"fmt"
"net/url"
"regexp"
"strings"
)
// ErrLocal indicates that a local URL was used as a remote URL.
var ErrLocal = errors.New("cannot use local Locator as remote")
// ErrRemote indicates that a remote URL was used as a local URL.
var ErrRemote = errors.New("cannot use remote Locator as local")
const (
SchemeHTTP = "http"
SchemeHTTPS = "https"
SchemeHelm = "helm"
SchemeFile = "file"
)
// TarNameRegex parses the name component of a URI and breaks it into a name and version.
//
// This borrows liberally from github.com/Masterminds/semver.
const TarNameRegex = `([0-9A-Za-z\-_/]+)-(v?([0-9]+)(\.[0-9]+)?(\.[0-9]+)?` +
`(-([0-9A-Za-z\-]+(\.[0-9A-Za-z\-]+)*))?` +
`(\+([0-9A-Za-z\-]+(\.[0-9A-Za-z\-]+)*))?)(.tgz)?`
var tnregexp *regexp.Regexp
func init() {
tnregexp = regexp.MustCompile("^" + TarNameRegex + "$")
}
type Locator struct {
// The scheme of the URL. Typically one of http, https, helm, or file.
Scheme string
// The host information, if applicable.
Host string
// The bucket name
Bucket string
// The chart name
Name string
// The version or version range.
Version string
// If this is a local chart, the path to the chart.
LocalRef string
isLocal bool
original string
}
func Parse(path string) (*Locator, error) {
u, err := url.Parse(path)
if err != nil {
return nil, err
}
switch u.Scheme {
case SchemeHelm:
parts := strings.SplitN(u.Opaque, "/", 3)
if len(parts) < 3 {
return nil, fmt.Errorf("both bucket and chart name are required in %s: %s", path, u.Path)
}
// Need to parse opaque data into bucket and chart.
return &Locator{
Scheme: u.Scheme,
Host: parts[0],
Bucket: parts[1],
Name: parts[2],
Version: u.Fragment,
original: path,
}, nil
case SchemeHTTP, SchemeHTTPS:
// Long name
parts := strings.SplitN(u.Path, "/", 3)
if len(parts) < 3 {
return nil, fmt.Errorf("both bucket and chart name are required in %s", path)
}
name, version, err := parseTarName(parts[2])
if err != nil {
return nil, err
}
return &Locator{
Scheme: u.Scheme,
Host: u.Host,
Bucket: parts[1],
Name: name,
Version: version,
original: path,
}, nil
case SchemeFile:
return &Locator{
LocalRef: u.Path,
isLocal: true,
original: path,
}, nil
default:
// In this case...
// - if the path is relative or absolute, return it as-is.
// - if it's a URL of an unknown scheme, return it as is.
return &Locator{
LocalRef: path,
isLocal: true,
original: path,
}, nil
}
}
// IsLocal returns true if this is a local path.
func (u *Locator) IsLocal() bool {
return u.isLocal
}
// Local returns a local version of the path.
//
// This will return an error if the URL does not reference a local chart.
func (u *Locator) Local() (string, error) {
return u.LocalRef, nil
}
// Short returns a short form URL.
//
// This will return an error if the URL references a local chart.
func (u *Locator) Short() (string, error) {
if u.IsLocal() {
return "", ErrLocal
}
fname := fmt.Sprintf("%s/%s/%s", u.Host, u.Bucket, u.Name)
return (&url.URL{
Scheme: SchemeHelm,
Opaque: fname,
Fragment: u.Version,
}).String(), nil
}
// Long returns a long-form URL.
//
// If secure is true, this will return an HTTPS URL, otherwise HTTP.
//
// This will return an error if the URL references a local chart.
func (u *Locator) Long(secure bool) (string, error) {
if u.IsLocal() {
return "", ErrLocal
}
scheme := SchemeHTTPS
if !secure {
scheme = SchemeHTTP
}
fname := fmt.Sprintf("%s/%s-%s.tgz", u.Bucket, u.Name, u.Version)
return (&url.URL{
Scheme: scheme,
Host: u.Host,
Path: fname,
}).String(), nil
}
// parseTarName parses a long-form tarfile name.
func parseTarName(name string) (string, string, error) {
if strings.HasSuffix(name, ".tgz") {
name = strings.TrimSuffix(name, ".tgz")
}
v := tnregexp.FindStringSubmatch(name)
if v == nil {
return name, "", fmt.Errorf("invalid name %s", name)
}
return v[1], v[2], 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 (
"testing"
)
func TestParse(t *testing.T) {
tests := map[string]Locator{
"helm:host/bucket/name#1.2.3": Locator{Scheme: "helm", Host: "host", Bucket: "bucket", Name: "name", Version: "1.2.3"},
"https://host/bucket/name-1.2.3.tgz": Locator{Scheme: "https", Host: "host", Bucket: "bucket", Name: "name", Version: "1.2.3"},
"http://host/bucket/name-1.2.3.tgz": Locator{Scheme: "http", Host: "host", Bucket: "bucket", Name: "name", Version: "1.2.3"},
}
for start, expect := range tests {
u, err := Parse(start)
if err != nil {
t.Errorf("Failed parsing %s: %s", start, err)
}
if expect.Scheme != u.Scheme {
t.Errorf("Unexpected scheme: %s", u.Scheme)
}
if expect.Host != u.Host {
t.Errorf("Unexpected host: %q", u.Host)
}
if expect.Bucket != u.Bucket {
t.Errorf("Unexpected bucket: %q", u.Bucket)
}
if expect.Name != u.Name {
t.Errorf("Unexpected name: %q", u.Name)
}
if expect.Version != u.Version {
t.Errorf("Unexpected version: %q", u.Version)
}
if expect.LocalRef != u.LocalRef {
t.Errorf("Unexpected local dir: %q", u.LocalRef)
}
}
}
func TestShort(t *testing.T) {
tests := map[string]string{
"https://example.com/foo/bar-1.2.3.tgz": "helm:example.com/foo/bar#1.2.3",
"http://example.com/foo/bar-1.2.3.tgz": "helm:example.com/foo/bar#1.2.3",
"helm:example.com/foo/bar#1.2.3": "helm:example.com/foo/bar#1.2.3",
"helm:example.com/foo/bar#>1.2.3": "helm:example.com/foo/bar#%3E1.2.3",
}
for start, expect := range tests {
u, err := Parse(start)
if err != nil {
t.Errorf("Failed to parse: %s", err)
continue
}
short, err := u.Short()
if err != nil {
t.Errorf("Failed to generate short: %s", err)
continue
}
if short != expect {
t.Errorf("Expected %q, got %q", expect, short)
}
}
fails := []string{"./this/is/local", "file:///this/is/local"}
for _, f := range fails {
u, err := Parse(f)
if err != nil {
t.Errorf("Failed to parse: %s", err)
continue
}
if _, err := u.Short(); err == nil {
t.Errorf("%q should have caused an error for Short()", f)
}
}
}
func TestLong(t *testing.T) {
tests := map[string]string{
"https://example.com/foo/bar-1.2.3.tgz": "https://example.com/foo/bar-1.2.3.tgz",
"http://example.com/foo/bar-1.2.3.tgz": "https://example.com/foo/bar-1.2.3.tgz",
"helm:example.com/foo/bar#1.2.3": "https://example.com/foo/bar-1.2.3.tgz",
"helm:example.com/foo/bar#>1.2.3": "https://example.com/foo/bar-%3E1.2.3.tgz",
}
for start, expect := range tests {
t.Logf("Parsing %s", start)
u, err := Parse(start)
if err != nil {
t.Errorf("Failed to parse: %s", err)
continue
}
long, err := u.Long(true)
if err != nil {
t.Errorf("Failed to generate long: %s", err)
continue
}
if long != expect {
t.Errorf("Expected %q, got %q", expect, long)
}
}
fails := []string{"./this/is/local", "file:///this/is/local"}
for _, f := range fails {
u, err := Parse(f)
if err != nil {
t.Errorf("Failed to parse: %s", err)
continue
}
if _, err := u.Long(false); err == nil {
t.Errorf("%q should have caused an error for Long()", f)
}
}
}
func TestLocal(t *testing.T) {
tests := map[string]string{
"file:///foo/bar-1.2.3.tgz": "/foo/bar-1.2.3.tgz",
"file:///foo/bar": "/foo/bar",
"./foo/bar": "./foo/bar",
"/foo/bar": "/foo/bar",
"file://localhost/etc/fstab": "/etc/fstab",
// https://blogs.msdn.microsoft.com/ie/2006/12/06/file-uris-in-windows/
"file:///C:/WINDOWS/clock.avi": "/C:/WINDOWS/clock.avi",
}
for start, expect := range tests {
u, err := Parse(start)
if err != nil {
t.Errorf("Failed parse: %s", err)
continue
}
fin, err := u.Local()
if err != nil {
t.Errorf("Failed Local(): %s", err)
continue
}
if fin != expect {
t.Errorf("Expected %q, got %q", expect, fin)
}
}
}
func TestParseTarName(t *testing.T) {
tests := []struct{ start, name, version string }{
{"butcher-1.2.3", "butcher", "1.2.3"},
{"butcher-1.2.3.tgz", "butcher", "1.2.3"},
{"butcher-1.2.3-beta1+1234", "butcher", "1.2.3-beta1+1234"},
{"butcher-1.2.3-beta1+1234.tgz", "butcher", "1.2.3-beta1+1234"},
{"foo/butcher-1.2.3.tgz", "foo/butcher", "1.2.3"},
}
for _, tt := range tests {
n, v, e := parseTarName(tt.start)
if e != nil {
t.Errorf("Error parsing %s: %s", tt.start, e)
continue
}
if n != tt.name {
t.Errorf("Expected name %q, got %q", tt.name, n)
}
if v != tt.version {
t.Errorf("Expected version %q, got %q", tt.version, v)
}
}
}
/*
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"
"github.com/kubernetes/deployment-manager/log"
)
// 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()
basename := filepath.Base(dir)
pdir := filepath.Dir(dir)
if basename == "." {
basename = fname(cfile.Name)
}
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 {
log.Warn("Removing incomplete archive %s", filename)
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"
"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/LICENSE",
"sprocket/README.md",
"sprocket/docs",
"sprocket/docs/README.md",
"sprocket/hooks",
"sprocket/hooks/pre-install.py",
"sprocket/icon.svg",
"sprocket/templates",
"sprocket/templates/placeholder.txt",
}
if len(expect) != len(files) {
t.Errorf("Expected %d files, found %d", len(expect), len(files))
return
}
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
}
The testdata directory here holds charts that match the specification.
The `fromnitz/` directory contains a chart that matches the chart
specification.
The `frobnitz-0.0.1.tgz` file is an archive of the `frobnitz` directory.
The `ill` chart and directory is a chart that is not 100% compatible,
but which should still be parseable.
#helm:generate foo
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
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
This diff is collapsed.
# 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>
# Google Cloud Deployment Manager template
resources:
- name: nfs-disk
type: compute.v1.disk
properties:
zone: us-central1-b
sizeGb: 200
- name: mysql-disk
type: compute.v1.disk
properties:
zone: us-central1-b
sizeGb: 200
#helm:generate dm_template
{% set PROPERTIES = properties or {} %}
{% set PROJECT = PROPERTIES['project'] or 'dm-k8s-testing' %}
{% set NFS_SERVER = PROPERTIES['nfs-server'] or {} %}
{% set NFS_SERVER_IP = NFS_SERVER['ip'] or '10.0.253.247' %}
{% set NFS_SERVER_PORT = NFS_SERVER['port'] or 2049 %}
{% set NFS_SERVER_DISK = NFS_SERVER['disk'] or 'nfs-disk' %}
{% set NFS_SERVER_DISK_FSTYPE = NFS_SERVER['fstype'] or 'ext4' %}
{% set NGINX = PROPERTIES['nginx'] or {} %}
{% set NGINX_PORT = 80 %}
{% set NGINX_REPLICAS = NGINX['replicas'] or 2 %}
{% set WORDPRESS_PHP = PROPERTIES['wordpress-php'] or {} %}
{% set WORDPRESS_PHP_REPLICAS = WORDPRESS_PHP['replicas'] or 2 %}
{% set WORDPRESS_PHP_PORT = WORDPRESS_PHP['port'] or 9000 %}
{% set MYSQL = PROPERTIES['mysql'] or {} %}
{% set MYSQL_PORT = MYSQL['port'] or 3306 %}
{% set MYSQL_PASSWORD = MYSQL['password'] or 'mysql-password' %}
{% set MYSQL_DISK = MYSQL['disk'] or 'mysql-disk' %}
{% set MYSQL_DISK_FSTYPE = MYSQL['fstype'] or 'ext4' %}
resources:
- name: nfs
type: github.com/kubernetes/application-dm-templates/storage/nfs:v1
properties:
ip: {{ NFS_SERVER_IP }}
port: {{ NFS_SERVER_PORT }}
disk: {{ NFS_SERVER_DISK }}
fstype: {{NFS_SERVER_DISK_FSTYPE }}
- name: nginx
type: github.com/kubernetes/application-dm-templates/common/replicatedservice:v2
properties:
service_port: {{ NGINX_PORT }}
container_port: {{ NGINX_PORT }}
replicas: {{ NGINX_REPLICAS }}
external_service: true
image: gcr.io/{{ PROJECT }}/nginx:latest
volumes:
- mount_path: /var/www/html
persistentVolumeClaim:
claimName: nfs
- name: mysql
type: github.com/kubernetes/application-dm-templates/common/replicatedservice:v2
properties:
service_port: {{ MYSQL_PORT }}
container_port: {{ MYSQL_PORT }}
replicas: 1
image: mysql:5.6
env:
- name: MYSQL_ROOT_PASSWORD
value: {{ MYSQL_PASSWORD }}
volumes:
- mount_path: /var/lib/mysql
gcePersistentDisk:
pdName: {{ MYSQL_DISK }}
fsType: {{ MYSQL_DISK_FSTYPE }}
- name: wordpress-php
type: github.com/kubernetes/application-dm-templates/common/replicatedservice:v2
properties:
service_name: wordpress-php
service_port: {{ WORDPRESS_PHP_PORT }}
container_port: {{ WORDPRESS_PHP_PORT }}
replicas: 2
image: wordpress:fpm
env:
- name: WORDPRESS_DB_PASSWORD
value: {{ MYSQL_PASSWORD }}
- name: WORDPRESS_DB_HOST
value: mysql-service
volumes:
- mount_path: /var/www/html
persistentVolumeClaim:
claimName: nfs
info:
title: Wordpress
description: |
Defines a Wordpress website by defining four replicated services: an NFS service, an nginx service, a wordpress-php service, and a MySQL service.
The nginx service and the Wordpress-php service both use NFS to share files.
properties:
project:
type: string
default: dm-k8s-testing
description: Project location to load the images from.
nfs-service:
type: object
properties:
ip:
type: string
default: 10.0.253.247
description: The IP of the NFS service.
port:
type: int
default: 2049
description: The port of the NFS service.
disk:
type: string
default: nfs-disk
description: The name of the persistent disk the NFS service uses.
fstype:
type: string
default: ext4
description: The filesystem the disk of the NFS service uses.
nginx:
type: object
properties:
replicas:
type: int
default: 2
description: The number of replicas for the nginx service.
wordpress-php:
type: object
properties:
replicas:
type: int
default: 2
description: The number of replicas for the wordpress-php service.
port:
type: int
default: 9000
description: The port the wordpress-php service runs on.
mysql:
type: object
properties:
port:
type: int
default: 3306
description: The port the MySQL service runs on.
password:
type: string
default: mysql-password
description: The root password of the MySQL service.
disk:
type: string
default: mysql-disk
description: The name of the persistent disk the MySQL service uses.
fstype:
type: string
default: ext4
description: The filesystem the disk of the MySQL service uses.
imports:
- path: wordpress.jinja
resources:
- name: wordpress
type: wordpress.jinja
#helm:generate foo
name: ill
description: This is a frobniz.
version: "1.2.3-alpha.1+12345"
keywords:
- ill
- 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
This diff is collapsed.
# 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.
# Google Cloud Deployment Manager template
resources:
- name: nfs-disk
type: compute.v1.disk
properties:
zone: us-central1-b
sizeGb: 200
- name: mysql-disk
type: compute.v1.disk
properties:
zone: us-central1-b
sizeGb: 200
#helm:generate dm_template
{% set PROPERTIES = properties or {} %}
{% set PROJECT = PROPERTIES['project'] or 'dm-k8s-testing' %}
{% set NFS_SERVER = PROPERTIES['nfs-server'] or {} %}
{% set NFS_SERVER_IP = NFS_SERVER['ip'] or '10.0.253.247' %}
{% set NFS_SERVER_PORT = NFS_SERVER['port'] or 2049 %}
{% set NFS_SERVER_DISK = NFS_SERVER['disk'] or 'nfs-disk' %}
{% set NFS_SERVER_DISK_FSTYPE = NFS_SERVER['fstype'] or 'ext4' %}
{% set NGINX = PROPERTIES['nginx'] or {} %}
{% set NGINX_PORT = 80 %}
{% set NGINX_REPLICAS = NGINX['replicas'] or 2 %}
{% set WORDPRESS_PHP = PROPERTIES['wordpress-php'] or {} %}
{% set WORDPRESS_PHP_REPLICAS = WORDPRESS_PHP['replicas'] or 2 %}
{% set WORDPRESS_PHP_PORT = WORDPRESS_PHP['port'] or 9000 %}
{% set MYSQL = PROPERTIES['mysql'] or {} %}
{% set MYSQL_PORT = MYSQL['port'] or 3306 %}
{% set MYSQL_PASSWORD = MYSQL['password'] or 'mysql-password' %}
{% set MYSQL_DISK = MYSQL['disk'] or 'mysql-disk' %}
{% set MYSQL_DISK_FSTYPE = MYSQL['fstype'] or 'ext4' %}
resources:
- name: nfs
type: github.com/kubernetes/application-dm-templates/storage/nfs:v1
properties:
ip: {{ NFS_SERVER_IP }}
port: {{ NFS_SERVER_PORT }}
disk: {{ NFS_SERVER_DISK }}
fstype: {{NFS_SERVER_DISK_FSTYPE }}
- name: nginx
type: github.com/kubernetes/application-dm-templates/common/replicatedservice:v2
properties:
service_port: {{ NGINX_PORT }}
container_port: {{ NGINX_PORT }}
replicas: {{ NGINX_REPLICAS }}
external_service: true
image: gcr.io/{{ PROJECT }}/nginx:latest
volumes:
- mount_path: /var/www/html
persistentVolumeClaim:
claimName: nfs
- name: mysql
type: github.com/kubernetes/application-dm-templates/common/replicatedservice:v2
properties:
service_port: {{ MYSQL_PORT }}
container_port: {{ MYSQL_PORT }}
replicas: 1
image: mysql:5.6
env:
- name: MYSQL_ROOT_PASSWORD
value: {{ MYSQL_PASSWORD }}
volumes:
- mount_path: /var/lib/mysql
gcePersistentDisk:
pdName: {{ MYSQL_DISK }}
fsType: {{ MYSQL_DISK_FSTYPE }}
- name: wordpress-php
type: github.com/kubernetes/application-dm-templates/common/replicatedservice:v2
properties:
service_name: wordpress-php
service_port: {{ WORDPRESS_PHP_PORT }}
container_port: {{ WORDPRESS_PHP_PORT }}
replicas: 2
image: wordpress:fpm
env:
- name: WORDPRESS_DB_PASSWORD
value: {{ MYSQL_PASSWORD }}
- name: WORDPRESS_DB_HOST
value: mysql-service
volumes:
- mount_path: /var/www/html
persistentVolumeClaim:
claimName: nfs
info:
title: Wordpress
description: |
Defines a Wordpress website by defining four replicated services: an NFS service, an nginx service, a wordpress-php service, and a MySQL service.
The nginx service and the Wordpress-php service both use NFS to share files.
properties:
project:
type: string
default: dm-k8s-testing
description: Project location to load the images from.
nfs-service:
type: object
properties:
ip:
type: string
default: 10.0.253.247
description: The IP of the NFS service.
port:
type: int
default: 2049
description: The port of the NFS service.
disk:
type: string
default: nfs-disk
description: The name of the persistent disk the NFS service uses.
fstype:
type: string
default: ext4
description: The filesystem the disk of the NFS service uses.
nginx:
type: object
properties:
replicas:
type: int
default: 2
description: The number of replicas for the nginx service.
wordpress-php:
type: object
properties:
replicas:
type: int
default: 2
description: The number of replicas for the wordpress-php service.
port:
type: int
default: 9000
description: The port the wordpress-php service runs on.
mysql:
type: object
properties:
port:
type: int
default: 3306
description: The port the MySQL service runs on.
password:
type: string
default: mysql-password
description: The root password of the MySQL service.
disk:
type: string
default: mysql-disk
description: The name of the persistent disk the MySQL service uses.
fstype:
type: string
default: ext4
description: The filesystem the disk of the MySQL service uses.
imports:
- path: wordpress.jinja
resources:
- name: wordpress
type: wordpress.jinja
name: sprocket
description: This is a sprocket.
version: 1.2.3-alpha.1+12345
home: ""
Placeholder for license.
# Sprocket
This is an example chart.
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>
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: []
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
/* 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...)
}
}
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()
}
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