diff --git a/cmd/helm/template.go b/cmd/helm/template.go index 1838bb758..41f275836 100644 --- a/cmd/helm/template.go +++ b/cmd/helm/template.go @@ -35,7 +35,6 @@ import ( "k8s.io/helm/pkg/proto/hapi/chart" "k8s.io/helm/pkg/proto/hapi/release" "k8s.io/helm/pkg/renderutil" - "k8s.io/helm/pkg/tiller" "k8s.io/helm/pkg/timeconv" ) @@ -226,7 +225,7 @@ func (t *templateCmd) run(cmd *cobra.Command, args []string) error { manifestsToRender = listManifests } - for _, m := range tiller.SortByKind(manifestsToRender) { + for _, m := range manifest.SortByKind(manifestsToRender) { data := m.Content b := filepath.Base(m.Name) if !t.showNotes && b == "NOTES.txt" { diff --git a/pkg/manifest/hooks.go b/pkg/manifest/hooks.go new file mode 100644 index 000000000..d1d3839c5 --- /dev/null +++ b/pkg/manifest/hooks.go @@ -0,0 +1,201 @@ +/* +Copyright The Helm Authors. + +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 manifest + +import ( + "fmt" + "log" + "path" + "strconv" + "strings" + + "github.com/ghodss/yaml" + + "k8s.io/helm/pkg/chartutil" + "k8s.io/helm/pkg/hooks" + "k8s.io/helm/pkg/proto/hapi/release" + util "k8s.io/helm/pkg/releaseutil" +) + +type result struct { + hooks []*release.Hook + generic []Manifest +} + +type manifestFile struct { + entries map[string]string + path string + apis chartutil.VersionSet +} + +// Partition takes a map of filename/YAML contents, splits the file by manifest +// entries, and sorts the entries 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 +// 'generic' bucket. +// +// Files that do not parse into the expected format are simply placed into a map and +// returned. +func Partition(files map[string]string, apis chartutil.VersionSet, sort SortOrder) ([]*release.Hook, []Manifest, error) { + result := &result{} + + for filePath, c := range files { + + // Skip partials. We could return these as a separate map, but there doesn't + // seem to be any need for that at this time. + if strings.HasPrefix(path.Base(filePath), "_") { + continue + } + // Skip empty files and log this. + if len(strings.TrimSpace(c)) == 0 { + log.Printf("info: manifest %q is empty. Skipping.", filePath) + continue + } + + manifestFile := &manifestFile{ + entries: util.SplitManifests(c), + path: filePath, + apis: apis, + } + + if err := manifestFile.sort(result); err != nil { + return result.hooks, result.generic, err + } + } + + return result.hooks, sortByKind(result.generic, sort), nil +} + +// sort takes a manifestFile object which may contain multiple resource definition +// entries and sorts each entry by hook types, and saves the resulting hooks and +// generic manifests (or non-hooks) to the result struct. +// +// To determine hook type, it looks for a YAML structure like this: +// +// kind: SomeKind +// apiVersion: v1 +// metadata: +// annotations: +// helm.sh/hook: pre-install +// +// To determine the policy to delete the hook, it looks for a YAML structure like this: +// +// kind: SomeKind +// apiVersion: v1 +// metadata: +// annotations: +// helm.sh/hook-delete-policy: hook-succeeded +func (file *manifestFile) sort(result *result) error { + for _, m := range file.entries { + var entry util.SimpleHead + err := yaml.Unmarshal([]byte(m), &entry) + + if err != nil { + e := fmt.Errorf("YAML parse error on %s: %s", file.path, err) + return e + } + + if !hasAnyAnnotation(entry) { + result.generic = append(result.generic, Manifest{ + Name: file.path, + Content: m, + Head: &entry, + }) + continue + } + + hookTypes, ok := entry.Metadata.Annotations[hooks.HookAnno] + if !ok { + result.generic = append(result.generic, Manifest{ + Name: file.path, + Content: m, + Head: &entry, + }) + continue + } + + hw := calculateHookWeight(entry) + + h := &release.Hook{ + Name: entry.Metadata.Name, + Kind: entry.Kind, + Path: file.path, + Manifest: m, + Events: []release.Hook_Event{}, + Weight: hw, + DeletePolicies: []release.Hook_DeletePolicy{}, + } + + isUnknownHook := false + for _, hookType := range strings.Split(hookTypes, ",") { + hookType = strings.ToLower(strings.TrimSpace(hookType)) + e, ok := hooks.Events[hookType] + if !ok { + isUnknownHook = true + break + } + h.Events = append(h.Events, e) + } + + if isUnknownHook { + log.Printf("info: skipping unknown hook: %q", hookTypes) + continue + } + + result.hooks = append(result.hooks, h) + + operateAnnotationValues(entry, hooks.HookDeleteAnno, func(value string) { + policy, exist := hooks.DeletePolices[value] + if exist { + h.DeletePolicies = append(h.DeletePolicies, policy) + } else { + log.Printf("info: skipping unknown hook delete policy: %q", value) + } + }) + } + return nil +} + +func hasAnyAnnotation(entry util.SimpleHead) bool { + if entry.Metadata == nil || + entry.Metadata.Annotations == nil || + len(entry.Metadata.Annotations) == 0 { + return false + } + + return true +} + +func calculateHookWeight(entry util.SimpleHead) int32 { + hws := entry.Metadata.Annotations[hooks.HookWeightAnno] + hw, err := strconv.Atoi(hws) + if err != nil { + hw = 0 + } + + return int32(hw) +} + +func operateAnnotationValues(entry util.SimpleHead, annotation string, operate func(p string)) { + if dps, ok := entry.Metadata.Annotations[annotation]; ok { + for _, dp := range strings.Split(dps, ",") { + dp = strings.ToLower(strings.TrimSpace(dp)) + operate(dp) + } + } +} diff --git a/pkg/tiller/hooks_test.go b/pkg/manifest/hooks_test.go similarity index 98% rename from pkg/tiller/hooks_test.go rename to pkg/manifest/hooks_test.go index 8bd928500..e0ba3420b 100644 --- a/pkg/tiller/hooks_test.go +++ b/pkg/manifest/hooks_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package tiller +package manifest import ( "reflect" @@ -140,7 +140,7 @@ metadata: manifests[o.path] = o.manifest } - hs, generic, err := sortManifests(manifests, chartutil.NewVersionSet("v1", "v1beta1"), InstallOrder) + hs, generic, err := Partition(manifests, chartutil.NewVersionSet("v1", "v1beta1"), InstallOrder) if err != nil { t.Fatalf("Unexpected error: %s", err) } diff --git a/pkg/manifest/kind_sorter.go b/pkg/manifest/kind_sorter.go new file mode 100644 index 000000000..f1cf92f3f --- /dev/null +++ b/pkg/manifest/kind_sorter.go @@ -0,0 +1,158 @@ +/* +Copyright The Helm Authors. + +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 manifest + +import ( + "sort" +) + +// SortOrder is an ordering of Kinds. +type SortOrder []string + +// InstallOrder is the order in which manifests should be installed (by Kind). +// +// Those occurring earlier in the list get installed before those occurring later in the list. +var InstallOrder SortOrder = []string{ + "Namespace", + "ResourceQuota", + "LimitRange", + "PodSecurityPolicy", + "PodDisruptionBudget", + "Secret", + "ConfigMap", + "StorageClass", + "PersistentVolume", + "PersistentVolumeClaim", + "ServiceAccount", + "CustomResourceDefinition", + "ClusterRole", + "ClusterRoleBinding", + "Role", + "RoleBinding", + "Service", + "DaemonSet", + "Pod", + "ReplicationController", + "ReplicaSet", + "Deployment", + "StatefulSet", + "Job", + "CronJob", + "Ingress", + "APIService", +} + +// UninstallOrder is the order in which manifests should be uninstalled (by Kind). +// +// Those occurring earlier in the list get uninstalled before those occurring later in the list. +var UninstallOrder SortOrder = []string{ + "APIService", + "Ingress", + "Service", + "CronJob", + "Job", + "StatefulSet", + "Deployment", + "ReplicaSet", + "ReplicationController", + "Pod", + "DaemonSet", + "RoleBinding", + "Role", + "ClusterRoleBinding", + "ClusterRole", + "CustomResourceDefinition", + "ServiceAccount", + "PersistentVolumeClaim", + "PersistentVolume", + "StorageClass", + "ConfigMap", + "Secret", + "PodDisruptionBudget", + "PodSecurityPolicy", + "LimitRange", + "ResourceQuota", + "Namespace", +} + +// sortByKind does an in-place sort of manifests by Kind. +// +// Results are sorted by 'ordering' +func sortByKind(manifests []Manifest, ordering SortOrder) []Manifest { + ks := newKindSorter(manifests, ordering) + sort.Sort(ks) + return ks.manifests +} + +type kindSorter struct { + ordering map[string]int + manifests []Manifest +} + +func newKindSorter(m []Manifest, s SortOrder) *kindSorter { + o := make(map[string]int, len(s)) + for v, k := range s { + o[k] = v + } + + return &kindSorter{ + manifests: m, + ordering: o, + } +} + +func (k *kindSorter) Len() int { return len(k.manifests) } + +func (k *kindSorter) Swap(i, j int) { k.manifests[i], k.manifests[j] = k.manifests[j], k.manifests[i] } + +func (k *kindSorter) Less(i, j int) bool { + a := k.manifests[i] + b := k.manifests[j] + first, aok := k.ordering[a.Head.Kind] + second, bok := k.ordering[b.Head.Kind] + + if !aok && !bok { + // if both are unknown then sort alphabetically by kind and name + if a.Head.Kind != b.Head.Kind { + return a.Head.Kind < b.Head.Kind + } + return a.Name < b.Name + } + + // unknown kind is last + if !aok { + return false + } + if !bok { + return true + } + + // if same kind sub sort alphanumeric + if first == second { + return a.Name < b.Name + } + // sort different kinds + return first < second +} + +// SortByKind sorts manifests in InstallOrder +func SortByKind(manifests []Manifest) []Manifest { + ordering := InstallOrder + ks := newKindSorter(manifests, ordering) + sort.Sort(ks) + return ks.manifests +} diff --git a/pkg/tiller/kind_sorter_test.go b/pkg/manifest/kind_sorter_test.go similarity index 99% rename from pkg/tiller/kind_sorter_test.go rename to pkg/manifest/kind_sorter_test.go index 56822f995..92e00230f 100644 --- a/pkg/tiller/kind_sorter_test.go +++ b/pkg/manifest/kind_sorter_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package tiller +package manifest import ( "bytes" diff --git a/pkg/tiller/hooks.go b/pkg/tiller/hooks.go index 41892e5ef..ed4add0e6 100644 --- a/pkg/tiller/hooks.go +++ b/pkg/tiller/hooks.go @@ -17,189 +17,8 @@ limitations under the License. package tiller import ( - "fmt" - "log" - "path" - "strconv" - "strings" - - "github.com/ghodss/yaml" - - "k8s.io/helm/pkg/chartutil" - "k8s.io/helm/pkg/hooks" "k8s.io/helm/pkg/manifest" - "k8s.io/helm/pkg/proto/hapi/release" - util "k8s.io/helm/pkg/releaseutil" ) -// Manifest represents a manifest file, which has a name and some content. +// Manifest has been aliased to avoid breaking API type Manifest = manifest.Manifest - -type result struct { - hooks []*release.Hook - generic []Manifest -} - -type manifestFile struct { - entries map[string]string - path string - apis chartutil.VersionSet -} - -// sortManifests takes a map of filename/YAML contents, splits the file -// by manifest entries, and sorts the entries 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 -// 'generic' bucket. -// -// Files that do not parse into the expected format are simply placed into a map and -// returned. -func sortManifests(files map[string]string, apis chartutil.VersionSet, sort SortOrder) ([]*release.Hook, []Manifest, error) { - result := &result{} - - for filePath, c := range files { - - // Skip partials. We could return these as a separate map, but there doesn't - // seem to be any need for that at this time. - if strings.HasPrefix(path.Base(filePath), "_") { - continue - } - // Skip empty files and log this. - if len(strings.TrimSpace(c)) == 0 { - log.Printf("info: manifest %q is empty. Skipping.", filePath) - continue - } - - manifestFile := &manifestFile{ - entries: util.SplitManifests(c), - path: filePath, - apis: apis, - } - - if err := manifestFile.sort(result); err != nil { - return result.hooks, result.generic, err - } - } - - return result.hooks, sortByKind(result.generic, sort), nil -} - -// sort takes a manifestFile object which may contain multiple resource definition -// entries and sorts each entry by hook types, and saves the resulting hooks and -// generic manifests (or non-hooks) to the result struct. -// -// To determine hook type, it looks for a YAML structure like this: -// -// kind: SomeKind -// apiVersion: v1 -// metadata: -// annotations: -// helm.sh/hook: pre-install -// -// To determine the policy to delete the hook, it looks for a YAML structure like this: -// -// kind: SomeKind -// apiVersion: v1 -// metadata: -// annotations: -// helm.sh/hook-delete-policy: hook-succeeded -func (file *manifestFile) sort(result *result) error { - for _, m := range file.entries { - var entry util.SimpleHead - err := yaml.Unmarshal([]byte(m), &entry) - - if err != nil { - e := fmt.Errorf("YAML parse error on %s: %s", file.path, err) - return e - } - - if !hasAnyAnnotation(entry) { - result.generic = append(result.generic, Manifest{ - Name: file.path, - Content: m, - Head: &entry, - }) - continue - } - - hookTypes, ok := entry.Metadata.Annotations[hooks.HookAnno] - if !ok { - result.generic = append(result.generic, Manifest{ - Name: file.path, - Content: m, - Head: &entry, - }) - continue - } - - hw := calculateHookWeight(entry) - - h := &release.Hook{ - Name: entry.Metadata.Name, - Kind: entry.Kind, - Path: file.path, - Manifest: m, - Events: []release.Hook_Event{}, - Weight: hw, - DeletePolicies: []release.Hook_DeletePolicy{}, - } - - isUnknownHook := false - for _, hookType := range strings.Split(hookTypes, ",") { - hookType = strings.ToLower(strings.TrimSpace(hookType)) - e, ok := hooks.Events[hookType] - if !ok { - isUnknownHook = true - break - } - h.Events = append(h.Events, e) - } - - if isUnknownHook { - log.Printf("info: skipping unknown hook: %q", hookTypes) - continue - } - - result.hooks = append(result.hooks, h) - - operateAnnotationValues(entry, hooks.HookDeleteAnno, func(value string) { - policy, exist := hooks.DeletePolices[value] - if exist { - h.DeletePolicies = append(h.DeletePolicies, policy) - } else { - log.Printf("info: skipping unknown hook delete policy: %q", value) - } - }) - } - return nil -} - -func hasAnyAnnotation(entry util.SimpleHead) bool { - if entry.Metadata == nil || - entry.Metadata.Annotations == nil || - len(entry.Metadata.Annotations) == 0 { - return false - } - - return true -} - -func calculateHookWeight(entry util.SimpleHead) int32 { - hws := entry.Metadata.Annotations[hooks.HookWeightAnno] - hw, err := strconv.Atoi(hws) - if err != nil { - hw = 0 - } - - return int32(hw) -} - -func operateAnnotationValues(entry util.SimpleHead, annotation string, operate func(p string)) { - if dps, ok := entry.Metadata.Annotations[annotation]; ok { - for _, dp := range strings.Split(dps, ",") { - dp = strings.ToLower(strings.TrimSpace(dp)) - operate(dp) - } - } -} diff --git a/pkg/tiller/kind_sorter.go b/pkg/tiller/kind_sorter.go index f980277d2..3afb7715d 100644 --- a/pkg/tiller/kind_sorter.go +++ b/pkg/tiller/kind_sorter.go @@ -17,142 +17,19 @@ limitations under the License. package tiller import ( - "sort" + "k8s.io/helm/pkg/manifest" ) -// SortOrder is an ordering of Kinds. -type SortOrder []string +// SortOrder has been aliased to avoid breaking API +type SortOrder = manifest.SortOrder -// InstallOrder is the order in which manifests should be installed (by Kind). -// -// Those occurring earlier in the list get installed before those occurring later in the list. -var InstallOrder SortOrder = []string{ - "Namespace", - "ResourceQuota", - "LimitRange", - "PodSecurityPolicy", - "PodDisruptionBudget", - "Secret", - "ConfigMap", - "StorageClass", - "PersistentVolume", - "PersistentVolumeClaim", - "ServiceAccount", - "CustomResourceDefinition", - "ClusterRole", - "ClusterRoleBinding", - "Role", - "RoleBinding", - "Service", - "DaemonSet", - "Pod", - "ReplicationController", - "ReplicaSet", - "Deployment", - "StatefulSet", - "Job", - "CronJob", - "Ingress", - "APIService", -} +var ( + // InstallOrder has been aliased to avoid breaking API + InstallOrder SortOrder = manifest.InstallOrder -// UninstallOrder is the order in which manifests should be uninstalled (by Kind). -// -// Those occurring earlier in the list get uninstalled before those occurring later in the list. -var UninstallOrder SortOrder = []string{ - "APIService", - "Ingress", - "Service", - "CronJob", - "Job", - "StatefulSet", - "Deployment", - "ReplicaSet", - "ReplicationController", - "Pod", - "DaemonSet", - "RoleBinding", - "Role", - "ClusterRoleBinding", - "ClusterRole", - "CustomResourceDefinition", - "ServiceAccount", - "PersistentVolumeClaim", - "PersistentVolume", - "StorageClass", - "ConfigMap", - "Secret", - "PodDisruptionBudget", - "PodSecurityPolicy", - "LimitRange", - "ResourceQuota", - "Namespace", -} + // UninstallOrder has been aliased to avoid breaking API + UninstallOrder SortOrder = manifest.UninstallOrder -// sortByKind does an in-place sort of manifests by Kind. -// -// Results are sorted by 'ordering' -func sortByKind(manifests []Manifest, ordering SortOrder) []Manifest { - ks := newKindSorter(manifests, ordering) - sort.Sort(ks) - return ks.manifests -} - -type kindSorter struct { - ordering map[string]int - manifests []Manifest -} - -func newKindSorter(m []Manifest, s SortOrder) *kindSorter { - o := make(map[string]int, len(s)) - for v, k := range s { - o[k] = v - } - - return &kindSorter{ - manifests: m, - ordering: o, - } -} - -func (k *kindSorter) Len() int { return len(k.manifests) } - -func (k *kindSorter) Swap(i, j int) { k.manifests[i], k.manifests[j] = k.manifests[j], k.manifests[i] } - -func (k *kindSorter) Less(i, j int) bool { - a := k.manifests[i] - b := k.manifests[j] - first, aok := k.ordering[a.Head.Kind] - second, bok := k.ordering[b.Head.Kind] - - if !aok && !bok { - // if both are unknown then sort alphabetically by kind and name - if a.Head.Kind != b.Head.Kind { - return a.Head.Kind < b.Head.Kind - } - return a.Name < b.Name - } - - // unknown kind is last - if !aok { - return false - } - if !bok { - return true - } - - // if same kind sub sort alphanumeric - if first == second { - return a.Name < b.Name - } - // sort different kinds - return first < second -} - -// SortByKind sorts manifests in InstallOrder -func SortByKind(manifests []Manifest) []Manifest { - ordering := InstallOrder - ks := newKindSorter(manifests, ordering) - sort.Sort(ks) - return ks.manifests -} + // SortByKind has been aliased to avoid breaking API + SortByKind func([]manifest.Manifest) []manifest.Manifest = manifest.SortByKind +) diff --git a/pkg/tiller/release_modules.go b/pkg/tiller/release_modules.go index 360794481..98db9c637 100644 --- a/pkg/tiller/release_modules.go +++ b/pkg/tiller/release_modules.go @@ -26,6 +26,7 @@ import ( "k8s.io/helm/pkg/chartutil" "k8s.io/helm/pkg/kube" + "k8s.io/helm/pkg/manifest" "k8s.io/helm/pkg/proto/hapi/release" rudderAPI "k8s.io/helm/pkg/proto/hapi/rudder" "k8s.io/helm/pkg/proto/hapi/services" @@ -161,7 +162,7 @@ func (m *RemoteReleaseModule) Delete(r *release.Release, req *services.Uninstall // DeleteRelease is a helper that allows Rudder to delete a release without exposing most of Tiller inner functions func DeleteRelease(rel *release.Release, vs chartutil.VersionSet, kubeClient environment.KubeClient) (kept string, errs []error) { manifests := relutil.SplitManifests(rel.Manifest) - _, files, err := sortManifests(manifests, vs, UninstallOrder) + _, files, err := manifest.Partition(manifests, vs, manifest.UninstallOrder) if err != nil { // We could instead just delete everything in no particular order. // FIXME: One way to delete at this point would be to try a label-based diff --git a/pkg/tiller/release_server.go b/pkg/tiller/release_server.go index 2e3ade605..fcd58e2f8 100644 --- a/pkg/tiller/release_server.go +++ b/pkg/tiller/release_server.go @@ -32,6 +32,7 @@ import ( "k8s.io/helm/pkg/chartutil" "k8s.io/helm/pkg/hooks" + "k8s.io/helm/pkg/manifest" "k8s.io/helm/pkg/proto/hapi/chart" "k8s.io/helm/pkg/proto/hapi/release" "k8s.io/helm/pkg/proto/hapi/services" @@ -330,7 +331,7 @@ 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 := sortManifests(files, vs, InstallOrder) + hooks, manifests, err := manifest.Partition(files, vs, manifest.InstallOrder) if err != nil { // By catching parse errors here, we can prevent bogus releases from going // to Kubernetes.