github.com/tilotech/tilores-cli@v0.28.0/internal/pkg/upgradesteps.go (about)

     1  package pkg
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"fmt"
     7  	"html/template"
     8  	"io"
     9  	"io/fs"
    10  	"os"
    11  	"path/filepath"
    12  	"regexp"
    13  	"sort"
    14  	"strings"
    15  
    16  	"github.com/tilotech/tilores-cli/internal/pkg/step"
    17  	"github.com/tilotech/tilores-cli/templates"
    18  )
    19  
    20  // CreateUpgradeSteps creates the upgrade steps for the provided version.
    21  func CreateUpgradeSteps(upgradeVersion string, variables map[string]interface{}) ([]step.Step, error) { //nolint:gocognit
    22  	stepFiles, err := templates.Upgrades.ReadDir(fmt.Sprintf("upgrades/%v", upgradeVersion))
    23  	if err != nil {
    24  		return nil, err
    25  	}
    26  	sort.Sort(ByName(stepFiles))
    27  
    28  	steps := []step.Step{}
    29  	for _, stepFile := range stepFiles {
    30  		if stepFile.IsDir() {
    31  			continue
    32  		}
    33  		fn := stepFile.Name()
    34  		parts := strings.SplitN(fn, "_", 2)
    35  		if len(parts) != 2 {
    36  			return nil, fmt.Errorf("internal upgrade error: invalid file %v", fn)
    37  		}
    38  		action := parts[1]
    39  		stepFile := fmt.Sprintf("upgrades/%v/%v", upgradeVersion, fn)
    40  		switch {
    41  		case strings.HasPrefix(action, "dependencies"):
    42  			addDepsSteps, err := createAddDependencySteps(stepFile)
    43  			if err != nil {
    44  				return nil, err
    45  			}
    46  			steps = append(steps, addDepsSteps...)
    47  		case strings.HasPrefix(action, "replace"):
    48  			replaceSteps, err := createReplaceSteps(stepFile, variables)
    49  			if err != nil {
    50  				return nil, err
    51  			}
    52  			steps = append(steps, replaceSteps...)
    53  		case strings.HasPrefix(action, "create"):
    54  			createSteps, err := createCreateSteps(stepFile, variables)
    55  			if err != nil {
    56  				return nil, err
    57  			}
    58  			steps = append(steps, createSteps...)
    59  		case strings.HasPrefix(action, "delete"):
    60  			deleteSteps, err := createDeleteSteps(stepFile, variables)
    61  			if err != nil {
    62  				return nil, err
    63  			}
    64  			steps = append(steps, deleteSteps...)
    65  		case strings.HasPrefix(action, "install_plugin"):
    66  			installPluginSteps, err := createInstallPluginSteps(stepFile)
    67  			if err != nil {
    68  				return nil, err
    69  			}
    70  			steps = append(steps, installPluginSteps...)
    71  		default:
    72  			return nil, fmt.Errorf("internal upgrade error: invalid file %v", fn)
    73  		}
    74  	}
    75  
    76  	return steps, nil
    77  }
    78  
    79  // ByName implements the sortable interface to order by a file name.
    80  type ByName []fs.DirEntry
    81  
    82  func (a ByName) Len() int           { return len(a) }
    83  func (a ByName) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
    84  func (a ByName) Less(i, j int) bool { return a[i].Name() < a[j].Name() }
    85  
    86  func createAddDependencySteps(fileName string) ([]step.Step, error) {
    87  	deps := []string{}
    88  	err := decodeStepFile(fileName, &deps)
    89  	if err != nil {
    90  		return nil, err
    91  	}
    92  
    93  	return []step.Step{
    94  		step.GetDependencies(deps),
    95  	}, nil
    96  }
    97  
    98  func createReplaceSteps(fileName string, variables map[string]interface{}) ([]step.Step, error) {
    99  	replace := &struct {
   100  		Target      string
   101  		OldTemplate string
   102  		NewTemplate string
   103  	}{}
   104  
   105  	err := decodeStepFile(fileName, replace)
   106  	if err != nil {
   107  		return nil, err
   108  	}
   109  
   110  	data, err := templates.Upgrades.ReadFile(replace.OldTemplate)
   111  	if err != nil {
   112  		return nil, err
   113  	}
   114  
   115  	tmpl, err := template.New("t").Parse(string(data))
   116  	if err != nil {
   117  		return nil, err
   118  	}
   119  
   120  	expectedFileContent := &bytes.Buffer{}
   121  	err = tmpl.Execute(expectedFileContent, variables)
   122  	if err != nil {
   123  		return nil, err
   124  	}
   125  	expectedFileContentBB := expectedFileContent.Bytes()
   126  
   127  	targetFile, err := os.Open(replace.Target)
   128  	if err != nil {
   129  		return nil, err
   130  	}
   131  	defer func() {
   132  		_ = targetFile.Close()
   133  	}()
   134  	actualFileContent, err := io.ReadAll(targetFile)
   135  	if err != nil {
   136  		return nil, err
   137  	}
   138  
   139  	if strings.HasSuffix(replace.Target, ".go") {
   140  		r1 := regexp.MustCompile(`// Code generated by github\.com/99designs/gqlgen version .*\n`)
   141  		actualFileContent = r1.ReplaceAll(actualFileContent, []byte(""))
   142  
   143  		r2 := regexp.MustCompile(`[\n\r\s]+`)
   144  		expectedFileContentBB = r2.ReplaceAll(expectedFileContentBB, []byte(" "))
   145  		actualFileContent = r2.ReplaceAll(actualFileContent, []byte(" "))
   146  	}
   147  
   148  	steps := []step.Step{}
   149  	if !bytes.Equal(expectedFileContentBB, actualFileContent) {
   150  		steps = append(
   151  			steps,
   152  			step.Backup(replace.Target),
   153  		)
   154  	}
   155  
   156  	steps = append(
   157  		steps,
   158  		step.RenderTemplate(templates.Upgrades, replace.NewTemplate, replace.Target, variables),
   159  	)
   160  
   161  	return steps, nil
   162  }
   163  
   164  func createCreateSteps(fileName string, variables map[string]interface{}) ([]step.Step, error) {
   165  	create := &struct {
   166  		Target      string
   167  		NewTemplate string
   168  	}{}
   169  
   170  	err := decodeStepFile(fileName, create)
   171  	if err != nil {
   172  		return nil, err
   173  	}
   174  
   175  	steps := []step.Step{}
   176  	if _, err := os.Stat(create.Target); err == nil {
   177  		steps = append(
   178  			steps,
   179  			step.Backup(create.Target),
   180  		)
   181  	}
   182  
   183  	steps = append(
   184  		steps,
   185  		step.RenderTemplate(templates.Upgrades, create.NewTemplate, create.Target, variables),
   186  	)
   187  
   188  	return steps, nil
   189  }
   190  
   191  func createDeleteSteps(fileName string, variables map[string]interface{}) ([]step.Step, error) { //nolint:gocognit
   192  	del := &struct {
   193  		Target      string
   194  		OldTemplate *string
   195  	}{}
   196  
   197  	err := decodeStepFile(fileName, del)
   198  	if err != nil {
   199  		return nil, err
   200  	}
   201  
   202  	matchingFiles, err := filepath.Glob(del.Target)
   203  	if err != nil {
   204  		return nil, err
   205  	}
   206  
   207  	var expectedFileContent *bytes.Buffer
   208  	if del.OldTemplate != nil {
   209  		data, err := templates.Upgrades.ReadFile(*del.OldTemplate)
   210  		if err != nil {
   211  			return nil, err
   212  		}
   213  
   214  		tmpl, err := template.New("t").Parse(string(data))
   215  		if err != nil {
   216  			return nil, err
   217  		}
   218  
   219  		expectedFileContent = &bytes.Buffer{}
   220  		err = tmpl.Execute(expectedFileContent, variables)
   221  		if err != nil {
   222  			return nil, err
   223  		}
   224  	}
   225  
   226  	steps := []step.Step{}
   227  	for _, file := range matchingFiles {
   228  		renamed := false
   229  		if expectedFileContent != nil {
   230  			targetFile, err := os.Open(file) //nolint:gosec
   231  			if err != nil {
   232  				return nil, err
   233  			}
   234  			defer func() {
   235  				_ = targetFile.Close()
   236  			}()
   237  			actualFileContent, err := io.ReadAll(targetFile)
   238  			if err != nil {
   239  				return nil, err
   240  			}
   241  
   242  			if !bytes.Equal(expectedFileContent.Bytes(), actualFileContent) {
   243  				steps = append(
   244  					steps,
   245  					step.Backup(file),
   246  				)
   247  				renamed = true
   248  			}
   249  		}
   250  
   251  		if !renamed {
   252  			steps = append(
   253  				steps,
   254  				step.Delete(file),
   255  			)
   256  		}
   257  	}
   258  
   259  	return steps, nil
   260  }
   261  
   262  func createInstallPluginSteps(fileName string) ([]step.Step, error) {
   263  	install := &struct {
   264  		Pkg     string
   265  		Version string
   266  		Target  string
   267  	}{}
   268  
   269  	err := decodeStepFile(fileName, install)
   270  	if err != nil {
   271  		return nil, err
   272  	}
   273  
   274  	return []step.Step{
   275  		step.PluginInstall(install.Pkg, install.Version, install.Target),
   276  	}, nil
   277  }
   278  
   279  func decodeStepFile(fileName string, v interface{}) error {
   280  	f, err := templates.Upgrades.Open(fileName)
   281  	if err != nil {
   282  		return err
   283  	}
   284  	defer func() {
   285  		_ = f.Close()
   286  	}()
   287  
   288  	decoder := json.NewDecoder(f)
   289  
   290  	return decoder.Decode(v)
   291  }