github.com/zntrio/harp/v2@v2.0.9/pkg/bundle/compare/diff.go (about)

     1  // Licensed to Elasticsearch B.V. under one or more contributor
     2  // license agreements. See the NOTICE file distributed with
     3  // this work for additional information regarding copyright
     4  // ownership. Elasticsearch B.V. licenses this file to you under
     5  // the Apache License, Version 2.0 (the "License"); you may
     6  // not use this file except in compliance with the License.
     7  // You may obtain a copy of the License at
     8  //
     9  //     http://www.apache.org/licenses/LICENSE-2.0
    10  //
    11  // Unless required by applicable law or agreed to in writing,
    12  // software distributed under the License is distributed on an
    13  // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
    14  // KIND, either express or implied.  See the License for the
    15  // specific language governing permissions and limitations
    16  // under the License.
    17  
    18  package compare
    19  
    20  import (
    21  	"fmt"
    22  	"sort"
    23  	"strings"
    24  
    25  	bundlev1 "github.com/zntrio/harp/v2/api/gen/go/harp/bundle/v1"
    26  	"github.com/zntrio/harp/v2/pkg/bundle/secret"
    27  	"github.com/zntrio/harp/v2/pkg/sdk/security"
    28  )
    29  
    30  const (
    31  	// Add describes an operation where the traget path object has been added.
    32  	Add string = "add"
    33  	// Remove describes that entity has been removed.
    34  	Remove string = "remove"
    35  	// Replace describes an operation to replace content of the target path object.
    36  	Replace string = "replace"
    37  )
    38  
    39  // OpLog represents operation log calculate from bundle differences.
    40  type OpLog []DiffItem
    41  
    42  // DiffItem represents bundle comparison operations.
    43  type DiffItem struct {
    44  	Operation string `json:"op"`
    45  	Type      string `json:"type"`
    46  	Path      string `json:"path"`
    47  	Value     string `json:"value,omitempty"`
    48  }
    49  
    50  // -----------------------------------------------------------------------------
    51  
    52  // Diff calculates bundle differences.
    53  //
    54  //nolint:funlen,gocognit,gocyclo // To refactor
    55  func Diff(src, dst *bundlev1.Bundle) ([]DiffItem, error) {
    56  	// Check arguments
    57  	if src == nil {
    58  		return nil, fmt.Errorf("unable to diff with a nil source")
    59  	}
    60  	if dst == nil {
    61  		return nil, fmt.Errorf("unable to diff with a nil destination")
    62  	}
    63  
    64  	diffs := []DiffItem{}
    65  
    66  	// Index source packages
    67  	srcIndex := map[string]*bundlev1.Package{}
    68  	for _, srcPkg := range src.Packages {
    69  		if srcPkg == nil || srcPkg.Secrets == nil {
    70  			continue
    71  		}
    72  
    73  		srcIndex[srcPkg.Name] = srcPkg
    74  	}
    75  
    76  	// Index destination packages
    77  	dstIndex := map[string]*bundlev1.Package{}
    78  	for _, dstPkg := range dst.Packages {
    79  		if dstPkg == nil || dstPkg.Secrets == nil {
    80  			continue
    81  		}
    82  
    83  		dstIndex[dstPkg.Name] = dstPkg
    84  		if _, ok := srcIndex[dstPkg.Name]; !ok {
    85  			// Package has been added
    86  			diffs = append(diffs, DiffItem{
    87  				Operation: Add,
    88  				Type:      "package",
    89  				Path:      dstPkg.Name,
    90  			})
    91  
    92  			// Add keys
    93  			for _, s := range dstPkg.Secrets.Data {
    94  				if s == nil {
    95  					continue
    96  				}
    97  
    98  				// Unpack secret value
    99  				var data string
   100  				if err := secret.Unpack(s.Value, &data); err != nil {
   101  					return nil, fmt.Errorf("unable to unpack %q - %q secret value: %w", dstPkg.Name, s.Key, err)
   102  				}
   103  
   104  				diffs = append(diffs, DiffItem{
   105  					Operation: Add,
   106  					Type:      "secret",
   107  					Path:      fmt.Sprintf("%s#%s", dstPkg.Name, s.Key),
   108  					Value:     data,
   109  				})
   110  			}
   111  		}
   112  	}
   113  
   114  	// Compute package changes
   115  	for n, sp := range srcIndex {
   116  		dp, ok := dstIndex[n]
   117  		if !ok {
   118  			// Not exist in destination bundle
   119  			diffs = append(diffs, DiffItem{
   120  				Operation: Remove,
   121  				Type:      "package",
   122  				Path:      sp.Name,
   123  			})
   124  			continue
   125  		}
   126  
   127  		// Index secret data
   128  		srcSecretIndex := map[string]*bundlev1.KV{}
   129  		for _, ss := range sp.Secrets.Data {
   130  			if ss == nil {
   131  				continue
   132  			}
   133  			srcSecretIndex[ss.Key] = ss
   134  		}
   135  		dstSecretIndex := map[string]*bundlev1.KV{}
   136  		for _, ds := range dp.Secrets.Data {
   137  			if ds == nil {
   138  				continue
   139  			}
   140  
   141  			dstSecretIndex[ds.Key] = ds
   142  			oldValue, ok := srcSecretIndex[ds.Key]
   143  			if !ok {
   144  				// Secret has been added
   145  				var data string
   146  				if err := secret.Unpack(ds.Value, &data); err != nil {
   147  					return nil, fmt.Errorf("unable to unpack %q - %q secret value: %w", dp.Name, ds.Key, err)
   148  				}
   149  
   150  				diffs = append(diffs, DiffItem{
   151  					Operation: Add,
   152  					Type:      "secret",
   153  					Path:      fmt.Sprintf("%s#%s", dp.Name, ds.Key),
   154  					Value:     data,
   155  				})
   156  				continue
   157  			}
   158  
   159  			// Skip if key does not match
   160  			if !strings.EqualFold(oldValue.Key, ds.Key) {
   161  				continue
   162  			}
   163  
   164  			// Compare values
   165  			if !security.SecureCompare(oldValue.Value, ds.Value) {
   166  				// Secret has been replaced
   167  				var data string
   168  				if err := secret.Unpack(ds.Value, &data); err != nil {
   169  					return nil, fmt.Errorf("unable to unpack %q - %q secret value: %w", dp.Name, ds.Key, err)
   170  				}
   171  
   172  				diffs = append(diffs, DiffItem{
   173  					Operation: Replace,
   174  					Type:      "secret",
   175  					Path:      fmt.Sprintf("%s#%s", dp.Name, ds.Key),
   176  					Value:     data,
   177  				})
   178  			}
   179  		}
   180  
   181  		// Clean removed source secrets
   182  		for k := range srcSecretIndex {
   183  			if _, ok := dstSecretIndex[k]; !ok {
   184  				diffs = append(diffs, DiffItem{
   185  					Operation: Remove,
   186  					Type:      "secret",
   187  					Path:      fmt.Sprintf("%s#%s", dp.Name, k),
   188  				})
   189  			}
   190  		}
   191  	}
   192  
   193  	// Sort diff
   194  	sort.SliceStable(diffs, func(i, j int) bool {
   195  		var (
   196  			x = diffs[i]
   197  			y = diffs[j]
   198  		)
   199  
   200  		// Sort by patch descending
   201  		return x.Path < y.Path
   202  	})
   203  
   204  	// No error
   205  	return diffs, nil
   206  }