From cb0a6c7e0718705a16af4798f717e25754b5320b Mon Sep 17 00:00:00 2001 From: Matt Butcher Date: Thu, 5 Jan 2017 19:06:44 -0700 Subject: [PATCH] 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 --- docs/chart_template_guide/builtin_objects.md | 5 ++ docs/charts.md | 4 ++ pkg/chartutil/capabilities.go | 56 ++++++++++++++++++++ pkg/chartutil/capabilities_test.go | 54 +++++++++++++++++++ pkg/chartutil/values.go | 18 ++++++- pkg/chartutil/values_test.go | 24 +++++++-- pkg/engine/engine.go | 9 ++-- pkg/lint/rules/template.go | 3 +- pkg/tiller/hooks.go | 18 +------ pkg/tiller/hooks_test.go | 5 +- pkg/tiller/release_server.go | 50 +++++++++++------ 11 files changed, 203 insertions(+), 43 deletions(-) create mode 100644 pkg/chartutil/capabilities.go create mode 100644 pkg/chartutil/capabilities_test.go diff --git a/docs/chart_template_guide/builtin_objects.md b/docs/chart_template_guide/builtin_objects.md index 077b2fc30..3c393e7bb 100644 --- a/docs/chart_template_guide/builtin_objects.md +++ b/docs/chart_template_guide/builtin_objects.md @@ -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.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. +- `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_. diff --git a/docs/charts.md b/docs/charts.md index 55382264e..2a119ab48 100644 --- a/docs/charts.md +++ b/docs/charts.md @@ -305,6 +305,10 @@ sensitive_. 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 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 be accessible inside of the `Chart` object. Thus, Chart.yaml cannot be diff --git a/pkg/chartutil/capabilities.go b/pkg/chartutil/capabilities.go new file mode 100644 index 000000000..6b231b447 --- /dev/null +++ b/pkg/chartutil/capabilities.go @@ -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 +} diff --git a/pkg/chartutil/capabilities_test.go b/pkg/chartutil/capabilities_test.go new file mode 100644 index 000000000..ac20f0038 --- /dev/null +++ b/pkg/chartutil/capabilities_test.go @@ -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") + } +} diff --git a/pkg/chartutil/values.go b/pkg/chartutil/values.go index a4cb1a222..499cb89b0 100644 --- a/pkg/chartutil/values.go +++ b/pkg/chartutil/values.go @@ -332,7 +332,20 @@ type ReleaseOptions struct { } // 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{}{ @@ -344,8 +357,9 @@ func ToRenderValues(chrt *chart.Chart, chrtVals *chart.Config, options ReleaseOp "Revision": options.Revision, "Service": "Tiller", }, - "Chart": chrt.Metadata, - "Files": NewFiles(chrt.Files), + "Chart": chrt.Metadata, + "Files": NewFiles(chrt.Files), + "Capabilities": caps, } vals, err := CoalesceValues(chrt, chrtVals) diff --git a/pkg/chartutil/values_test.go b/pkg/chartutil/values_test.go index e7b120924..2c519b099 100644 --- a/pkg/chartutil/values_test.go +++ b/pkg/chartutil/values_test.go @@ -24,8 +24,11 @@ import ( "text/template" "github.com/golang/protobuf/ptypes/any" + "k8s.io/helm/pkg/proto/hapi/chart" "k8s.io/helm/pkg/timeconv" + "k8s.io/helm/pkg/version" + kversion "k8s.io/kubernetes/pkg/version" ) func TestReadValues(t *testing.T) { @@ -69,7 +72,7 @@ water: } } -func TestToRenderValues(t *testing.T) { +func TestToRenderValuesCaps(t *testing.T) { chartValues := ` name: al Rashid @@ -108,7 +111,13 @@ where: 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 { t.Fatal(err) } @@ -125,7 +134,7 @@ where: t.Errorf("Expected release revision %d, got %q", 5, rev) } if relmap["IsUpgrade"].(bool) { - t.Errorf("Expected upgrade to be false.") + t.Error("Expected upgrade to be false.") } if !relmap["IsInstall"].(bool) { 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" { 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 vals = res["Values"].(Values) diff --git a/pkg/engine/engine.go b/pkg/engine/engine.go index 93b0b0b60..34d7ad79d 100644 --- a/pkg/engine/engine.go +++ b/pkg/engine/engine.go @@ -222,10 +222,11 @@ func recAllTpls(c *chart.Chart, templates map[string]renderable, parentVals char } cvals = map[string]interface{}{ - "Values": newVals, - "Release": parentVals["Release"], - "Chart": c.Metadata, - "Files": chartutil.NewFiles(c.Files), + "Values": newVals, + "Release": parentVals["Release"], + "Chart": c.Metadata, + "Files": chartutil.NewFiles(c.Files), + "Capabilities": parentVals["Capabilities"], } } diff --git a/pkg/lint/rules/template.go b/pkg/lint/rules/template.go index fe395a974..5b98d4886 100644 --- a/pkg/lint/rules/template.go +++ b/pkg/lint/rules/template.go @@ -51,7 +51,8 @@ func Templates(linter *support.Linter) { } 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 { // FIXME: This seems to generate a duplicate, but I can't find where the first // error is coming from. diff --git a/pkg/tiller/hooks.go b/pkg/tiller/hooks.go index c03107cbe..1687f2f3b 100644 --- a/pkg/tiller/hooks.go +++ b/pkg/tiller/hooks.go @@ -24,6 +24,7 @@ import ( "github.com/ghodss/yaml" + "k8s.io/helm/pkg/chartutil" "k8s.io/helm/pkg/proto/hapi/release" ) @@ -61,21 +62,6 @@ type simpleHead struct { } `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. type manifest struct { 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 // 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{} generic := []manifest{} diff --git a/pkg/tiller/hooks_test.go b/pkg/tiller/hooks_test.go index f20a1bde6..b9bfed71a 100644 --- a/pkg/tiller/hooks_test.go +++ b/pkg/tiller/hooks_test.go @@ -21,6 +21,7 @@ import ( "github.com/ghodss/yaml" + "k8s.io/helm/pkg/chartutil" "k8s.io/helm/pkg/proto/hapi/release" ) @@ -118,7 +119,7 @@ metadata: 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 { t.Fatalf("Unexpected error: %s", err) } @@ -183,7 +184,7 @@ metadata: } 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 { t.Errorf("Expected 4, got %d", l) diff --git a/pkg/tiller/release_server.go b/pkg/tiller/release_server.go index 30bebe636..01f653be1 100644 --- a/pkg/tiller/release_server.go +++ b/pkg/tiller/release_server.go @@ -361,12 +361,16 @@ func (s *ReleaseServer) prepareUpdate(req *services.UpdateReleaseRequest) (*rele 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 { 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 { return nil, nil, err } @@ -589,6 +593,23 @@ func (s *ReleaseServer) InstallRelease(c ctx.Context, req *services.InstallRelea 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. func (s *ReleaseServer) prepareRelease(req *services.InstallReleaseRequest) (*release.Release, error) { if req.Chart == nil { @@ -600,6 +621,11 @@ func (s *ReleaseServer) prepareRelease(req *services.InstallReleaseRequest) (*re return nil, err } + caps, err := capabilities(s.clientset.Discovery()) + if err != nil { + return nil, err + } + revision := 1 ts := timeconv.Now() options := chartutil.ReleaseOptions{ @@ -609,12 +635,12 @@ func (s *ReleaseServer) prepareRelease(req *services.InstallReleaseRequest) (*re Revision: revision, IsInstall: true, } - valuesToRender, err := chartutil.ToRenderValues(req.Chart, req.Values, options) + valuesToRender, err := chartutil.ToRenderValuesCaps(req.Chart, req.Values, options, caps) if err != nil { 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 { // Return a release with partial data so that client can show debugging // information. @@ -659,12 +685,10 @@ func (s *ReleaseServer) prepareRelease(req *services.InstallReleaseRequest) (*re return rel, err } -func getVersionSet(client discovery.ServerGroupsInterface) (versionSet, error) { - defVersions := newVersionSet("v1") - +func getVersionSet(client discovery.ServerGroupsInterface) (chartutil.VersionSet, error) { groups, err := client.ServerGroups() if err != nil { - return defVersions, err + return chartutil.DefaultVersionSet, err } // 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 // odd-ball case. if groups == nil { - return defVersions, nil + return chartutil.DefaultVersionSet, nil } 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) files, err := renderer.Render(ch, values) 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, // as partials are not used after renderer.Render. Empty manifests are also // 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) if err != nil { // By catching parse errors here, we can prevent bogus releases from going