github.com/stefanmcshane/helm@v0.0.0-20221213002717-88a4a2c6e77d/pkg/releaseutil/manifest_sorter.go (about) 1 /* 2 Copyright The Helm Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package releaseutil 18 19 import ( 20 "log" 21 "path" 22 "sort" 23 "strconv" 24 "strings" 25 26 "github.com/pkg/errors" 27 "sigs.k8s.io/yaml" 28 29 "github.com/stefanmcshane/helm/pkg/chartutil" 30 "github.com/stefanmcshane/helm/pkg/release" 31 ) 32 33 // Manifest represents a manifest file, which has a name and some content. 34 type Manifest struct { 35 Name string 36 Content string 37 Head *SimpleHead 38 } 39 40 // manifestFile represents a file that contains a manifest. 41 type manifestFile struct { 42 entries map[string]string 43 path string 44 apis chartutil.VersionSet 45 } 46 47 // result is an intermediate structure used during sorting. 48 type result struct { 49 hooks []*release.Hook 50 generic []Manifest 51 } 52 53 // TODO: Refactor this out. It's here because naming conventions were not followed through. 54 // So fix the Test hook names and then remove this. 55 var events = map[string]release.HookEvent{ 56 release.HookPreInstall.String(): release.HookPreInstall, 57 release.HookPostInstall.String(): release.HookPostInstall, 58 release.HookPreDelete.String(): release.HookPreDelete, 59 release.HookPostDelete.String(): release.HookPostDelete, 60 release.HookPreUpgrade.String(): release.HookPreUpgrade, 61 release.HookPostUpgrade.String(): release.HookPostUpgrade, 62 release.HookPreRollback.String(): release.HookPreRollback, 63 release.HookPostRollback.String(): release.HookPostRollback, 64 release.HookTest.String(): release.HookTest, 65 // Support test-success for backward compatibility with Helm 2 tests 66 "test-success": release.HookTest, 67 } 68 69 // SortManifests takes a map of filename/YAML contents, splits the file 70 // by manifest entries, and sorts the entries into hook types. 71 // 72 // The resulting hooks struct will be populated with all of the generated hooks. 73 // Any file that does not declare one of the hook types will be placed in the 74 // 'generic' bucket. 75 // 76 // Files that do not parse into the expected format are simply placed into a map and 77 // returned. 78 func SortManifests(files map[string]string, apis chartutil.VersionSet, ordering KindSortOrder) ([]*release.Hook, []Manifest, error) { 79 result := &result{} 80 81 var sortedFilePaths []string 82 for filePath := range files { 83 sortedFilePaths = append(sortedFilePaths, filePath) 84 } 85 sort.Strings(sortedFilePaths) 86 87 for _, filePath := range sortedFilePaths { 88 content := files[filePath] 89 90 // Skip partials. We could return these as a separate map, but there doesn't 91 // seem to be any need for that at this time. 92 if strings.HasPrefix(path.Base(filePath), "_") { 93 continue 94 } 95 // Skip empty files and log this. 96 if strings.TrimSpace(content) == "" { 97 continue 98 } 99 100 manifestFile := &manifestFile{ 101 entries: SplitManifests(content), 102 path: filePath, 103 apis: apis, 104 } 105 106 if err := manifestFile.sort(result); err != nil { 107 return result.hooks, result.generic, err 108 } 109 } 110 111 return sortHooksByKind(result.hooks, ordering), sortManifestsByKind(result.generic, ordering), nil 112 } 113 114 // sort takes a manifestFile object which may contain multiple resource definition 115 // entries and sorts each entry by hook types, and saves the resulting hooks and 116 // generic manifests (or non-hooks) to the result struct. 117 // 118 // To determine hook type, it looks for a YAML structure like this: 119 // 120 // kind: SomeKind 121 // apiVersion: v1 122 // metadata: 123 // annotations: 124 // helm.sh/hook: pre-install 125 // 126 // To determine the policy to delete the hook, it looks for a YAML structure like this: 127 // 128 // kind: SomeKind 129 // apiVersion: v1 130 // metadata: 131 // annotations: 132 // helm.sh/hook-delete-policy: hook-succeeded 133 func (file *manifestFile) sort(result *result) error { 134 // Go through manifests in order found in file (function `SplitManifests` creates integer-sortable keys) 135 var sortedEntryKeys []string 136 for entryKey := range file.entries { 137 sortedEntryKeys = append(sortedEntryKeys, entryKey) 138 } 139 sort.Sort(BySplitManifestsOrder(sortedEntryKeys)) 140 141 for _, entryKey := range sortedEntryKeys { 142 m := file.entries[entryKey] 143 144 var entry SimpleHead 145 if err := yaml.Unmarshal([]byte(m), &entry); err != nil { 146 return errors.Wrapf(err, "YAML parse error on %s", file.path) 147 } 148 149 if !hasAnyAnnotation(entry) { 150 result.generic = append(result.generic, Manifest{ 151 Name: file.path, 152 Content: m, 153 Head: &entry, 154 }) 155 continue 156 } 157 158 hookTypes, ok := entry.Metadata.Annotations[release.HookAnnotation] 159 if !ok { 160 result.generic = append(result.generic, Manifest{ 161 Name: file.path, 162 Content: m, 163 Head: &entry, 164 }) 165 continue 166 } 167 168 hw := calculateHookWeight(entry) 169 170 h := &release.Hook{ 171 Name: entry.Metadata.Name, 172 Kind: entry.Kind, 173 Path: file.path, 174 Manifest: m, 175 Events: []release.HookEvent{}, 176 Weight: hw, 177 DeletePolicies: []release.HookDeletePolicy{}, 178 } 179 180 isUnknownHook := false 181 for _, hookType := range strings.Split(hookTypes, ",") { 182 hookType = strings.ToLower(strings.TrimSpace(hookType)) 183 e, ok := events[hookType] 184 if !ok { 185 isUnknownHook = true 186 break 187 } 188 h.Events = append(h.Events, e) 189 } 190 191 if isUnknownHook { 192 log.Printf("info: skipping unknown hook: %q", hookTypes) 193 continue 194 } 195 196 result.hooks = append(result.hooks, h) 197 198 operateAnnotationValues(entry, release.HookDeleteAnnotation, func(value string) { 199 h.DeletePolicies = append(h.DeletePolicies, release.HookDeletePolicy(value)) 200 }) 201 } 202 203 return nil 204 } 205 206 // hasAnyAnnotation returns true if the given entry has any annotations at all. 207 func hasAnyAnnotation(entry SimpleHead) bool { 208 return entry.Metadata != nil && 209 entry.Metadata.Annotations != nil && 210 len(entry.Metadata.Annotations) != 0 211 } 212 213 // calculateHookWeight finds the weight in the hook weight annotation. 214 // 215 // If no weight is found, the assigned weight is 0 216 func calculateHookWeight(entry SimpleHead) int { 217 hws := entry.Metadata.Annotations[release.HookWeightAnnotation] 218 hw, err := strconv.Atoi(hws) 219 if err != nil { 220 hw = 0 221 } 222 return hw 223 } 224 225 // operateAnnotationValues finds the given annotation and runs the operate function with the value of that annotation 226 func operateAnnotationValues(entry SimpleHead, annotation string, operate func(p string)) { 227 if dps, ok := entry.Metadata.Annotations[annotation]; ok { 228 for _, dp := range strings.Split(dps, ",") { 229 dp = strings.ToLower(strings.TrimSpace(dp)) 230 operate(dp) 231 } 232 } 233 }