|
|
|
/*
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
|
|
|
request, err = expansion.ValidateProperties(request)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|