diff --git a/cmd/tiller/hooks.go b/cmd/tiller/hooks.go index c0fd5be3cb29122b5a466f2cd2b3be59aece6051..b291dff6bf5ce138a36a38c2ab5f67e9b90fde54 100644 --- a/cmd/tiller/hooks.go +++ b/cmd/tiller/hooks.go @@ -71,6 +71,13 @@ func (v versionSet) Has(apiVersion string) bool { return ok } +// manifest represents a manifest file, which has a name and some content. +type manifest struct { + name string + content string + head *simpleHead +} + // 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. @@ -92,9 +99,9 @@ func (v versionSet) Has(apiVersion string) bool { // // 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) ([]*release.Hook, map[string]string, error) { +func sortManifests(files map[string]string, apis versionSet, sort SortOrder) ([]*release.Hook, []manifest, error) { hs := []*release.Hook{} - generic := map[string]string{} + generic := []manifest{} for n, c := range files { // Skip partials. We could return these as a separate map, but there doesn't @@ -121,13 +128,13 @@ func sortManifests(files map[string]string, apis versionSet) ([]*release.Hook, m } if sh.Metadata == nil || sh.Metadata.Annotations == nil || len(sh.Metadata.Annotations) == 0 { - generic[n] = c + generic = append(generic, manifest{name: n, content: c, head: &sh}) continue } hookTypes, ok := sh.Metadata.Annotations[hookAnno] if !ok { - generic[n] = c + generic = append(generic, manifest{name: n, content: c, head: &sh}) continue } h := &release.Hook{ diff --git a/cmd/tiller/hooks_test.go b/cmd/tiller/hooks_test.go index 14287a7e690657f5a31883bbd859284c3a6d6fbd..b43df6391b029cd97422d8eb9005350e5d2823be 100644 --- a/cmd/tiller/hooks_test.go +++ b/cmd/tiller/hooks_test.go @@ -116,7 +116,7 @@ metadata: manifests[o.path] = o.manifest } - hs, generic, err := sortManifests(manifests, newVersionSet("v1", "v1beta1")) + hs, generic, err := sortManifests(manifests, newVersionSet("v1", "v1beta1"), InstallOrder) if err != nil { t.Fatalf("Unexpected error: %s", err) } diff --git a/cmd/tiller/kind_sorter.go b/cmd/tiller/kind_sorter.go new file mode 100644 index 0000000000000000000000000000000000000000..3ee4797fafc4a6aab48eab61fbbb945acc7ee24c --- /dev/null +++ b/cmd/tiller/kind_sorter.go @@ -0,0 +1,75 @@ +/* +Copyright 2016 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 main + +import ( + "sort" +) + +// SortOrder is an ordering of Kinds. +type SortOrder []string + +// InstallOrder is the order in which manifests should be installed (by Kind) +var InstallOrder SortOrder = []string{"Namespace", "Secret", "ConfigMap", "PersistentVolume", "ServiceAccount", "Service", "Pod", "ReplicationController", "Deployment", "DaemonSet", "Ingress", "Job"} + +// UninstallOrder is the order in which manifests should be uninstalled (by Kind) +var UninstallOrder SortOrder = []string{"Service", "Pod", "ReplicationController", "Deployment", "DaemonSet", "ConfigMap", "Secret", "PersistentVolume", "ServiceAccount", "Ingress", "Job", "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, ok := k.ordering[a.head.Kind] + if !ok { + // Unknown is always last + return false + } + second, ok := k.ordering[b.head.Kind] + if !ok { + return true + } + return first < second +} diff --git a/cmd/tiller/kind_sorter_test.go b/cmd/tiller/kind_sorter_test.go new file mode 100644 index 0000000000000000000000000000000000000000..46de376dd7fe0eb860e9615bb390298644a0c2cf --- /dev/null +++ b/cmd/tiller/kind_sorter_test.go @@ -0,0 +1,72 @@ +/* +Copyright 2016 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 main + +import ( + "testing" +) + +func TestKindSorter(t *testing.T) { + manifests := []manifest{ + { + name: "m", + content: "", + head: &simpleHead{Kind: "Deployment"}, + }, + { + name: "l", + content: "", + head: &simpleHead{Kind: "Service"}, + }, + { + name: "!", + content: "", + head: &simpleHead{Kind: "HonkyTonkSet"}, + }, + { + name: "h", + content: "", + head: &simpleHead{Kind: "Namespace"}, + }, + { + name: "e", + content: "", + head: &simpleHead{Kind: "ConfigMap"}, + }, + } + + res := sortByKind(manifests, InstallOrder) + got := "" + expect := "helm!" + for _, r := range res { + got += r.name + } + if got != expect { + t.Errorf("Expected %q, got %q", expect, got) + } + + expect = "lmeh!" + got = "" + res = sortByKind(manifests, UninstallOrder) + for _, r := range res { + got += r.name + } + if got != expect { + t.Errorf("Expected %q, got %q", expect, got) + } + +} diff --git a/cmd/tiller/release_server.go b/cmd/tiller/release_server.go index 8820afbdaf4e8349d788eeccc62a3639f1bdc274..c161abb79e8e5f824297a97f6317019f3dd4caca 100644 --- a/cmd/tiller/release_server.go +++ b/cmd/tiller/release_server.go @@ -548,7 +548,7 @@ func (s *releaseServer) renderResources(ch *chart.Chart, values chartutil.Values if err != nil { return nil, nil, "", fmt.Errorf("Could not get apiVersions from Kubernetes: %s", err) } - hooks, manifests, err := sortManifests(files, vs) + hooks, manifests, err := sortManifests(files, vs, InstallOrder) if err != nil { // By catching parse errors here, we can prevent bogus releases from going // to Kubernetes. @@ -557,9 +557,9 @@ func (s *releaseServer) renderResources(ch *chart.Chart, values chartutil.Values // Aggregate all valid manifests into one big doc. b := bytes.NewBuffer(nil) - for name, file := range manifests { - b.WriteString("\n---\n# Source: " + name + "\n") - b.WriteString(file) + for _, m := range manifests { + b.WriteString("\n---\n# Source: " + m.name + "\n") + b.WriteString(m.content) } return hooks, b, notes, nil @@ -707,11 +707,27 @@ func (s *releaseServer) UninstallRelease(c ctx.Context, req *services.UninstallR } } - b := bytes.NewBuffer([]byte(rel.Manifest)) - if err := s.env.KubeClient.Delete(rel.Namespace, b); err != nil { - log.Printf("uninstall: Failed deletion of %q: %s", req.Name, err) + vs, err := s.getVersionSet() + if err != nil { + return nil, fmt.Errorf("Could not get apiVersions from Kubernetes: %s", err) + } + + manifests := splitManifests(rel.Manifest) + _, files, err := sortManifests(manifests, vs, UninstallOrder) + if err != nil { + // We could instead just delete everything in no particular order. return nil, err } + // Note: We could re-join these into one file and delete just that one. Or + // we could collect errors (instead of bailing on the first error) and try + // to delete as much as possible instead of failing at the first error. + for _, file := range files { + b := bytes.NewBufferString(file.content) + if err := s.env.KubeClient.Delete(rel.Namespace, b); err != nil { + log.Printf("uninstall: Failed deletion of %q: %s", req.Name, err) + return nil, err + } + } if !req.DisableHooks { if err := s.execHook(rel.Hooks, rel.Name, rel.Namespace, postDelete); err != nil { @@ -754,3 +770,18 @@ func (r byDate) Swap(p, q int) { func (r byDate) Less(p, q int) bool { return r[p].Info.LastDeployed.Seconds < r[q].Info.LastDeployed.Seconds } + +func splitManifests(bigfile string) map[string]string { + // This is not the best way of doing things, but it's how k8s itself does it. + // Basically, we're quickly splitting a stream of YAML documents into an + // array of YAML docs. In the current implementation, the file name is just + // a place holder, and doesn't have any further meaning. + sep := "\n---\n" + tpl := "manifest-%d" + res := map[string]string{} + tmp := strings.Split(bigfile, sep) + for i, d := range tmp { + res[fmt.Sprintf(tpl, i)] = d + } + return res +}