github.com/opendevstack/tailor@v1.3.5-0.20220119161809-cab064e60a67/pkg/cli/options.go (about) 1 package cli 2 3 import ( 4 "errors" 5 "fmt" 6 "io/ioutil" 7 "os" 8 "os/exec" 9 "strings" 10 11 "github.com/opendevstack/tailor/pkg/utils" 12 ) 13 14 // GlobalOptions are app-wide. 15 type GlobalOptions struct { 16 Verbose bool 17 Debug bool 18 NonInteractive bool 19 OcBinary string 20 File string 21 Force bool 22 IsLoggedIn bool 23 ClusterRequired bool 24 fs utils.FileStater 25 } 26 27 // NamespaceOptions define which namespace Tailor works against. 28 type NamespaceOptions struct { 29 Namespace string 30 CheckedNamespaces []string 31 } 32 33 // CompareOptions define how to compare desired and current state. 34 type CompareOptions struct { 35 *GlobalOptions 36 *NamespaceOptions 37 Selector string 38 Excludes []string 39 TemplateDir string 40 ParamDir string 41 PrivateKey string 42 Passphrase string 43 Labels string 44 Params []string 45 ParamFiles []string 46 PreservePaths []string 47 PreserveImmutableFields bool 48 IgnoreUnknownParameters bool 49 UpsertOnly bool 50 AllowRecreate bool 51 RevealSecrets bool 52 Verify bool 53 Resource string 54 } 55 56 // ExportOptions define how the export should be done. 57 type ExportOptions struct { 58 *GlobalOptions 59 *NamespaceOptions 60 Selector string 61 Excludes []string 62 TemplateDir string 63 ParamDir string 64 WithAnnotations bool 65 WithHardcodedNamespace bool 66 TrimAnnotations []string 67 Resource string 68 } 69 70 // SecretsOptions define how to work with encrypted files. 71 type SecretsOptions struct { 72 *GlobalOptions 73 ParamDir string 74 PublicKeyDir string 75 PrivateKey string 76 Passphrase string 77 } 78 79 // InitGlobalOptions creates a new pointer to GlobalOptions with a given filesystem. 80 func InitGlobalOptions(fs utils.FileStater) *GlobalOptions { 81 return &GlobalOptions{fs: fs} 82 } 83 84 // NewGlobalOptions returns new global options based on file/flags. 85 // Those options are shared across all commands. 86 func NewGlobalOptions( 87 clusterRequired bool, 88 fileFlag string, 89 verboseFlag bool, 90 debugFlag bool, 91 nonInteractiveFlag bool, 92 ocBinaryFlag string, 93 forceFlag bool) (*GlobalOptions, error) { 94 o := InitGlobalOptions(&utils.OsFS{}) 95 o.ClusterRequired = clusterRequired 96 97 fileFlags, err := getFileFlags(fileFlag, verbose) 98 if err != nil { 99 return o, fmt.Errorf("Could not read %s: %s", fileFlag, err) 100 } 101 102 if verboseFlag { 103 o.Verbose = true 104 } else if fileFlags["verbose"] == "true" { 105 o.Verbose = true 106 } 107 108 if debugFlag { 109 o.Debug = true 110 } else if fileFlags["debug"] == "true" { 111 o.Debug = true 112 } 113 114 if nonInteractiveFlag { 115 o.NonInteractive = true 116 } else if fileFlags["non-interactive"] == "true" { 117 o.NonInteractive = true 118 } 119 120 if len(fileFlag) > 0 { 121 o.File = fileFlag 122 } 123 124 if len(ocBinaryFlag) > 0 { 125 o.OcBinary = ocBinaryFlag 126 } else if val, ok := fileFlags["oc-binary"]; ok { 127 o.OcBinary = val 128 } 129 130 if forceFlag { 131 o.Force = true 132 } else if fileFlags["force"] == "true" { 133 o.Force = true 134 } 135 136 verbose = o.Verbose || o.Debug 137 debug = o.Debug 138 ocBinary = o.OcBinary 139 140 DebugMsg(fmt.Sprintf("%#v", o)) 141 142 return o, o.check(clusterRequired) 143 } 144 145 // NewCompareOptions returns new options for the diff/apply command based on file/flags. 146 func NewCompareOptions( 147 globalOptions *GlobalOptions, 148 namespaceFlag string, 149 selectorFlag string, 150 excludeFlag []string, 151 templateDirFlag string, 152 paramDirFlag string, 153 publicKeyDirFlag string, 154 privateKeyFlag string, 155 passphraseFlag string, 156 labelsFlag string, 157 paramFlag []string, 158 paramFileFlag []string, 159 preserveFlag []string, 160 preserveImmutableFieldsFlag bool, 161 ignoreUnknownParametersFlag bool, 162 upsertOnlyFlag bool, 163 allowRecreateFlag bool, 164 revealSecretsFlag bool, 165 verifyFlag bool, 166 resourceArg string) (*CompareOptions, error) { 167 o := &CompareOptions{ 168 GlobalOptions: globalOptions, 169 NamespaceOptions: &NamespaceOptions{}, 170 } 171 filename := o.resolvedFile(namespaceFlag) 172 173 fileFlags, err := getFileFlags(filename, verbose) 174 if err != nil { 175 return o, fmt.Errorf("Could not read '%s': %s", filename, err) 176 } 177 178 if len(namespaceFlag) > 0 { 179 o.Namespace = namespaceFlag 180 } else if val, ok := fileFlags["namespace"]; ok { 181 o.Namespace = val 182 } 183 184 if len(selectorFlag) > 0 { 185 o.Selector = selectorFlag 186 } else if val, ok := fileFlags["selector"]; ok { 187 o.Selector = val 188 } 189 190 o.Excludes = []string{} 191 if len(excludeFlag) > 0 { 192 for _, val := range excludeFlag { 193 o.Excludes = append(o.Excludes, strings.Split(val, ",")...) 194 } 195 } else if val, ok := fileFlags["exclude"]; ok { 196 o.Excludes = strings.Split(val, ",") 197 } 198 199 o.TemplateDir = "." 200 if templateDirFlag != "." { 201 o.TemplateDir = templateDirFlag 202 } else if val, ok := fileFlags["template-dir"]; ok { 203 o.TemplateDir = val 204 } 205 206 o.ParamDir = "." 207 if paramDirFlag != "." { 208 o.ParamDir = paramDirFlag 209 } else if val, ok := fileFlags["param-dir"]; ok { 210 o.ParamDir = val 211 } 212 213 o.PrivateKey = "private.key" 214 if privateKeyFlag != "private.key" { 215 o.PrivateKey = privateKeyFlag 216 } else if val, ok := fileFlags["private-key"]; ok { 217 o.PrivateKey = val 218 } 219 220 if len(passphraseFlag) > 0 { 221 o.Passphrase = passphraseFlag 222 } else if val, ok := fileFlags["passphrase"]; ok { 223 o.Passphrase = val 224 } 225 226 if len(labelsFlag) > 0 { 227 o.Labels = labelsFlag 228 } else if val, ok := fileFlags["labels"]; ok { 229 o.Labels = val 230 } 231 232 if val, ok := fileFlags["param"]; ok { 233 o.Params = strings.Split(val, ",") 234 } 235 if len(paramFlag) > 0 { 236 params := map[string]string{} 237 for _, setParam := range o.Params { 238 setPair := strings.SplitN(setParam, "=", 2) 239 key := setPair[0] 240 params[key] = setPair[1] 241 for _, newParam := range paramFlag { 242 newPair := strings.SplitN(newParam, "=", 2) 243 if key == newPair[0] { 244 params[key] = newPair[1] 245 break 246 } 247 } 248 } 249 o.Params = []string{} 250 for k, v := range params { 251 o.Params = append(o.Params, k+"="+v) 252 } 253 for _, v := range paramFlag { 254 pair := strings.SplitN(v, "=", 2) 255 if _, ok := params[pair[0]]; !ok { 256 o.Params = append(o.Params, v) 257 } 258 } 259 } 260 261 if len(paramFileFlag) > 0 { 262 o.ParamFiles = paramFileFlag 263 } else if val, ok := fileFlags["param-file"]; ok { 264 o.ParamFiles = strings.Split(val, ",") 265 } 266 267 if len(preserveFlag) > 0 { 268 o.PreservePaths = preserveFlag 269 } else if val, ok := fileFlags["ignore-path"]; ok { 270 o.PreservePaths = strings.Split(val, ",") 271 } else if val, ok := fileFlags["preserve"]; ok { 272 o.PreservePaths = strings.Split(val, ",") 273 } 274 275 if preserveImmutableFieldsFlag { 276 o.PreserveImmutableFields = true 277 } else if fileFlags["preserve-immutable-fields"] == "true" { 278 o.PreserveImmutableFields = true 279 } 280 281 if ignoreUnknownParametersFlag { 282 o.IgnoreUnknownParameters = true 283 } else if fileFlags["ignore-unknown-parameters"] == "true" { 284 o.IgnoreUnknownParameters = true 285 } 286 287 if upsertOnlyFlag { 288 o.UpsertOnly = true 289 } else if fileFlags["upsert-only"] == "true" { 290 o.UpsertOnly = true 291 } 292 293 if allowRecreateFlag { 294 o.AllowRecreate = true 295 } else if fileFlags["allow-recreate"] == "true" { 296 o.AllowRecreate = true 297 } 298 299 if revealSecretsFlag { 300 o.RevealSecrets = true 301 } else if fileFlags["reveal-secrets"] == "true" { 302 o.RevealSecrets = true 303 } 304 305 if verifyFlag { 306 o.Verify = true 307 } else if fileFlags["verify"] == "true" { 308 o.Verify = true 309 } 310 311 if len(resourceArg) > 0 { 312 o.Resource = resourceArg 313 } else if val, ok := fileFlags["resource"]; ok { 314 o.Resource = val 315 } 316 317 DebugMsg(fmt.Sprintf("%#v", o)) 318 319 return o, o.check(o.ClusterRequired) 320 } 321 322 // NewExportOptions returns new options for the export command based on file/flags. 323 func NewExportOptions( 324 globalOptions *GlobalOptions, 325 namespaceFlag string, 326 selectorFlag string, 327 excludeFlag []string, 328 templateDirFlag string, 329 paramDirFlag string, 330 withAnnotationsFlag bool, 331 withHardcodedNamespaceFlag bool, 332 trimAnnotationsFlag []string, 333 resourceArg string) (*ExportOptions, error) { 334 o := &ExportOptions{ 335 GlobalOptions: globalOptions, 336 NamespaceOptions: &NamespaceOptions{}, 337 } 338 filename := o.resolvedFile(namespaceFlag) 339 340 fileFlags, err := getFileFlags(filename, verbose) 341 if err != nil { 342 return o, fmt.Errorf("Could not read %s: %s", filename, err) 343 } 344 345 if len(namespaceFlag) > 0 { 346 o.Namespace = namespaceFlag 347 } else if val, ok := fileFlags["namespace"]; ok { 348 o.Namespace = val 349 } 350 351 if len(selectorFlag) > 0 { 352 o.Selector = selectorFlag 353 } else if val, ok := fileFlags["selector"]; ok { 354 o.Selector = val 355 } 356 357 o.Excludes = []string{} 358 if len(excludeFlag) > 0 { 359 for _, val := range excludeFlag { 360 o.Excludes = append(o.Excludes, strings.Split(val, ",")...) 361 } 362 } else if val, ok := fileFlags["exclude"]; ok { 363 o.Excludes = strings.Split(val, ",") 364 } 365 366 o.TemplateDir = "." 367 if templateDirFlag != "." { 368 o.TemplateDir = templateDirFlag 369 } else if val, ok := fileFlags["template-dir"]; ok { 370 o.TemplateDir = val 371 } 372 373 o.ParamDir = "." 374 if paramDirFlag != "." { 375 o.ParamDir = paramDirFlag 376 } else if val, ok := fileFlags["param-dir"]; ok { 377 o.ParamDir = val 378 } 379 380 if withAnnotationsFlag { 381 o.WithAnnotations = true 382 } else if fileFlags["with-annotations"] == "true" { 383 o.WithAnnotations = true 384 } 385 386 if withHardcodedNamespaceFlag { 387 o.WithHardcodedNamespace = true 388 } else if fileFlags["with-hardcoded-namespace"] == "true" { 389 o.WithHardcodedNamespace = true 390 } 391 392 if len(trimAnnotationsFlag) > 0 { 393 o.TrimAnnotations = trimAnnotationsFlag 394 } else if val, ok := fileFlags["trim-annotation"]; ok { 395 o.TrimAnnotations = strings.Split(val, ",") 396 } 397 398 if len(resourceArg) > 0 { 399 o.Resource = resourceArg 400 } else if val, ok := fileFlags["resource"]; ok { 401 o.Resource = val 402 } 403 404 DebugMsg(fmt.Sprintf("%#v", o)) 405 406 return o, o.check() 407 } 408 409 // NewSecretsOptions returns new options for the secrets subcommand based on file/flags. 410 func NewSecretsOptions( 411 globalOptions *GlobalOptions, 412 paramDirFlag string, 413 publicKeyDirFlag string, 414 privateKeyFlag string, 415 passphraseFlag string) (*SecretsOptions, error) { 416 o := &SecretsOptions{ 417 GlobalOptions: globalOptions, 418 } 419 namespaceFlag := "" // namespace does not make sense for secrets 420 filename := o.resolvedFile(namespaceFlag) 421 422 fileFlags, err := getFileFlags(filename, verbose) 423 if err != nil { 424 return o, fmt.Errorf("Could not read %s: %s", filename, err) 425 } 426 427 o.ParamDir = "." 428 if paramDirFlag != "." { 429 o.ParamDir = paramDirFlag 430 } else if val, ok := fileFlags["param-dir"]; ok { 431 o.ParamDir = val 432 } 433 434 o.PublicKeyDir = "." 435 if publicKeyDirFlag != "." { 436 o.PublicKeyDir = publicKeyDirFlag 437 } else if val, ok := fileFlags["public-key-dir"]; ok { 438 o.PublicKeyDir = val 439 } 440 441 o.PrivateKey = "private.key" 442 if privateKeyFlag != "private.key" { 443 o.PrivateKey = privateKeyFlag 444 } else if val, ok := fileFlags["private-key"]; ok { 445 o.PrivateKey = val 446 } 447 448 DebugMsg(fmt.Sprintf("%#v", o)) 449 450 return o, o.check() 451 } 452 453 // resolvedFile returns either the user-supplied value, or, if the default is used 454 // AND a namespaceFlag is given, "Tailorfile.${NAMESPACE}" (if it exists). 455 func (o *GlobalOptions) resolvedFile(namespaceFlag string) string { 456 if o.File != "Tailorfile" { 457 return o.File 458 } 459 if len(namespaceFlag) == 0 { 460 return o.File 461 } 462 namespacedFile := fmt.Sprintf("%s.%s", o.File, namespaceFlag) 463 if _, err := o.fs.Stat(namespacedFile); os.IsNotExist(err) { 464 return o.File 465 } 466 return namespacedFile 467 } 468 469 // FileExists checks whether given file exists. 470 func (o *GlobalOptions) FileExists(file string) bool { 471 _, err := o.fs.Stat(file) 472 return !os.IsNotExist(err) 473 } 474 475 func (o *GlobalOptions) check(clusterRequired bool) error { 476 if !o.checkOcBinary() { 477 return fmt.Errorf("No such oc binary: %s", o.OcBinary) 478 } 479 if clusterRequired { 480 if !o.checkLoggedIn() { 481 return errors.New("You need to login with 'oc login' first") 482 } 483 c := NewOcClient("") 484 if v := ocVersion(c); !v.ExactMatch() { 485 if v.Incomplete() { 486 VerboseMsg(fmt.Sprintf("Version information is incomplete: client (%s) and server (%s) detected. "+ 487 "This is likely due to a local cluster setup. "+ 488 "If not, this could lead to incorrect behaviour.", v.client, v.server)) 489 } else { 490 errorMsg := fmt.Sprintf("Version mismatch between client (%s) and server (%s) detected. "+ 491 "This can lead to incorrect behaviour. "+ 492 "Update your oc binary or point to an alternative binary with --oc-binary.", v.client, v.server) 493 if !o.Force { 494 return fmt.Errorf("%s\n\nRefusing to continue without --force", errorMsg) 495 } 496 } 497 } 498 } 499 return nil 500 } 501 502 func (o *GlobalOptions) checkLoggedIn() bool { 503 if !o.IsLoggedIn { 504 c := NewOcClient("") 505 loggedIn, err := c.CheckLoggedIn() 506 if err != nil { 507 VerboseMsg(err.Error()) 508 } 509 o.IsLoggedIn = loggedIn 510 } 511 return o.IsLoggedIn 512 } 513 514 func (o *GlobalOptions) checkOcBinary() bool { 515 if !strings.Contains(o.OcBinary, string(os.PathSeparator)) { 516 _, err := exec.LookPath(o.OcBinary) 517 return err == nil 518 } 519 _, err := os.Stat(o.OcBinary) 520 return !os.IsNotExist(err) 521 } 522 523 func (o *CompareOptions) check(clusterRequired bool) error { 524 // Check if template dir exists 525 if o.TemplateDir != "." { 526 td := o.TemplateDir 527 if _, err := os.Stat(td); os.IsNotExist(err) { 528 return fmt.Errorf("Template directory '%s' does not exist", td) 529 } 530 } 531 // Check if param dir exists 532 if o.ParamDir != "." { 533 pd := o.ParamDir 534 if _, err := os.Stat(pd); os.IsNotExist(err) { 535 return fmt.Errorf("Param directory '%s' does not exist", pd) 536 } 537 } 538 539 if strings.Contains(o.Resource, "/") && len(o.Selector) > 0 { 540 DebugMsg("Ignoring selector", o.Selector, "as resource is given") 541 o.Selector = "" 542 } 543 544 return o.setNamespace(clusterRequired) 545 } 546 547 func (o *CompareOptions) PathsToPreserve() []string { 548 pathsToPreserve := []string{} 549 if o.PreserveImmutableFields { 550 pathsToPreserve = append( 551 pathsToPreserve, 552 "pvc:/spec/accessModes", 553 "pvc:/spec/storageClassName", 554 "pvc:/spec/resources/requests/storage", 555 "route:/spec/host", 556 "secret:/type", 557 ) 558 } 559 return append(pathsToPreserve, o.PreservePaths...) 560 } 561 562 func (o *ExportOptions) check() error { 563 if strings.Contains(o.Resource, "/") && len(o.Selector) > 0 { 564 DebugMsg("Ignoring selector", o.Selector, "as resource is given") 565 o.Selector = "" 566 } 567 568 return o.setNamespace(o.ClusterRequired) 569 } 570 571 func (o *SecretsOptions) check() error { 572 return nil 573 } 574 575 func (o *NamespaceOptions) setNamespace(clusterRequired bool) error { 576 if clusterRequired { 577 if len(o.Namespace) == 0 { 578 n, err := getOcNamespace() 579 if err != nil { 580 return err 581 } 582 o.Namespace = n 583 } else { 584 err := o.checkOcNamespace(o.Namespace) 585 if err != nil { 586 return fmt.Errorf("No such project: %s", o.Namespace) 587 } 588 } 589 } 590 return nil 591 } 592 593 func (o *NamespaceOptions) checkOcNamespace(n string) error { 594 if utils.Includes(o.CheckedNamespaces, n) { 595 return nil 596 } 597 c := NewOcClient("") 598 exists, err := c.CheckProjectExists(n) 599 if exists { 600 o.CheckedNamespaces = append(o.CheckedNamespaces, n) 601 } 602 return err 603 } 604 605 func getOcNamespace() (string, error) { 606 c := NewOcClient("") 607 return c.CurrentProject() 608 } 609 610 func getFileFlags(filename string, verbose bool) (map[string]string, error) { 611 fileFlags := make(map[string]string) 612 if _, err := os.Stat(filename); os.IsNotExist(err) { 613 if filename == "Tailorfile" { 614 if verbose { 615 PrintBluef("--> No file '%s' found.\n", filename) 616 } 617 return fileFlags, nil 618 } 619 return fileFlags, err 620 } 621 622 b, err := ioutil.ReadFile(filename) 623 if err != nil { 624 return fileFlags, err 625 } 626 content := string(b) 627 text := strings.TrimSuffix(content, "\n") 628 lines := strings.Split(text, "\n") 629 630 for _, untrimmedLine := range lines { 631 line := strings.TrimSpace(untrimmedLine) 632 if len(line) == 0 || strings.HasPrefix(line, "#") || strings.HasPrefix(line, "//") { 633 continue 634 } 635 pair := strings.SplitN(line, " ", 2) 636 if len(pair) == 2 { 637 key := pair[0] 638 value := strings.TrimSpace(pair[1]) 639 if val, ok := fileFlags[key]; ok { 640 value = val + "," + value 641 } 642 fileFlags[key] = value 643 } else { 644 fileFlags["resource"] = pair[0] 645 } 646 } 647 return fileFlags, nil 648 }