Commit 131ddcbc authored by Dave Cunningham's avatar Dave Cunningham

Merge pull request #406 from jackgr/server-charts

WIP: Frame out repo package and modify charts package to support chart download
parents 7a3fd09c 33ca00f8
...@@ -20,6 +20,7 @@ import ( ...@@ -20,6 +20,7 @@ import (
"archive/tar" "archive/tar"
"bytes" "bytes"
"compress/gzip" "compress/gzip"
"encoding/base64"
"errors" "errors"
"fmt" "fmt"
"io" "io"
...@@ -243,25 +244,7 @@ func LoadDir(chart string) (*Chart, error) { ...@@ -243,25 +244,7 @@ func LoadDir(chart string) (*Chart, error) {
// LoadData loads a chart from data, where data is a []byte containing a gzipped tar file. // LoadData loads a chart from data, where data is a []byte containing a gzipped tar file.
func LoadData(data []byte) (*Chart, error) { func LoadData(data []byte) (*Chart, error) {
b := bytes.NewBuffer(data) return LoadDataFromReader(bytes.NewBuffer(data))
unzipped, err := gzip.NewReader(b)
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
} }
// Load loads a chart from a chart archive. // Load loads a chart from a chart archive.
...@@ -281,7 +264,11 @@ func Load(archive string) (*Chart, error) { ...@@ -281,7 +264,11 @@ func Load(archive string) (*Chart, error) {
} }
defer raw.Close() defer raw.Close()
unzipped, err := gzip.NewReader(raw) return LoadDataFromReader(raw)
}
func LoadDataFromReader(r io.Reader) (*Chart, error) {
unzipped, err := gzip.NewReader(r)
if err != nil { if err != nil {
return nil, err return nil, err
} }
...@@ -367,3 +354,65 @@ func loadTar(r *tar.Reader) (*tarChart, error) { ...@@ -367,3 +354,65 @@ func loadTar(r *tar.Reader) (*tarChart, error) {
return c, nil return c, nil
} }
// ChartMember is a file in a chart.
type ChartMember struct {
Path string `json:"path"` // Path from the root of the chart.
Content []byte `json:"content"` // Base64 encoded content.
}
// LoadTemplates loads the members of TemplatesDir().
func (c *Chart) LoadTemplates() ([]*ChartMember, error) {
dir := c.TemplatesDir()
return c.loadDirectory(dir)
}
// loadDirectory loads the members of a directory.
func (c *Chart) loadDirectory(dir string) ([]*ChartMember, error) {
files, err := ioutil.ReadDir(dir)
if err != nil {
return nil, err
}
members := []*ChartMember{}
for _, file := range files {
filename := filepath.Join(dir, file.Name())
member, err := c.loadMember(filename)
if err != nil {
return nil, err
}
members = append(members, member)
}
return members, nil
}
// path is from the root of the chart.
func (c *Chart) LoadMember(path string) (*ChartMember, error) {
filename := filepath.Join(c.loader.dir(), path)
return c.loadMember(filename)
}
// loadMember loads and base 64 encodes a file.
func (c *Chart) loadMember(filename string) (*ChartMember, error) {
dir := c.Dir()
if !strings.HasPrefix(filename, dir) {
err := fmt.Errorf("File %s is outside chart directory %s", filename, dir)
return nil, err
}
b, err := ioutil.ReadFile(filename)
if err != nil {
return nil, err
}
path := strings.TrimPrefix(filename, dir)
content := base64.StdEncoding.EncodeToString(b)
result := &ChartMember{
Path: path,
Content: []byte(content),
}
return result, nil
}
...@@ -17,6 +17,8 @@ limitations under the License. ...@@ -17,6 +17,8 @@ limitations under the License.
package chart package chart
import ( import (
"encoding/base64"
"fmt"
"io/ioutil" "io/ioutil"
"path/filepath" "path/filepath"
"testing" "testing"
...@@ -30,6 +32,7 @@ const ( ...@@ -30,6 +32,7 @@ const (
testarchive = "testdata/frobnitz-0.0.1.tgz" testarchive = "testdata/frobnitz-0.0.1.tgz"
testill = "testdata/ill-1.2.3.tgz" testill = "testdata/ill-1.2.3.tgz"
testnochart = "testdata/nochart.tgz" testnochart = "testdata/nochart.tgz"
testmember = "templates/wordpress.jinja"
) )
// Type canaries. If these fail, they will fail at compile time. // Type canaries. If these fail, they will fail at compile time.
...@@ -160,3 +163,91 @@ func TestChart(t *testing.T) { ...@@ -160,3 +163,91 @@ func TestChart(t *testing.T) {
t.Errorf("Unexpectedly, icon is in %s", i) t.Errorf("Unexpectedly, icon is in %s", i)
} }
} }
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 %s 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 []*ChartMember) error {
for _, member := range members {
if member.Path == path {
filename := filepath.Join(root, path)
if err := compareContent(filename, string(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, string(member.Content)); err != nil {
t.Fatal(err)
}
}
func compareContent(filename, content string) error {
b, err := ioutil.ReadFile(filename)
if err != nil {
return fmt.Errorf("Cannot read test file %s: %s", filename, err)
}
compare := base64.StdEncoding.EncodeToString(b)
if content != compare {
return fmt.Errorf("Expected member content\n%v\ngot\n%v", []byte(compare), []byte(content))
}
return nil
}
...@@ -34,6 +34,8 @@ type Chartfile struct { ...@@ -34,6 +34,8 @@ type Chartfile struct {
Home string `yaml:"home"` Home string `yaml:"home"`
Dependencies []*Dependency `yaml:"dependencies,omitempty"` Dependencies []*Dependency `yaml:"dependencies,omitempty"`
Environment []*EnvConstraint `yaml:"environment,omitempty"` Environment []*EnvConstraint `yaml:"environment,omitempty"`
Expander *Expander `yaml:"expander,omitempty"`
Schema string `yaml:"schema,omitempty"`
} }
// Maintainer describes a chart maintainer. // Maintainer describes a chart maintainer.
...@@ -57,6 +59,14 @@ type EnvConstraint struct { ...@@ -57,6 +59,14 @@ type EnvConstraint struct {
APIGroups []string `yaml:"apiGroups,omitempty"` APIGroups []string `yaml:"apiGroups,omitempty"`
} }
// Expander controls how template/ is evaluated.
type Expander struct {
// Currently just Expandybird or GoTemplate
Name string `json:"name"`
// During evaluation, which file to start from.
Entrypoint string `json:"entrypoint"`
}
// LoadChartfile loads a Chart.yaml file into a *Chart. // LoadChartfile loads a Chart.yaml file into a *Chart.
func LoadChartfile(filename string) (*Chartfile, error) { func LoadChartfile(filename string) (*Chartfile, error) {
b, err := ioutil.ReadFile(filename) b, err := ioutil.ReadFile(filename)
......
...@@ -50,6 +50,23 @@ func TestLoadChartfile(t *testing.T) { ...@@ -50,6 +50,23 @@ func TestLoadChartfile(t *testing.T) {
if f.Source[0] != "https://example.com/foo/bar" { if f.Source[0] != "https://example.com/foo/bar" {
t.Errorf("Expected https://example.com/foo/bar, got %s", f.Source) t.Errorf("Expected https://example.com/foo/bar, got %s", f.Source)
} }
expander := f.Expander
if expander == nil {
t.Errorf("No expander found in %s", testfile)
} else {
if expander.Name != "Expandybird" {
t.Errorf("Expected expander name Expandybird, got %s", expander.Name)
}
if expander.Entrypoint != "templates/wordpress.jinja" {
t.Errorf("Expected expander entrypoint templates/wordpress.jinja, got %s", expander.Entrypoint)
}
}
if f.Schema != "wordpress.jinja.schema" {
t.Errorf("Expected schema wordpress.jinja.schema, got %s", f.Schema)
}
} }
func TestVersionOK(t *testing.T) { func TestVersionOK(t *testing.T) {
......
...@@ -26,3 +26,8 @@ environment: ...@@ -26,3 +26,8 @@ environment:
- extensions/v1beta1/daemonset - extensions/v1beta1/daemonset
apiGroups: apiGroups:
- 3rdParty - 3rdParty
expander:
name: Expandybird
entrypoint: templates/wordpress.jinja
schema: wordpress.jinja.schema
\ No newline at end of file
...@@ -97,28 +97,6 @@ type Manifest struct { ...@@ -97,28 +97,6 @@ type Manifest struct {
Layout *Layout `json:"layout,omitempty"` Layout *Layout `json:"layout,omitempty"`
} }
// Expander controls how template/ is evaluated.
type Expander struct {
// Currently just Expandybird or GoTemplate
Name string `json:"name"`
// During evaluation, which file to start from.
Entrypoint string `json:"entry_point"`
}
// ChartFile is a file in a chart that is not chart.yaml.
type ChartFile struct {
Path string `json:"path"` // Path from the root of the chart.
Content string `json:"content"` // Base64 encoded file content.
}
// Chart is our internal representation of the chart.yaml (in structured form) + all supporting files.
type Chart struct {
Name string `json:"name"`
Expander *Expander `json:"expander"`
Schema interface{} `json:"schema"`
Files []*ChartFile `json:"files"`
}
// Template describes a set of resources to be deployed. // Template describes a set of resources to be deployed.
// Manager expands a Template into a Configuration, which // Manager expands a Template into a Configuration, which
// describes the set in a form that can be instantiated. // describes the set in a form that can be instantiated.
...@@ -227,10 +205,10 @@ type Registry struct { ...@@ -227,10 +205,10 @@ type Registry struct {
CredentialName string `json:"credentialname,omitempty"` // Name of the credential to use CredentialName string `json:"credentialname,omitempty"` // Name of the credential to use
} }
// RegistryType defines the technology that implements the registry // RegistryType defines the technology that implements a registry.
type RegistryType string type RegistryType string
// Constants that identify the supported registry layouts. // Constants that identify the supported registry types.
const ( const (
GithubRegistryType RegistryType = "github" GithubRegistryType RegistryType = "github"
GCSRegistryType RegistryType = "gcs" GCSRegistryType RegistryType = "gcs"
...@@ -256,6 +234,34 @@ const ( ...@@ -256,6 +234,34 @@ const (
OneLevelRegistry RegistryFormat = "onelevel" OneLevelRegistry RegistryFormat = "onelevel"
) )
// RepoType defines the technology that implements a repository.
type RepoType string
// Constants that identify the supported repository types.
const (
GCSRepoType RepoType = "gcs"
)
// RepoFormat is a semi-colon delimited string that describes the format
// of a repository.
type RepoFormat string
const (
// Versioning.
// VersionedRepo identifies a versioned repository, where types appear under versions.
VersionedRepo RepoFormat = "versioned"
// UnversionedRepo identifies an unversioned repository, where types appear under their names.
UnversionedRepo RepoFormat = "unversioned"
// Organization.
// CollectionRepo identfies a collection repository, where types are grouped into collections.
CollectionRepo RepoFormat = "collection"
// OneLevelRepo identifies a one level repository, where all types appear at the top level.
OneLevelRepo RepoFormat = "onelevel"
)
// RegistryService maintains a set of registries that defines the scope of all // RegistryService maintains a set of registries that defines the scope of all
// registry based operations, such as search and type resolution. // registry based operations, such as search and type resolution.
type RegistryService interface { type RegistryService interface {
......
/*
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 repo
import (
"github.com/kubernetes/helm/pkg/chart"
"github.com/kubernetes/helm/pkg/common"
"github.com/kubernetes/helm/pkg/util"
storage "google.golang.org/api/storage/v1"
"fmt"
"net/http"
"net/url"
"regexp"
)
// GCSRepo implements the ObjectStorageRepo interface
// for Google Cloud Storage.
//
// A GCSRepo root must be a directory that contains all the available charts.
type GCSRepo struct {
chartRepo // A GCSRepo is a chartRepo
bucket string
credentialName string
httpClient *http.Client
service *storage.Service
}
// URLFormatMatcher matches the GCS URL format (gs:).
var URLFormatMatcher = regexp.MustCompile("gs://(.*)")
var GCSRepoFormat = common.RepoFormat(fmt.Sprintf("%s;%s", common.UnversionedRepo, common.OneLevelRepo))
// NewGCSRepo creates a GCS repository.
func NewGCSRepo(name, URL string, httpClient *http.Client) (*GCSRepo, error) {
m := URLFormatMatcher.FindStringSubmatch(URL)
if len(m) != 2 {
return nil, fmt.Errorf("URL must be of the form gs://<bucket>, was %s", URL)
}
cr, err := newRepo(name, URL, string(GCSRepoFormat), string(common.GCSRepoType))
if err != nil {
return nil, err
}
if httpClient == nil {
httpClient = http.DefaultClient
}
gs, err := storage.New(httpClient)
if err != nil {
return nil, fmt.Errorf("cannot create storage service for %s: %s", URL, err)
}
result := &GCSRepo{
chartRepo: *cr,
httpClient: httpClient,
service: gs,
bucket: m[1],
}
return result, nil
}
// GetBucket returns the repository bucket.
func (g *GCSRepo) GetBucket() string {
return g.bucket
}
// ListCharts lists charts in this chart repository whose string values conform to the
// supplied regular expression, or all charts, if the regular expression is nil.
func (g *GCSRepo) ListCharts(regex *regexp.Regexp) ([]string, error) {
// List all files in the bucket/prefix that contain the
charts := []string{}
// List all objects in a bucket using pagination
pageToken := ""
for {
call := g.service.Objects.List(g.bucket)
call.Delimiter("/")
if pageToken != "" {
call = call.PageToken(pageToken)
}
res, err := call.Do()
if err != nil {
return nil, err
}
for _, object := range res.Items {
// Charts should be named bucket/chart-X.Y.Z.tgz, so tease apart the version here
m := ChartNameMatcher.FindStringSubmatch(object.Name)
if len(m) != 3 {
continue
}
if regex == nil || regex.MatchString(object.Name) {
charts = append(charts, object.Name)
}
}
if pageToken = res.NextPageToken; pageToken == "" {
break
}
}
return charts, nil
}
// GetChart retrieves, unpacks and returns a chart by name.
func (g *GCSRepo) GetChart(name string) (*chart.Chart, error) {
// Charts should be named bucket/chart-X.Y.Z.tgz, so tease apart the version here
if !ChartNameMatcher.MatchString(name) {
return nil, fmt.Errorf("name must be of the form <name>-<version>.tgz, was %s", name)
}
call := g.service.Objects.Get(g.bucket, name)
object, err := call.Do()
if err != nil {
return nil, err
}
u, err := url.Parse(object.MediaLink)
if err != nil {
return nil, fmt.Errorf("Cannot parse URL %s for chart %s/%s: %s",
object.MediaLink, object.Bucket, object.Name, err)
}
getter := util.NewHTTPClient(3, g.httpClient, util.NewSleeper())
body, code, err := getter.Get(u.String())
if err != nil {
return nil, fmt.Errorf("Cannot fetch URL %s for chart %s/%s: %d %s",
object.MediaLink, object.Bucket, object.Name, code, err)
}
return chart.Load(body)
}
// Do performs an HTTP operation on the receiver's httpClient.
func (g *GCSRepo) Do(req *http.Request) (resp *http.Response, err error) {
return g.httpClient.Do(req)
}
// TODO: Remove GetShortURL when no longer needed.
// GetShortURL returns the URL without the scheme.
func (g GCSRepo) GetShortURL() string {
return util.TrimURLScheme(g.URL)
}
/*
Copyright 2016 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 repo
import (
"github.com/kubernetes/helm/pkg/chart"
"github.com/kubernetes/helm/pkg/common"
"os"
"reflect"
"regexp"
"testing"
)
var (
TestArchiveBucket = os.Getenv("TEST_ARCHIVE_BUCKET")
TestArchiveName = "frobnitz-0.0.1.tgz"
TestChartFile = "testdata/frobnitz/Chart.yaml"
TestShouldFindRegex = regexp.MustCompile(TestArchiveName)
TestShouldNotFindRegex = regexp.MustCompile("foobar")
)
func TestValidGSURL(t *testing.T) {
var validURL = "gs://bucket"
tr, err := NewGCSRepo("testName", validURL, nil)
if err != nil {
t.Fatal(err)
}
wantType := common.GCSRepoType
haveType := tr.GetRepoType()
if haveType != wantType {
t.Fatalf("unexpected repo type; want: %s, have %s.", wantType, haveType)
}
wantFormat := GCSRepoFormat
haveFormat := tr.GetRepoFormat()
if haveFormat != wantFormat {
t.Fatalf("unexpected repo format; want: %s, have %s.", wantFormat, haveFormat)
}
}
func TestInvalidGSURL(t *testing.T) {
var invalidURL = "https://bucket"
_, err := NewGCSRepo("testName", invalidURL, nil)
if err == nil {
t.Fatalf("expected error did not occur for invalid URL")
}
}
func TestListCharts(t *testing.T) {
if TestArchiveBucket != "" {
tr, err := NewGCSRepo("testName", TestArchiveBucket, nil)
if err != nil {
t.Fatal(err)
}
charts, err := tr.ListCharts(nil)
if err != nil {
t.Fatal(err)
}
if len(charts) != 1 {
t.Fatalf("expected one chart in test bucket, got %d", len(charts))
}
name := charts[0]
if name != TestArchiveName {
t.Fatalf("expected chart named %s in test bucket, got %s", TestArchiveName, name)
}
}
}
func TestListChartsWithShouldFindRegex(t *testing.T) {
if TestArchiveBucket != "" {
tr, err := NewGCSRepo("testName", TestArchiveBucket, nil)
if err != nil {
t.Fatal(err)
}
charts, err := tr.ListCharts(TestShouldFindRegex)
if err != nil {
t.Fatal(err)
}
if len(charts) != 1 {
t.Fatalf("expected one chart to match regex, got %d", len(charts))
}
}
}
func TestListChartsWithShouldNotFindRegex(t *testing.T) {
if TestArchiveBucket != "" {
tr, err := NewGCSRepo("testName", TestArchiveBucket, nil)
if err != nil {
t.Fatal(err)
}
charts, err := tr.ListCharts(TestShouldNotFindRegex)
if err != nil {
t.Fatal(err)
}
if len(charts) != 0 {
t.Fatalf("expected zero charts to match regex, got %d", len(charts))
}
}
}
func TestGetChart(t *testing.T) {
if TestArchiveBucket != "" {
tr, err := NewGCSRepo("testName", TestArchiveBucket, nil)
if err != nil {
t.Fatal(err)
}
tc, err := tr.GetChart(TestArchiveName)
if err != nil {
t.Fatal(err)
}
have := tc.Chartfile()
want, err := chart.LoadChartfile(TestChartFile)
if err != nil {
t.Fatal(err)
}
if reflect.DeepEqual(want, have) {
t.Fatalf("retrieved an invalid chart\nwant:%#v\nhave:\n%#v\n", want, have)
}
}
}
func TestGetChartWithInvalidName(t *testing.T) {
var invalidURL = "https://bucket"
_, err := NewGCSRepo("testName", invalidURL, nil)
if err == nil {
t.Fatalf("expected error did not occur for invalid URL")
}
}
/*
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 repo
import (
"github.com/kubernetes/helm/pkg/chart"
"github.com/kubernetes/helm/pkg/common"
"fmt"
"net/url"
"regexp"
)
// ChartRepo abstracts a place that holds charts, which can be
// used in a Deployment Manager configuration. There can be multiple
// ChartRepo implementations.
type ChartRepo interface {
// GetRepoName returns the name of this ChartRepo.
GetRepoName() string
// GetRepoType returns the type of this repo.
GetRepoType() common.RepoType
// GetRepoURL returns the URL to the root of this ChartRepo.
GetRepoURL() string
// GetRepoFormat returns the format of this ChartRepo.
GetRepoFormat() common.RepoFormat
// ListCharts lists charts in this repository whose string values
// conform to the supplied regular expression or all charts if regex is nil
ListCharts(regex *regexp.Regexp) ([]string, error)
// GetChart retrieves, unpacks and returns a chart by name.
GetChart(name string) (*chart.Chart, error)
}
// ObjectStorageRepo abstracts a repository that resides in an Object Storage, for
// example Google Cloud Storage or AWS S3, etc.
type ObjectStorageRepo interface {
ChartRepo // An ObjectStorageRepo is a ChartRepo
GetBucket() string
}
type chartRepo struct {
Name string `json:"name,omitempty"` // The name of this ChartRepo
URL string `json:"url,omitempty"` // The URL to the root of this ChartRepo
Format common.RepoFormat `json:"format,omitempty"` // The format of this ChartRepo
Type common.RepoType `json:"type,omitempty"` // The type of this ChartRepo
}
// ChartNameMatcher matches the chart name format
var ChartNameMatcher = regexp.MustCompile("(.*)-(.*).tgz")
func newRepo(name, URL, format, t string) (*chartRepo, error) {
_, err := url.Parse(URL)
if err != nil {
return nil, fmt.Errorf("invalid URL (%s): %s", URL, err)
}
result := &chartRepo{
Name: name,
URL: URL,
Format: common.RepoFormat(format),
Type: common.RepoType(t),
}
return result, nil
}
// GetRepoName returns the name of this ChartRepo.
func (cr *chartRepo) GetRepoName() string {
return cr.Name
}
// GetRepoType returns the type of this repo.
func (cr *chartRepo) GetRepoType() common.RepoType {
return cr.Type
}
// GetRepoURL returns the URL to the root of this ChartRepo.
func (cr *chartRepo) GetRepoURL() string {
return cr.URL
}
// GetRepoFormat returns the format of this ChartRepo.
func (cr *chartRepo) GetRepoFormat() common.RepoFormat {
return cr.Format
}
/*
Copyright 2016 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 repo
import (
"testing"
)
func TestValidURL(t *testing.T) {
var wantName = "wantName"
var wantType = "wantType"
var validURL = "http://valid/url"
var wantFormat = "wantFormat"
tr, err := newRepo(wantName, validURL, wantFormat, wantType)
if err != nil {
t.Fatal(err)
}
haveName := tr.GetRepoName()
if haveName != wantName {
t.Fatalf("unexpected repo name; want: %s, have %s.", wantName, haveName)
}
haveType := string(tr.GetRepoType())
if haveType != wantType {
t.Fatalf("unexpected repo type; want: %s, have %s.", wantType, haveType)
}
haveURL := tr.GetRepoURL()
if haveURL != validURL {
t.Fatalf("unexpected repo url; want: %s, have %s.", validURL, haveURL)
}
haveFormat := string(tr.GetRepoFormat())
if haveFormat != wantFormat {
t.Fatalf("unexpected repo format; want: %s, have %s.", wantFormat, haveFormat)
}
}
func TestInvalidURL(t *testing.T) {
_, err := newRepo("testName", "%:invalid&url:%", "testFormat", "testType")
if err == nil {
t.Fatalf("expected error did not occur for invalid URL")
}
}
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.
#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
expander:
name: Expandybird
entrypoint: templates/wordpress.jinja
schema: wordpress.jinja.schema
\ No newline at end of file
# 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
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