From e4e0e320645c75bd7d6fe8f73c4c017e4ba5e245 Mon Sep 17 00:00:00 2001 From: Matt Butcher Date: Thu, 6 Apr 2017 16:50:55 -0600 Subject: [PATCH] feat(tiller): support version constraint on chart This provides the Chart.yaml field `tillerVersion`, which is a semver range. It allows users to choose to constrain a chart to a specific version. The reason for this is that we keep introducing new template functions, but we have no way of saying "this chart will only work with Tiller newer than...". The check on version is _only_ done on Tiller. The client does not check at all, since it does not do any template expansion on its own. --- _proto/hapi/chart/metadata.proto | 6 ++- docs/chart_best_practices/conventions.md | 20 +++++++- docs/charts.md | 1 + pkg/proto/hapi/chart/metadata.pb.go | 50 ++++++++++--------- pkg/tiller/release_server.go | 7 +++ pkg/tiller/release_server_test.go | 61 +++++++++++++++++++++--- pkg/version/compatible.go | 11 +++++ pkg/version/compatible_test.go | 23 +++++++++ 8 files changed, 147 insertions(+), 32 deletions(-) diff --git a/_proto/hapi/chart/metadata.proto b/_proto/hapi/chart/metadata.proto index 4a1a623bb..7416c7706 100644 --- a/_proto/hapi/chart/metadata.proto +++ b/_proto/hapi/chart/metadata.proto @@ -73,7 +73,11 @@ message Metadata { // The version of the application enclosed inside of this chart. string appVersion = 13; - + // Whether or not this chart is deprecated bool deprecated = 14; + + // TillerVersion is a SemVer constraints on what version of Tiller is required. + // See SemVer ranges here: https://github.com/Masterminds/semver#basic-comparisons + string tillerVersion = 15; } diff --git a/docs/chart_best_practices/conventions.md b/docs/chart_best_practices/conventions.md index f9d6ed7ba..324ef88f9 100644 --- a/docs/chart_best_practices/conventions.md +++ b/docs/chart_best_practices/conventions.md @@ -38,4 +38,22 @@ There are a few small conventions followed for using the words Helm, helm, Tille - `tiller` is the name of the binary run on the backend - The term 'chart' does not need to be capitalized, as it is not a proper noun. -When in doubt, use _Helm_ (with an uppercase 'H'). \ No newline at end of file +When in doubt, use _Helm_ (with an uppercase 'H'). + +## Restricting Tiller by Version + +A `Chart.yaml` file can specify a `tillerVersion` SemVer constraint: + +```yaml +name: mychart +version: 0.2.0 +tillerVersion: ">=2.4.0" +``` + +This constraint should be set when templates use a new feature that was not +supported in older versions of Helm. While this parameter will accept sophisticated +SemVer rules, the best practice is to default to the form `>=2.4.0`, where `2.4.0` +is the version that introduced the new feature used in the chart. + +This feature was introduced in Helm 2.4.0, so any version of Tiller older than +2.4.0 will simply ignore this field. diff --git a/docs/charts.md b/docs/charts.md index 414f80bf7..b1aa10ad4 100644 --- a/docs/charts.md +++ b/docs/charts.md @@ -55,6 +55,7 @@ engine: gotpl # The name of the template engine (optional, defaults to gotpl) icon: A URL to an SVG or PNG image to be used as an icon (optional). appVersion: The version of the app that this contains (optional). This needn't be SemVer. deprecated: Whether or not this chart is deprecated (optional, boolean) +tillerVersion: The version of Tiller that this chart requires. This should be expressed as a SemVer range: ">2.0.0" (optional) ``` If you are familiar with the `Chart.yaml` file format for Helm Classic, you will diff --git a/pkg/proto/hapi/chart/metadata.pb.go b/pkg/proto/hapi/chart/metadata.pb.go index 5feee0bf4..f1ee5eb36 100644 --- a/pkg/proto/hapi/chart/metadata.pb.go +++ b/pkg/proto/hapi/chart/metadata.pb.go @@ -79,6 +79,9 @@ type Metadata struct { AppVersion string `protobuf:"bytes,13,opt,name=appVersion" json:"appVersion,omitempty"` // Whether or not this chart is deprecated Deprecated bool `protobuf:"varint,14,opt,name=deprecated" json:"deprecated,omitempty"` + // TillerVersion is a SemVer constraints on what version of Tiller is required. + // See SemVer ranges here: https://github.com/Masterminds/semver#basic-comparisons + TillerVersion string `protobuf:"bytes,15,opt,name=tillerVersion" json:"tillerVersion,omitempty"` } func (m *Metadata) Reset() { *m = Metadata{} } @@ -102,27 +105,28 @@ func init() { func init() { proto.RegisterFile("hapi/chart/metadata.proto", fileDescriptor2) } var fileDescriptor2 = []byte{ - // 339 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x6c, 0x92, 0x4f, 0x4b, 0xeb, 0x40, - 0x14, 0xc5, 0x5f, 0x5e, 0x9a, 0x7f, 0x37, 0xef, 0x3d, 0xca, 0xf0, 0x28, 0xa3, 0x88, 0x84, 0xae, - 0xb2, 0x4a, 0x41, 0x41, 0x5c, 0x0b, 0xe2, 0x42, 0xdb, 0x4a, 0xf0, 0x0f, 0xb8, 0x1b, 0x93, 0x4b, - 0x3b, 0x68, 0x66, 0xc2, 0xcc, 0xa8, 0xf8, 0xe5, 0xfc, 0x6c, 0x32, 0x93, 0xa4, 0xcd, 0xc2, 0xdd, - 0x3d, 0xe7, 0x97, 0x7b, 0x92, 0x73, 0x09, 0x1c, 0x6c, 0x59, 0xcb, 0x17, 0xd5, 0x96, 0x29, 0xb3, - 0x68, 0xd0, 0xb0, 0x9a, 0x19, 0x56, 0xb4, 0x4a, 0x1a, 0x49, 0xc0, 0xa2, 0xc2, 0xa1, 0xf9, 0x19, - 0xc0, 0x92, 0x71, 0x61, 0x18, 0x17, 0xa8, 0x08, 0x81, 0x89, 0x60, 0x0d, 0x52, 0x2f, 0xf3, 0xf2, - 0xa4, 0x74, 0x33, 0xf9, 0x0f, 0x01, 0x36, 0x8c, 0xbf, 0xd2, 0xdf, 0xce, 0xec, 0xc4, 0xfc, 0xcb, - 0x87, 0x78, 0xd9, 0xc7, 0xfe, 0xb8, 0x46, 0x60, 0xb2, 0x95, 0x0d, 0xf6, 0x5b, 0x6e, 0x26, 0x14, - 0x22, 0x2d, 0xdf, 0x54, 0x85, 0x9a, 0xfa, 0x99, 0x9f, 0x27, 0xe5, 0x20, 0x2d, 0x79, 0x47, 0xa5, - 0xb9, 0x14, 0x74, 0xe2, 0x16, 0x06, 0x49, 0x32, 0x48, 0x6b, 0xd4, 0x95, 0xe2, 0xad, 0xb1, 0x34, - 0x70, 0x74, 0x6c, 0x91, 0x43, 0x88, 0x5f, 0xf0, 0xf3, 0x43, 0xaa, 0x5a, 0xd3, 0xd0, 0xc5, 0xee, - 0x34, 0x39, 0x87, 0xb4, 0xd9, 0xd5, 0xd3, 0x34, 0xca, 0xfc, 0x3c, 0x3d, 0x99, 0x15, 0xfb, 0x03, - 0x14, 0xfb, 0xf6, 0xe5, 0xf8, 0x51, 0x32, 0x83, 0x10, 0xc5, 0x86, 0x0b, 0xa4, 0xb1, 0x7b, 0x65, - 0xaf, 0x6c, 0x2f, 0x5e, 0x49, 0x41, 0x93, 0xae, 0x97, 0x9d, 0xc9, 0x31, 0x00, 0x6b, 0xf9, 0x43, - 0x5f, 0x00, 0x1c, 0x19, 0x39, 0xe4, 0x08, 0x92, 0x4a, 0x8a, 0x9a, 0xbb, 0x06, 0xa9, 0xc3, 0x7b, - 0xc3, 0x26, 0x1a, 0xb6, 0xd1, 0xf4, 0x4f, 0x97, 0x68, 0xe7, 0x2e, 0xb1, 0x1d, 0x12, 0xff, 0x0e, - 0x89, 0x83, 0x63, 0x79, 0x8d, 0xad, 0xc2, 0x8a, 0x19, 0xac, 0xe9, 0xbf, 0xcc, 0xcb, 0xe3, 0x72, - 0xe4, 0xcc, 0x33, 0x08, 0x2f, 0xbb, 0xef, 0x4d, 0x21, 0xba, 0x5f, 0x5d, 0xaf, 0xd6, 0x8f, 0xab, - 0xe9, 0x2f, 0x92, 0x40, 0x70, 0xb5, 0xbe, 0xbb, 0xbd, 0x99, 0x7a, 0x17, 0xd1, 0x53, 0xe0, 0x0e, - 0xf0, 0x1c, 0xba, 0x9f, 0xe2, 0xf4, 0x3b, 0x00, 0x00, 0xff, 0xff, 0x08, 0xf3, 0xcc, 0x66, 0x31, - 0x02, 0x00, 0x00, + // 354 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x6c, 0x92, 0x5f, 0x4b, 0xe3, 0x40, + 0x14, 0xc5, 0x37, 0x9b, 0xe6, 0xdf, 0xcd, 0x76, 0xb7, 0x0c, 0x4b, 0x99, 0x5d, 0x44, 0x42, 0xf1, + 0x21, 0x4f, 0x29, 0x28, 0x88, 0xcf, 0x82, 0xf8, 0xa0, 0x6d, 0x25, 0xf8, 0x07, 0x7c, 0x1b, 0x93, + 0x4b, 0x3b, 0xd8, 0xcc, 0x84, 0xc9, 0xa8, 0xf8, 0x7d, 0xfd, 0x20, 0x32, 0x93, 0xa4, 0x8d, 0xe0, + 0xdb, 0x3d, 0xe7, 0xe4, 0xfe, 0xc2, 0xb9, 0x0c, 0xfc, 0xdb, 0xb0, 0x9a, 0xcf, 0x8b, 0x0d, 0x53, + 0x7a, 0x5e, 0xa1, 0x66, 0x25, 0xd3, 0x2c, 0xab, 0x95, 0xd4, 0x92, 0x80, 0x89, 0x32, 0x1b, 0xcd, + 0x4e, 0x01, 0x16, 0x8c, 0x0b, 0xcd, 0xb8, 0x40, 0x45, 0x08, 0x8c, 0x04, 0xab, 0x90, 0x3a, 0x89, + 0x93, 0x46, 0xb9, 0x9d, 0xc9, 0x5f, 0xf0, 0xb0, 0x62, 0x7c, 0x4b, 0x7f, 0x5a, 0xb3, 0x15, 0xb3, + 0x0f, 0x17, 0xc2, 0x45, 0x87, 0xfd, 0x76, 0x8d, 0xc0, 0x68, 0x23, 0x2b, 0xec, 0xb6, 0xec, 0x4c, + 0x28, 0x04, 0x8d, 0x7c, 0x51, 0x05, 0x36, 0xd4, 0x4d, 0xdc, 0x34, 0xca, 0x7b, 0x69, 0x92, 0x57, + 0x54, 0x0d, 0x97, 0x82, 0x8e, 0xec, 0x42, 0x2f, 0x49, 0x02, 0x71, 0x89, 0x4d, 0xa1, 0x78, 0xad, + 0x4d, 0xea, 0xd9, 0x74, 0x68, 0x91, 0xff, 0x10, 0x3e, 0xe3, 0xfb, 0x9b, 0x54, 0x65, 0x43, 0x7d, + 0x8b, 0xdd, 0x69, 0x72, 0x06, 0x71, 0xb5, 0xab, 0xd7, 0xd0, 0x20, 0x71, 0xd3, 0xf8, 0x78, 0x9a, + 0xed, 0x0f, 0x90, 0xed, 0xdb, 0xe7, 0xc3, 0x4f, 0xc9, 0x14, 0x7c, 0x14, 0x6b, 0x2e, 0x90, 0x86, + 0xf6, 0x97, 0x9d, 0x32, 0xbd, 0x78, 0x21, 0x05, 0x8d, 0xda, 0x5e, 0x66, 0x26, 0x87, 0x00, 0xac, + 0xe6, 0xf7, 0x5d, 0x01, 0xb0, 0xc9, 0xc0, 0x21, 0x07, 0x10, 0x15, 0x52, 0x94, 0xdc, 0x36, 0x88, + 0x6d, 0xbc, 0x37, 0x0c, 0x51, 0xb3, 0x75, 0x43, 0x7f, 0xb5, 0x44, 0x33, 0xb7, 0xc4, 0xba, 0x27, + 0x8e, 0x7b, 0x62, 0xef, 0x98, 0xbc, 0xc4, 0x5a, 0x61, 0xc1, 0x34, 0x96, 0xf4, 0x77, 0xe2, 0xa4, + 0x61, 0x3e, 0x70, 0xc8, 0x11, 0x8c, 0x35, 0xdf, 0x6e, 0x51, 0xf5, 0x88, 0x3f, 0x16, 0xf1, 0xd5, + 0x9c, 0x25, 0xe0, 0x5f, 0xb4, 0xad, 0x62, 0x08, 0xee, 0x96, 0x57, 0xcb, 0xd5, 0xc3, 0x72, 0xf2, + 0x83, 0x44, 0xe0, 0x5d, 0xae, 0x6e, 0x6f, 0xae, 0x27, 0xce, 0x79, 0xf0, 0xe8, 0xd9, 0x33, 0x3d, + 0xf9, 0xf6, 0xe9, 0x9c, 0x7c, 0x06, 0x00, 0x00, 0xff, 0xff, 0xea, 0xb5, 0x4c, 0xbe, 0x57, 0x02, + 0x00, 0x00, } diff --git a/pkg/tiller/release_server.go b/pkg/tiller/release_server.go index ccc378421..3279a8f22 100644 --- a/pkg/tiller/release_server.go +++ b/pkg/tiller/release_server.go @@ -777,6 +777,13 @@ func getVersionSet(client discovery.ServerGroupsInterface) (chartutil.VersionSet } func (s *ReleaseServer) renderResources(ch *chart.Chart, values chartutil.Values, vs chartutil.VersionSet) ([]*release.Hook, *bytes.Buffer, string, error) { + // Guard to make sure Tiller is at the right version to handle this chart. + sver := version.GetVersion() + if ch.Metadata.TillerVersion != "" && + !version.IsCompatibleRange(ch.Metadata.TillerVersion, sver) { + return nil, nil, "", fmt.Errorf("Chart incompatible with Tiller %s", sver) + } + renderer := s.engine(ch) files, err := renderer.Render(ch, values) if err != nil { diff --git a/pkg/tiller/release_server_test.go b/pkg/tiller/release_server_test.go index d4e47a07f..636874b20 100644 --- a/pkg/tiller/release_server_test.go +++ b/pkg/tiller/release_server_test.go @@ -325,7 +325,7 @@ func TestInstallRelease(t *testing.T) { } } -func TestInstallReleaseWithNotes(t *testing.T) { +func TestInstallRelease_WithNotes(t *testing.T) { c := helm.NewContext() rs := rsFixture() @@ -394,7 +394,7 @@ func TestInstallReleaseWithNotes(t *testing.T) { } } -func TestInstallReleaseWithNotesRendered(t *testing.T) { +func TestInstallRelease_WithNotesRendered(t *testing.T) { c := helm.NewContext() rs := rsFixture() @@ -464,7 +464,54 @@ func TestInstallReleaseWithNotesRendered(t *testing.T) { } } -func TestInstallReleaseWithChartAndDependencyNotes(t *testing.T) { +func TestInstallRelease_TillerVersion(t *testing.T) { + c := helm.NewContext() + rs := rsFixture() + + // TODO: Refactor this into a mock. + req := &services.InstallReleaseRequest{ + Namespace: "spaced", + Chart: &chart.Chart{ + Metadata: &chart.Metadata{Name: "hello", TillerVersion: ">=2.2.0"}, + Templates: []*chart.Template{ + {Name: "templates/hello", Data: []byte("hello: world")}, + {Name: "templates/hooks", Data: []byte(manifestWithHook)}, + }, + }, + } + _, err := rs.InstallRelease(c, req) + if err != nil { + t.Fatalf("Expected valid range. Got %q", err) + } +} + +func TestInstallRelease_WrongTillerVersion(t *testing.T) { + c := helm.NewContext() + rs := rsFixture() + + // TODO: Refactor this into a mock. + req := &services.InstallReleaseRequest{ + Namespace: "spaced", + Chart: &chart.Chart{ + Metadata: &chart.Metadata{Name: "hello", TillerVersion: "<2.0.0"}, + Templates: []*chart.Template{ + {Name: "templates/hello", Data: []byte("hello: world")}, + {Name: "templates/hooks", Data: []byte(manifestWithHook)}, + }, + }, + } + _, err := rs.InstallRelease(c, req) + if err == nil { + t.Fatalf("Expected to fail because of wrong version") + } + + expect := "Chart incompatible with Tiller" + if !strings.Contains(err.Error(), expect) { + t.Errorf("Expected %q to contain %q", err.Error(), expect) + } +} + +func TestInstallRelease_WithChartAndDependencyNotes(t *testing.T) { c := helm.NewContext() rs := rsFixture() @@ -515,7 +562,7 @@ func TestInstallReleaseWithChartAndDependencyNotes(t *testing.T) { } } -func TestInstallReleaseDryRun(t *testing.T) { +func TestInstallRelease_DryRun(t *testing.T) { c := helm.NewContext() rs := rsFixture() @@ -568,7 +615,7 @@ func TestInstallReleaseDryRun(t *testing.T) { } } -func TestInstallReleaseNoHooks(t *testing.T) { +func TestInstallRelease_NoHooks(t *testing.T) { c := helm.NewContext() rs := rsFixture() rs.env.Releases.Create(releaseStub()) @@ -587,7 +634,7 @@ func TestInstallReleaseNoHooks(t *testing.T) { } } -func TestInstallReleaseFailedHooks(t *testing.T) { +func TestInstallRelease_FailedHooks(t *testing.T) { c := helm.NewContext() rs := rsFixture() rs.env.Releases.Create(releaseStub()) @@ -606,7 +653,7 @@ func TestInstallReleaseFailedHooks(t *testing.T) { } } -func TestInstallReleaseReuseName(t *testing.T) { +func TestInstallRelease_ReuseName(t *testing.T) { c := helm.NewContext() rs := rsFixture() rel := releaseStub() diff --git a/pkg/version/compatible.go b/pkg/version/compatible.go index 4a7b0d4bc..c8f359971 100644 --- a/pkg/version/compatible.go +++ b/pkg/version/compatible.go @@ -38,6 +38,17 @@ func IsCompatible(client, server string) bool { constraint = cv.String() } + return IsCompatibleRange(constraint, server) +} + +// IsCompatibleRange compares a version to a constraint. +// It returns true if the version matches the constraint, and false in all other cases. +func IsCompatibleRange(constraint, ver string) bool { + sv, err := semver.NewVersion(ver) + if err != nil { + return false + } + c, err := semver.NewConstraint(constraint) if err != nil { return false diff --git a/pkg/version/compatible_test.go b/pkg/version/compatible_test.go index 3b92fa25f..adc1c489e 100644 --- a/pkg/version/compatible_test.go +++ b/pkg/version/compatible_test.go @@ -41,3 +41,26 @@ func TestIsCompatible(t *testing.T) { } } } + +func TestIsCompatibleRange(t *testing.T) { + tests := []struct { + constraint string + ver string + expected bool + }{ + {"v2.0.0-alpha.4", "v2.0.0-alpha.4", true}, + {"v2.0.0-alpha.3", "v2.0.0-alpha.4", false}, + {"v2.0.0", "v2.0.0-alpha.4", false}, + {"v2.0.0-alpha.4", "v2.0.0", false}, + {"~v2.0.0", "v2.0.1", true}, + {"v2", "v2.0.0", true}, + {">2.0.0", "v2.1.1", true}, + {"v2.1.*", "v2.1.1", true}, + } + + for _, tt := range tests { + if IsCompatibleRange(tt.constraint, tt.ver) != tt.expected { + t.Errorf("expected constraint %s to be %v for %s", tt.constraint, tt.expected, tt.ver) + } + } +}