mirror of https://github.com/helm/helm
commit
a2a5564c79
@ -0,0 +1,143 @@
|
||||
/*
|
||||
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 expander
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/Masterminds/sprig"
|
||||
"github.com/cloudfoundry-incubator/candiedyaml"
|
||||
"github.com/ghodss/yaml"
|
||||
"github.com/kubernetes/helm/pkg/expansion"
|
||||
)
|
||||
|
||||
// parseYAMLStream takes an encoded YAML stream and turns it into a slice of JSON-marshalable
|
||||
// objects, one for each document in the stream.
|
||||
func parseYAMLStream(in io.Reader) ([]interface{}, error) {
|
||||
// Use candiedyaml because it's the only one that supports streams.
|
||||
decoder := candiedyaml.NewDecoder(in)
|
||||
var document interface{}
|
||||
stream := []interface{}{}
|
||||
for {
|
||||
err := decoder.Decode(&document)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "Expected document start at line") {
|
||||
return stream, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
// Now it's held in document but we have to do a bit of a dance to get it in a form that can
|
||||
// be marshaled as JSON for our API response. The fundamental problem is that YAML is a
|
||||
// superset of JSON in that it can represent non-string keys, and full IEEE floating point
|
||||
// values (NaN etc). JSON only allows string keys and its definition of a number is based
|
||||
// around a sequence of digits.
|
||||
|
||||
// Kubernetes does not make use of these features, as it uses YAML as just "pretty JSON".
|
||||
// Consequently this does not affect Helm either. However, both candiedyaml and go-yaml
|
||||
// return types that are too wide for JSON marshalling (i.e. map[interface{}]interface{}
|
||||
// instead of map[string]interface{}), so we have to do some explicit conversion. Luckily,
|
||||
// ghodss/yaml has code to help with this, since decoding from YAML to JSON-marshalable
|
||||
// values is exactly the problem that it was designed to solve.
|
||||
|
||||
// 1) Marshal it back to YAML string.
|
||||
yamlBytes, err := candiedyaml.Marshal(document)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 2) Use ghodss/yaml to unmarshal that string into JSON-compatible data structures.
|
||||
var jsonObj interface{}
|
||||
if err := yaml.Unmarshal(yamlBytes, &jsonObj); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Now it's suitable for embedding in an API response.
|
||||
stream = append(stream, jsonObj)
|
||||
}
|
||||
}
|
||||
|
||||
type expander struct {
|
||||
}
|
||||
|
||||
// NewExpander returns an Go Templating expander.
|
||||
func NewExpander() expansion.Expander {
|
||||
return &expander{}
|
||||
}
|
||||
|
||||
// ExpandChart resolves the given files to a sequence of JSON-marshalable values.
|
||||
func (e *expander) ExpandChart(request *expansion.ServiceRequest) (*expansion.ServiceResponse, error) {
|
||||
|
||||
err := expansion.ValidateRequest(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// TODO(dcunnin): Validate via JSONschema.
|
||||
|
||||
chartInv := request.ChartInvocation
|
||||
chartMembers := request.Chart.Members
|
||||
|
||||
resources := []interface{}{}
|
||||
for _, file := range chartMembers {
|
||||
name := file.Path
|
||||
content := file.Content
|
||||
tmpl := template.New(name).Funcs(sprig.HermeticTxtFuncMap())
|
||||
|
||||
for _, otherFile := range chartMembers {
|
||||
otherName := otherFile.Path
|
||||
otherContent := otherFile.Content
|
||||
if name == otherName {
|
||||
continue
|
||||
}
|
||||
_, err := tmpl.Parse(string(otherContent))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Have to put something in that resolves non-empty or Go templates get confused.
|
||||
_, err := tmpl.Parse("# Content begins now")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tmpl, err = tmpl.Parse(string(content))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
generated := bytes.NewBuffer(nil)
|
||||
if err := tmpl.ExecuteTemplate(generated, name, chartInv.Properties); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
stream, err := parseYAMLStream(generated)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s\nContent:\n%s", err.Error(), generated)
|
||||
}
|
||||
|
||||
for _, doc := range stream {
|
||||
resources = append(resources, doc)
|
||||
}
|
||||
}
|
||||
|
||||
return &expansion.ServiceResponse{Resources: resources}, nil
|
||||
}
|
@ -0,0 +1,262 @@
|
||||
/*
|
||||
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 expander
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/kubernetes/helm/pkg/chart"
|
||||
"github.com/kubernetes/helm/pkg/common"
|
||||
"github.com/kubernetes/helm/pkg/expansion"
|
||||
)
|
||||
|
||||
// content provides an easy way to provide file content verbatim in tests.
|
||||
func content(lines []string) []byte {
|
||||
return []byte(strings.Join(lines, "\n") + "\n")
|
||||
}
|
||||
|
||||
func testExpansion(t *testing.T, req *expansion.ServiceRequest,
|
||||
expResponse *expansion.ServiceResponse, expError string) {
|
||||
backend := NewExpander()
|
||||
response, err := backend.ExpandChart(req)
|
||||
if err != nil {
|
||||
message := err.Error()
|
||||
if expResponse != nil || !strings.Contains(message, expError) {
|
||||
t.Fatalf("unexpected error: %s\n", message)
|
||||
}
|
||||
} else {
|
||||
if expResponse == nil {
|
||||
t.Fatalf("expected error did not occur: %s\n", expError)
|
||||
}
|
||||
if !reflect.DeepEqual(response, expResponse) {
|
||||
message := fmt.Sprintf(
|
||||
"want:\n%s\nhave:\n%s\n", expResponse, response)
|
||||
t.Fatalf("output mismatch:\n%s\n", message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var goExpander = &chart.Expander{
|
||||
Name: "GoTemplating",
|
||||
Entrypoint: "templates/main.py",
|
||||
}
|
||||
|
||||
func TestEmpty(t *testing.T) {
|
||||
testExpansion(
|
||||
t,
|
||||
&expansion.ServiceRequest{
|
||||
ChartInvocation: &common.Resource{
|
||||
Name: "test_invocation",
|
||||
Type: "gs://kubernetes-charts-testing/Test-1.2.3.tgz",
|
||||
},
|
||||
Chart: &chart.Content{
|
||||
Chartfile: &chart.Chartfile{
|
||||
Name: "Test",
|
||||
Expander: goExpander,
|
||||
},
|
||||
},
|
||||
},
|
||||
&expansion.ServiceResponse{
|
||||
Resources: []interface{}{},
|
||||
},
|
||||
"", // Error
|
||||
)
|
||||
}
|
||||
|
||||
func TestSingle(t *testing.T) {
|
||||
testExpansion(
|
||||
t,
|
||||
&expansion.ServiceRequest{
|
||||
ChartInvocation: &common.Resource{
|
||||
Name: "test_invocation",
|
||||
Type: "gs://kubernetes-charts-testing/Test-1.2.3.tgz",
|
||||
},
|
||||
Chart: &chart.Content{
|
||||
Chartfile: &chart.Chartfile{
|
||||
Name: "Test",
|
||||
Expander: goExpander,
|
||||
},
|
||||
Members: []*chart.Member{
|
||||
{
|
||||
Path: "templates/main.yaml",
|
||||
Content: content([]string{
|
||||
"name: foo",
|
||||
"type: bar",
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
&expansion.ServiceResponse{
|
||||
Resources: []interface{}{
|
||||
map[string]interface{}{
|
||||
"name": "foo",
|
||||
"type": "bar",
|
||||
},
|
||||
},
|
||||
},
|
||||
"", // Error
|
||||
)
|
||||
}
|
||||
|
||||
func TestProperties(t *testing.T) {
|
||||
testExpansion(
|
||||
t,
|
||||
&expansion.ServiceRequest{
|
||||
ChartInvocation: &common.Resource{
|
||||
Name: "test_invocation",
|
||||
Type: "gs://kubernetes-charts-testing/Test-1.2.3.tgz",
|
||||
Properties: map[string]interface{}{
|
||||
"prop1": 3.0,
|
||||
"prop2": "foo",
|
||||
},
|
||||
},
|
||||
Chart: &chart.Content{
|
||||
Chartfile: &chart.Chartfile{
|
||||
Name: "Test",
|
||||
Expander: goExpander,
|
||||
},
|
||||
Members: []*chart.Member{
|
||||
{
|
||||
Path: "templates/main.yaml",
|
||||
Content: content([]string{
|
||||
"name: foo",
|
||||
"type: {{ .prop2 }}",
|
||||
"properties:",
|
||||
" something: {{ .prop1 }}",
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
&expansion.ServiceResponse{
|
||||
Resources: []interface{}{
|
||||
map[string]interface{}{
|
||||
"name": "foo",
|
||||
"properties": map[string]interface{}{
|
||||
"something": 3.0,
|
||||
},
|
||||
"type": "foo",
|
||||
},
|
||||
},
|
||||
},
|
||||
"", // Error
|
||||
)
|
||||
}
|
||||
|
||||
func TestComplex(t *testing.T) {
|
||||
testExpansion(
|
||||
t,
|
||||
&expansion.ServiceRequest{
|
||||
ChartInvocation: &common.Resource{
|
||||
Name: "test_invocation",
|
||||
Type: "gs://kubernetes-charts-testing/Test-1.2.3.tgz",
|
||||
Properties: map[string]interface{}{
|
||||
"DatabaseName": "mydb",
|
||||
"NumRepicas": 3,
|
||||
},
|
||||
},
|
||||
Chart: &chart.Content{
|
||||
Chartfile: &chart.Chartfile{
|
||||
Name: "Test",
|
||||
Expander: goExpander,
|
||||
},
|
||||
Members: []*chart.Member{
|
||||
{
|
||||
Path: "templates/bar.tmpl",
|
||||
Content: content([]string{
|
||||
`{{ template "banana" . }}`,
|
||||
}),
|
||||
},
|
||||
{
|
||||
Path: "templates/base.tmpl",
|
||||
Content: content([]string{
|
||||
`{{ define "apple" }}`,
|
||||
`name: Abby`,
|
||||
`kind: Apple`,
|
||||
`dbname: {{default "whatdb" .DatabaseName}}`,
|
||||
`{{ end }}`,
|
||||
``,
|
||||
`{{ define "banana" }}`,
|
||||
`name: Bobby`,
|
||||
`kind: Banana`,
|
||||
`dbname: {{default "whatdb" .DatabaseName}}`,
|
||||
`{{ end }}`,
|
||||
}),
|
||||
},
|
||||
{
|
||||
Path: "templates/foo.tmpl",
|
||||
Content: content([]string{
|
||||
`---`,
|
||||
`foo:`,
|
||||
` bar: baz`,
|
||||
`---`,
|
||||
`{{ template "apple" . }}`,
|
||||
`---`,
|
||||
`{{ template "apple" . }}`,
|
||||
`...`,
|
||||
}),
|
||||
},
|
||||
{
|
||||
Path: "templates/docs.txt",
|
||||
Content: content([]string{
|
||||
`{{/*`,
|
||||
`File contains only a comment.`,
|
||||
`Suitable for documentation within templates/`,
|
||||
`*/}}`,
|
||||
}),
|
||||
},
|
||||
{
|
||||
Path: "templates/docs2.txt",
|
||||
Content: content([]string{
|
||||
`# File contains only a comment.`,
|
||||
`# Suitable for documentation within templates/`,
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
&expansion.ServiceResponse{
|
||||
Resources: []interface{}{
|
||||
map[string]interface{}{
|
||||
"name": "Bobby",
|
||||
"kind": "Banana",
|
||||
"dbname": "mydb",
|
||||
},
|
||||
map[string]interface{}{
|
||||
"foo": map[string]interface{}{
|
||||
"bar": "baz",
|
||||
},
|
||||
},
|
||||
map[string]interface{}{
|
||||
"name": "Abby",
|
||||
"kind": "Apple",
|
||||
"dbname": "mydb",
|
||||
},
|
||||
map[string]interface{}{
|
||||
"name": "Abby",
|
||||
"kind": "Apple",
|
||||
"dbname": "mydb",
|
||||
},
|
||||
},
|
||||
},
|
||||
"", // Error
|
||||
)
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
/*
|
||||
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 main
|
||||
|
||||
import (
|
||||
"github.com/kubernetes/helm/cmd/goexpander/expander"
|
||||
"github.com/kubernetes/helm/pkg/expansion"
|
||||
"github.com/kubernetes/helm/pkg/version"
|
||||
|
||||
"flag"
|
||||
"log"
|
||||
)
|
||||
|
||||
// interface that we are going to listen on
|
||||
var address = flag.String("address", "", "Interface to listen on")
|
||||
|
||||
// port that we are going to listen on
|
||||
var port = flag.Int("port", 8080, "Port to listen on")
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
backend := expander.NewExpander()
|
||||
service := expansion.NewService(*address, *port, backend)
|
||||
log.Printf("Version: %s", version.Version)
|
||||
log.Printf("Listening on http://%s:%d/expand", *address, *port)
|
||||
log.Fatal(service.ListenAndServe())
|
||||
}
|
Loading…
Reference in new issue