|
1 | 1 | package terraform |
2 | 2 |
|
3 | 3 | import ( |
| 4 | + "bytes" |
4 | 5 | "context" |
5 | | - "fmt" |
6 | 6 | "os" |
7 | 7 |
|
8 | 8 | "github.com/clouddrove/smurf/internal/ai" |
9 | 9 | "github.com/hashicorp/terraform-exec/tfexec" |
10 | 10 | ) |
11 | 11 |
|
12 | | -func Plan( |
13 | | - vars []string, |
14 | | - varFiles []string, |
15 | | - dir string, |
16 | | - destroy bool, |
17 | | - targets []string, |
18 | | - refresh bool, |
19 | | - state string, |
20 | | - out string, |
21 | | - jsonOutput bool, |
22 | | - pushToCloud bool, |
23 | | - useAI bool, |
24 | | -) error { |
25 | | - |
| 12 | +// Plan runs 'terraform plan' and outputs the plan to the console. |
| 13 | +// It allows setting variables either via command-line arguments or variable files. |
| 14 | +// The function provides user feedback through spinners and colored messages, |
| 15 | +// and handles any errors that occur during the planning process. |
| 16 | +func Plan(vars []string, varFiles []string, |
| 17 | + dir string, destroy bool, |
| 18 | + targets []string, refresh bool, |
| 19 | + state string, out string, |
| 20 | + useAI bool) error { |
26 | 21 | tf, err := GetTerraform(dir) |
27 | 22 | if err != nil { |
28 | | - Error("Failed to initialize Terraform: %v", err) |
| 23 | + Error("Failed to initialize Terraform client: %v", err) |
29 | 24 | ai.AIExplainError(useAI, err.Error()) |
30 | 25 | return err |
31 | 26 | } |
32 | 27 |
|
33 | | - tf.SetStdout(os.Stdout) |
| 28 | + var outputBuffer bytes.Buffer |
| 29 | + customWriter := &CustomColorWriter{ |
| 30 | + Buffer: &outputBuffer, |
| 31 | + Writer: os.Stdout, |
| 32 | + } |
| 33 | + |
| 34 | + tf.SetStdout(customWriter) |
34 | 35 | tf.SetStderr(os.Stderr) |
35 | 36 |
|
| 37 | + // Start planning process |
| 38 | + Info("Starting infrastructure planning in directory: %s", dir) |
| 39 | + |
36 | 40 | planOptions := []tfexec.PlanOption{} |
37 | 41 |
|
| 42 | + // Handle state file |
38 | 43 | if state != "" { |
| 44 | + Info("Using custom state file: %s", state) |
39 | 45 | planOptions = append(planOptions, tfexec.State(state)) |
40 | 46 | } |
41 | 47 |
|
| 48 | + // Handle output plan file |
42 | 49 | if out != "" { |
| 50 | + Info("Saving execution plan to: %s", out) |
43 | 51 | planOptions = append(planOptions, tfexec.Out(out)) |
44 | 52 | } |
45 | 53 |
|
46 | | - for _, v := range vars { |
47 | | - planOptions = append(planOptions, tfexec.Var(v)) |
| 54 | + // Apply variables |
| 55 | + if len(vars) > 0 { |
| 56 | + for _, v := range vars { |
| 57 | + Info("Applying variable: %s", v) |
| 58 | + planOptions = append(planOptions, tfexec.Var(v)) |
| 59 | + } |
48 | 60 | } |
49 | 61 |
|
50 | | - for _, vf := range varFiles { |
51 | | - planOptions = append(planOptions, tfexec.VarFile(vf)) |
| 62 | + // Apply variable files |
| 63 | + if len(varFiles) > 0 { |
| 64 | + for _, vf := range varFiles { |
| 65 | + Info("Loading variable file: %s", vf) |
| 66 | + planOptions = append(planOptions, tfexec.VarFile(vf)) |
| 67 | + } |
52 | 68 | } |
53 | 69 |
|
54 | | - for _, target := range targets { |
55 | | - planOptions = append(planOptions, tfexec.Target(target)) |
| 70 | + // Handle targets |
| 71 | + if len(targets) > 0 { |
| 72 | + Info("Targeting %d resource(s)...", len(targets)) |
| 73 | + for _, target := range targets { |
| 74 | + Info("Using target: %s", target) |
| 75 | + planOptions = append(planOptions, tfexec.Target(target)) |
| 76 | + } |
56 | 77 | } |
57 | 78 |
|
| 79 | + // Destroy flag support |
58 | 80 | if destroy { |
| 81 | + Warn("Planning for destruction of infrastructure resources...") |
59 | 82 | planOptions = append(planOptions, tfexec.Destroy(true)) |
60 | 83 | } |
61 | 84 |
|
| 85 | + // Refresh flag support |
62 | 86 | if !refresh { |
| 87 | + Info("Skipping state refresh...") |
63 | 88 | planOptions = append(planOptions, tfexec.Refresh(false)) |
64 | 89 | } |
65 | 90 |
|
66 | | - // Execute plan |
| 91 | + // Execute Terraform plan and get the hasChanges boolean |
67 | 92 | hasChanges, err := tf.Plan(context.Background(), planOptions...) |
| 93 | + |
68 | 94 | if err != nil { |
69 | 95 | ai.AIExplainError(useAI, err.Error()) |
70 | 96 | return err |
71 | 97 | } |
72 | 98 |
|
| 99 | + // Check if there are any changes |
73 | 100 | if !hasChanges { |
74 | | - Success("✔ No changes. Infrastructure is up-to-date.") |
75 | | - return nil |
76 | | - } |
77 | | - |
78 | | - Success("Plan completed successfully.") |
| 101 | + Success("\nNo changes. Your infrastructure matches the configuration.") |
79 | 102 |
|
80 | | - // ----------------------------- |
81 | | - // JSON Output Section |
82 | | - // ----------------------------- |
83 | | - if jsonOutput { |
84 | | - |
85 | | - if out == "" { |
86 | | - return fmt.Errorf("JSON output requires --out plan file") |
87 | | - } |
88 | | - |
89 | | - Info("Generating JSON plan output...") |
90 | | - |
91 | | - planJSON, err := tf.ShowPlanFileRaw(context.Background(), out) |
92 | | - if err != nil { |
93 | | - return err |
94 | | - } |
95 | | - |
96 | | - jsonFile := "plan.json" |
97 | | - |
98 | | - // FIXED HERE 👇 |
99 | | - err = os.WriteFile(jsonFile, []byte(planJSON), 0644) |
100 | | - if err != nil { |
101 | | - return err |
| 103 | + if out != "" { |
| 104 | + Info("Note: Plan file was saved even though no changes detected: %s", out) |
102 | 105 | } |
| 106 | + return nil |
| 107 | + } |
103 | 108 |
|
104 | | - Success("JSON plan saved to %s", jsonFile) |
| 109 | + // Changes detected |
| 110 | + if out != "" { |
| 111 | + Success("Terraform plan saved to: %s", out) |
| 112 | + Info("To apply this plan, run: smurf stf apply %s", out) |
| 113 | + } else { |
| 114 | + Success("Terraform plan executed successfully. Review the changes above before applying.") |
| 115 | + Warn("Note: No plan file was saved. To save and apply later, use: smurf stf plan --out=plan.tfplan") |
105 | 116 | } |
106 | 117 |
|
107 | 118 | return nil |
|
0 commit comments