mirror of
https://github.com/ysoftdevs/gardener-extension-shoot-fleet-agent.git
synced 2026-05-31 02:50:51 +02:00
Initial v1.0.0 commit
This commit is contained in:
+71
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
Copyright The Helm Authors.
|
||||
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 chartutil
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
|
||||
"k8s.io/apimachinery/pkg/version"
|
||||
tversion "k8s.io/helm/pkg/proto/hapi/version"
|
||||
)
|
||||
|
||||
var (
|
||||
// DefaultVersionSet is the default version set, which includes only Core V1 ("v1").
|
||||
DefaultVersionSet = NewVersionSet("v1")
|
||||
|
||||
// DefaultKubeVersion is the default kubernetes version
|
||||
DefaultKubeVersion = &version.Info{
|
||||
Major: "1",
|
||||
Minor: "9",
|
||||
GitVersion: "v1.9.0",
|
||||
GoVersion: runtime.Version(),
|
||||
Compiler: runtime.Compiler,
|
||||
Platform: fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH),
|
||||
}
|
||||
)
|
||||
|
||||
// Capabilities describes the capabilities of the Kubernetes cluster that Tiller is attached to.
|
||||
type Capabilities struct {
|
||||
// APIVersions list of all supported API versions
|
||||
APIVersions VersionSet
|
||||
// KubeVersion is the Kubernetes version
|
||||
KubeVersion *version.Info
|
||||
// TillerVersion is the Tiller version
|
||||
//
|
||||
// This always comes from pkg/version.GetVersionProto().
|
||||
TillerVersion *tversion.Version
|
||||
}
|
||||
|
||||
// VersionSet is a set of Kubernetes API versions.
|
||||
type VersionSet map[string]interface{}
|
||||
|
||||
// NewVersionSet creates a new version set from a list of strings.
|
||||
func NewVersionSet(apiVersions ...string) VersionSet {
|
||||
vs := VersionSet{}
|
||||
for _, v := range apiVersions {
|
||||
vs[v] = struct{}{}
|
||||
}
|
||||
return vs
|
||||
}
|
||||
|
||||
// Has returns true if the version string is in the set.
|
||||
//
|
||||
// vs.Has("extensions/v1beta1")
|
||||
func (v VersionSet) Has(apiVersion string) bool {
|
||||
_, ok := v[apiVersion]
|
||||
return ok
|
||||
}
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
Copyright The Helm Authors.
|
||||
|
||||
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 chartutil
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/ghodss/yaml"
|
||||
|
||||
"k8s.io/helm/pkg/proto/hapi/chart"
|
||||
)
|
||||
|
||||
// ApiVersionV1 is the API version number for version 1.
|
||||
//
|
||||
// This is ApiVersionV1 instead of APIVersionV1 to match the protobuf-generated name.
|
||||
const ApiVersionV1 = "v1" // nolint
|
||||
|
||||
// UnmarshalChartfile takes raw Chart.yaml data and unmarshals it.
|
||||
func UnmarshalChartfile(data []byte) (*chart.Metadata, error) {
|
||||
y := &chart.Metadata{}
|
||||
err := yaml.Unmarshal(data, y)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return y, nil
|
||||
}
|
||||
|
||||
// LoadChartfile loads a Chart.yaml file into a *chart.Metadata.
|
||||
func LoadChartfile(filename string) (*chart.Metadata, error) {
|
||||
b, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return UnmarshalChartfile(b)
|
||||
}
|
||||
|
||||
// SaveChartfile saves the given metadata as a Chart.yaml file at the given path.
|
||||
//
|
||||
// 'filename' should be the complete path and filename ('foo/Chart.yaml')
|
||||
func SaveChartfile(filename string, cf *chart.Metadata) error {
|
||||
out, err := yaml.Marshal(cf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ioutil.WriteFile(filename, out, 0644)
|
||||
}
|
||||
|
||||
// IsChartDir validate a chart directory.
|
||||
//
|
||||
// Checks for a valid Chart.yaml.
|
||||
func IsChartDir(dirName string) (bool, error) {
|
||||
if fi, err := os.Stat(dirName); err != nil {
|
||||
return false, err
|
||||
} else if !fi.IsDir() {
|
||||
return false, fmt.Errorf("%q is not a directory", dirName)
|
||||
}
|
||||
|
||||
chartYaml := filepath.Join(dirName, "Chart.yaml")
|
||||
if _, err := os.Stat(chartYaml); os.IsNotExist(err) {
|
||||
return false, fmt.Errorf("no Chart.yaml exists in directory %q", dirName)
|
||||
}
|
||||
|
||||
chartYamlContent, err := ioutil.ReadFile(chartYaml)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("cannot read Chart.Yaml in directory %q", dirName)
|
||||
}
|
||||
|
||||
chartContent, err := UnmarshalChartfile(chartYamlContent)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if chartContent == nil {
|
||||
return false, errors.New("chart metadata (Chart.yaml) missing")
|
||||
}
|
||||
if chartContent.Name == "" {
|
||||
return false, errors.New("invalid chart (Chart.yaml): name must not be empty")
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
+448
@@ -0,0 +1,448 @@
|
||||
/*
|
||||
Copyright The Helm Authors.
|
||||
|
||||
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 chartutil
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"k8s.io/helm/pkg/proto/hapi/chart"
|
||||
)
|
||||
|
||||
const (
|
||||
// ChartfileName is the default Chart file name.
|
||||
ChartfileName = "Chart.yaml"
|
||||
// ValuesfileName is the default values file name.
|
||||
ValuesfileName = "values.yaml"
|
||||
// TemplatesDir is the relative directory name for templates.
|
||||
TemplatesDir = "templates"
|
||||
// ChartsDir is the relative directory name for charts dependencies.
|
||||
ChartsDir = "charts"
|
||||
// IgnorefileName is the name of the Helm ignore file.
|
||||
IgnorefileName = ".helmignore"
|
||||
// IngressFileName is the name of the example ingress file.
|
||||
IngressFileName = "ingress.yaml"
|
||||
// DeploymentName is the name of the example deployment file.
|
||||
DeploymentName = "deployment.yaml"
|
||||
// ServiceName is the name of the example service file.
|
||||
ServiceName = "service.yaml"
|
||||
// NotesName is the name of the example NOTES.txt file.
|
||||
NotesName = "NOTES.txt"
|
||||
// HelpersName is the name of the example helpers file.
|
||||
HelpersName = "_helpers.tpl"
|
||||
// TemplatesTestsDir is the relative directory name for templates tests.
|
||||
TemplatesTestsDir = "templates/tests"
|
||||
// TestConnectionName is the name of the example connection test file.
|
||||
TestConnectionName = "test-connection.yaml"
|
||||
)
|
||||
|
||||
const defaultValues = `# Default values for %s.
|
||||
# This is a YAML-formatted file.
|
||||
# Declare variables to be passed into your templates.
|
||||
|
||||
replicaCount: 1
|
||||
|
||||
image:
|
||||
repository: nginx
|
||||
tag: stable
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
nameOverride: ""
|
||||
fullnameOverride: ""
|
||||
|
||||
service:
|
||||
type: ClusterIP
|
||||
port: 80
|
||||
|
||||
ingress:
|
||||
enabled: false
|
||||
annotations: {}
|
||||
# kubernetes.io/ingress.class: nginx
|
||||
# kubernetes.io/tls-acme: "true"
|
||||
hosts:
|
||||
- host: chart-example.local
|
||||
paths: []
|
||||
|
||||
tls: []
|
||||
# - secretName: chart-example-tls
|
||||
# hosts:
|
||||
# - chart-example.local
|
||||
|
||||
resources: {}
|
||||
# We usually recommend not to specify default resources and to leave this as a conscious
|
||||
# choice for the user. This also increases chances charts run on environments with little
|
||||
# resources, such as Minikube. If you do want to specify resources, uncomment the following
|
||||
# lines, adjust them as necessary, and remove the curly braces after 'resources:'.
|
||||
# limits:
|
||||
# cpu: 100m
|
||||
# memory: 128Mi
|
||||
# requests:
|
||||
# cpu: 100m
|
||||
# memory: 128Mi
|
||||
|
||||
nodeSelector: {}
|
||||
|
||||
tolerations: []
|
||||
|
||||
affinity: {}
|
||||
`
|
||||
|
||||
const defaultIgnore = `# Patterns to ignore when building packages.
|
||||
# This supports shell glob matching, relative path matching, and
|
||||
# negation (prefixed with !). Only one pattern per line.
|
||||
.DS_Store
|
||||
# Common VCS dirs
|
||||
.git/
|
||||
.gitignore
|
||||
.bzr/
|
||||
.bzrignore
|
||||
.hg/
|
||||
.hgignore
|
||||
.svn/
|
||||
# Common backup files
|
||||
*.swp
|
||||
*.bak
|
||||
*.tmp
|
||||
*~
|
||||
# Various IDEs
|
||||
.project
|
||||
.idea/
|
||||
*.tmproj
|
||||
.vscode/
|
||||
`
|
||||
|
||||
const defaultIngress = `{{- if .Values.ingress.enabled -}}
|
||||
{{- $fullName := include "<CHARTNAME>.fullname" . -}}
|
||||
apiVersion: extensions/v1beta1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: {{ $fullName }}
|
||||
labels:
|
||||
app.kubernetes.io/name: {{ include "<CHARTNAME>.name" . }}
|
||||
helm.sh/chart: {{ include "<CHARTNAME>.chart" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
{{- with .Values.ingress.annotations }}
|
||||
annotations:
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
{{- if .Values.ingress.tls }}
|
||||
tls:
|
||||
{{- range .Values.ingress.tls }}
|
||||
- hosts:
|
||||
{{- range .hosts }}
|
||||
- {{ . | quote }}
|
||||
{{- end }}
|
||||
secretName: {{ .secretName }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
rules:
|
||||
{{- range .Values.ingress.hosts }}
|
||||
- host: {{ .host | quote }}
|
||||
http:
|
||||
paths:
|
||||
{{- range .paths }}
|
||||
- path: {{ . }}
|
||||
backend:
|
||||
serviceName: {{ $fullName }}
|
||||
servicePort: http
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
`
|
||||
|
||||
const defaultDeployment = `apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ include "<CHARTNAME>.fullname" . }}
|
||||
labels:
|
||||
app.kubernetes.io/name: {{ include "<CHARTNAME>.name" . }}
|
||||
helm.sh/chart: {{ include "<CHARTNAME>.chart" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
spec:
|
||||
replicas: {{ .Values.replicaCount }}
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: {{ include "<CHARTNAME>.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: {{ include "<CHARTNAME>.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
spec:
|
||||
containers:
|
||||
- name: {{ .Chart.Name }}
|
||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 80
|
||||
protocol: TCP
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: http
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: http
|
||||
resources:
|
||||
{{- toYaml .Values.resources | nindent 12 }}
|
||||
{{- with .Values.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.affinity }}
|
||||
affinity:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.tolerations }}
|
||||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
`
|
||||
|
||||
const defaultService = `apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ include "<CHARTNAME>.fullname" . }}
|
||||
labels:
|
||||
app.kubernetes.io/name: {{ include "<CHARTNAME>.name" . }}
|
||||
helm.sh/chart: {{ include "<CHARTNAME>.chart" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
spec:
|
||||
type: {{ .Values.service.type }}
|
||||
ports:
|
||||
- port: {{ .Values.service.port }}
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
name: http
|
||||
selector:
|
||||
app.kubernetes.io/name: {{ include "<CHARTNAME>.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
`
|
||||
|
||||
const defaultNotes = `1. Get the application URL by running these commands:
|
||||
{{- if .Values.ingress.enabled }}
|
||||
{{- range $host := .Values.ingress.hosts }}
|
||||
{{- range .paths }}
|
||||
http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ . }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- else if contains "NodePort" .Values.service.type }}
|
||||
export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "<CHARTNAME>.fullname" . }})
|
||||
export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
|
||||
echo http://$NODE_IP:$NODE_PORT
|
||||
{{- else if contains "LoadBalancer" .Values.service.type }}
|
||||
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
|
||||
You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "<CHARTNAME>.fullname" . }}'
|
||||
export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "<CHARTNAME>.fullname" . }} -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
|
||||
echo http://$SERVICE_IP:{{ .Values.service.port }}
|
||||
{{- else if contains "ClusterIP" .Values.service.type }}
|
||||
export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "<CHARTNAME>.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
|
||||
echo "Visit http://127.0.0.1:8080 to use your application"
|
||||
kubectl port-forward $POD_NAME 8080:80
|
||||
{{- end }}
|
||||
`
|
||||
|
||||
const defaultHelpers = `{{/* vim: set filetype=mustache: */}}
|
||||
{{/*
|
||||
Expand the name of the chart.
|
||||
*/}}
|
||||
{{- define "<CHARTNAME>.name" -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Create a default fully qualified app name.
|
||||
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
|
||||
If release name contains chart name it will be used as a full name.
|
||||
*/}}
|
||||
{{- define "<CHARTNAME>.fullname" -}}
|
||||
{{- if .Values.fullnameOverride -}}
|
||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
|
||||
{{- else -}}
|
||||
{{- $name := default .Chart.Name .Values.nameOverride -}}
|
||||
{{- if contains $name .Release.Name -}}
|
||||
{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
|
||||
{{- else -}}
|
||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Create chart name and version as used by the chart label.
|
||||
*/}}
|
||||
{{- define "<CHARTNAME>.chart" -}}
|
||||
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
`
|
||||
|
||||
const defaultTestConnection = `apiVersion: v1
|
||||
kind: Pod
|
||||
metadata:
|
||||
name: "{{ include "<CHARTNAME>.fullname" . }}-test-connection"
|
||||
labels:
|
||||
app.kubernetes.io/name: {{ include "<CHARTNAME>.name" . }}
|
||||
helm.sh/chart: {{ include "<CHARTNAME>.chart" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
annotations:
|
||||
"helm.sh/hook": test-success
|
||||
spec:
|
||||
containers:
|
||||
- name: wget
|
||||
image: busybox
|
||||
command: ['wget']
|
||||
args: ['{{ include "<CHARTNAME>.fullname" . }}:{{ .Values.service.port }}']
|
||||
restartPolicy: Never
|
||||
`
|
||||
|
||||
// CreateFrom creates a new chart, but scaffolds it from the src chart.
|
||||
func CreateFrom(chartfile *chart.Metadata, dest string, src string) error {
|
||||
schart, err := Load(src)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not load %s: %s", src, err)
|
||||
}
|
||||
|
||||
schart.Metadata = chartfile
|
||||
|
||||
var updatedTemplates []*chart.Template
|
||||
|
||||
for _, template := range schart.Templates {
|
||||
newData := Transform(string(template.Data), "<CHARTNAME>", schart.Metadata.Name)
|
||||
updatedTemplates = append(updatedTemplates, &chart.Template{Name: template.Name, Data: newData})
|
||||
}
|
||||
|
||||
schart.Templates = updatedTemplates
|
||||
if schart.Values != nil {
|
||||
schart.Values = &chart.Config{Raw: string(Transform(schart.Values.Raw, "<CHARTNAME>", schart.Metadata.Name))}
|
||||
}
|
||||
return SaveDir(schart, dest)
|
||||
}
|
||||
|
||||
// Create 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 string will point to the newly created directory. It will be
|
||||
// an absolute path, even if the provided base directory was relative.
|
||||
//
|
||||
// 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 *chart.Metadata, dir string) (string, error) {
|
||||
path, err := filepath.Abs(dir)
|
||||
if err != nil {
|
||||
return path, err
|
||||
}
|
||||
|
||||
if fi, err := os.Stat(path); err != nil {
|
||||
return path, err
|
||||
} else if !fi.IsDir() {
|
||||
return path, fmt.Errorf("no such directory %s", path)
|
||||
}
|
||||
|
||||
n := chartfile.Name
|
||||
cdir := filepath.Join(path, n)
|
||||
if fi, err := os.Stat(cdir); err == nil && !fi.IsDir() {
|
||||
return cdir, fmt.Errorf("file %s already exists and is not a directory", cdir)
|
||||
}
|
||||
if err := os.MkdirAll(cdir, 0755); err != nil {
|
||||
return cdir, err
|
||||
}
|
||||
|
||||
cf := filepath.Join(cdir, ChartfileName)
|
||||
if _, err := os.Stat(cf); err != nil {
|
||||
if err := SaveChartfile(cf, chartfile); err != nil {
|
||||
return cdir, err
|
||||
}
|
||||
}
|
||||
|
||||
for _, d := range []string{TemplatesDir, TemplatesTestsDir, ChartsDir} {
|
||||
if err := os.MkdirAll(filepath.Join(cdir, d), 0755); err != nil {
|
||||
return cdir, err
|
||||
}
|
||||
}
|
||||
|
||||
files := []struct {
|
||||
path string
|
||||
content []byte
|
||||
}{
|
||||
{
|
||||
// values.yaml
|
||||
path: filepath.Join(cdir, ValuesfileName),
|
||||
content: []byte(fmt.Sprintf(defaultValues, chartfile.Name)),
|
||||
},
|
||||
{
|
||||
// .helmignore
|
||||
path: filepath.Join(cdir, IgnorefileName),
|
||||
content: []byte(defaultIgnore),
|
||||
},
|
||||
{
|
||||
// ingress.yaml
|
||||
path: filepath.Join(cdir, TemplatesDir, IngressFileName),
|
||||
content: Transform(defaultIngress, "<CHARTNAME>", chartfile.Name),
|
||||
},
|
||||
{
|
||||
// deployment.yaml
|
||||
path: filepath.Join(cdir, TemplatesDir, DeploymentName),
|
||||
content: Transform(defaultDeployment, "<CHARTNAME>", chartfile.Name),
|
||||
},
|
||||
{
|
||||
// service.yaml
|
||||
path: filepath.Join(cdir, TemplatesDir, ServiceName),
|
||||
content: Transform(defaultService, "<CHARTNAME>", chartfile.Name),
|
||||
},
|
||||
{
|
||||
// NOTES.txt
|
||||
path: filepath.Join(cdir, TemplatesDir, NotesName),
|
||||
content: Transform(defaultNotes, "<CHARTNAME>", chartfile.Name),
|
||||
},
|
||||
{
|
||||
// _helpers.tpl
|
||||
path: filepath.Join(cdir, TemplatesDir, HelpersName),
|
||||
content: Transform(defaultHelpers, "<CHARTNAME>", chartfile.Name),
|
||||
},
|
||||
{
|
||||
// test-connection.yaml
|
||||
path: filepath.Join(cdir, TemplatesTestsDir, TestConnectionName),
|
||||
content: Transform(defaultTestConnection, "<CHARTNAME>", chartfile.Name),
|
||||
},
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
if _, err := os.Stat(file.path); err == nil {
|
||||
// File exists and is okay. Skip it.
|
||||
continue
|
||||
}
|
||||
if err := ioutil.WriteFile(file.path, file.content, 0644); err != nil {
|
||||
return cdir, err
|
||||
}
|
||||
}
|
||||
return cdir, nil
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
Copyright The Helm Authors.
|
||||
|
||||
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 chartutil contains tools for working with charts.
|
||||
|
||||
Charts are described in the protocol buffer definition (pkg/proto/hapi/charts).
|
||||
This packe provides utilities for serializing and deserializing charts.
|
||||
|
||||
A chart can be represented on the file system in one of two ways:
|
||||
|
||||
- As a directory that contains a Chart.yaml file and other chart things.
|
||||
- As a tarred gzipped file containing a directory that then contains a
|
||||
Chart.yaml file.
|
||||
|
||||
This package provides utilities for working with those file formats.
|
||||
|
||||
The preferred way of loading a chart is using 'chartutil.Load`:
|
||||
|
||||
chart, err := chartutil.Load(filename)
|
||||
|
||||
This will attempt to discover whether the file at 'filename' is a directory or
|
||||
a chart archive. It will then load accordingly.
|
||||
|
||||
For accepting raw compressed tar file data from an io.Reader, the
|
||||
'chartutil.LoadArchive()' will read in the data, uncompress it, and unpack it
|
||||
into a Chart.
|
||||
|
||||
When creating charts in memory, use the 'k8s.io/helm/pkg/proto/hapi/chart'
|
||||
package directly.
|
||||
*/
|
||||
package chartutil // import "k8s.io/helm/pkg/chartutil"
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
Copyright The Helm Authors.
|
||||
|
||||
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 chartutil
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
securejoin "github.com/cyphar/filepath-securejoin"
|
||||
)
|
||||
|
||||
// Expand uncompresses and extracts a chart into the specified directory.
|
||||
func Expand(dir string, r io.Reader) error {
|
||||
files, err := loadArchiveFiles(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get the name of the chart
|
||||
var chartName string
|
||||
for _, file := range files {
|
||||
if file.Name == "Chart.yaml" {
|
||||
ch, err := UnmarshalChartfile(file.Data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
chartName = ch.GetName()
|
||||
}
|
||||
}
|
||||
if chartName == "" {
|
||||
return errors.New("chart name not specified")
|
||||
}
|
||||
|
||||
// Find the base directory
|
||||
chartdir, err := securejoin.SecureJoin(dir, chartName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Copy all files verbatim. We don't parse these files because parsing can remove
|
||||
// comments.
|
||||
for _, file := range files {
|
||||
outpath, err := securejoin.SecureJoin(chartdir, file.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Make sure the necessary subdirs get created.
|
||||
basedir := filepath.Dir(outpath)
|
||||
if err := os.MkdirAll(basedir, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := ioutil.WriteFile(outpath, file.Data, 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ExpandFile expands the src file into the dest directory.
|
||||
func ExpandFile(dest, src string) error {
|
||||
h, err := os.Open(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer h.Close()
|
||||
return Expand(dest, h)
|
||||
}
|
||||
+236
@@ -0,0 +1,236 @@
|
||||
/*
|
||||
Copyright The Helm Authors.
|
||||
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 chartutil
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/ghodss/yaml"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
"github.com/gobwas/glob"
|
||||
"github.com/golang/protobuf/ptypes/any"
|
||||
)
|
||||
|
||||
// Files is a map of files in a chart that can be accessed from a template.
|
||||
type Files map[string][]byte
|
||||
|
||||
// NewFiles creates a new Files from chart files.
|
||||
// Given an []*any.Any (the format for files in a chart.Chart), extract a map of files.
|
||||
func NewFiles(from []*any.Any) Files {
|
||||
files := map[string][]byte{}
|
||||
for _, f := range from {
|
||||
files[f.TypeUrl] = f.Value
|
||||
}
|
||||
return files
|
||||
}
|
||||
|
||||
// GetBytes gets a file by path.
|
||||
//
|
||||
// The returned data is raw. In a template context, this is identical to calling
|
||||
// {{index .Files $path}}.
|
||||
//
|
||||
// This is intended to be accessed from within a template, so a missed key returns
|
||||
// an empty []byte.
|
||||
func (f Files) GetBytes(name string) []byte {
|
||||
v, ok := f[name]
|
||||
if !ok {
|
||||
return []byte{}
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// Get returns a string representation of the given file.
|
||||
//
|
||||
// Fetch the contents of a file as a string. It is designed to be called in a
|
||||
// template.
|
||||
//
|
||||
// {{.Files.Get "foo"}}
|
||||
func (f Files) Get(name string) string {
|
||||
return string(f.GetBytes(name))
|
||||
}
|
||||
|
||||
// Glob takes a glob pattern and returns another files object only containing
|
||||
// matched files.
|
||||
//
|
||||
// This is designed to be called from a template.
|
||||
//
|
||||
// {{ range $name, $content := .Files.Glob("foo/**") }}
|
||||
// {{ $name }}: |
|
||||
// {{ .Files.Get($name) | indent 4 }}{{ end }}
|
||||
func (f Files) Glob(pattern string) Files {
|
||||
g, err := glob.Compile(pattern, '/')
|
||||
if err != nil {
|
||||
g, _ = glob.Compile("**")
|
||||
}
|
||||
|
||||
nf := NewFiles(nil)
|
||||
for name, contents := range f {
|
||||
if g.Match(name) {
|
||||
nf[name] = contents
|
||||
}
|
||||
}
|
||||
|
||||
return nf
|
||||
}
|
||||
|
||||
// AsConfig turns a Files group and flattens it to a YAML map suitable for
|
||||
// including in the 'data' section of a Kubernetes ConfigMap definition.
|
||||
// Duplicate keys will be overwritten, so be aware that your file names
|
||||
// (regardless of path) should be unique.
|
||||
//
|
||||
// This is designed to be called from a template, and will return empty string
|
||||
// (via ToYaml function) if it cannot be serialized to YAML, or if the Files
|
||||
// object is nil.
|
||||
//
|
||||
// The output will not be indented, so you will want to pipe this to the
|
||||
// 'indent' template function.
|
||||
//
|
||||
// data:
|
||||
// {{ .Files.Glob("config/**").AsConfig() | indent 4 }}
|
||||
func (f Files) AsConfig() string {
|
||||
if f == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
m := map[string]string{}
|
||||
|
||||
// Explicitly convert to strings, and file names
|
||||
for k, v := range f {
|
||||
m[path.Base(k)] = string(v)
|
||||
}
|
||||
|
||||
return ToYaml(m)
|
||||
}
|
||||
|
||||
// AsSecrets returns the base64-encoded value of a Files object suitable for
|
||||
// including in the 'data' section of a Kubernetes Secret definition.
|
||||
// Duplicate keys will be overwritten, so be aware that your file names
|
||||
// (regardless of path) should be unique.
|
||||
//
|
||||
// This is designed to be called from a template, and will return empty string
|
||||
// (via ToYaml function) if it cannot be serialized to YAML, or if the Files
|
||||
// object is nil.
|
||||
//
|
||||
// The output will not be indented, so you will want to pipe this to the
|
||||
// 'indent' template function.
|
||||
//
|
||||
// data:
|
||||
// {{ .Files.Glob("secrets/*").AsSecrets() }}
|
||||
func (f Files) AsSecrets() string {
|
||||
if f == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
m := map[string]string{}
|
||||
|
||||
for k, v := range f {
|
||||
m[path.Base(k)] = base64.StdEncoding.EncodeToString(v)
|
||||
}
|
||||
|
||||
return ToYaml(m)
|
||||
}
|
||||
|
||||
// Lines returns each line of a named file (split by "\n") as a slice, so it can
|
||||
// be ranged over in your templates.
|
||||
//
|
||||
// This is designed to be called from a template.
|
||||
//
|
||||
// {{ range .Files.Lines "foo/bar.html" }}
|
||||
// {{ . }}{{ end }}
|
||||
func (f Files) Lines(path string) []string {
|
||||
if f == nil || f[path] == nil {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
return strings.Split(string(f[path]), "\n")
|
||||
}
|
||||
|
||||
// ToYaml takes an interface, marshals it to yaml, and returns a string. It will
|
||||
// always return a string, even on marshal error (empty string).
|
||||
//
|
||||
// This is designed to be called from a template.
|
||||
func ToYaml(v interface{}) string {
|
||||
data, err := yaml.Marshal(v)
|
||||
if err != nil {
|
||||
// Swallow errors inside of a template.
|
||||
return ""
|
||||
}
|
||||
return string(data)
|
||||
}
|
||||
|
||||
// FromYaml converts a YAML document into a map[string]interface{}.
|
||||
//
|
||||
// This is not a general-purpose YAML parser, and will not parse all valid
|
||||
// YAML documents. Additionally, because its intended use is within templates
|
||||
// it tolerates errors. It will insert the returned error message string into
|
||||
// m["Error"] in the returned map.
|
||||
func FromYaml(str string) map[string]interface{} {
|
||||
m := map[string]interface{}{}
|
||||
|
||||
if err := yaml.Unmarshal([]byte(str), &m); err != nil {
|
||||
m["Error"] = err.Error()
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// ToToml takes an interface, marshals it to toml, and returns a string. It will
|
||||
// always return a string, even on marshal error (empty string).
|
||||
//
|
||||
// This is designed to be called from a template.
|
||||
func ToToml(v interface{}) string {
|
||||
b := bytes.NewBuffer(nil)
|
||||
e := toml.NewEncoder(b)
|
||||
err := e.Encode(v)
|
||||
if err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// ToJson takes an interface, marshals it to json, and returns a string. It will
|
||||
// always return a string, even on marshal error (empty string).
|
||||
//
|
||||
// This is designed to be called from a template.
|
||||
// TODO: change the function signature in Helm 3
|
||||
func ToJson(v interface{}) string { // nolint
|
||||
data, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
// Swallow errors inside of a template.
|
||||
return ""
|
||||
}
|
||||
return string(data)
|
||||
}
|
||||
|
||||
// FromJson converts a JSON document into a map[string]interface{}.
|
||||
//
|
||||
// This is not a general-purpose JSON parser, and will not parse all valid
|
||||
// JSON documents. Additionally, because its intended use is within templates
|
||||
// it tolerates errors. It will insert the returned error message string into
|
||||
// m["Error"] in the returned map.
|
||||
// TODO: change the function signature in Helm 3
|
||||
func FromJson(str string) map[string]interface{} { // nolint
|
||||
m := map[string]interface{}{}
|
||||
|
||||
if err := json.Unmarshal([]byte(str), &m); err != nil {
|
||||
m["Error"] = err.Error()
|
||||
}
|
||||
return m
|
||||
}
|
||||
+328
@@ -0,0 +1,328 @@
|
||||
/*
|
||||
Copyright The Helm Authors.
|
||||
|
||||
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 chartutil
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/golang/protobuf/ptypes/any"
|
||||
|
||||
"k8s.io/helm/pkg/ignore"
|
||||
"k8s.io/helm/pkg/proto/hapi/chart"
|
||||
"k8s.io/helm/pkg/sympath"
|
||||
)
|
||||
|
||||
// Load takes a string name, tries to resolve it to a file or directory, and then loads it.
|
||||
//
|
||||
// This is the preferred way to load a chart. It will discover the chart encoding
|
||||
// and hand off to the appropriate chart reader.
|
||||
//
|
||||
// If a .helmignore file is present, the directory loader will skip loading any files
|
||||
// matching it. But .helmignore is not evaluated when reading out of an archive.
|
||||
func Load(name string) (*chart.Chart, error) {
|
||||
name = filepath.FromSlash(name)
|
||||
fi, err := os.Stat(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if fi.IsDir() {
|
||||
if validChart, err := IsChartDir(name); !validChart {
|
||||
return nil, err
|
||||
}
|
||||
return LoadDir(name)
|
||||
}
|
||||
return LoadFile(name)
|
||||
}
|
||||
|
||||
// BufferedFile represents an archive file buffered for later processing.
|
||||
type BufferedFile struct {
|
||||
Name string
|
||||
Data []byte
|
||||
}
|
||||
|
||||
var drivePathPattern = regexp.MustCompile(`^[a-zA-Z]:/`)
|
||||
|
||||
// loadArchiveFiles loads files out of an archive
|
||||
func loadArchiveFiles(in io.Reader) ([]*BufferedFile, error) {
|
||||
unzipped, err := gzip.NewReader(in)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer unzipped.Close()
|
||||
|
||||
files := []*BufferedFile{}
|
||||
tr := tar.NewReader(unzipped)
|
||||
for {
|
||||
b := bytes.NewBuffer(nil)
|
||||
hd, err := tr.Next()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if hd.FileInfo().IsDir() {
|
||||
// Use this instead of hd.Typeflag because we don't have to do any
|
||||
// inference chasing.
|
||||
continue
|
||||
}
|
||||
|
||||
switch hd.Typeflag {
|
||||
// We don't want to process these extension header files.
|
||||
case tar.TypeXGlobalHeader, tar.TypeXHeader:
|
||||
continue
|
||||
}
|
||||
|
||||
// Archive could contain \ if generated on Windows
|
||||
delimiter := "/"
|
||||
if strings.ContainsRune(hd.Name, '\\') {
|
||||
delimiter = "\\"
|
||||
}
|
||||
|
||||
parts := strings.Split(hd.Name, delimiter)
|
||||
n := strings.Join(parts[1:], delimiter)
|
||||
|
||||
// Normalize the path to the / delimiter
|
||||
n = strings.Replace(n, delimiter, "/", -1)
|
||||
|
||||
if path.IsAbs(n) {
|
||||
return nil, errors.New("chart illegally contains absolute paths")
|
||||
}
|
||||
|
||||
n = path.Clean(n)
|
||||
if n == "." {
|
||||
// In this case, the original path was relative when it should have been absolute.
|
||||
return nil, errors.New("chart illegally contains empty path")
|
||||
}
|
||||
if strings.HasPrefix(n, "..") {
|
||||
return nil, errors.New("chart illegally references parent directory")
|
||||
}
|
||||
|
||||
// In some particularly arcane acts of path creativity, it is possible to intermix
|
||||
// UNIX and Windows style paths in such a way that you produce a result of the form
|
||||
// c:/foo even after all the built-in absolute path checks. So we explicitly check
|
||||
// for this condition.
|
||||
if drivePathPattern.MatchString(n) {
|
||||
return nil, errors.New("chart contains illegally named files")
|
||||
}
|
||||
|
||||
if parts[0] == "Chart.yaml" {
|
||||
return nil, errors.New("chart yaml not in base directory")
|
||||
}
|
||||
|
||||
if _, err := io.Copy(b, tr); err != nil {
|
||||
return files, err
|
||||
}
|
||||
|
||||
files = append(files, &BufferedFile{Name: n, Data: b.Bytes()})
|
||||
b.Reset()
|
||||
}
|
||||
|
||||
if len(files) == 0 {
|
||||
return nil, errors.New("no files in chart archive")
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
|
||||
// LoadArchive loads from a reader containing a compressed tar archive.
|
||||
func LoadArchive(in io.Reader) (*chart.Chart, error) {
|
||||
files, err := loadArchiveFiles(in)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return LoadFiles(files)
|
||||
}
|
||||
|
||||
// LoadFiles loads from in-memory files.
|
||||
func LoadFiles(files []*BufferedFile) (*chart.Chart, error) {
|
||||
c := &chart.Chart{}
|
||||
subcharts := map[string][]*BufferedFile{}
|
||||
|
||||
for _, f := range files {
|
||||
if f.Name == "Chart.yaml" {
|
||||
m, err := UnmarshalChartfile(f.Data)
|
||||
if err != nil {
|
||||
return c, err
|
||||
}
|
||||
c.Metadata = m
|
||||
} else if f.Name == "values.toml" {
|
||||
return c, errors.New("values.toml is illegal as of 2.0.0-alpha.2")
|
||||
} else if f.Name == "values.yaml" {
|
||||
c.Values = &chart.Config{Raw: string(f.Data)}
|
||||
} else if strings.HasPrefix(f.Name, "templates/") {
|
||||
c.Templates = append(c.Templates, &chart.Template{Name: f.Name, Data: f.Data})
|
||||
} else if strings.HasPrefix(f.Name, "charts/") {
|
||||
if filepath.Ext(f.Name) == ".prov" {
|
||||
c.Files = append(c.Files, &any.Any{TypeUrl: f.Name, Value: f.Data})
|
||||
continue
|
||||
}
|
||||
cname := strings.TrimPrefix(f.Name, "charts/")
|
||||
if strings.IndexAny(cname, "._") == 0 {
|
||||
// Ignore charts/ that start with . or _.
|
||||
continue
|
||||
}
|
||||
parts := strings.SplitN(cname, "/", 2)
|
||||
scname := parts[0]
|
||||
subcharts[scname] = append(subcharts[scname], &BufferedFile{Name: cname, Data: f.Data})
|
||||
} else {
|
||||
c.Files = append(c.Files, &any.Any{TypeUrl: f.Name, Value: f.Data})
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure that we got a Chart.yaml file
|
||||
if c.Metadata == nil {
|
||||
return c, errors.New("chart metadata (Chart.yaml) missing")
|
||||
}
|
||||
if c.Metadata.Name == "" {
|
||||
return c, errors.New("invalid chart (Chart.yaml): name must not be empty")
|
||||
}
|
||||
|
||||
for n, files := range subcharts {
|
||||
var sc *chart.Chart
|
||||
var err error
|
||||
if strings.IndexAny(n, "_.") == 0 {
|
||||
continue
|
||||
} else if filepath.Ext(n) == ".tgz" {
|
||||
file := files[0]
|
||||
if file.Name != n {
|
||||
return c, fmt.Errorf("error unpacking tar in %s: expected %s, got %s", c.Metadata.Name, n, file.Name)
|
||||
}
|
||||
// Untar the chart and add to c.Dependencies
|
||||
b := bytes.NewBuffer(file.Data)
|
||||
sc, err = LoadArchive(b)
|
||||
} else {
|
||||
// We have to trim the prefix off of every file, and ignore any file
|
||||
// that is in charts/, but isn't actually a chart.
|
||||
buff := make([]*BufferedFile, 0, len(files))
|
||||
for _, f := range files {
|
||||
parts := strings.SplitN(f.Name, "/", 2)
|
||||
if len(parts) < 2 {
|
||||
continue
|
||||
}
|
||||
f.Name = parts[1]
|
||||
buff = append(buff, f)
|
||||
}
|
||||
sc, err = LoadFiles(buff)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return c, fmt.Errorf("error unpacking %s in %s: %s", n, c.Metadata.Name, err)
|
||||
}
|
||||
|
||||
c.Dependencies = append(c.Dependencies, sc)
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// LoadFile loads from an archive file.
|
||||
func LoadFile(name string) (*chart.Chart, error) {
|
||||
if fi, err := os.Stat(name); err != nil {
|
||||
return nil, err
|
||||
} else if fi.IsDir() {
|
||||
return nil, errors.New("cannot load a directory")
|
||||
}
|
||||
|
||||
raw, err := os.Open(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer raw.Close()
|
||||
|
||||
return LoadArchive(raw)
|
||||
}
|
||||
|
||||
// LoadDir loads from a directory.
|
||||
//
|
||||
// This loads charts only from directories.
|
||||
func LoadDir(dir string) (*chart.Chart, error) {
|
||||
topdir, err := filepath.Abs(dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Just used for errors.
|
||||
c := &chart.Chart{}
|
||||
|
||||
rules := ignore.Empty()
|
||||
ifile := filepath.Join(topdir, ignore.HelmIgnore)
|
||||
if _, err := os.Stat(ifile); err == nil {
|
||||
r, err := ignore.ParseFile(ifile)
|
||||
if err != nil {
|
||||
return c, err
|
||||
}
|
||||
rules = r
|
||||
}
|
||||
rules.AddDefaults()
|
||||
|
||||
files := []*BufferedFile{}
|
||||
topdir += string(filepath.Separator)
|
||||
|
||||
walk := func(name string, fi os.FileInfo, err error) error {
|
||||
n := strings.TrimPrefix(name, topdir)
|
||||
if n == "" {
|
||||
// No need to process top level. Avoid bug with helmignore .* matching
|
||||
// empty names. See issue 1779.
|
||||
return nil
|
||||
}
|
||||
|
||||
// Normalize to / since it will also work on Windows
|
||||
n = filepath.ToSlash(n)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if fi.IsDir() {
|
||||
// Directory-based ignore rules should involve skipping the entire
|
||||
// contents of that directory.
|
||||
if rules.Ignore(n, fi) {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// If a .helmignore file matches, skip this file.
|
||||
if rules.Ignore(n, fi) {
|
||||
return nil
|
||||
}
|
||||
|
||||
data, err := ioutil.ReadFile(name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading %s: %s", n, err)
|
||||
}
|
||||
|
||||
files = append(files, &BufferedFile{Name: n, Data: data})
|
||||
return nil
|
||||
}
|
||||
if err = sympath.Walk(topdir, walk); err != nil {
|
||||
return c, err
|
||||
}
|
||||
|
||||
return LoadFiles(files)
|
||||
}
|
||||
+471
@@ -0,0 +1,471 @@
|
||||
/*
|
||||
Copyright The Helm Authors.
|
||||
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 chartutil
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ghodss/yaml"
|
||||
"k8s.io/helm/pkg/proto/hapi/chart"
|
||||
"k8s.io/helm/pkg/version"
|
||||
)
|
||||
|
||||
const (
|
||||
requirementsName = "requirements.yaml"
|
||||
lockfileName = "requirements.lock"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrRequirementsNotFound indicates that a requirements.yaml is not found.
|
||||
ErrRequirementsNotFound = errors.New(requirementsName + " not found")
|
||||
// ErrLockfileNotFound indicates that a requirements.lock is not found.
|
||||
ErrLockfileNotFound = errors.New(lockfileName + " not found")
|
||||
)
|
||||
|
||||
// Dependency describes a chart upon which another chart depends.
|
||||
//
|
||||
// Dependencies can be used to express developer intent, or to capture the state
|
||||
// of a chart.
|
||||
type Dependency struct {
|
||||
// Name is the name of the dependency.
|
||||
//
|
||||
// This must match the name in the dependency's Chart.yaml.
|
||||
Name string `json:"name"`
|
||||
// Version is the version (range) of this chart.
|
||||
//
|
||||
// A lock file will always produce a single version, while a dependency
|
||||
// may contain a semantic version range.
|
||||
Version string `json:"version,omitempty"`
|
||||
// The URL to the repository.
|
||||
//
|
||||
// Appending `index.yaml` to this string should result in a URL that can be
|
||||
// used to fetch the repository index.
|
||||
Repository string `json:"repository"`
|
||||
// A yaml path that resolves to a boolean, used for enabling/disabling charts (e.g. subchart1.enabled )
|
||||
Condition string `json:"condition,omitempty"`
|
||||
// Tags can be used to group charts for enabling/disabling together
|
||||
Tags []string `json:"tags,omitempty"`
|
||||
// Enabled bool determines if chart should be loaded
|
||||
Enabled bool `json:"enabled,omitempty"`
|
||||
// ImportValues holds the mapping of source values to parent key to be imported. Each item can be a
|
||||
// string or pair of child/parent sublist items.
|
||||
ImportValues []interface{} `json:"import-values,omitempty"`
|
||||
// Alias usable alias to be used for the chart
|
||||
Alias string `json:"alias,omitempty"`
|
||||
}
|
||||
|
||||
// ErrNoRequirementsFile to detect error condition
|
||||
type ErrNoRequirementsFile error
|
||||
|
||||
// Requirements is a list of requirements for a chart.
|
||||
//
|
||||
// Requirements are charts upon which this chart depends. This expresses
|
||||
// developer intent.
|
||||
type Requirements struct {
|
||||
Dependencies []*Dependency `json:"dependencies"`
|
||||
}
|
||||
|
||||
// RequirementsLock is a lock file for requirements.
|
||||
//
|
||||
// It represents the state that the dependencies should be in.
|
||||
type RequirementsLock struct {
|
||||
// Generated is the date the lock file was last generated.
|
||||
Generated time.Time `json:"generated"`
|
||||
// Digest is a hash of the requirements file used to generate it.
|
||||
Digest string `json:"digest"`
|
||||
// Dependencies is the list of dependencies that this lock file has locked.
|
||||
Dependencies []*Dependency `json:"dependencies"`
|
||||
}
|
||||
|
||||
// LoadRequirements loads a requirements file from an in-memory chart.
|
||||
func LoadRequirements(c *chart.Chart) (*Requirements, error) {
|
||||
var data []byte
|
||||
for _, f := range c.Files {
|
||||
if f.TypeUrl == requirementsName {
|
||||
data = f.Value
|
||||
}
|
||||
}
|
||||
if len(data) == 0 {
|
||||
return nil, ErrRequirementsNotFound
|
||||
}
|
||||
r := &Requirements{}
|
||||
return r, yaml.Unmarshal(data, r)
|
||||
}
|
||||
|
||||
// LoadRequirementsLock loads a requirements lock file.
|
||||
func LoadRequirementsLock(c *chart.Chart) (*RequirementsLock, error) {
|
||||
var data []byte
|
||||
for _, f := range c.Files {
|
||||
if f.TypeUrl == lockfileName {
|
||||
data = f.Value
|
||||
}
|
||||
}
|
||||
if len(data) == 0 {
|
||||
return nil, ErrLockfileNotFound
|
||||
}
|
||||
r := &RequirementsLock{}
|
||||
return r, yaml.Unmarshal(data, r)
|
||||
}
|
||||
|
||||
// ProcessRequirementsConditions disables charts based on condition path value in values
|
||||
func ProcessRequirementsConditions(reqs *Requirements, cvals Values) {
|
||||
var cond string
|
||||
var conds []string
|
||||
if reqs == nil || len(reqs.Dependencies) == 0 {
|
||||
return
|
||||
}
|
||||
for _, r := range reqs.Dependencies {
|
||||
var hasTrue, hasFalse bool
|
||||
cond = string(r.Condition)
|
||||
// check for list
|
||||
if len(cond) > 0 {
|
||||
if strings.Contains(cond, ",") {
|
||||
conds = strings.Split(strings.TrimSpace(cond), ",")
|
||||
} else {
|
||||
conds = []string{strings.TrimSpace(cond)}
|
||||
}
|
||||
for _, c := range conds {
|
||||
if len(c) > 0 {
|
||||
// retrieve value
|
||||
vv, err := cvals.PathValue(c)
|
||||
if err == nil {
|
||||
// if not bool, warn
|
||||
if bv, ok := vv.(bool); ok {
|
||||
if bv {
|
||||
hasTrue = true
|
||||
} else {
|
||||
hasFalse = true
|
||||
}
|
||||
} else {
|
||||
log.Printf("Warning: Condition path '%s' for chart %s returned non-bool value", c, r.Name)
|
||||
}
|
||||
} else if _, ok := err.(ErrNoValue); !ok {
|
||||
// this is a real error
|
||||
log.Printf("Warning: PathValue returned error %v", err)
|
||||
|
||||
}
|
||||
if vv != nil {
|
||||
// got first value, break loop
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if !hasTrue && hasFalse {
|
||||
r.Enabled = false
|
||||
} else if hasTrue {
|
||||
r.Enabled = true
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// ProcessRequirementsTags disables charts based on tags in values
|
||||
func ProcessRequirementsTags(reqs *Requirements, cvals Values) {
|
||||
vt, err := cvals.Table("tags")
|
||||
if err != nil {
|
||||
return
|
||||
|
||||
}
|
||||
if reqs == nil || len(reqs.Dependencies) == 0 {
|
||||
return
|
||||
}
|
||||
for _, r := range reqs.Dependencies {
|
||||
if len(r.Tags) > 0 {
|
||||
tags := r.Tags
|
||||
|
||||
var hasTrue, hasFalse bool
|
||||
for _, k := range tags {
|
||||
if b, ok := vt[k]; ok {
|
||||
// if not bool, warn
|
||||
if bv, ok := b.(bool); ok {
|
||||
if bv {
|
||||
hasTrue = true
|
||||
} else {
|
||||
hasFalse = true
|
||||
}
|
||||
} else {
|
||||
log.Printf("Warning: Tag '%s' for chart %s returned non-bool value", k, r.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
if !hasTrue && hasFalse {
|
||||
r.Enabled = false
|
||||
} else if hasTrue || !hasTrue && !hasFalse {
|
||||
r.Enabled = true
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func getAliasDependency(charts []*chart.Chart, aliasChart *Dependency) *chart.Chart {
|
||||
var chartFound chart.Chart
|
||||
for _, existingChart := range charts {
|
||||
if existingChart == nil {
|
||||
continue
|
||||
}
|
||||
if existingChart.Metadata == nil {
|
||||
continue
|
||||
}
|
||||
if existingChart.Metadata.Name != aliasChart.Name {
|
||||
continue
|
||||
}
|
||||
if !version.IsCompatibleRange(aliasChart.Version, existingChart.Metadata.Version) {
|
||||
continue
|
||||
}
|
||||
chartFound = *existingChart
|
||||
newMetadata := *existingChart.Metadata
|
||||
if aliasChart.Alias != "" {
|
||||
newMetadata.Name = aliasChart.Alias
|
||||
}
|
||||
chartFound.Metadata = &newMetadata
|
||||
return &chartFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ProcessRequirementsEnabled removes disabled charts from dependencies
|
||||
func ProcessRequirementsEnabled(c *chart.Chart, v *chart.Config) error {
|
||||
reqs, err := LoadRequirements(c)
|
||||
if err != nil {
|
||||
// if not just missing requirements file, return error
|
||||
if nerr, ok := err.(ErrNoRequirementsFile); !ok {
|
||||
return nerr
|
||||
}
|
||||
|
||||
// no requirements to process
|
||||
return nil
|
||||
}
|
||||
|
||||
var chartDependencies []*chart.Chart
|
||||
// If any dependency is not a part of requirements.yaml
|
||||
// then this should be added to chartDependencies.
|
||||
// However, if the dependency is already specified in requirements.yaml
|
||||
// we should not add it, as it would be anyways processed from requirements.yaml
|
||||
|
||||
for _, existingDependency := range c.Dependencies {
|
||||
var dependencyFound bool
|
||||
for _, req := range reqs.Dependencies {
|
||||
if existingDependency.Metadata.Name == req.Name && version.IsCompatibleRange(req.Version, existingDependency.Metadata.Version) {
|
||||
dependencyFound = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !dependencyFound {
|
||||
chartDependencies = append(chartDependencies, existingDependency)
|
||||
}
|
||||
}
|
||||
|
||||
for _, req := range reqs.Dependencies {
|
||||
if chartDependency := getAliasDependency(c.Dependencies, req); chartDependency != nil {
|
||||
chartDependencies = append(chartDependencies, chartDependency)
|
||||
}
|
||||
if req.Alias != "" {
|
||||
req.Name = req.Alias
|
||||
}
|
||||
}
|
||||
c.Dependencies = chartDependencies
|
||||
|
||||
// set all to true
|
||||
for _, lr := range reqs.Dependencies {
|
||||
lr.Enabled = true
|
||||
}
|
||||
cvals, err := CoalesceValues(c, v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// convert our values back into config
|
||||
yvals, err := cvals.YAML()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cc := chart.Config{Raw: yvals}
|
||||
// flag dependencies as enabled/disabled
|
||||
ProcessRequirementsTags(reqs, cvals)
|
||||
ProcessRequirementsConditions(reqs, cvals)
|
||||
// make a map of charts to remove
|
||||
rm := map[string]bool{}
|
||||
for _, r := range reqs.Dependencies {
|
||||
if !r.Enabled {
|
||||
// remove disabled chart
|
||||
rm[r.Name] = true
|
||||
}
|
||||
}
|
||||
// don't keep disabled charts in new slice
|
||||
cd := []*chart.Chart{}
|
||||
copy(cd, c.Dependencies[:0])
|
||||
for _, n := range c.Dependencies {
|
||||
if _, ok := rm[n.Metadata.Name]; !ok {
|
||||
cd = append(cd, n)
|
||||
}
|
||||
|
||||
}
|
||||
// recursively call self to process sub dependencies
|
||||
for _, t := range cd {
|
||||
err := ProcessRequirementsEnabled(t, &cc)
|
||||
// if its not just missing requirements file, return error
|
||||
if nerr, ok := err.(ErrNoRequirementsFile); !ok && err != nil {
|
||||
return nerr
|
||||
}
|
||||
}
|
||||
c.Dependencies = cd
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// pathToMap creates a nested map given a YAML path in dot notation.
|
||||
func pathToMap(path string, data map[string]interface{}) map[string]interface{} {
|
||||
if path == "." {
|
||||
return data
|
||||
}
|
||||
ap := strings.Split(path, ".")
|
||||
if len(ap) == 0 {
|
||||
return nil
|
||||
}
|
||||
n := []map[string]interface{}{}
|
||||
// created nested map for each key, adding to slice
|
||||
for _, v := range ap {
|
||||
nm := make(map[string]interface{})
|
||||
nm[v] = make(map[string]interface{})
|
||||
n = append(n, nm)
|
||||
}
|
||||
// find the last key (map) and set our data
|
||||
for i, d := range n {
|
||||
for k := range d {
|
||||
z := i + 1
|
||||
if z == len(n) {
|
||||
n[i][k] = data
|
||||
break
|
||||
}
|
||||
n[i][k] = n[z]
|
||||
}
|
||||
}
|
||||
|
||||
return n[0]
|
||||
}
|
||||
|
||||
// getParents returns a slice of parent charts in reverse order.
|
||||
func getParents(c *chart.Chart, out []*chart.Chart) []*chart.Chart {
|
||||
if len(out) == 0 {
|
||||
out = []*chart.Chart{c}
|
||||
}
|
||||
for _, ch := range c.Dependencies {
|
||||
if len(ch.Dependencies) > 0 {
|
||||
out = append(out, ch)
|
||||
out = getParents(ch, out)
|
||||
}
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// processImportValues merges values from child to parent based on the chart's dependencies' ImportValues field.
|
||||
func processImportValues(c *chart.Chart) error {
|
||||
reqs, err := LoadRequirements(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// combine chart values and empty config to get Values
|
||||
cvals, err := CoalesceValues(c, &chart.Config{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b := make(map[string]interface{}, 0)
|
||||
// import values from each dependency if specified in import-values
|
||||
for _, r := range reqs.Dependencies {
|
||||
// only process raw requirement that is found in chart's dependencies (enabled)
|
||||
found := false
|
||||
name := r.Name
|
||||
for _, v := range c.Dependencies {
|
||||
if v.Metadata.Name == r.Name {
|
||||
found = true
|
||||
}
|
||||
if v.Metadata.Name == r.Alias {
|
||||
found = true
|
||||
name = r.Alias
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
continue
|
||||
}
|
||||
if len(r.ImportValues) > 0 {
|
||||
var outiv []interface{}
|
||||
for _, riv := range r.ImportValues {
|
||||
switch iv := riv.(type) {
|
||||
case map[string]interface{}:
|
||||
nm := map[string]string{
|
||||
"child": iv["child"].(string),
|
||||
"parent": iv["parent"].(string),
|
||||
}
|
||||
outiv = append(outiv, nm)
|
||||
s := name + "." + nm["child"]
|
||||
// get child table
|
||||
vv, err := cvals.Table(s)
|
||||
if err != nil {
|
||||
log.Printf("Warning: ImportValues missing table: %v", err)
|
||||
continue
|
||||
}
|
||||
// create value map from child to be merged into parent
|
||||
vm := pathToMap(nm["parent"], vv.AsMap())
|
||||
b = coalesceTables(cvals, vm, c.Metadata.Name)
|
||||
case string:
|
||||
nm := map[string]string{
|
||||
"child": "exports." + iv,
|
||||
"parent": ".",
|
||||
}
|
||||
outiv = append(outiv, nm)
|
||||
s := name + "." + nm["child"]
|
||||
vm, err := cvals.Table(s)
|
||||
if err != nil {
|
||||
log.Printf("Warning: ImportValues missing table: %v", err)
|
||||
continue
|
||||
}
|
||||
b = coalesceTables(b, vm.AsMap(), c.Metadata.Name)
|
||||
}
|
||||
}
|
||||
// set our formatted import values
|
||||
r.ImportValues = outiv
|
||||
}
|
||||
}
|
||||
b = coalesceTables(b, cvals, c.Metadata.Name)
|
||||
y, err := yaml.Marshal(b)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// set the new values
|
||||
c.Values = &chart.Config{Raw: string(y)}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ProcessRequirementsImportValues imports specified chart values from child to parent.
|
||||
func ProcessRequirementsImportValues(c *chart.Chart) error {
|
||||
pc := getParents(c, nil)
|
||||
for i := len(pc) - 1; i >= 0; i-- {
|
||||
processImportValues(pc[i])
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
+227
@@ -0,0 +1,227 @@
|
||||
/*
|
||||
Copyright The Helm Authors.
|
||||
|
||||
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 chartutil
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"compress/gzip"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/ghodss/yaml"
|
||||
|
||||
"k8s.io/helm/pkg/proto/hapi/chart"
|
||||
)
|
||||
|
||||
var headerBytes = []byte("+aHR0cHM6Ly95b3V0dS5iZS96OVV6MWljandyTQo=")
|
||||
|
||||
// SaveDir saves a chart as files in a directory.
|
||||
func SaveDir(c *chart.Chart, dest string) error {
|
||||
// Create the chart directory
|
||||
outdir := filepath.Join(dest, c.Metadata.Name)
|
||||
if err := os.Mkdir(outdir, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Save the chart file.
|
||||
if err := SaveChartfile(filepath.Join(outdir, ChartfileName), c.Metadata); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Save values.yaml
|
||||
if c.Values != nil && len(c.Values.Raw) > 0 {
|
||||
vf := filepath.Join(outdir, ValuesfileName)
|
||||
if err := ioutil.WriteFile(vf, []byte(c.Values.Raw), 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for _, d := range []string{TemplatesDir, ChartsDir, TemplatesTestsDir} {
|
||||
if err := os.MkdirAll(filepath.Join(outdir, d), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Save templates
|
||||
for _, f := range c.Templates {
|
||||
n := filepath.Join(outdir, f.Name)
|
||||
|
||||
d := filepath.Dir(n)
|
||||
if err := os.MkdirAll(d, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := ioutil.WriteFile(n, f.Data, 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Save files
|
||||
for _, f := range c.Files {
|
||||
n := filepath.Join(outdir, f.TypeUrl)
|
||||
|
||||
d := filepath.Dir(n)
|
||||
if err := os.MkdirAll(d, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := ioutil.WriteFile(n, f.Value, 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Save dependencies
|
||||
base := filepath.Join(outdir, ChartsDir)
|
||||
for _, dep := range c.Dependencies {
|
||||
// Here, we write each dependency as a tar file.
|
||||
if _, err := Save(dep, base); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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.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)
|
||||
}
|
||||
|
||||
if c.Metadata == nil {
|
||||
return "", errors.New("no Chart.yaml data")
|
||||
}
|
||||
|
||||
cfile := c.Metadata
|
||||
if cfile.Name == "" {
|
||||
return "", errors.New("no chart name specified (Chart.yaml)")
|
||||
} else if cfile.Version == "" {
|
||||
return "", errors.New("no chart version specified (Chart.yaml)")
|
||||
}
|
||||
|
||||
filename := fmt.Sprintf("%s-%s.tgz", cfile.Name, cfile.Version)
|
||||
filename = filepath.Join(outDir, filename)
|
||||
if stat, err := os.Stat(filepath.Dir(filename)); os.IsNotExist(err) {
|
||||
if err := os.MkdirAll(filepath.Dir(filename), 0755); !os.IsExist(err) {
|
||||
return "", err
|
||||
}
|
||||
} else if !stat.IsDir() {
|
||||
return "", fmt.Errorf("is not a directory: %s", filepath.Dir(filename))
|
||||
}
|
||||
|
||||
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 {
|
||||
os.Remove(filename)
|
||||
}
|
||||
}()
|
||||
|
||||
if err := writeTarContents(twriter, c, ""); err != nil {
|
||||
rollback = true
|
||||
}
|
||||
return filename, err
|
||||
}
|
||||
|
||||
func writeTarContents(out *tar.Writer, c *chart.Chart, prefix string) error {
|
||||
base := filepath.Join(prefix, c.Metadata.Name)
|
||||
|
||||
// Save Chart.yaml
|
||||
cdata, err := yaml.Marshal(c.Metadata)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := writeToTar(out, base+"/Chart.yaml", cdata); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Save values.yaml
|
||||
if c.Values != nil && len(c.Values.Raw) > 0 {
|
||||
if err := writeToTar(out, base+"/values.yaml", []byte(c.Values.Raw)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Save templates
|
||||
for _, f := range c.Templates {
|
||||
n := filepath.Join(base, f.Name)
|
||||
if err := writeToTar(out, n, f.Data); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Save files
|
||||
for _, f := range c.Files {
|
||||
n := filepath.Join(base, f.TypeUrl)
|
||||
if err := writeToTar(out, n, f.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Save dependencies
|
||||
for _, dep := range c.Dependencies {
|
||||
if err := writeTarContents(out, dep, base+"/charts"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// writeToTar writes a single file to a tar archive.
|
||||
func writeToTar(out *tar.Writer, name string, body []byte) error {
|
||||
// TODO: Do we need to create dummy parent directory names if none exist?
|
||||
h := &tar.Header{
|
||||
Name: filepath.ToSlash(name),
|
||||
Mode: 0755,
|
||||
Size: int64(len(body)),
|
||||
ModTime: time.Now(),
|
||||
}
|
||||
if err := out.WriteHeader(h); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := out.Write(body); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
Copyright The Helm Authors.
|
||||
|
||||
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 chartutil
|
||||
|
||||
import "strings"
|
||||
|
||||
// Transform performs a string replacement of the specified source for
|
||||
// a given key with the replacement string
|
||||
func Transform(src string, key string, replacement string) []byte {
|
||||
return []byte(strings.Replace(src, key, replacement, -1))
|
||||
}
|
||||
+451
@@ -0,0 +1,451 @@
|
||||
/*
|
||||
Copyright The Helm Authors.
|
||||
|
||||
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 chartutil
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
"github.com/ghodss/yaml"
|
||||
"github.com/golang/protobuf/ptypes/timestamp"
|
||||
"k8s.io/helm/pkg/proto/hapi/chart"
|
||||
)
|
||||
|
||||
// ErrNoTable indicates that a chart does not have a matching table.
|
||||
type ErrNoTable error
|
||||
|
||||
// ErrNoValue indicates that Values does not contain a key with a value
|
||||
type ErrNoValue error
|
||||
|
||||
// GlobalKey is the name of the Values key that is used for storing global vars.
|
||||
const GlobalKey = "global"
|
||||
|
||||
// Values represents a collection of chart values.
|
||||
type Values map[string]interface{}
|
||||
|
||||
// YAML encodes the Values into a YAML string.
|
||||
func (v Values) YAML() (string, error) {
|
||||
b, err := yaml.Marshal(v)
|
||||
return string(b), err
|
||||
}
|
||||
|
||||
// Table gets a table (YAML subsection) from a Values object.
|
||||
//
|
||||
// The table is returned as a Values.
|
||||
//
|
||||
// Compound table names may be specified with dots:
|
||||
//
|
||||
// foo.bar
|
||||
//
|
||||
// The above will be evaluated as "The table bar inside the table
|
||||
// foo".
|
||||
//
|
||||
// An ErrNoTable is returned if the table does not exist.
|
||||
func (v Values) Table(name string) (Values, error) {
|
||||
names := strings.Split(name, ".")
|
||||
table := v
|
||||
var err error
|
||||
|
||||
for _, n := range names {
|
||||
table, err = tableLookup(table, n)
|
||||
if err != nil {
|
||||
return table, err
|
||||
}
|
||||
}
|
||||
return table, err
|
||||
}
|
||||
|
||||
// AsMap is a utility function for converting Values to a map[string]interface{}.
|
||||
//
|
||||
// It protects against nil map panics.
|
||||
func (v Values) AsMap() map[string]interface{} {
|
||||
if v == nil || len(v) == 0 {
|
||||
return map[string]interface{}{}
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// Encode writes serialized Values information to the given io.Writer.
|
||||
func (v Values) Encode(w io.Writer) error {
|
||||
//return yaml.NewEncoder(w).Encode(v)
|
||||
out, err := yaml.Marshal(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = w.Write(out)
|
||||
return err
|
||||
}
|
||||
|
||||
// MergeInto takes the properties in src and merges them into Values. Maps
|
||||
// are merged while values and arrays are replaced.
|
||||
func (v Values) MergeInto(src Values) {
|
||||
for key, srcVal := range src {
|
||||
destVal, found := v[key]
|
||||
|
||||
if found && istable(srcVal) && istable(destVal) {
|
||||
destMap := destVal.(map[string]interface{})
|
||||
srcMap := srcVal.(map[string]interface{})
|
||||
Values(destMap).MergeInto(Values(srcMap))
|
||||
} else {
|
||||
v[key] = srcVal
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func tableLookup(v Values, simple string) (Values, error) {
|
||||
v2, ok := v[simple]
|
||||
if !ok {
|
||||
return v, ErrNoTable(fmt.Errorf("no table named %q (%v)", simple, v))
|
||||
}
|
||||
if vv, ok := v2.(map[string]interface{}); ok {
|
||||
return vv, nil
|
||||
}
|
||||
|
||||
// This catches a case where a value is of type Values, but doesn't (for some
|
||||
// reason) match the map[string]interface{}. This has been observed in the
|
||||
// wild, and might be a result of a nil map of type Values.
|
||||
if vv, ok := v2.(Values); ok {
|
||||
return vv, nil
|
||||
}
|
||||
|
||||
var e ErrNoTable = fmt.Errorf("no table named %q", simple)
|
||||
return map[string]interface{}{}, e
|
||||
}
|
||||
|
||||
// ReadValues will parse YAML byte data into a Values.
|
||||
func ReadValues(data []byte) (vals Values, err error) {
|
||||
err = yaml.Unmarshal(data, &vals)
|
||||
if len(vals) == 0 {
|
||||
vals = Values{}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// ReadValuesFile will parse a YAML file into a map of values.
|
||||
func ReadValuesFile(filename string) (Values, error) {
|
||||
data, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
return map[string]interface{}{}, err
|
||||
}
|
||||
return ReadValues(data)
|
||||
}
|
||||
|
||||
// CoalesceValues coalesces all of the values in a chart (and its subcharts).
|
||||
//
|
||||
// Values are coalesced together using the following rules:
|
||||
//
|
||||
// - Values in a higher level chart always override values in a lower-level
|
||||
// dependency chart
|
||||
// - Scalar values and arrays are replaced, maps are merged
|
||||
// - A chart has access to all of the variables for it, as well as all of
|
||||
// the values destined for its dependencies.
|
||||
func CoalesceValues(chrt *chart.Chart, vals *chart.Config) (Values, error) {
|
||||
cvals := Values{}
|
||||
// Parse values if not nil. We merge these at the top level because
|
||||
// the passed-in values are in the same namespace as the parent chart.
|
||||
if vals != nil {
|
||||
evals, err := ReadValues([]byte(vals.Raw))
|
||||
if err != nil {
|
||||
return cvals, err
|
||||
}
|
||||
cvals, err = coalesce(chrt, evals)
|
||||
if err != nil {
|
||||
return cvals, err
|
||||
}
|
||||
}
|
||||
|
||||
var err error
|
||||
cvals, err = coalesceDeps(chrt, cvals)
|
||||
return cvals, err
|
||||
}
|
||||
|
||||
// coalesce coalesces the dest values and the chart values, giving priority to the dest values.
|
||||
//
|
||||
// This is a helper function for CoalesceValues.
|
||||
func coalesce(ch *chart.Chart, dest map[string]interface{}) (map[string]interface{}, error) {
|
||||
var err error
|
||||
dest, err = coalesceValues(ch, dest)
|
||||
if err != nil {
|
||||
return dest, err
|
||||
}
|
||||
coalesceDeps(ch, dest)
|
||||
return dest, nil
|
||||
}
|
||||
|
||||
// coalesceDeps coalesces the dependencies of the given chart.
|
||||
func coalesceDeps(chrt *chart.Chart, dest map[string]interface{}) (map[string]interface{}, error) {
|
||||
for _, subchart := range chrt.Dependencies {
|
||||
if c, ok := dest[subchart.Metadata.Name]; !ok {
|
||||
// If dest doesn't already have the key, create it.
|
||||
dest[subchart.Metadata.Name] = map[string]interface{}{}
|
||||
} else if !istable(c) {
|
||||
return dest, fmt.Errorf("type mismatch on %s: %t", subchart.Metadata.Name, c)
|
||||
}
|
||||
if dv, ok := dest[subchart.Metadata.Name]; ok {
|
||||
dvmap := dv.(map[string]interface{})
|
||||
|
||||
// Get globals out of dest and merge them into dvmap.
|
||||
coalesceGlobals(dvmap, dest, chrt.Metadata.Name)
|
||||
|
||||
var err error
|
||||
// Now coalesce the rest of the values.
|
||||
dest[subchart.Metadata.Name], err = coalesce(subchart, dvmap)
|
||||
if err != nil {
|
||||
return dest, err
|
||||
}
|
||||
}
|
||||
}
|
||||
return dest, nil
|
||||
}
|
||||
|
||||
// coalesceGlobals copies the globals out of src and merges them into dest.
|
||||
//
|
||||
// For convenience, returns dest.
|
||||
func coalesceGlobals(dest, src map[string]interface{}, chartName string) map[string]interface{} {
|
||||
var dg, sg map[string]interface{}
|
||||
|
||||
if destglob, ok := dest[GlobalKey]; !ok {
|
||||
dg = map[string]interface{}{}
|
||||
} else if dg, ok = destglob.(map[string]interface{}); !ok {
|
||||
log.Printf("Warning: Skipping globals for chart '%s' because destination '%s' is not a table.", chartName, GlobalKey)
|
||||
return dg
|
||||
}
|
||||
|
||||
if srcglob, ok := src[GlobalKey]; !ok {
|
||||
sg = map[string]interface{}{}
|
||||
} else if sg, ok = srcglob.(map[string]interface{}); !ok {
|
||||
log.Printf("Warning: skipping globals for chart '%s' because source '%s' is not a table.", chartName, GlobalKey)
|
||||
return dg
|
||||
}
|
||||
|
||||
// EXPERIMENTAL: In the past, we have disallowed globals to test tables. This
|
||||
// reverses that decision. It may somehow be possible to introduce a loop
|
||||
// here, but I haven't found a way. So for the time being, let's allow
|
||||
// tables in globals.
|
||||
for key, val := range sg {
|
||||
if istable(val) {
|
||||
vv := copyMap(val.(map[string]interface{}))
|
||||
if destv, ok := dg[key]; ok {
|
||||
if destvmap, ok := destv.(map[string]interface{}); ok {
|
||||
// Basically, we reverse order of coalesce here to merge
|
||||
// top-down.
|
||||
coalesceTables(vv, destvmap, chartName)
|
||||
dg[key] = vv
|
||||
continue
|
||||
} else {
|
||||
log.Printf("Warning: For chart '%s', cannot merge map onto non-map for key '%q'. Skipping.", chartName, key)
|
||||
}
|
||||
} else {
|
||||
// Here there is no merge. We're just adding.
|
||||
dg[key] = vv
|
||||
}
|
||||
} else if dv, ok := dg[key]; ok && istable(dv) {
|
||||
// It's not clear if this condition can actually ever trigger.
|
||||
log.Printf("Warning: For chart '%s', key '%s' is a table. Skipping.", chartName, key)
|
||||
continue
|
||||
}
|
||||
// TODO: Do we need to do any additional checking on the value?
|
||||
dg[key] = val
|
||||
}
|
||||
dest[GlobalKey] = dg
|
||||
return dest
|
||||
}
|
||||
|
||||
func copyMap(src map[string]interface{}) map[string]interface{} {
|
||||
dest := make(map[string]interface{}, len(src))
|
||||
for k, v := range src {
|
||||
dest[k] = v
|
||||
}
|
||||
return dest
|
||||
}
|
||||
|
||||
// coalesceValues builds up a values map for a particular chart.
|
||||
//
|
||||
// Values in v will override the values in the chart.
|
||||
func coalesceValues(c *chart.Chart, v map[string]interface{}) (map[string]interface{}, error) {
|
||||
// If there are no values in the chart, we just return the given values
|
||||
if c.Values == nil || c.Values.Raw == "" {
|
||||
return v, nil
|
||||
}
|
||||
|
||||
nv, err := ReadValues([]byte(c.Values.Raw))
|
||||
if err != nil {
|
||||
// On error, we return just the overridden values.
|
||||
// FIXME: We should log this error. It indicates that the YAML data
|
||||
// did not parse.
|
||||
return v, fmt.Errorf("Error: Reading chart '%s' default values (%s): %s", c.Metadata.Name, c.Values.Raw, err)
|
||||
}
|
||||
|
||||
for key, val := range nv {
|
||||
if value, ok := v[key]; ok {
|
||||
if value == nil {
|
||||
// When the YAML value is null, we remove the value's key.
|
||||
// This allows Helm's various sources of values (value files or --set) to
|
||||
// remove incompatible keys from any previous chart, file, or set values.
|
||||
delete(v, key)
|
||||
} else if dest, ok := value.(map[string]interface{}); ok {
|
||||
// if v[key] is a table, merge nv's val table into v[key].
|
||||
src, ok := val.(map[string]interface{})
|
||||
if !ok {
|
||||
log.Printf("Warning: Building values map for chart '%s'. Skipped value (%+v) for '%s', as it is not a table.", c.Metadata.Name, src, key)
|
||||
continue
|
||||
}
|
||||
// Because v has higher precedence than nv, dest values override src
|
||||
// values.
|
||||
coalesceTables(dest, src, c.Metadata.Name)
|
||||
}
|
||||
} else {
|
||||
// If the key is not in v, copy it from nv.
|
||||
v[key] = val
|
||||
}
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// coalesceTables merges a source map into a destination map.
|
||||
//
|
||||
// dest is considered authoritative.
|
||||
func coalesceTables(dst, src map[string]interface{}, chartName string) map[string]interface{} {
|
||||
// Because dest has higher precedence than src, dest values override src
|
||||
// values.
|
||||
for key, val := range src {
|
||||
if istable(val) {
|
||||
if innerdst, ok := dst[key]; !ok {
|
||||
dst[key] = val
|
||||
} else if istable(innerdst) {
|
||||
coalesceTables(innerdst.(map[string]interface{}), val.(map[string]interface{}), chartName)
|
||||
} else {
|
||||
log.Printf("Warning: Merging destination map for chart '%s'. Cannot overwrite table item '%s', with non table value: %v", chartName, key, val)
|
||||
}
|
||||
continue
|
||||
} else if dv, ok := dst[key]; ok && istable(dv) {
|
||||
log.Printf("Warning: Merging destination map for chart '%s'. The destination item '%s' is a table and ignoring the source '%s' as it has a non-table value of: %v", chartName, key, key, val)
|
||||
continue
|
||||
} else if !ok { // <- ok is still in scope from preceding conditional.
|
||||
dst[key] = val
|
||||
continue
|
||||
}
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// ReleaseOptions represents the additional release options needed
|
||||
// for the composition of the final values struct
|
||||
type ReleaseOptions struct {
|
||||
Name string
|
||||
Time *timestamp.Timestamp
|
||||
Namespace string
|
||||
IsUpgrade bool
|
||||
IsInstall bool
|
||||
Revision int
|
||||
}
|
||||
|
||||
// ToRenderValues composes the struct from the data coming from the Releases, Charts and Values files
|
||||
//
|
||||
// WARNING: This function is deprecated for Helm > 2.1.99 Use ToRenderValuesCaps() instead. It will
|
||||
// remain in the codebase to stay SemVer compliant.
|
||||
//
|
||||
// In Helm 3.0, this will be changed to accept Capabilities as a fourth parameter.
|
||||
func ToRenderValues(chrt *chart.Chart, chrtVals *chart.Config, options ReleaseOptions) (Values, error) {
|
||||
caps := &Capabilities{APIVersions: DefaultVersionSet}
|
||||
return ToRenderValuesCaps(chrt, chrtVals, options, caps)
|
||||
}
|
||||
|
||||
// ToRenderValuesCaps composes the struct from the data coming from the Releases, Charts and Values files
|
||||
//
|
||||
// This takes both ReleaseOptions and Capabilities to merge into the render values.
|
||||
func ToRenderValuesCaps(chrt *chart.Chart, chrtVals *chart.Config, options ReleaseOptions, caps *Capabilities) (Values, error) {
|
||||
|
||||
top := map[string]interface{}{
|
||||
"Release": map[string]interface{}{
|
||||
"Name": options.Name,
|
||||
"Time": options.Time,
|
||||
"Namespace": options.Namespace,
|
||||
"IsUpgrade": options.IsUpgrade,
|
||||
"IsInstall": options.IsInstall,
|
||||
"Revision": options.Revision,
|
||||
"Service": "Tiller",
|
||||
},
|
||||
"Chart": chrt.Metadata,
|
||||
"Files": NewFiles(chrt.Files),
|
||||
"Capabilities": caps,
|
||||
}
|
||||
|
||||
vals, err := CoalesceValues(chrt, chrtVals)
|
||||
if err != nil {
|
||||
return top, err
|
||||
}
|
||||
|
||||
top["Values"] = vals
|
||||
return top, nil
|
||||
}
|
||||
|
||||
// istable is a special-purpose function to see if the present thing matches the definition of a YAML table.
|
||||
func istable(v interface{}) bool {
|
||||
_, ok := v.(map[string]interface{})
|
||||
return ok
|
||||
}
|
||||
|
||||
// PathValue takes a path that traverses a YAML structure and returns the value at the end of that path.
|
||||
// The path starts at the root of the YAML structure and is comprised of YAML keys separated by periods.
|
||||
// Given the following YAML data the value at path "chapter.one.title" is "Loomings".
|
||||
//
|
||||
// chapter:
|
||||
// one:
|
||||
// title: "Loomings"
|
||||
func (v Values) PathValue(ypath string) (interface{}, error) {
|
||||
if len(ypath) == 0 {
|
||||
return nil, errors.New("YAML path string cannot be zero length")
|
||||
}
|
||||
yps := strings.Split(ypath, ".")
|
||||
if len(yps) == 1 {
|
||||
// if exists must be root key not table
|
||||
vals := v.AsMap()
|
||||
k := yps[0]
|
||||
if _, ok := vals[k]; ok && !istable(vals[k]) {
|
||||
// key found
|
||||
return vals[yps[0]], nil
|
||||
}
|
||||
// key not found
|
||||
return nil, ErrNoValue(fmt.Errorf("%v is not a value", k))
|
||||
}
|
||||
// join all elements of YAML path except last to get string table path
|
||||
ypsLen := len(yps)
|
||||
table := yps[:ypsLen-1]
|
||||
st := strings.Join(table, ".")
|
||||
// get the last element as a string key
|
||||
key := yps[ypsLen-1:]
|
||||
sk := string(key[0])
|
||||
// get our table for table path
|
||||
t, err := v.Table(st)
|
||||
if err != nil {
|
||||
//no table
|
||||
return nil, ErrNoValue(fmt.Errorf("%v is not a value", sk))
|
||||
}
|
||||
// check table for key and ensure value is not a table
|
||||
if k, ok := t[sk]; ok && !istable(k) {
|
||||
// key found
|
||||
return k, nil
|
||||
}
|
||||
|
||||
// key not found
|
||||
return nil, ErrNoValue(fmt.Errorf("key not found: %s", sk))
|
||||
}
|
||||
Reference in New Issue
Block a user