github.com/replicatedcom/ship@v0.50.0/integration/lib.go (about)

     1  package integration
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"io/ioutil"
     7  	"os"
     8  	"path/filepath"
     9  	"regexp"
    10  	"strings"
    11  
    12  	. "github.com/onsi/gomega"
    13  	"github.com/pkg/errors"
    14  	"github.com/pmezard/go-difflib/difflib"
    15  )
    16  
    17  // files and directories with non-deterministic output
    18  var skipFiles = []string{
    19  	"installer/terraform/.terraform/plugins",
    20  	"installer/terraform/plan.tfplan",
    21  	"installer/charts/rendered/secrets.yaml",
    22  	"installer/base/consul-test.yaml",
    23  	"installer/base/gossip-secret.yaml",
    24  	"installer/consul-rendered.yaml",
    25  }
    26  
    27  func skipCheck(filePath string, ignoredFiles []string) bool {
    28  	for _, f := range ignoredFiles {
    29  		if strings.HasSuffix(filePath, f) {
    30  			return true
    31  		}
    32  	}
    33  
    34  	for _, f := range skipFiles {
    35  		if strings.HasSuffix(filePath, f) {
    36  			return true
    37  		}
    38  	}
    39  	return false
    40  }
    41  
    42  // CompareDir returns false if the two directories have different contents
    43  func CompareDir(expected, actual string, replacements map[string]string, ignoredFiles []string, ignoredKeys map[string][]string) (bool, error) {
    44  	if skipCheck(actual, ignoredFiles) {
    45  		return true, nil
    46  	}
    47  
    48  	expectedDir, err := ioutil.ReadDir(expected)
    49  	Expect(err).NotTo(HaveOccurred())
    50  
    51  	actualDir, err := ioutil.ReadDir(actual)
    52  	Expect(err).NotTo(HaveOccurred())
    53  
    54  	expectedMap := make(map[string]os.FileInfo)
    55  	expectedFilenamesMap := make(map[string]struct{})
    56  	for _, file := range expectedDir {
    57  		expectedMap[file.Name()] = file
    58  		expectedFilenamesMap[file.Name()] = struct{}{}
    59  	}
    60  
    61  	actualMap := make(map[string]os.FileInfo)
    62  	actualFilenamesMap := make(map[string]struct{})
    63  	for _, file := range actualDir {
    64  		actualMap[file.Name()] = file
    65  		actualFilenamesMap[file.Name()] = struct{}{}
    66  	}
    67  
    68  	Expect(actualFilenamesMap).To(Equal(expectedFilenamesMap), fmt.Sprintf("Contents of directories %s (expected) and %s (actual) did not match", expected, actual))
    69  
    70  	for name, expectedFile := range expectedMap {
    71  		actualFile, ok := actualMap[name]
    72  		Expect(ok).To(BeTrue())
    73  		Expect(actualFile.IsDir()).To(Equal(expectedFile.IsDir()))
    74  
    75  		expectedFilePath := filepath.Join(expected, expectedFile.Name())
    76  		actualFilePath := filepath.Join(actual, actualFile.Name())
    77  
    78  		if expectedFile.IsDir() {
    79  			// compare child items
    80  			result, err := CompareDir(expectedFilePath, actualFilePath, replacements, ignoredFiles, ignoredKeys)
    81  			if !result || err != nil {
    82  				return result, err
    83  			}
    84  		} else if skipCheck(expectedFilePath, ignoredFiles) {
    85  			continue
    86  		} else {
    87  			// compare expectedFile contents
    88  			expectedContentsBytes, err := ioutil.ReadFile(expectedFilePath)
    89  			Expect(err).NotTo(HaveOccurred())
    90  			actualContentsBytes, err := ioutil.ReadFile(actualFilePath)
    91  			Expect(err).NotTo(HaveOccurred())
    92  
    93  			// another hack for ease of testing -- pretty print json before comparing so diffs
    94  			// are easier to read
    95  			if strings.HasSuffix(actualFilePath, ".json") {
    96  				cwd, err := os.Getwd()
    97  				Expect(err).NotTo(HaveOccurred())
    98  
    99  				relativeActualFilePath, err := filepath.Rel(cwd, actualFilePath)
   100  				Expect(err).NotTo(HaveOccurred())
   101  
   102  				fileIgnorePaths := ignoredKeys[relativeActualFilePath]
   103  
   104  				expectedContentsBytes, err = prettyAndCleanJSON(expectedContentsBytes, fileIgnorePaths)
   105  				Expect(err).NotTo(HaveOccurred())
   106  
   107  				actualContentsBytes, err = prettyAndCleanJSON(actualContentsBytes, fileIgnorePaths)
   108  				Expect(err).NotTo(HaveOccurred())
   109  			}
   110  
   111  			// kind of a hack -- remove any trailing newlines (because text editors are hard to use)
   112  			expectedContents := strings.TrimRight(string(expectedContentsBytes), "\n")
   113  			actualContents := strings.TrimRight(string(actualContentsBytes), "\n")
   114  
   115  			// find and replace strings from the expected contents (customerID, installationID, etc)
   116  			for k, v := range replacements {
   117  				re := regexp.MustCompile(k)
   118  				expectedContents = re.ReplaceAllString(expectedContents, v)
   119  			}
   120  
   121  			// find and replace strings from the actual contents (datetime, signature, etc)
   122  			for k, v := range replacements {
   123  				re := regexp.MustCompile(k)
   124  				actualContents = re.ReplaceAllString(actualContents, v)
   125  			}
   126  
   127  			diff := difflib.UnifiedDiff{
   128  				A:        difflib.SplitLines(expectedContents),
   129  				B:        difflib.SplitLines(actualContents),
   130  				FromFile: "expected contents",
   131  				ToFile:   "actual contents",
   132  				Context:  3,
   133  			}
   134  
   135  			diffText, err := difflib.GetUnifiedDiffString(diff)
   136  			Expect(err).NotTo(HaveOccurred())
   137  			Expect(diffText).To(BeEmpty(), fmt.Sprintf("Contents of files %s (expected) and %s (actual) did not match", expectedFilePath, actualFilePath))
   138  		}
   139  	}
   140  
   141  	return true, nil
   142  }
   143  
   144  func prettyAndCleanJSON(data []byte, keysToIgnore []string) ([]byte, error) {
   145  	var obj interface{}
   146  	err := json.Unmarshal(data, &obj)
   147  	if err != nil {
   148  		return nil, errors.Wrap(err, "unmarshal")
   149  	}
   150  
   151  	if _, ok := obj.(map[string]interface{}); ok && keysToIgnore != nil {
   152  		for _, key := range keysToIgnore {
   153  			obj = replaceInJSON(obj.(map[string]interface{}), key)
   154  		}
   155  	}
   156  
   157  	data, err = json.MarshalIndent(obj, "", "  ")
   158  	if err != nil {
   159  		return nil, errors.Wrap(err, "marshal")
   160  	}
   161  	return data, nil
   162  }
   163  
   164  func replaceInJSON(obj map[string]interface{}, path string) map[string]interface{} {
   165  	// split path on '.'
   166  
   167  	if path == "" {
   168  		return obj
   169  	}
   170  
   171  	fullpath := strings.Split(path, ".")
   172  
   173  	// if the object to delete is at this level, delete it from the map and return
   174  	if len(fullpath) == 1 {
   175  		delete(obj, fullpath[0])
   176  		return obj
   177  	}
   178  
   179  	// the object to delete is at a deeper level - check if the specified key exists, if it does not then return
   180  	// else recursively call replaceInJSON
   181  
   182  	if _, exists := obj[fullpath[0]]; !exists {
   183  		// the object to delete does not exist
   184  		return obj
   185  	}
   186  
   187  	subObj, ok := obj[fullpath[0]].(map[string]interface{})
   188  	if !ok {
   189  		fmt.Printf("looking for %q and this is not a map[string]interface{}: %+v\n", path, obj[fullpath[0]])
   190  		delete(obj, fullpath[0])
   191  		return obj
   192  	}
   193  
   194  	replacedObj := replaceInJSON(subObj, strings.Join(fullpath[1:], "."))
   195  	obj[fullpath[0]] = replacedObj
   196  
   197  	if len(replacedObj) == 0 {
   198  		delete(obj, fullpath[0])
   199  	}
   200  
   201  	return obj
   202  }
   203  
   204  func RecursiveCopy(sourceDir, destDir string) {
   205  	err := os.MkdirAll(destDir, os.ModePerm)
   206  	Expect(err).NotTo(HaveOccurred())
   207  	srcFiles, err := ioutil.ReadDir(sourceDir)
   208  	Expect(err).NotTo(HaveOccurred())
   209  	for _, file := range srcFiles {
   210  		if file.IsDir() {
   211  			RecursiveCopy(filepath.Join(sourceDir, file.Name()), filepath.Join(destDir, file.Name()))
   212  		} else {
   213  			// is file
   214  			contents, err := ioutil.ReadFile(filepath.Join(sourceDir, file.Name()))
   215  			Expect(err).NotTo(HaveOccurred())
   216  
   217  			err = ioutil.WriteFile(filepath.Join(destDir, file.Name()), contents, file.Mode())
   218  			Expect(err).NotTo(HaveOccurred())
   219  		}
   220  	}
   221  }