diff --git a/cmd/tiller/environment/environment.go b/cmd/tiller/environment/environment.go index 5e0e812160270e15b8b76fbe7f57fb141f917f13..72fd510be18e1b8f6f124c0f11ce1770aebc8fe0 100644 --- a/cmd/tiller/environment/environment.go +++ b/cmd/tiller/environment/environment.go @@ -23,7 +23,6 @@ These dependencies are expressed as interfaces so that alternate implementations package environment import ( - "errors" "io" "k8s.io/helm/pkg/chartutil" @@ -33,6 +32,7 @@ import ( "k8s.io/helm/pkg/storage" "k8s.io/helm/pkg/storage/driver" "k8s.io/kubernetes/pkg/client/unversioned" + "k8s.io/kubernetes/pkg/client/unversioned/testclient" ) // TillerNamespace is the namespace tiller is running in. @@ -150,7 +150,7 @@ type PrintingKubeClient struct { // The printing client does not have access to a Kubernetes client at all. So it // will always return an error if the client is accessed. func (p *PrintingKubeClient) APIClient() (unversioned.Interface, error) { - return nil, errors.New("no API client found") + return testclient.NewSimpleFake(), nil } // Create prints the values of what would be created with a real KubeClient. diff --git a/cmd/tiller/hooks.go b/cmd/tiller/hooks.go index e102699c7fdba1c1fe790d59f09fafaa19b99140..c0fd5be3cb29122b5a466f2cd2b3be59aece6051 100644 --- a/cmd/tiller/hooks.go +++ b/cmd/tiller/hooks.go @@ -48,6 +48,7 @@ var events = map[string]release.Hook_Event{ } type simpleHead struct { + Version string `json:"apiVersion"` Kind string `json:"kind,omitempty"` Metadata *struct { Name string `json:"name"` @@ -55,7 +56,22 @@ type simpleHead struct { } `json:"metadata,omitempty"` } -// sortHooks takes a map of filename/YAML contents and sorts them into hook types. +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 +} + +// sortManifests takes a map of filename/YAML contents and sorts them into hook types. // // The resulting hooks struct will be populated with all of the generated hooks. // Any file that does not declare one of the hook types will be placed in the @@ -64,6 +80,7 @@ type simpleHead struct { // To determine hook type, this looks for a YAML structure like this: // // kind: SomeKind +// apiVersion: v1 // metadata: // annotations: // helm.sh/hook: pre-install @@ -75,7 +92,7 @@ type simpleHead struct { // // Files that do not parse into the expected format are simply placed into a map and // returned. -func sortHooks(files map[string]string) ([]*release.Hook, map[string]string, error) { +func sortManifests(files map[string]string, apis versionSet) ([]*release.Hook, map[string]string, error) { hs := []*release.Hook{} generic := map[string]string{} @@ -99,6 +116,10 @@ func sortHooks(files map[string]string) ([]*release.Hook, map[string]string, err return hs, generic, e } + if sh.Version != "" && !apis.Has(sh.Version) { + return hs, generic, fmt.Errorf("apiVersion %q in %s is not available", sh.Version, n) + } + if sh.Metadata == nil || sh.Metadata.Annotations == nil || len(sh.Metadata.Annotations) == 0 { generic[n] = c continue diff --git a/cmd/tiller/hooks_test.go b/cmd/tiller/hooks_test.go index 4d054ec90caa735feb687bc0c56d82ce939daeaa..14287a7e690657f5a31883bbd859284c3a6d6fbd 100644 --- a/cmd/tiller/hooks_test.go +++ b/cmd/tiller/hooks_test.go @@ -22,7 +22,7 @@ import ( "k8s.io/helm/pkg/proto/hapi/release" ) -func TestSortHooks(t *testing.T) { +func TestSortManifests(t *testing.T) { data := []struct { name string @@ -52,6 +52,7 @@ metadata: kind: "ReplicaSet", hooks: []release.Hook_Event{release.Hook_POST_INSTALL}, manifest: `kind: ReplicaSet +apiVersion: v1beta1 metadata: name: second annotations: @@ -63,6 +64,7 @@ metadata: kind: "ReplicaSet", hooks: []release.Hook_Event{}, manifest: `kind: ReplicaSet +apiVersion: v1beta1 metadata: name: third annotations: @@ -74,6 +76,7 @@ metadata: kind: "Pod", hooks: []release.Hook_Event{}, manifest: `kind: Pod +apiVersion: v1 metadata: name: fourth annotations: @@ -85,6 +88,7 @@ metadata: kind: "ReplicaSet", hooks: []release.Hook_Event{release.Hook_POST_DELETE, release.Hook_POST_INSTALL}, manifest: `kind: ReplicaSet +apiVersion: v1beta1 metadata: name: fifth annotations: @@ -112,7 +116,7 @@ metadata: manifests[o.path] = o.manifest } - hs, generic, err := sortHooks(manifests) + hs, generic, err := sortManifests(manifests, newVersionSet("v1", "v1beta1")) if err != nil { t.Fatalf("Unexpected error: %s", err) } @@ -153,3 +157,19 @@ metadata: } } + +func TestVersionSet(t *testing.T) { + vs := newVersionSet("v1", "v1beta1", "extensions/alpha5", "batch/v1") + + if l := len(vs); l != 4 { + t.Errorf("Expected 4, got %d", l) + } + + if !vs.Has("extensions/alpha5") { + t.Error("No match for alpha5") + } + + if vs.Has("nosuch/extension") { + t.Error("Found nonexistent extension") + } +} diff --git a/cmd/tiller/release_server.go b/cmd/tiller/release_server.go index c0336396f5766ff236efc3313984e95d17829c9c..865cf91f5868e2e1097ff1129e8198107c92adcb 100644 --- a/cmd/tiller/release_server.go +++ b/cmd/tiller/release_server.go @@ -35,6 +35,7 @@ import ( "k8s.io/helm/pkg/proto/hapi/services" "k8s.io/helm/pkg/storage/driver" "k8s.io/helm/pkg/timeconv" + "k8s.io/kubernetes/pkg/api/unversioned" ) var srv *releaseServer @@ -411,6 +412,31 @@ func (s *releaseServer) prepareRelease(req *services.InstallReleaseRequest) (*re return rel, nil } +func (s *releaseServer) getVersionSet() (versionSet, error) { + defVersions := newVersionSet("v1") + cli, err := s.env.KubeClient.APIClient() + if err != nil { + log.Printf("API Client for Kubernetes is missing: %s.", err) + return defVersions, err + } + + groups, err := cli.Discovery().ServerGroups() + if err != nil { + return defVersions, err + } + + // FIXME: The Kubernetes test fixture for cli appears to always return nil + // for calls to Discovery().ServerGroups(). So in this case, we return + // the default API list. This is also a safe value to return in any other + // odd-ball case. + if groups == nil { + return defVersions, nil + } + + versions := unversioned.ExtractGroupVersions(groups) + return newVersionSet(versions...), nil +} + func (s *releaseServer) renderResources(ch *chart.Chart, values chartutil.Values) ([]*release.Hook, *bytes.Buffer, error) { renderer := s.engine(ch) files, err := renderer.Render(ch, values) @@ -421,7 +447,11 @@ 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. - hooks, manifests, err := sortHooks(files) + vs, err := s.getVersionSet() + if err != nil { + return nil, nil, fmt.Errorf("Could not get apiVersions from Kubernetes: %s", err) + } + hooks, manifests, err := sortManifests(files, vs) if err != nil { // By catching parse errors here, we can prevent bogus releases from going // to Kubernetes. diff --git a/cmd/tiller/release_server_test.go b/cmd/tiller/release_server_test.go index 3b86c2232d563bfcece109ccf592775aa1278931..53bb8fdca01f0b746eea9afd02da9a3e722288f7 100644 --- a/cmd/tiller/release_server_test.go +++ b/cmd/tiller/release_server_test.go @@ -112,6 +112,20 @@ func namedReleaseStub(name string, status release.Status_Code) *release.Release } } +func TestGetVersionSet(t *testing.T) { + rs := rsFixture() + vs, err := rs.getVersionSet() + if err != nil { + t.Error(err) + } + if !vs.Has("v1") { + t.Errorf("Expected supported versions to at least include v1.") + } + if vs.Has("nosuchversion/v1") { + t.Error("Non-existent version is reported found.") + } +} + func TestUniqName(t *testing.T) { rs := rsFixture()