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 }