github.com/opendevstack/tailor@v1.3.5-0.20220119161809-cab064e60a67/pkg/openshift/item.go (about) 1 package openshift 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "regexp" 7 "strconv" 8 "strings" 9 10 "github.com/ghodss/yaml" 11 "github.com/opendevstack/tailor/pkg/cli" 12 "github.com/opendevstack/tailor/pkg/utils" 13 "github.com/xeipuuv/gojsonpointer" 14 ) 15 16 var ( 17 annotationsPath = "/metadata/annotations" 18 platformManagedSimpleFields = []string{ 19 "/groupNames", 20 "/imagePullSecrets", 21 "/metadata/creationTimestamp", 22 "/metadata/generation", 23 "/metadata/managedFields", 24 "/metadata/namespace", 25 "/metadata/resourceVersion", 26 "/metadata/selfLink", 27 "/metadata/uid", 28 "/secrets", 29 "/spec/clusterIP", 30 "/spec/clusterIPs", 31 "/spec/jobTemplate/metadata/creationTimestamp", 32 "/spec/jobTemplate/spec/template/metadata/creationTimestamp", 33 "/spec/selector/matchLabels/controller-uid", 34 "/spec/tags", 35 "/spec/template/metadata/creationTimestamp", 36 "/spec/template/metadata/labels/controller-uid", 37 "/spec/volumeName", 38 "/status", 39 "/userNames", 40 } 41 platformManagedRegexFields = []string{ 42 "^/spec/triggers/[0-9]*/imageChangeParams/lastTriggeredImage", 43 } 44 immutableFields = map[string][]string{ 45 "PersistentVolumeClaim": { 46 "/spec/accessModes", 47 "/spec/storageClassName", 48 "/spec/resources/requests/storage", 49 }, 50 "Route": { 51 "/spec/host", 52 }, 53 "Secret": { 54 "/type", 55 }, 56 } 57 58 KindMapping = map[string]string{ 59 "svc": "Service", 60 "service": "Service", 61 "route": "Route", 62 "dc": "DeploymentConfig", 63 "deploymentconfig": "DeploymentConfig", 64 "deployment": "Deployment", 65 "bc": "BuildConfig", 66 "buildconfig": "BuildConfig", 67 "is": "ImageStream", 68 "imagestream": "ImageStream", 69 "pvc": "PersistentVolumeClaim", 70 "persistentvolumeclaim": "PersistentVolumeClaim", 71 "template": "Template", 72 "cm": "ConfigMap", 73 "configmap": "ConfigMap", 74 "secret": "Secret", 75 "rolebinding": "RoleBinding", 76 "serviceaccount": "ServiceAccount", 77 "cronjob": "CronJob", 78 "cj": "CronJob", 79 "job": "Job", 80 "limitrange": "LimitRange", 81 "resourcequota": "ResourceQuota", 82 "quota": "ResourceQuota", 83 "hpa": "HorizontalPodAutoscaler", 84 "statefulset": "StatefulSet", 85 } 86 ) 87 88 type ResourceItem struct { 89 Source string 90 Kind string 91 Name string 92 Labels map[string]interface{} 93 Annotations map[string]interface{} 94 Paths []string 95 Config map[string]interface{} 96 AnnotationsPresent bool 97 LastAppliedConfiguration map[string]interface{} 98 LastAppliedAnnotations map[string]interface{} 99 Comparable bool 100 } 101 102 func NewResourceItem(m map[string]interface{}, source string) (*ResourceItem, error) { 103 item := &ResourceItem{Source: source} 104 err := item.parseConfig(m) 105 return item, err 106 } 107 108 // FullName returns kind/name, with kind being the long form (e.g. "DeploymentConfig"). 109 func (i *ResourceItem) FullName() string { 110 return i.Kind + "/" + i.Name 111 } 112 113 // ShortName returns kind/name, with kind being the shortest possible 114 // reference of kind (e.g. "dc" for "DeploymentConfig"). 115 func (i *ResourceItem) ShortName() string { 116 return kindToShortMapping[i.Kind] + "/" + i.Name 117 } 118 119 func (i *ResourceItem) HasLabel(label string) bool { 120 labelParts := strings.Split(label, "=") 121 if _, ok := i.Labels[labelParts[0]]; !ok { 122 return false 123 } else if i.Labels[labelParts[0]].(string) != labelParts[1] { 124 return false 125 } 126 return true 127 } 128 129 func (i *ResourceItem) DesiredConfig() (string, error) { 130 y, _ := yaml.Marshal(i.Config) 131 return string(y), nil 132 } 133 134 func (i *ResourceItem) YamlConfig() string { 135 y, _ := yaml.Marshal(i.Config) 136 return string(y) 137 } 138 139 // parseConfig uses the config to initialise an item. The logic is the same 140 // for template and platform items, with no knowledge of the "other" item - it 141 // may or may not exist. 142 func (i *ResourceItem) parseConfig(m map[string]interface{}) error { 143 // Extract kind 144 kindPointer, _ := gojsonpointer.NewJsonPointer("/kind") 145 kind, _, err := kindPointer.Get(m) 146 if err != nil { 147 return err 148 } 149 i.Kind = kind.(string) 150 151 // Extract name 152 namePointer, _ := gojsonpointer.NewJsonPointer("/metadata/name") 153 name, _, noNameErr := namePointer.Get(m) 154 if noNameErr == nil { 155 i.Name = name.(string) 156 } else { 157 generateNamePointer, _ := gojsonpointer.NewJsonPointer("/metadata/generateName") 158 generateName, _, err := generateNamePointer.Get(m) 159 if err != nil { 160 return fmt.Errorf("Resource does not have paths /metadata/name or /metadata/generateName: %s", err) 161 } 162 i.Name = generateName.(string) 163 } 164 165 // Determine if item is comparable and therefore relevant for Tailor 166 i.Comparable = true 167 // Secrets of type "kubernetes.io/dockercfg" and 168 // "kubernetes.io/service-account-token" were not returned in "oc export". 169 // Those secrets are generated by OpenShift automatically and should not 170 // be controlled by Tailor. 171 if i.Kind == "Secret" { 172 typePointer, _ := gojsonpointer.NewJsonPointer("/type") 173 typeVal, _, err := typePointer.Get(m) 174 if err != nil { 175 return fmt.Errorf("Secret has no field /type: %s", err) 176 } 177 irrelevantSecrets := []string{ 178 "kubernetes.io/dockercfg", 179 "kubernetes.io/service-account-token", 180 } 181 if utils.Includes(irrelevantSecrets, typeVal.(string)) { 182 i.Comparable = false 183 cli.DebugMsg( 184 "Removed secret", 185 i.Name, 186 "of type", 187 typeVal.(string), 188 "as it cannot be compared properly", 189 ) 190 } 191 } 192 193 // Extract labels 194 labelsPointer, _ := gojsonpointer.NewJsonPointer("/metadata/labels") 195 labels, _, err := labelsPointer.Get(m) 196 if err != nil { 197 i.Labels = make(map[string]interface{}) 198 } else { 199 i.Labels = labels.(map[string]interface{}) 200 } 201 202 // Extract annotations 203 annotationsPointer, _ := gojsonpointer.NewJsonPointer("/metadata/annotations") 204 annotations, _, err := annotationsPointer.Get(m) 205 i.Annotations = make(map[string]interface{}) 206 i.AnnotationsPresent = false 207 if err == nil { 208 i.AnnotationsPresent = true 209 for k, v := range annotations.(map[string]interface{}) { 210 i.Annotations[k] = v 211 } 212 } 213 214 i.LastAppliedConfiguration = make(map[string]interface{}) 215 i.LastAppliedAnnotations = make(map[string]interface{}) 216 217 // kubectl.kubernetes.io/last-applied-configuration 218 lastAppliedConfigurationPointer, _ := gojsonpointer.NewJsonPointer("/metadata/annotations/kubectl.kubernetes.io~1last-applied-configuration") 219 lastAppliedConfiguration, _, err := lastAppliedConfigurationPointer.Get(m) 220 if err == nil { 221 s := lastAppliedConfiguration.(string) 222 var f interface{} 223 err := json.Unmarshal([]byte(s), &f) 224 if err != nil { 225 return err 226 } 227 lac := f.(map[string]interface{}) 228 i.LastAppliedConfiguration = lac 229 } 230 // kubectl.kubernetes.io/last-applied-configuration -> annotations 231 lastAppliedAnnotationsPointer, _ := gojsonpointer.NewJsonPointer("/metadata/annotations") 232 lastAppliedAnnotations, _, err := lastAppliedAnnotationsPointer.Get(i.LastAppliedConfiguration) 233 if err == nil { 234 i.LastAppliedAnnotations = lastAppliedAnnotations.(map[string]interface{}) 235 } 236 237 // kubectl.kubernetes.io/last-applied-configuration -> container images 238 // get all container image definitions, and paste them into the spec. 239 if i.Kind == "DeploymentConfig" { 240 containerSpecsPointer, _ := gojsonpointer.NewJsonPointer("/spec/template/spec/containers") 241 appliedContainerSpecs, _, err := containerSpecsPointer.Get(i.LastAppliedConfiguration) 242 if err == nil { 243 for i, val := range appliedContainerSpecs.([]interface{}) { 244 acs := val.(map[string]interface{}) 245 if appliedImageVal, ok := acs["image"]; ok { 246 _, _, err := containerSpecsPointer.Get(m) 247 if err == nil { 248 imagePointer, _ := gojsonpointer.NewJsonPointer(fmt.Sprintf("/spec/template/spec/containers/%d/image", i)) 249 _, err := imagePointer.Set(m, appliedImageVal) 250 if err != nil { 251 cli.VerboseMsg("could not apply:", err.Error()) 252 } 253 } 254 } 255 } 256 } else { // backwards compatibility for pre 0.13.0 257 tailorAppliedConfigAnnotation := "tailor.opendevstack.org/applied-config" 258 escapedTailorAppliedConfigAnnotation := strings.Replace(tailorAppliedConfigAnnotation, "/", "~1", -1) 259 tailorAppliedConfigAnnotationPath := annotationsPath + "/" + escapedTailorAppliedConfigAnnotation 260 261 tailorAppliedConfigAnnotationPointer, err := gojsonpointer.NewJsonPointer(tailorAppliedConfigAnnotationPath) 262 if err != nil { 263 return fmt.Errorf("Could not create JSON pointer %s: %s", tailorAppliedConfigAnnotationPath, err) 264 } 265 val, _, err := tailorAppliedConfigAnnotationPointer.Get(m) 266 if err == nil { 267 valBytes := []byte(val.(string)) 268 tacFields := map[string]string{} 269 err = json.Unmarshal(valBytes, &tacFields) 270 if err != nil { 271 return fmt.Errorf("Could not unmarshal JSON %s: %s", tailorAppliedConfigAnnotationPath, val) 272 } 273 for k, v := range tacFields { 274 specPointer, err := gojsonpointer.NewJsonPointer(k) 275 if err != nil { 276 return fmt.Errorf("Could not create JSON pointer %s: %s", k, err) 277 } 278 _, err = specPointer.Set(m, v) 279 if err != nil { 280 return fmt.Errorf("Could not set %s: %s", k, err) 281 } 282 } 283 _, err = tailorAppliedConfigAnnotationPointer.Delete(m) 284 if err != nil { 285 return fmt.Errorf("Could not delete %s: %s", tailorAppliedConfigAnnotationPath, err) 286 } 287 } 288 delete(i.Annotations, tailorAppliedConfigAnnotation) 289 } 290 } 291 292 // Remove platform-managed simple fields 293 legacyFields := []string{"/userNames", "/groupNames"} 294 for _, p := range platformManagedSimpleFields { 295 deletePointer, _ := gojsonpointer.NewJsonPointer(p) 296 _, _ = deletePointer.Delete(m) 297 if utils.Includes(legacyFields, p) { 298 cli.DebugMsg("Removed", p, "which is used for legacy clients, but not supported by Tailor") 299 } 300 } 301 302 i.Config = m 303 304 // Build list of JSON pointers 305 i.walkMap(m, "") 306 307 // Iterate over extracted paths and massage as necessary 308 newPaths := []string{} 309 deletedPathIndices := []int{} 310 for pathIndex, path := range i.Paths { 311 312 // Remove platform-managed regex fields 313 for _, platformManagedField := range platformManagedRegexFields { 314 matched, _ := regexp.MatchString(platformManagedField, path) 315 if matched { 316 deletePointer, _ := gojsonpointer.NewJsonPointer(path) 317 _, _ = deletePointer.Delete(i.Config) 318 deletedPathIndices = append(deletedPathIndices, pathIndex) 319 } 320 } 321 } 322 323 // As we delete items from a slice, we need to adjust the pre-calculated 324 // indices to delete (shift to left by one for each deletion). 325 indexOffset := 0 326 for _, pathIndex := range deletedPathIndices { 327 deletionIndex := pathIndex + indexOffset 328 cli.DebugMsg("Removing platform managed path", i.Paths[deletionIndex]) 329 i.Paths = append(i.Paths[:deletionIndex], i.Paths[deletionIndex+1:]...) 330 indexOffset = indexOffset - 1 331 } 332 if len(newPaths) > 0 { 333 i.Paths = append(i.Paths, newPaths...) 334 } 335 336 return nil 337 } 338 339 func (i *ResourceItem) isImmutableField(field string) bool { 340 for _, key := range immutableFields[i.Kind] { 341 if key == field { 342 return true 343 } 344 } 345 return false 346 } 347 348 func (i *ResourceItem) walkMap(m map[string]interface{}, pointer string) { 349 for k, v := range m { 350 i.handleKeyValue(k, v, pointer) 351 } 352 } 353 354 func (i *ResourceItem) walkArray(a []interface{}, pointer string) { 355 for k, v := range a { 356 i.handleKeyValue(k, v, pointer) 357 } 358 } 359 360 func (i *ResourceItem) handleKeyValue(k interface{}, v interface{}, pointer string) { 361 362 strK := "" 363 switch kv := k.(type) { 364 case string: 365 strK = kv 366 case int: 367 strK = strconv.Itoa(kv) 368 } 369 370 relativePointer := utils.JSONPointerPath(strK) 371 absolutePointer := pointer + "/" + relativePointer 372 i.Paths = append(i.Paths, absolutePointer) 373 374 switch vv := v.(type) { 375 case []interface{}: 376 i.walkArray(vv, absolutePointer) 377 case map[string]interface{}: 378 i.walkMap(vv, absolutePointer) 379 } 380 } 381 382 func (i *ResourceItem) removeAnnotion(annotation string) { 383 path := "/metadata/annotations/" + utils.JSONPointerPath(annotation) 384 deletePointer, _ := gojsonpointer.NewJsonPointer(path) 385 _, err := deletePointer.Delete(i.Config) 386 if err != nil { 387 cli.DebugMsg(fmt.Sprintf("Could not remove annotation %s from item", annotation)) 388 } 389 } 390 391 // prepareForComparisonWithPlatformItem massages template item in such a way 392 // that it can be compared with the given platform item: 393 // - copy value from platformItem to templateItem for externally modified paths 394 func (templateItem *ResourceItem) prepareForComparisonWithPlatformItem(platformItem *ResourceItem, preservePaths []string) error { 395 for _, path := range preservePaths { 396 cli.DebugMsg("Trying to preserve path", path, "in platform item", platformItem.FullName()) 397 pathPointer, _ := gojsonpointer.NewJsonPointer(path) 398 platformItemVal, _, err := pathPointer.Get(platformItem.Config) 399 if err != nil { 400 cli.DebugMsg("No such path", path, "in platform item", platformItem.FullName()) 401 // As the current state for this path is "undefined" we need to make 402 // sure that the desired state does not define any value for it, 403 // otherwise it will show in the diff. 404 _, _ = pathPointer.Delete(templateItem.Config) 405 } else { 406 _, err = pathPointer.Set(templateItem.Config, platformItemVal) 407 if err != nil { 408 cli.DebugMsg(fmt.Sprintf( 409 "Could not set %s to %v in template item %s", 410 path, 411 platformItemVal, 412 templateItem.FullName(), 413 )) 414 } else { 415 // Add preserved path and its subpaths to the paths slice 416 // of the template item. 417 templateItem.Paths = append(templateItem.Paths, path) 418 switch vv := platformItemVal.(type) { 419 case []interface{}: 420 templateItem.walkArray(vv, path) 421 case map[string]interface{}: 422 templateItem.walkMap(vv, path) 423 } 424 } 425 } 426 } 427 428 return nil 429 } 430 431 // prepareForComparisonWithTemplateItem massages platform item in such a way 432 // that it can be compared with the given template item: 433 // - remove all annotations which are not managed 434 func (platformItem *ResourceItem) prepareForComparisonWithTemplateItem(templateItem *ResourceItem) error { 435 // Fix apiVersion 436 // When running "oc process" on a template with a "Deployment" in 437 // "apps/v1", and then running "oc export", the export contains 438 // "apiVersion=extensions/v1beta1". If "oc process" is run *after* 439 // "oc export", this issue is not present. Tailor runs "oc process" first 440 // because it uncovers potential issues with local, desired state. 441 // Therefore, we use the last applied apiVersion if we find 442 // "apiVersion=extensions/v1beta1" so that no drift is reported. 443 apiVersionPath := "/apiVersion" 444 apiVersionPointer, _ := gojsonpointer.NewJsonPointer(apiVersionPath) 445 apiVersion, _, err := apiVersionPointer.Get(platformItem.Config) 446 if err == nil { 447 lastAppliedAPIVersion, _, err := apiVersionPointer.Get(platformItem.LastAppliedConfiguration) 448 if err == nil { 449 if apiVersion.(string) == "extensions/v1beta1" { 450 _, err := apiVersionPointer.Set(platformItem.Config, lastAppliedAPIVersion) 451 if err != nil { 452 cli.DebugMsg("could not set apiVersion:", err.Error()) 453 } 454 } 455 } 456 } 457 458 // Annotations 459 unmanagedAnnotations := []string{} 460 for a := range platformItem.Annotations { 461 if _, ok := templateItem.Annotations[a]; ok { 462 continue 463 } 464 if _, ok := platformItem.LastAppliedAnnotations[a]; ok { 465 continue 466 } 467 unmanagedAnnotations = append(unmanagedAnnotations, a) 468 } 469 for _, a := range unmanagedAnnotations { 470 path := "/metadata/annotations/" + utils.JSONPointerPath(a) 471 deletePointer, _ := gojsonpointer.NewJsonPointer(path) 472 _, err := deletePointer.Delete(platformItem.Config) 473 if err != nil { 474 return fmt.Errorf("Could not delete %s from configuration", path) 475 } 476 platformItem.Paths = utils.Remove(platformItem.Paths, path) 477 } 478 479 return nil 480 }