Skip to content

Commit

Permalink
dev-2668 migrate to charmbracelet logger
Browse files Browse the repository at this point in the history
  • Loading branch information
mcalhoun committed Jan 30, 2025
2 parents b5e8a44 + 75d6c46 commit 5154db6
Show file tree
Hide file tree
Showing 14 changed files with 213 additions and 35 deletions.
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ SHELL := /bin/bash
#GOARCH=amd64
VERSION=test

export CGO_ENABLED=0
# List of targets the `readme` target should call before generating the readme
export README_DEPS ?= docs/targets.md

Expand Down
3 changes: 0 additions & 3 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,9 +101,6 @@ func setupLogger(atmosConfig *schema.AtmosConfiguration) {
log.SetLevel(log.InfoLevel)
}

// Disable timestamp in logs so snapshots work. We will address this in a future PR updating styles, etc.
log.SetReportTimestamp(false)

if atmosConfig.Logs.File != "/dev/stderr" {
logFile, err := os.OpenFile(atmosConfig.Logs.File, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
Expand Down
8 changes: 2 additions & 6 deletions internal/exec/terraform.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ const (
outFlag = "-out"
varFileFlag = "-var-file"
skipTerraformLockFileFlag = "--skip-lock-file"
everythingFlag = "--everything"
forceFlag = "--force"
)

Expand All @@ -37,13 +36,10 @@ func shouldProcessStacks(info *schema.ConfigAndStacksInfo) (bool, bool) {
shouldProcessStacks := true
shouldCheckStack := true

if info.SubCommand == "clean" &&
(u.SliceContainsString(info.AdditionalArgsAndFlags, everythingFlag) ||
u.SliceContainsString(info.AdditionalArgsAndFlags, forceFlag)) {
if info.SubCommand == "clean" {
if info.ComponentFromArg == "" {
shouldProcessStacks = false
}

shouldCheckStack = info.Stack != ""

}
Expand Down Expand Up @@ -128,7 +124,7 @@ func ExecuteTerraform(info schema.ConfigAndStacksInfo) error {
}
}

if !info.ComponentIsEnabled {
if !info.ComponentIsEnabled && info.SubCommand != "clean" {
u.LogInfo(fmt.Sprintf("component '%s' is not enabled and skipped", info.ComponentFromArg))
return nil
}
Expand Down
9 changes: 4 additions & 5 deletions internal/exec/terraform_clean.go
Original file line number Diff line number Diff line change
Expand Up @@ -347,8 +347,8 @@ func handleTFDataDir(componentPath string, relativePath string, atmosConfig sche
}

}
func initializeFilesToClear(info schema.ConfigAndStacksInfo, atmosConfig schema.AtmosConfiguration, everything bool) []string {
if everything && info.Stack == "" {
func initializeFilesToClear(info schema.ConfigAndStacksInfo, atmosConfig schema.AtmosConfiguration) []string {
if info.ComponentFromArg == "" {
return []string{".terraform", ".terraform.lock.hcl", "*.tfvar.json", "terraform.tfstate.d"}
}
varFile := constructTerraformComponentVarfileName(info)
Expand Down Expand Up @@ -407,8 +407,7 @@ func handleCleanSubCommand(info schema.ConfigAndStacksInfo, componentPath string
}

force := u.SliceContainsString(info.AdditionalArgsAndFlags, forceFlag)
everything := u.SliceContainsString(info.AdditionalArgsAndFlags, everythingFlag)
filesToClear := initializeFilesToClear(info, atmosConfig, everything)
filesToClear := initializeFilesToClear(info, atmosConfig)
folders, err := CollectDirectoryObjects(cleanPath, filesToClear)
if err != nil {
u.LogTrace(fmt.Errorf("error collecting folders and files: %v", err).Error())
Expand Down Expand Up @@ -456,7 +455,7 @@ func handleCleanSubCommand(info schema.ConfigAndStacksInfo, componentPath string
u.PrintMessage(fmt.Sprintf("Do you want to delete the folder '%s'? ", tfDataDir))
}
var message string
if everything && info.ComponentFromArg == "" {
if info.ComponentFromArg == "" {
message = fmt.Sprintf("This will delete %v local terraform state files affecting all components", total)
} else if info.Component != "" && info.Stack != "" {
message = fmt.Sprintf("This will delete %v local terraform state files for component '%s' in stack '%s'", total, info.Component, info.Stack)
Expand Down
7 changes: 5 additions & 2 deletions internal/exec/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -687,9 +687,12 @@ func processArgsAndFlags(
var additionalArgsAndFlags []string
var globalOptions []string
var indexesToRemove []int
if len(inputArgsAndFlags) == 1 && inputArgsAndFlags[0] == "clean" {
info.SubCommand = inputArgsAndFlags[0]
}

// For commands like `atmos terraform clean` and `atmos terraform plan`, show the command help
if len(inputArgsAndFlags) == 1 && inputArgsAndFlags[0] != "version" {
// For commands like `atmos terraform plan`, show the command help
if len(inputArgsAndFlags) == 1 && inputArgsAndFlags[0] != "version" && info.SubCommand == "" {
info.SubCommand = inputArgsAndFlags[0]
info.NeedHelp = true
return info, nil
Expand Down
5 changes: 5 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
package main

import (
"github.com/charmbracelet/log"

"github.com/cloudposse/atmos/cmd"
u "github.com/cloudposse/atmos/pkg/utils"
)

func main() {
// Disable timestamp in logs so snapshots work. We will address this in a future PR updating styles, etc.
log.Default().SetReportTimestamp(false)

err := cmd.Execute()
if err != nil {
u.LogErrorAndExit(err)
Expand Down
2 changes: 1 addition & 1 deletion pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -391,7 +391,7 @@ func processConfigFile(
defer func(reader *os.File) {
err := reader.Close()
if err != nil {
u.LogWarning(fmt.Sprintf("error closing file '" + configPath + "'. " + err.Error()))
u.LogWarning(fmt.Sprintf("error closing file '%s'. %v", configPath, err))
}
}(reader)

Expand Down
175 changes: 175 additions & 0 deletions tests/cli_terraform_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
package tests

import (
"bytes"
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"testing"
)

func TestCLITerraformClean(t *testing.T) {
// Capture the starting working directory
startingDir, err := os.Getwd()
if err != nil {
t.Fatalf("Failed to get the current working directory: %v", err)
}

// Initialize PathManager and update PATH
pathManager := NewPathManager()
pathManager.Prepend("../build", "..")
err = pathManager.Apply()
if err != nil {
t.Fatalf("Failed to apply updated PATH: %v", err)
}
fmt.Printf("Updated PATH: %s\n", pathManager.GetPath())
defer func() {
// Change back to the original working directory after the test
if err := os.Chdir(startingDir); err != nil {
t.Fatalf("Failed to change back to the starting directory: %v", err)
}
}()

// Define the work directory and change to it
workDir := "../examples/quick-start-simple"
if err := os.Chdir(workDir); err != nil {
t.Fatalf("Failed to change directory to %q: %v", workDir, err)
}

// Find the binary path for "atmos"
binaryPath, err := exec.LookPath("atmos")
if err != nil {
t.Fatalf("Binary not found: %s. Current PATH: %s", "atmos", pathManager.GetPath())
}

// Force clean everything
runTerraformCleanCommand(t, binaryPath, "--force")
// Clean everything
runTerraformCleanCommand(t, binaryPath)
// Clean specific component
runTerraformCleanCommand(t, binaryPath, "station")
// Clean component with stack
runTerraformCleanCommand(t, binaryPath, "station", "-s", "dev")

// Run terraform apply for prod environment
runTerraformApply(t, binaryPath, "prod")
verifyStateFilesExist(t, []string{"./components/terraform/weather/terraform.tfstate.d/prod-station"})
runCLITerraformCleanComponent(t, binaryPath, "prod")
verifyStateFilesDeleted(t, []string{"./components/terraform/weather/terraform.tfstate.d/prod-station"})

// Run terraform apply for dev environment
runTerraformApply(t, binaryPath, "dev")

// Verify if state files exist before cleaning
stateFiles := []string{
"./components/terraform/weather/.terraform",
"./components/terraform/weather/terraform.tfstate.d",
"./components/terraform/weather/.terraform.lock.hcl",
}
verifyStateFilesExist(t, stateFiles)

// Run terraform clean
runTerraformClean(t, binaryPath)

// Verify if state files have been deleted after clean
verifyStateFilesDeleted(t, stateFiles)

}

// runTerraformApply runs the terraform apply command for a given environment.
func runTerraformApply(t *testing.T, binaryPath, environment string) {
cmd := exec.Command(binaryPath, "terraform", "apply", "station", "-s", environment)
envVars := os.Environ()
envVars = append(envVars, "ATMOS_COMPONENTS_TERRAFORM_APPLY_AUTO_APPROVE=true")
cmd.Env = envVars

var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
t.Log(stdout.String())
if err != nil {
t.Fatalf("Failed to run terraform apply station -s %s: %v", environment, stderr.String())
}
}

// verifyStateFilesExist checks if the state files exist before cleaning.
func verifyStateFilesExist(t *testing.T, stateFiles []string) {
for _, file := range stateFiles {
fileAbs, err := filepath.Abs(file)
if err != nil {
t.Fatalf("Failed to resolve absolute path for %q: %v", file, err)
}
if _, err := os.Stat(fileAbs); errors.Is(err, os.ErrNotExist) {
t.Errorf("Expected file to exist before cleaning: %q", fileAbs)
}
}
}

// runTerraformClean runs the terraform clean command.
func runTerraformClean(t *testing.T, binaryPath string) {
cmd := exec.Command(binaryPath, "terraform", "clean", "--force")
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
t.Logf("Clean command output:\n%s", stdout.String())
if err != nil {
t.Fatalf("Failed to run terraform clean: %v", stderr.String())
}
}

// verifyStateFilesDeleted checks if the state files have been deleted after cleaning.
func verifyStateFilesDeleted(t *testing.T, stateFiles []string) {
for _, file := range stateFiles {
fileAbs, err := filepath.Abs(file)
if err != nil {
t.Fatalf("Failed to resolve absolute path for %q: %v", file, err)
}
_, err = os.Stat(fileAbs)
if err == nil {
t.Errorf("Expected Terraform state file to be deleted: %q", fileAbs)
} else if !errors.Is(err, os.ErrNotExist) {
t.Errorf("Unexpected error checking file %q: %v", fileAbs, err)
}
}
}

func runCLITerraformCleanComponent(t *testing.T, binaryPath, environment string) {
cmd := exec.Command(binaryPath, "terraform", "clean", "station", "-s", environment, "--force")
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
t.Logf("Clean command output:\n%s", stdout.String())
if err != nil {
t.Fatalf("Failed to run terraform clean: %v", stderr.String())
}
}
func runCLITerraformClean(t *testing.T, binaryPath string) {
cmd := exec.Command(binaryPath, "terraform", "clean")
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
t.Logf("Clean command output:\n%s", stdout.String())
if err != nil {
t.Fatalf("Failed to run terraform clean: %v", stderr.String())
}

}

func runTerraformCleanCommand(t *testing.T, binaryPath string, args ...string) {
cmdArgs := append([]string{"terraform", "clean"}, args...)
cmd := exec.Command(binaryPath, cmdArgs...)
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
t.Logf("Clean command output:\n%s", stdout.String())
if err != nil {
t.Fatalf("Failed to run terraform clean: %v", stderr.String())
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 4 additions & 5 deletions website/docs/cli/commands/terraform/terraform-clean.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,12 @@ Execute the `terraform clean` command like this:

```shell
atmos terraform clean <component> -s <stack> [--skip-lock-file] [--everything] [--force]
```

:::warning
The `--everything` flag will delete all Terraform-related files including state files. The `--force` flag will bypass confirmation prompts.
The `clean` command, by default, deletes all Terraform-related files, including local state files, but will prompt for confirmation before proceeding. Using the `--force` flag skips the confirmation prompt and executes the deletion immediately.
Use these flags with extreme caution as they can lead to irreversible data loss.
:::
```

:::tip
Run `atmos terraform clean --help` to see all the available options
Expand All @@ -36,10 +36,9 @@ Run `atmos terraform clean --help` to see all the available options

```shell
# Delete all Terraform-related files for all components (with confirmation)
atmos terraform clean --everything

atmos terraform clean
# Force delete all Terraform-related files for all components (no confirmation)
atmos terraform clean --everything --force
atmos terraform clean --force
atmos terraform clean top-level-component1 -s tenant1-ue2-dev
atmos terraform clean infra/vpc -s tenant1-ue2-staging
atmos terraform clean infra/vpc -s tenant1-ue2-staging --skip-lock-file
Expand Down
17 changes: 10 additions & 7 deletions website/docs/cli/commands/terraform/usage.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,14 @@ HCL-based domain-specific language and its interpreter. Atmos works with [OpenTo

- `atmos terraform clean` command deletes the `.terraform` folder, `.terraform.lock.hcl` lock file, and the previously generated `planfile`
and `varfile` for the specified component and stack. Use the `--skip-lock-file` flag to skip deleting the `.terraform.lock.hcl` file.
Use the `--everything` flag to delete all the local Terraform state files and directories (including `terraform.tfstate.d`) for all components and stacks.
Use the `--force` flag to bypass the safety confirmation prompt and force the deletion (use with caution).
It deletes all local Terraform state files and directories
(including [`terraform.tfstate.d`](https://developer.hashicorp.com/terraform/cli/workspaces#workspace-internals)
used for local state) for a component in a stack.
The `--force` flag bypasses the safety confirmation prompt and forces the deletion. Use with caution.

:::warning
The `--everything` flag performs destructive operations that can lead to permanent state loss. Always ensure you have remote state configured in your components before proceeding.
The `clean` command performs destructive operations that can lead to permanent state loss, if not using remote backends.
Always ensure you have remote state configured in your components before proceeding.
:::

- `atmos terraform workspace` command first runs `terraform init -reconfigure`, then `terraform workspace select`, and if the workspace was not
Expand Down Expand Up @@ -113,16 +116,16 @@ atmos terraform destroy test/test-component-override -s tenant1-ue2-dev --redire
atmos terraform init test/test-component-override-3 -s tenant1-ue2-dev

# Clean all components (with confirmation)
atmos terraform clean --everything
atmos terraform clean

# Clean a specific component
atmos terraform clean vpc --everything
atmos terraform clean vpc

# Clean a specific component in a stack
atmos terraform clean vpc --stack dev --everything
atmos terraform clean vpc --stack dev

# Clean without confirmation prompt
atmos terraform clean --everything --force
atmos terraform clean --force
atmos terraform clean test/test-component-override-3 -s tenant1-ue2-dev

atmos terraform workspace test/test-component-override-3 -s tenant1-ue2-dev
Expand Down
4 changes: 2 additions & 2 deletions website/src/components/Screengrabs/atmos-terraform--help.html

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 5154db6

Please sign in to comment.