feat(tiller): add {{.Capabilities}} object

This adds the {{.Capabilities}} object to the template variables so that
chart authors can write charts that are aware of teh Kubernetes
capabilities of the current cluster.

Closes #1608
pull/1792/head
Matt Butcher 8 years ago
parent a2543f87aa
commit cb0a6c7e07
No known key found for this signature in database
GPG Key ID: DCD5F5E5EF32C345

@ -20,6 +20,11 @@ In the previous section, we use `{{.Release.Name}}` to insert the name of a rele
- `Files`: This provides access to all non-special files in a chart. While you cannot use it to access templates, you can use it to access other files in the chart. See the section _Accessing Files_ for more. - `Files`: This provides access to all non-special files in a chart. While you cannot use it to access templates, you can use it to access other files in the chart. See the section _Accessing Files_ for more.
- `Files.Get` is a function for getting a file by name (`.Files.Get config.ini`) - `Files.Get` is a function for getting a file by name (`.Files.Get config.ini`)
- `Files.GetBytes` is a function for getting the contents of a file as an array of bytes instead of as a string. This is useful for things like images. - `Files.GetBytes` is a function for getting the contents of a file as an array of bytes instead of as a string. This is useful for things like images.
- `Capabilities`: This provides information about what capabilities the Kubernetes cluster supports.
- `Capabilities.APIVersions` is a set of versions.
- `Capabilities.APIVersions.Has $version` indicates whether a version (`batch/v1`) is enabled on the cluster.
- `Capabilities.KubeVersion` provides a way to look up the Kubernetes version. It has the following values: `Major`, `Minor`, `GitVersion`, `GitCommit`, `GitTreeState`, `BuildDate`, `GoVersion`, `Compiler`, and `Platform`.
- `Capabilities.TillerVersion` provides a way to look up the Tiller version. It has the following values: `SemVer`, `GitCommit`, and `GitTreeState`.
The values are available to any top-level template. As we will see later, this does not necessarily mean that they will be available _everywhere_. The values are available to any top-level template. As we will see later, this does not necessarily mean that they will be available _everywhere_.

@ -305,6 +305,10 @@ sensitive_.
files that are present. Files can be accessed using `{{index .Files "file.name"}}` files that are present. Files can be accessed using `{{index .Files "file.name"}}`
or using the `{{.Files.Get name}}` or `{{.Files.GetString name}}` functions. You can or using the `{{.Files.Get name}}` or `{{.Files.GetString name}}` functions. You can
also access the contents of the file as `[]byte` using `{{.Files.GetBytes}}` also access the contents of the file as `[]byte` using `{{.Files.GetBytes}}`
- `Capabilities`: A map-like object that contains information about the versions
of Kubernetes (`{{.Capabilities.KubeVersion}}`, Tiller
(`{{.Capabilities.TillerVersion}}`, and the supported Kubernetes API versions
(`{{.Capabilities.APIVersions.Has "batch/v1"`)
**NOTE:** Any unknown Chart.yaml fields will be dropped. They will not **NOTE:** Any unknown Chart.yaml fields will be dropped. They will not
be accessible inside of the `Chart` object. Thus, Chart.yaml cannot be be accessible inside of the `Chart` object. Thus, Chart.yaml cannot be

@ -0,0 +1,56 @@
/*
Copyright 2017 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 chartutil
import (
tversion "k8s.io/helm/pkg/proto/hapi/version"
"k8s.io/kubernetes/pkg/version"
)
// DefaultVersionSet is the default version set, which includes only Core V1 ("v1").
var DefaultVersionSet = NewVersionSet("v1")
// Capabilities describes the capabilities of the Kubernetes cluster that Tiller is attached to.
type Capabilities struct {
// List of all supported API versions
APIVersions VersionSet
// KubeVerison 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
}

@ -0,0 +1,54 @@
/*
Copyright 2017 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 chartutil
import (
"testing"
)
func TestVersionSet(t *testing.T) {
vs := NewVersionSet("v1", "extensions/v1beta1")
if d := len(vs); d != 2 {
t.Errorf("Expected 2 versions, got %d", d)
}
if !vs.Has("extensions/v1beta1") {
t.Error("Expected to find extensions/v1beta1")
}
if vs.Has("Spanish/inquisition") {
t.Error("No one expects the Spanish/inquisition")
}
}
func TestDefaultVersionSet(t *testing.T) {
if !DefaultVersionSet.Has("v1") {
t.Error("Expected core v1 version set")
}
if d := len(DefaultVersionSet); d != 1 {
t.Errorf("Expected only one version, got %d", d)
}
}
func TestCapabilities(t *testing.T) {
cap := Capabilities{
APIVersions: DefaultVersionSet,
}
if !cap.APIVersions.Has("v1") {
t.Error("APIVersions should have v1")
}
}

@ -332,7 +332,20 @@ type ReleaseOptions struct {
} }
// ToRenderValues composes the struct from the data coming from the Releases, Charts and Values files // 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) { 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{}{ top := map[string]interface{}{
"Release": map[string]interface{}{ "Release": map[string]interface{}{
@ -344,8 +357,9 @@ func ToRenderValues(chrt *chart.Chart, chrtVals *chart.Config, options ReleaseOp
"Revision": options.Revision, "Revision": options.Revision,
"Service": "Tiller", "Service": "Tiller",
}, },
"Chart": chrt.Metadata, "Chart": chrt.Metadata,
"Files": NewFiles(chrt.Files), "Files": NewFiles(chrt.Files),
"Capabilities": caps,
} }
vals, err := CoalesceValues(chrt, chrtVals) vals, err := CoalesceValues(chrt, chrtVals)

@ -24,8 +24,11 @@ import (
"text/template" "text/template"
"github.com/golang/protobuf/ptypes/any" "github.com/golang/protobuf/ptypes/any"
"k8s.io/helm/pkg/proto/hapi/chart" "k8s.io/helm/pkg/proto/hapi/chart"
"k8s.io/helm/pkg/timeconv" "k8s.io/helm/pkg/timeconv"
"k8s.io/helm/pkg/version"
kversion "k8s.io/kubernetes/pkg/version"
) )
func TestReadValues(t *testing.T) { func TestReadValues(t *testing.T) {
@ -69,7 +72,7 @@ water:
} }
} }
func TestToRenderValues(t *testing.T) { func TestToRenderValuesCaps(t *testing.T) {
chartValues := ` chartValues := `
name: al Rashid name: al Rashid
@ -108,7 +111,13 @@ where:
Revision: 5, Revision: 5,
} }
res, err := ToRenderValues(c, v, o) caps := &Capabilities{
APIVersions: DefaultVersionSet,
TillerVersion: version.GetVersionProto(),
KubeVersion: &kversion.Info{Major: "1"},
}
res, err := ToRenderValuesCaps(c, v, o, caps)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -125,7 +134,7 @@ where:
t.Errorf("Expected release revision %d, got %q", 5, rev) t.Errorf("Expected release revision %d, got %q", 5, rev)
} }
if relmap["IsUpgrade"].(bool) { if relmap["IsUpgrade"].(bool) {
t.Errorf("Expected upgrade to be false.") t.Error("Expected upgrade to be false.")
} }
if !relmap["IsInstall"].(bool) { if !relmap["IsInstall"].(bool) {
t.Errorf("Expected install to be true.") t.Errorf("Expected install to be true.")
@ -133,6 +142,15 @@ where:
if data := res["Files"].(Files)["scheherazade/shahryar.txt"]; string(data) != "1,001 Nights" { if data := res["Files"].(Files)["scheherazade/shahryar.txt"]; string(data) != "1,001 Nights" {
t.Errorf("Expected file '1,001 Nights', got %q", string(data)) t.Errorf("Expected file '1,001 Nights', got %q", string(data))
} }
if !res["Capabilities"].(*Capabilities).APIVersions.Has("v1") {
t.Error("Expected Capabilities to have v1 as an API")
}
if res["Capabilities"].(*Capabilities).TillerVersion.SemVer == "" {
t.Error("Expected Capabilities to have a Tiller version")
}
if res["Capabilities"].(*Capabilities).KubeVersion.Major != "1" {
t.Error("Expected Capabilities to have a Kube version")
}
var vals Values var vals Values
vals = res["Values"].(Values) vals = res["Values"].(Values)

@ -222,10 +222,11 @@ func recAllTpls(c *chart.Chart, templates map[string]renderable, parentVals char
} }
cvals = map[string]interface{}{ cvals = map[string]interface{}{
"Values": newVals, "Values": newVals,
"Release": parentVals["Release"], "Release": parentVals["Release"],
"Chart": c.Metadata, "Chart": c.Metadata,
"Files": chartutil.NewFiles(c.Files), "Files": chartutil.NewFiles(c.Files),
"Capabilities": parentVals["Capabilities"],
} }
} }

@ -51,7 +51,8 @@ func Templates(linter *support.Linter) {
} }
options := chartutil.ReleaseOptions{Name: "testRelease", Time: timeconv.Now(), Namespace: "testNamespace"} options := chartutil.ReleaseOptions{Name: "testRelease", Time: timeconv.Now(), Namespace: "testNamespace"}
valuesToRender, err := chartutil.ToRenderValues(chart, chart.Values, options) caps := &chartutil.Capabilities{APIVersions: chartutil.DefaultVersionSet}
valuesToRender, err := chartutil.ToRenderValuesCaps(chart, chart.Values, options, caps)
if err != nil { if err != nil {
// FIXME: This seems to generate a duplicate, but I can't find where the first // FIXME: This seems to generate a duplicate, but I can't find where the first
// error is coming from. // error is coming from.

@ -24,6 +24,7 @@ import (
"github.com/ghodss/yaml" "github.com/ghodss/yaml"
"k8s.io/helm/pkg/chartutil"
"k8s.io/helm/pkg/proto/hapi/release" "k8s.io/helm/pkg/proto/hapi/release"
) )
@ -61,21 +62,6 @@ type simpleHead struct {
} `json:"metadata,omitempty"` } `json:"metadata,omitempty"`
} }
type versionSet map[string]struct{}
func newVersionSet(apiVersions ...string) versionSet {
vs := versionSet{}
for _, v := range apiVersions {
vs[v] = struct{}{}
}
return vs
}
func (v versionSet) Has(apiVersion string) bool {
_, ok := v[apiVersion]
return ok
}
// manifest represents a manifest file, which has a name and some content. // manifest represents a manifest file, which has a name and some content.
type manifest struct { type manifest struct {
name string name string
@ -104,7 +90,7 @@ type manifest struct {
// //
// Files that do not parse into the expected format are simply placed into a map and // Files that do not parse into the expected format are simply placed into a map and
// returned. // returned.
func sortManifests(files map[string]string, apis versionSet, sort SortOrder) ([]*release.Hook, []manifest, error) { func sortManifests(files map[string]string, apis chartutil.VersionSet, sort SortOrder) ([]*release.Hook, []manifest, error) {
hs := []*release.Hook{} hs := []*release.Hook{}
generic := []manifest{} generic := []manifest{}

@ -21,6 +21,7 @@ import (
"github.com/ghodss/yaml" "github.com/ghodss/yaml"
"k8s.io/helm/pkg/chartutil"
"k8s.io/helm/pkg/proto/hapi/release" "k8s.io/helm/pkg/proto/hapi/release"
) )
@ -118,7 +119,7 @@ metadata:
manifests[o.path] = o.manifest manifests[o.path] = o.manifest
} }
hs, generic, err := sortManifests(manifests, newVersionSet("v1", "v1beta1"), InstallOrder) hs, generic, err := sortManifests(manifests, chartutil.NewVersionSet("v1", "v1beta1"), InstallOrder)
if err != nil { if err != nil {
t.Fatalf("Unexpected error: %s", err) t.Fatalf("Unexpected error: %s", err)
} }
@ -183,7 +184,7 @@ metadata:
} }
func TestVersionSet(t *testing.T) { func TestVersionSet(t *testing.T) {
vs := newVersionSet("v1", "v1beta1", "extensions/alpha5", "batch/v1") vs := chartutil.NewVersionSet("v1", "v1beta1", "extensions/alpha5", "batch/v1")
if l := len(vs); l != 4 { if l := len(vs); l != 4 {
t.Errorf("Expected 4, got %d", l) t.Errorf("Expected 4, got %d", l)

@ -361,12 +361,16 @@ func (s *ReleaseServer) prepareUpdate(req *services.UpdateReleaseRequest) (*rele
Revision: int(revision), Revision: int(revision),
} }
valuesToRender, err := chartutil.ToRenderValues(req.Chart, req.Values, options) caps, err := capabilities(s.clientset.Discovery())
if err != nil {
return nil, nil, err
}
valuesToRender, err := chartutil.ToRenderValuesCaps(req.Chart, req.Values, options, caps)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
hooks, manifestDoc, notesTxt, err := s.renderResources(req.Chart, valuesToRender) hooks, manifestDoc, notesTxt, err := s.renderResources(req.Chart, valuesToRender, caps.APIVersions)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@ -589,6 +593,23 @@ func (s *ReleaseServer) InstallRelease(c ctx.Context, req *services.InstallRelea
return res, err return res, err
} }
// capabilities builds a Capabilities from discovery information.
func capabilities(disc discovery.DiscoveryInterface) (*chartutil.Capabilities, error) {
sv, err := disc.ServerVersion()
if err != nil {
return nil, err
}
vs, err := getVersionSet(disc)
if err != nil {
return nil, fmt.Errorf("Could not get apiVersions from Kubernetes: %s", err)
}
return &chartutil.Capabilities{
APIVersions: vs,
KubeVersion: sv,
TillerVersion: version.GetVersionProto(),
}, nil
}
// prepareRelease builds a release for an install operation. // prepareRelease builds a release for an install operation.
func (s *ReleaseServer) prepareRelease(req *services.InstallReleaseRequest) (*release.Release, error) { func (s *ReleaseServer) prepareRelease(req *services.InstallReleaseRequest) (*release.Release, error) {
if req.Chart == nil { if req.Chart == nil {
@ -600,6 +621,11 @@ func (s *ReleaseServer) prepareRelease(req *services.InstallReleaseRequest) (*re
return nil, err return nil, err
} }
caps, err := capabilities(s.clientset.Discovery())
if err != nil {
return nil, err
}
revision := 1 revision := 1
ts := timeconv.Now() ts := timeconv.Now()
options := chartutil.ReleaseOptions{ options := chartutil.ReleaseOptions{
@ -609,12 +635,12 @@ func (s *ReleaseServer) prepareRelease(req *services.InstallReleaseRequest) (*re
Revision: revision, Revision: revision,
IsInstall: true, IsInstall: true,
} }
valuesToRender, err := chartutil.ToRenderValues(req.Chart, req.Values, options) valuesToRender, err := chartutil.ToRenderValuesCaps(req.Chart, req.Values, options, caps)
if err != nil { if err != nil {
return nil, err return nil, err
} }
hooks, manifestDoc, notesTxt, err := s.renderResources(req.Chart, valuesToRender) hooks, manifestDoc, notesTxt, err := s.renderResources(req.Chart, valuesToRender, caps.APIVersions)
if err != nil { if err != nil {
// Return a release with partial data so that client can show debugging // Return a release with partial data so that client can show debugging
// information. // information.
@ -659,12 +685,10 @@ func (s *ReleaseServer) prepareRelease(req *services.InstallReleaseRequest) (*re
return rel, err return rel, err
} }
func getVersionSet(client discovery.ServerGroupsInterface) (versionSet, error) { func getVersionSet(client discovery.ServerGroupsInterface) (chartutil.VersionSet, error) {
defVersions := newVersionSet("v1")
groups, err := client.ServerGroups() groups, err := client.ServerGroups()
if err != nil { if err != nil {
return defVersions, err return chartutil.DefaultVersionSet, err
} }
// FIXME: The Kubernetes test fixture for cli appears to always return nil // FIXME: The Kubernetes test fixture for cli appears to always return nil
@ -672,14 +696,14 @@ func getVersionSet(client discovery.ServerGroupsInterface) (versionSet, error) {
// the default API list. This is also a safe value to return in any other // the default API list. This is also a safe value to return in any other
// odd-ball case. // odd-ball case.
if groups == nil { if groups == nil {
return defVersions, nil return chartutil.DefaultVersionSet, nil
} }
versions := unversioned.ExtractGroupVersions(groups) versions := unversioned.ExtractGroupVersions(groups)
return newVersionSet(versions...), nil return chartutil.NewVersionSet(versions...), nil
} }
func (s *ReleaseServer) renderResources(ch *chart.Chart, values chartutil.Values) ([]*release.Hook, *bytes.Buffer, string, error) { func (s *ReleaseServer) renderResources(ch *chart.Chart, values chartutil.Values, vs chartutil.VersionSet) ([]*release.Hook, *bytes.Buffer, string, error) {
renderer := s.engine(ch) renderer := s.engine(ch)
files, err := renderer.Render(ch, values) files, err := renderer.Render(ch, values)
if err != nil { if err != nil {
@ -706,10 +730,6 @@ func (s *ReleaseServer) renderResources(ch *chart.Chart, values chartutil.Values
// Sort hooks, manifests, and partials. Only hooks and manifests are returned, // Sort hooks, manifests, and partials. Only hooks and manifests are returned,
// as partials are not used after renderer.Render. Empty manifests are also // as partials are not used after renderer.Render. Empty manifests are also
// removed here. // removed here.
vs, err := getVersionSet(s.clientset.Discovery())
if err != nil {
return nil, nil, "", fmt.Errorf("Could not get apiVersions from Kubernetes: %s", err)
}
hooks, manifests, err := sortManifests(files, vs, InstallOrder) hooks, manifests, err := sortManifests(files, vs, InstallOrder)
if err != nil { if err != nil {
// By catching parse errors here, we can prevent bogus releases from going // By catching parse errors here, we can prevent bogus releases from going

Loading…
Cancel
Save