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  }