diff --git a/.goreleaser.yaml b/.goreleaser.yaml new file mode 100644 index 0000000..4f7be2f --- /dev/null +++ b/.goreleaser.yaml @@ -0,0 +1,8 @@ +builds: + # GOOS list to build for. + # For more info refer to: https://golang.org/doc/install/source#environment + # + # Default: [ 'darwin', 'linux', 'windows' ] + goos: + - darwin + - linux \ No newline at end of file diff --git a/README.md b/README.md index 210a06c..a93d6df 100644 --- a/README.md +++ b/README.md @@ -14,11 +14,16 @@ Application manages inventory and executes opentofu with generated configs ./tofugu cook -o demo-org -d account:test-account -d datacenter:staging1 -t vpc -- apply ``` -Everything after `--` will be passed as parameters to the `cmd_to_exec` +- Everything after `--` will be passed as parameters to the `cmd_to_exec` +- `-c` = to force remove temo dir after execution (after `apply` or `destroy` and exitcode=0 from tofu/terraform) +- `-o` = name of the organisation (subfolder in inventory, tofies and tofugu config section) +- `-d` = dimension to attach to tofu/terraform. You may specifiy as many `-d` pairs as you need! +- `-t` = name of the tofi in the tofies folder ## Compatibility `tofugu` is OpenTofu/Terraform version agnostic! +Reuired external tools/binaries: `rsync`, `ln` ## $HOME/.tofurc @@ -47,8 +52,6 @@ When you set dimensions in the tofugu flags `-d datacenter:staging1 `, tofugu wi - var.tofugu_datacenter_name = will contain string `staging1` - var.tofugu_datacenter_manifest = will contain whole object from `staging1.json` -You may specifiy as many `-d` pairs as you need! - [datacenter.json example in inventory](examples/inventory/demo-org/datacenter/staging1.json) @@ -69,7 +72,6 @@ provider "aws" { } ``` - [Env variables used in code example](examples/tofies/demo-org/vpc/providers.tf) ## $HOME/.tofugu diff --git a/cmd/cook.go b/cmd/cook.go index a56e69f..07ed565 100644 --- a/cmd/cook.go +++ b/cmd/cook.go @@ -5,12 +5,12 @@ import ( "os" "os/exec" "os/signal" + "path/filepath" "strings" "syscall" "github.com/alt-dima/tofugu/utils" "github.com/spf13/cobra" - "github.com/spf13/viper" ) // cookCmd represents the cook command @@ -20,56 +20,52 @@ var cookCmd = &cobra.Command{ Args: cobra.MinimumNArgs(1), Long: `Execute OpenTofu with generated config from inventory and parameters after --`, Run: func(cmd *cobra.Command, args []string) { + //Creating signal to be handled and send to the child tofu/terraform sigs := make(chan os.Signal, 2) signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM, os.Interrupt) + var err error - //for key, value := range viper.GetViper().AllSettings() { - // log.Printf("key %v val %v", key, value) - //} + // Creating Tofug shared structure and filling with values + tofuguStruct := &utils.Tofugu{} - tofiName, _ := cmd.Flags().GetString("tofi") - orgName, _ := cmd.Flags().GetString("org") - dimensionsArgs, _ := cmd.Flags().GetStringSlice("dimension") + tofuguStruct.TofiName, _ = cmd.Flags().GetString("tofi") + tofuguStruct.OrgName, _ = cmd.Flags().GetString("org") + tofuguStruct.DimensionsFlags, _ = cmd.Flags().GetStringSlice("dimension") + tofuguStruct.TofiPath, _ = filepath.Abs(tofuguStruct.GetStringFromViperByOrgOrDefault("tofies_path") + "/" + tofuguStruct.OrgName + "/" + tofuguStruct.TofiName) + tofuguStruct.SharedModulesPath, _ = filepath.Abs(tofuguStruct.GetStringFromViperByOrgOrDefault("shared_modules_path")) + tofuguStruct.InventoryPath, _ = filepath.Abs(tofuguStruct.GetStringFromViperByOrgOrDefault("inventory_path") + "/" + tofuguStruct.OrgName) - cmdToExec := utils.GetConfigFromViperString("cmd_to_exec", orgName) - currentDir, _ := os.Getwd() - tofiPath := currentDir + "/" + utils.GetConfigFromViperString("tofies_path", orgName) + "/" + orgName + "/" + tofiName - manifest := utils.ParseTofiManifest(tofiPath + "/tofi_manifest.json") + tofuguStruct.ParseTofiManifest("tofi_manifest.json") + tofuguStruct.ParseDimensions() - //log.Println(manifest.Dimensions) - parsedDimensions := utils.ParseDimensions(manifest.Dimensions, dimensionsArgs) + tofuguStruct.SetupStateS3Path() - var stateS3Path string - if !viper.IsSet(orgName + ".s3_bucket_name") { - stateS3Path = stateS3Path + "org_" + orgName + "/" - } - for _, dimension := range manifest.Dimensions { - stateS3Path = stateS3Path + dimension + "_" + parsedDimensions[dimension] + "/" - } - stateS3Path = stateS3Path + tofiName + ".tfstate" - stateS3Region := utils.GetConfigFromViperString("s3_bucket_region", orgName) - stateS3Name := utils.GetConfigFromViperString("s3_bucket_name", orgName) + tofuguStruct.PrepareTemp() + + tofuguStruct.GenerateVarsByDims() + tofuguStruct.GenerateVarsByEnvVars() + //Local variables for child execution + stateS3Region := tofuguStruct.GetStringFromViperByOrgOrDefault("s3_bucket_region") + stateS3Name := tofuguStruct.GetStringFromViperByOrgOrDefault("s3_bucket_name") + forceCleanTempDir, _ := cmd.Flags().GetBool("clean") cmdArgs := args if args[0] == "init" { cmdArgs = append(cmdArgs, "-backend-config=bucket="+stateS3Name) - cmdArgs = append(cmdArgs, "-backend-config=key="+stateS3Path) + cmdArgs = append(cmdArgs, "-backend-config=key="+tofuguStruct.StateS3Path) cmdArgs = append(cmdArgs, "-backend-config=region="+stateS3Region) } + cmdToExec := tofuguStruct.GetStringFromViperByOrgOrDefault("cmd_to_exec") - cmdWorkTempDir := utils.PrepareTemp(tofiPath, currentDir+"/"+utils.GetConfigFromViperString("shared_modules_path", orgName), orgName+stateS3Path+tofiName) - - utils.GenerateVarsByDims(parsedDimensions, cmdWorkTempDir, currentDir+"/"+utils.GetConfigFromViperString("inventory_path", orgName)+"/"+orgName) - utils.GenerateVarsByEnvVars(cmdWorkTempDir) - + // Starting child and Waiting for it to finish, passing signals to it log.Println("TofuGu starting cooking: " + cmdToExec + " " + strings.Join(cmdArgs, " ")) execChildCommand := exec.Command(cmdToExec, cmdArgs...) - execChildCommand.Dir = cmdWorkTempDir + execChildCommand.Dir = tofuguStruct.CmdWorkTempDir execChildCommand.Env = os.Environ() execChildCommand.Stdin = os.Stdin execChildCommand.Stdout = os.Stdout execChildCommand.Stderr = os.Stderr - err := execChildCommand.Start() + err = execChildCommand.Start() if err != nil { log.Fatalf("cmd.Start() failed with %s\n", err) } @@ -84,18 +80,19 @@ var cookCmd = &cobra.Command{ exitCodeFinal := 0 if err != nil && execChildCommand.ProcessState.ExitCode() < 0 { exitCodeFinal = 1 - log.Println("OpenTofu failed " + err.Error()) + log.Println(cmdToExec + " failed " + err.Error()) } else if execChildCommand.ProcessState.ExitCode() == 143 { exitCodeFinal = 0 } else { exitCodeFinal = execChildCommand.ProcessState.ExitCode() } - if args[0] == "apply" || args[0] == "destroy" { - os.RemoveAll(cmdWorkTempDir) + if (exitCodeFinal == 0 && (args[0] == "apply" || args[0] == "destroy")) || forceCleanTempDir { + os.RemoveAll(tofuguStruct.CmdWorkTempDir) + log.Println("TofuGu removed tofi temp dir: " + tofuguStruct.CmdWorkTempDir) } - log.Printf("OpenTofu finished with code %v", exitCodeFinal) + log.Printf("TofuGu: %v finished with code %v", cmdToExec, exitCodeFinal) os.Exit(exitCodeFinal) }, } @@ -116,6 +113,7 @@ func init() { cookCmd.Flags().StringP("tofi", "t", "", "specify tofu unit") //viper.BindPFlag("tofi", cookCmd.Flags().Lookup("tofi")) cookCmd.Flags().StringP("org", "o", "", "specify org") + cookCmd.Flags().BoolP("clean", "c", false, "remove tmp after execution") //viper.BindPFlag("org", cookCmd.Flags().Lookup("org")) cookCmd.MarkFlagRequired("tofi") cookCmd.MarkFlagRequired("org") diff --git a/utils/dimensions.go b/utils/dimensions.go index c09caf9..6ea97b4 100644 --- a/utils/dimensions.go +++ b/utils/dimensions.go @@ -6,22 +6,23 @@ import ( "strings" ) -func ParseDimensions(dimensionsManifest []string, dimensionsArgs []string) map[string]string { - parsedDimArgs := parseDimArgs(dimensionsArgs) +func (tofuguStruct *Tofugu) ParseDimensions() { + parsedDimArgs := parseDimArgs(tofuguStruct.DimensionsFlags) - for _, dimension := range dimensionsManifest { + for _, dimension := range tofuguStruct.TofiManifest.Dimensions { if _, ok := parsedDimArgs[dimension]; !ok { log.Println("dimension " + dimension + " not passed with -d arg") os.Exit(1) } } - return parsedDimArgs + + tofuguStruct.ParsedDimensions = parsedDimArgs } func parseDimArgs(dimensionsArgs []string) map[string]string { parsedDimArgs := make(map[string]string) for _, dimension := range dimensionsArgs { - dimensionSlice := strings.Split(dimension, ":") + dimensionSlice := strings.SplitN(dimension, ":", 2) parsedDimArgs[dimensionSlice[0]] = dimensionSlice[1] } return parsedDimArgs diff --git a/utils/externals.go b/utils/externals.go index 38e2d3b..b60c1d0 100644 --- a/utils/externals.go +++ b/utils/externals.go @@ -13,10 +13,21 @@ func GetMD5Hash(text string) string { return hex.EncodeToString(hasher.Sum(nil)) } -func GetConfigFromViperString(keyName string, orgName string) string { - if viper.IsSet(orgName + "." + keyName) { - return viper.GetString(orgName + "." + keyName) +func (tofuguStruct *Tofugu) GetStringFromViperByOrgOrDefault(keyName string) string { + if viper.IsSet(tofuguStruct.OrgName + "." + keyName) { + return viper.GetString(tofuguStruct.OrgName + "." + keyName) } else { return viper.GetString("defaults." + keyName) } } + +func (tofuguStruct *Tofugu) SetupStateS3Path() { + var stateS3Path string + if !viper.IsSet(tofuguStruct.OrgName + ".s3_bucket_name") { + stateS3Path = stateS3Path + "org_" + tofuguStruct.OrgName + "/" + } + for _, dimension := range tofuguStruct.TofiManifest.Dimensions { + stateS3Path = stateS3Path + dimension + "_" + tofuguStruct.ParsedDimensions[dimension] + "/" + } + tofuguStruct.StateS3Path = stateS3Path + tofuguStruct.TofiName + ".tfstate" +} diff --git a/utils/generatevars.go b/utils/generatevars.go index db1f634..4af7a62 100644 --- a/utils/generatevars.go +++ b/utils/generatevars.go @@ -7,11 +7,11 @@ import ( "strings" ) -func GenerateVarsByDims(parsedDimensions map[string]string, cmdWorkTempDir string, inventoryPath string) { - for dimKey, dimValue := range parsedDimensions { +func (tofuguStruct *Tofugu) GenerateVarsByDims() { + for dimKey, dimValue := range tofuguStruct.ParsedDimensions { var inventroyJsonMap map[string]interface{} - inventroyJsonPath := inventoryPath + "/" + dimKey + "/" + dimValue + ".json" + inventroyJsonPath := tofuguStruct.InventoryPath + "/" + dimKey + "/" + dimValue + ".json" inventroyJsonBytes, err := os.ReadFile(inventroyJsonPath) if err != nil { @@ -24,13 +24,13 @@ func GenerateVarsByDims(parsedDimensions map[string]string, cmdWorkTempDir strin "tofugu_" + dimKey + "_name": dimValue, } - writeTfvarsMaps(targetAutoTfvarMap, dimKey, cmdWorkTempDir) + writeTfvarsMaps(targetAutoTfvarMap, dimKey, tofuguStruct.CmdWorkTempDir) log.Println("TofuGu generated tfvars for dimension: " + dimKey) } } -func GenerateVarsByEnvVars(cmdWorkTempDir string) { +func (tofuguStruct *Tofugu) GenerateVarsByEnvVars() { targetAutoTfvarMap := make(map[string]interface{}) for _, envVar := range os.Environ() { @@ -41,7 +41,7 @@ func GenerateVarsByEnvVars(cmdWorkTempDir string) { } if len(targetAutoTfvarMap) > 0 { - writeTfvarsMaps(targetAutoTfvarMap, "envivars", cmdWorkTempDir) + writeTfvarsMaps(targetAutoTfvarMap, "envivars", tofuguStruct.CmdWorkTempDir) log.Println("TofuGu generated tfvars for env variables") } } diff --git a/utils/preparetemp.go b/utils/preparetemp.go index 4615a17..c39ecb2 100644 --- a/utils/preparetemp.go +++ b/utils/preparetemp.go @@ -6,25 +6,26 @@ import ( "os/exec" ) -func PrepareTemp(tofiPath string, sharedModulesPath string, tmpFolderName string) string { - cmdTempDir := os.TempDir() + "/tofugu-" + GetMD5Hash(tmpFolderName) +func (tofuguStruct *Tofugu) PrepareTemp() { + tmpFolderNameSuffix := tofuguStruct.OrgName + tofuguStruct.StateS3Path + tofuguStruct.TofiName + cmdTempDirFullPath := os.TempDir() + "/tofugu-" + GetMD5Hash(tmpFolderNameSuffix) - command := exec.Command("rsync", "-a", "--delete", "--exclude=.terraform*", "--exclude=tofi_manifest.json", tofiPath+"/.", cmdTempDir) + command := exec.Command("rsync", "-a", "--delete", "--exclude=.terraform*", "--exclude=tofi_manifest.json", tofuguStruct.TofiPath+"/.", cmdTempDirFullPath) output, err := command.CombinedOutput() if err != nil { - os.RemoveAll(cmdTempDir) + os.RemoveAll(cmdTempDirFullPath) log.Printf("failed %s", output) - log.Fatalf("failed to copit tofi to tempdir %s\n", err) + log.Fatalf("failed to rsync tofi to tempdir %s\n", err) } - command = exec.Command("ln", "-sf", sharedModulesPath, cmdTempDir) + command = exec.Command("ln", "-sf", tofuguStruct.SharedModulesPath, cmdTempDirFullPath) output, err = command.CombinedOutput() if err != nil { - os.RemoveAll(cmdTempDir) + os.RemoveAll(cmdTempDirFullPath) log.Printf("failed %s", output) - log.Fatalf("failed to copit tofi to tempdir %s\n", err) + log.Fatalf("failed symlink shared_modules to tempdir %s\n", err) } - log.Println("TofuGu prepared tofi in temp dir: " + cmdTempDir) - return cmdTempDir + tofuguStruct.CmdWorkTempDir = cmdTempDirFullPath + log.Println("TofuGu prepared tofi in temp dir: " + tofuguStruct.CmdWorkTempDir) } diff --git a/utils/structures.go b/utils/structures.go new file mode 100644 index 0000000..e2ad71d --- /dev/null +++ b/utils/structures.go @@ -0,0 +1,19 @@ +package utils + +type Tofugu struct { + TofiName string + OrgName string + DimensionsFlags []string + TofiPath string + SharedModulesPath string + InventoryPath string + TofiManifestPath string + ParsedDimensions map[string]string + CmdWorkTempDir string + TofiManifest tofiManifestStruct + StateS3Path string +} + +type tofiManifestStruct struct { + Dimensions []string +} diff --git a/utils/tofimanifest.go b/utils/tofimanifest.go index f41d786..98b75e2 100644 --- a/utils/tofimanifest.go +++ b/utils/tofimanifest.go @@ -6,11 +6,8 @@ import ( "os" ) -type tofiManifestStruct struct { - Dimensions []string -} - -func ParseTofiManifest(tofiManifestPath string) tofiManifestStruct { +func (tofuguStruct *Tofugu) ParseTofiManifest(tofiManifestFileName string) { + tofiManifestPath := tofuguStruct.TofiPath + "/" + tofiManifestFileName // Let's first read the `config.json` file content, err := os.ReadFile(tofiManifestPath) if err != nil { @@ -23,6 +20,7 @@ func ParseTofiManifest(tofiManifestPath string) tofiManifestStruct { if err != nil { log.Fatal("Error during Unmarshal(): ", err) } + + tofuguStruct.TofiManifest = tofiManifest log.Println("TofuGu loaded tofi manifest: " + tofiManifestPath) - return tofiManifest }