github.com/opendevstack/tailor@v1.3.5-0.20220119161809-cab064e60a67/pkg/openshift/changeset.go (about) 1 package openshift 2 3 import ( 4 "fmt" 5 "sort" 6 "strings" 7 8 "github.com/opendevstack/tailor/pkg/utils" 9 "github.com/xeipuuv/gojsonpointer" 10 ) 11 12 var ( 13 // Resources with no dependencies go first 14 kindOrder = map[string]string{ 15 "Template": "a", 16 "ServiceAccount": "b", 17 "RoleBinding": "c", 18 "ConfigMap": "d", 19 "Secret": "e", 20 "LimitRange": "f", 21 "ResourceQuota": "g", 22 "PersistentVolumeClaim": "h", 23 "CronJob": "i", 24 "Job": "j", 25 "ImageStream": "k", 26 "BuildConfig": "l", 27 "StatefulSet": "m", 28 "DeploymentConfig": "n", 29 "Deployment": "o", 30 "HorizontalPodAutoscaler": "p", 31 "Service": "q", 32 "Route": "r", 33 } 34 ) 35 36 type Changeset struct { 37 Create []*Change 38 Update []*Change 39 Delete []*Change 40 Noop []*Change 41 } 42 43 func NewChangeset(platformBasedList, templateBasedList *ResourceList, upsertOnly bool, allowRecreate bool, preservePaths []string) (*Changeset, error) { 44 changeset := &Changeset{ 45 Create: []*Change{}, 46 Delete: []*Change{}, 47 Update: []*Change{}, 48 Noop: []*Change{}, 49 } 50 51 // items to delete 52 if !upsertOnly { 53 for _, item := range platformBasedList.Items { 54 if _, err := templateBasedList.getItem(item.Kind, item.Name); err != nil { 55 change := &Change{ 56 Action: "Delete", 57 Kind: item.Kind, 58 Name: item.Name, 59 CurrentState: item.YamlConfig(), 60 DesiredState: "", 61 } 62 changeset.Add(change) 63 } 64 } 65 } 66 67 // items to create 68 for _, item := range templateBasedList.Items { 69 if _, err := platformBasedList.getItem(item.Kind, item.Name); err != nil { 70 desiredState, err := item.DesiredConfig() 71 if err != nil { 72 return changeset, err 73 } 74 change := &Change{ 75 Action: "Create", 76 Kind: item.Kind, 77 Name: item.Name, 78 CurrentState: "", 79 DesiredState: desiredState, 80 } 81 changeset.Add(change) 82 } 83 } 84 85 // items to update 86 for _, templateItem := range templateBasedList.Items { 87 platformItem, err := platformBasedList.getItem( 88 templateItem.Kind, 89 templateItem.Name, 90 ) 91 if err == nil { 92 actualReservePaths := []string{} 93 for _, path := range preservePaths { 94 pathParts := strings.Split(path, ":") 95 if len(pathParts) > 3 { 96 return changeset, fmt.Errorf( 97 "%s is not a valid preserve argument", 98 path, 99 ) 100 } 101 // Preserved paths can be either: 102 // - globally (e.g. /spec/name) 103 // - per-kind (e.g. bc:/spec/name) 104 // - per-resource (e.g. bc:foo:/spec/name) 105 if len(pathParts) == 1 || 106 (len(pathParts) == 2 && 107 templateItem.Kind == KindMapping[strings.ToLower(pathParts[0])]) || 108 (len(pathParts) == 3 && 109 templateItem.Kind == KindMapping[strings.ToLower(pathParts[0])] && 110 templateItem.Name == strings.ToLower(pathParts[1])) { 111 // We only care about the last part (the JSON path) as we 112 // are already "inside" the item 113 actualReservePaths = append(actualReservePaths, pathParts[len(pathParts)-1]) 114 } 115 } 116 117 changes, err := calculateChanges(templateItem, platformItem, actualReservePaths, allowRecreate) 118 if err != nil { 119 return changeset, err 120 } 121 changeset.Add(changes...) 122 } 123 } 124 125 return changeset, nil 126 } 127 128 func calculateChanges(templateItem *ResourceItem, platformItem *ResourceItem, preservePaths []string, allowRecreate bool) ([]*Change, error) { 129 err := templateItem.prepareForComparisonWithPlatformItem(platformItem, preservePaths) 130 if err != nil { 131 return nil, err 132 } 133 err = platformItem.prepareForComparisonWithTemplateItem(templateItem) 134 if err != nil { 135 return nil, err 136 } 137 138 comparedPaths := map[string]bool{} 139 addedPaths := []string{} 140 141 for _, path := range templateItem.Paths { 142 143 // Skip subpaths of already added paths 144 if utils.IncludesPrefix(addedPaths, path) { 145 continue 146 } 147 148 // Paths that should be preserved are no-ops 149 if utils.IncludesPrefix(preservePaths, path) { 150 comparedPaths[path] = true 151 continue 152 } 153 154 pathPointer, _ := gojsonpointer.NewJsonPointer(path) 155 templateItemVal, _, _ := pathPointer.Get(templateItem.Config) 156 platformItemVal, _, err := pathPointer.Get(platformItem.Config) 157 158 if err != nil { 159 // Pointer does not exist in platformItem 160 if templateItem.isImmutableField(path) { 161 if allowRecreate { 162 return recreateChanges(templateItem, platformItem), nil 163 } else { 164 return nil, recreateProtectionError(path, platformItem.ShortName()) 165 } 166 167 } 168 comparedPaths[path] = true 169 170 // OpenShift sometimes removes the whole field when the value is an 171 // empty string. Therefore, we do not want to add the path in that 172 // case, otherwise we would cause endless drift. See 173 // https://github.com/opendevstack/tailor/issues/157. 174 if v, ok := templateItemVal.(string); ok && len(v) == 0 { 175 _, err := pathPointer.Delete(templateItem.Config) 176 if err != nil { 177 return nil, err 178 } 179 } else { 180 addedPaths = append(addedPaths, path) 181 } 182 } else { 183 // Pointer exists in both items 184 switch templateItemVal.(type) { 185 case []interface{}: 186 // slice content changed, continue ... 187 comparedPaths[path] = true 188 case []string: 189 // slice content changed, continue ... 190 comparedPaths[path] = true 191 case map[string]interface{}: 192 // map content changed, continue 193 comparedPaths[path] = true 194 default: 195 if templateItemVal == platformItemVal { 196 comparedPaths[path] = true 197 } else { 198 if templateItem.isImmutableField(path) { 199 if allowRecreate { 200 return recreateChanges(templateItem, platformItem), nil 201 } else { 202 return nil, recreateProtectionError(path, platformItem.ShortName()) 203 } 204 } 205 comparedPaths[path] = true 206 } 207 } 208 } 209 } 210 211 deletedPaths := []string{} 212 213 for _, path := range platformItem.Paths { 214 if _, ok := comparedPaths[path]; !ok { 215 // Do not delete subpaths of already deleted paths 216 if utils.IncludesPrefix(deletedPaths, path) { 217 continue 218 } 219 220 pp, _ := gojsonpointer.NewJsonPointer(path) 221 val, _, err := pp.Get(platformItem.Config) 222 if err != nil { 223 return nil, err 224 } 225 if val == nil { 226 continue 227 } 228 229 // Skip annotations 230 if path == annotationsPath { 231 if x, ok := val.(map[string]interface{}); ok { 232 if len(x) == 0 { 233 _, err := pp.Set(templateItem.Config, map[string]interface{}{}) 234 if err != nil { 235 return nil, err 236 } 237 } 238 } 239 continue 240 } 241 242 // If the value is an "empty value", there is no need to detect 243 // drift for it. This allows template authors to reduce boilerplate 244 // by omitting fields that have an "empty value". 245 if x, ok := val.(map[string]interface{}); ok { 246 if len(x) == 0 { 247 _, err := pp.Set(templateItem.Config, map[string]interface{}{}) 248 if err != nil { 249 return nil, err 250 } 251 continue 252 } 253 } 254 if x, ok := val.([]interface{}); ok { 255 if len(x) == 0 { 256 _, err := pp.Set(templateItem.Config, []interface{}{}) 257 if err != nil { 258 return nil, err 259 } 260 continue 261 } 262 } 263 if x, ok := val.([]string); ok { 264 if len(x) == 0 { 265 _, err := pp.Set(templateItem.Config, []string{}) 266 if err != nil { 267 return nil, err 268 } 269 continue 270 } 271 } 272 273 // Pointer exist only in platformItem 274 comparedPaths[path] = true 275 deletedPaths = append(deletedPaths, path) 276 } 277 } 278 279 c := NewChange(templateItem, platformItem) 280 281 return []*Change{c}, nil 282 } 283 284 // Blank is true when there is no change across Create, Update, Delete. 285 func (c *Changeset) Blank() bool { 286 return len(c.Create) == 0 && len(c.Update) == 0 && len(c.Delete) == 0 287 } 288 289 // ExactlyOne is true when there is just a single change across Create, Update, Delete. 290 func (c *Changeset) ExactlyOne() bool { 291 return len(c.Create)+len(c.Update)+len(c.Delete) == 1 292 } 293 294 // Add adds given changes to the changeset. 295 func (c *Changeset) Add(changes ...*Change) { 296 for _, change := range changes { 297 switch change.Action { 298 case "Create": 299 c.Create = append(c.Create, change) 300 sort.Slice(c.Create, func(i, j int) bool { 301 return kindOrder[c.Create[i].Kind] < kindOrder[c.Create[j].Kind] 302 }) 303 case "Update": 304 c.Update = append(c.Update, change) 305 sort.Slice(c.Update, func(i, j int) bool { 306 return kindOrder[c.Update[i].Kind] < kindOrder[c.Update[j].Kind] 307 }) 308 case "Delete": 309 c.Delete = append(c.Delete, change) 310 sort.Slice(c.Delete, func(i, j int) bool { 311 return kindOrder[c.Delete[i].Kind] > kindOrder[c.Delete[j].Kind] 312 }) 313 case "Noop": 314 c.Noop = append(c.Noop, change) 315 } 316 } 317 } 318 319 func recreateProtectionError(path string, itemName string) error { 320 return fmt.Errorf( 321 "Path '%s' of '%s' is immutable.\n"+ 322 "Changing its value would require to delete "+ 323 "and re-create the whole resource, which Tailor prevents by default.\n\n"+ 324 "You may pick one of the following options to resolve this:\n\n"+ 325 "* pass --allow-recreate to give permission to recreate the resource\n"+ 326 "* use --preserve-immutable-fields to keep the cluster state for all immutable paths\n"+ 327 "* change the template to be in sync with the cluster state\n"+ 328 "* exclude the resource from comparison via --exclude %s", 329 path, 330 itemName, 331 itemName, 332 ) 333 }