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.
parent a19dee52
......@@ -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;
}
......@@ -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.
......@@ -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
......
......@@ -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,
}
......@@ -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 {
......
......@@ -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()
......
......@@ -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
......
......@@ -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)
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment