Skip to content

Commit

Permalink
feat: prune selections prior to plan/apply (#42)
Browse files Browse the repository at this point in the history
  • Loading branch information
leg100 authored Apr 27, 2024
1 parent d9d40a1 commit b2b9902
Show file tree
Hide file tree
Showing 50 changed files with 710 additions and 179 deletions.
14 changes: 10 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,29 @@ A TUI application for terraform power users.

Invoke `init`, `validate`, and `fmt` across multiple modules.

![Modules demo](https://vhs.charm.sh/vhs-3Cy7AzQGztpAUvNmekM7f9.gif)
![Modules demo](https://vhs.charm.sh/vhs-2SDiU03uHVMZ1OweMU5qnw.gif)

## Workspaces

Pug supports workspaces. Invoke plan and apply on workspaces. Change the current workspace for a module.

![Workspaces demo](https://vhs.charm.sh/vhs-2VVSWika2ZVjUeBNGVXzYq.gif)

## Runs

Create multiple plans and apply them in parallel.

![Runs demo](https://vhs.charm.sh/vhs-141kKh9q7Ikije2rN8EBK1.gif)
![Runs demo](https://vhs.charm.sh/vhs-2XzgTM8B8zMmL5kXSSP8hv.gif)

View the output of plans and applies.

![Run demo](https://vhs.charm.sh/vhs-3qwIobBxxLGB6bC5OR5kYd.gif)
![Run demo](https://vhs.charm.sh/vhs-6SbXJmeccgQG0xoENCH20A.gif)

## State management

Manage state resources. Taint, untaint and delete multiple resources. Select resources for targeted plans.

![State demo](https://vhs.charm.sh/vhs-77uh1e8YRXR5Aux8mSU1Z3.gif)
![State demo](https://vhs.charm.sh/vhs-79IoDj23zTHZnbHKekcz0o.gif)

## FAQ

Expand Down
Binary file modified demos/modules/modules.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed demos/output/applied_runs.png
Binary file not shown.
Binary file removed demos/output/apply_awaiting_confirmation.png
Binary file not shown.
Binary file removed demos/output/applying_runs.png
Binary file not shown.
Binary file removed demos/output/modules.gif
Binary file not shown.
Binary file removed demos/output/run.gif
Binary file not shown.
Binary file removed demos/output/runs.gif
Binary file not shown.
Binary file removed demos/output/state.gif
Binary file not shown.
Binary file modified demos/run/run.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified demos/runs/applied_runs.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified demos/runs/apply_awaiting_confirmation.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified demos/runs/runs.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified demos/state/state.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
17 changes: 17 additions & 0 deletions demos/workspaces/configs/a/.terraform.lock.hcl

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

28 changes: 28 additions & 0 deletions demos/workspaces/configs/a/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
terraform {
backend "local" {}
required_providers {
random = {
version = "= 3.6.0"
}
}
}

resource "time_sleep" "wait_three_seconds" {
create_duration = "3s"
}

resource "random_pet" "pet" {
count = 10

keepers = {
now = timestamp()
}
}

output "waited" {
value = time_sleep.wait_three_seconds.create_duration
}

output "pets" {
value = random_pet.pet[*].id
}
Empty file.
Empty file.
16 changes: 16 additions & 0 deletions demos/workspaces/configs/b/.terraform.lock.hcl

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

28 changes: 28 additions & 0 deletions demos/workspaces/configs/b/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
terraform {
backend "local" {}
required_providers {
random = {
version = "= 3.6.0"
}
}
}

resource "time_sleep" "wait_three_seconds" {
create_duration = "3s"
}

resource "random_pet" "pet" {
count = 10

keepers = {
now = timestamp()
}
}

output "waited" {
value = time_sleep.wait_three_seconds.create_duration
}

output "pets" {
value = random_pet.pet[*].id
}
Empty file.
Empty file.
16 changes: 16 additions & 0 deletions demos/workspaces/configs/c/.terraform.lock.hcl

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

28 changes: 28 additions & 0 deletions demos/workspaces/configs/c/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
terraform {
backend "local" {}
required_providers {
random = {
version = "= 3.6.0"
}
}
}

resource "time_sleep" "wait_three_seconds" {
create_duration = "3s"
}

resource "random_pet" "pet" {
count = 10

keepers = {
now = timestamp()
}
}

output "waited" {
value = time_sleep.wait_three_seconds.create_duration
}

output "pets" {
value = random_pet.pet[*].id
}
Empty file.
Empty file.
5 changes: 5 additions & 0 deletions demos/workspaces/reset.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env bash

find ./demos/workspaces -name .terraform -exec rm -rf {} \; > /dev/null 2>&1 || true
find ./demos/workspaces -name .pug -exec rm -rf {} \; > /dev/null 2>&1 || true
find ./demos/workspaces -name terraform.tfstate -exec rm -rf {} \; > /dev/null 2>&1 || true
Binary file added demos/workspaces/workspaces.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
30 changes: 30 additions & 0 deletions demos/workspaces/workspaces.tape
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
Output demos/workspaces/workspaces.gif

Set Shell "bash"
Set FontSize 14
Set Width 1200
Set Height 800
Set Framerate 24
Set Padding 5

Hide
Type "demos/workspaces/reset.sh" Enter
Type `TF_CLI_CONFIG_FILE=$PWD/mirror/mirror.tfrc go run main.go -w demos/workspaces/configs` Enter
Sleep 1s
# run terraform init to initialize modules but don't show viewer this
Ctrl+a Type "i" Sleep 3s
Show

Sleep 1s

# switch to global workspace listing
Type "W" Sleep 3s
# trigger a plan on all workspaces
Ctrl+a Sleep 0.5s Type "p" Sleep 2s
# trigger an an apply on each workspace, and enter 'y' to confirm
Ctrl+a Sleep 0.5s Type "a" Sleep 1s
Type "y" Sleep 4s
# switch current workspace on module a from default to dev
Down Sleep 1s Type "C" Sleep 2s
# switch to global module listing, showing the current workspace has changed for module a
Type "M" Sleep 2s
4 changes: 4 additions & 0 deletions hacks/make_demos.sh
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
vhs demos/modules/modules.tape
vhs demos/workspaces/workspaces.tape
vhs demos/runs/runs.tape
vhs demos/run/run.tape
vhs demos/state/state.tape

MODULES_GIF_URL=$(vhs publish demos/modules/modules.gif)
sed -i -e "s|\(\!\[Modules demo\]\).*|\1($MODULES_GIF_URL)|" README.md

WORKSPACES_GIF_URL=$(vhs publish demos/workspaces/workspaces.gif)
sed -i -e "s|\(\!\[Workspaces demo\]\).*|\1($WORKSPACES_GIF_URL)|" README.md

RUNS_GIF_URL=$(vhs publish demos/runs/runs.gif)
sed -i -e "s|\(\!\[Runs demo\]\).*|\1($RUNS_GIF_URL)|" README.md

Expand Down
7 changes: 7 additions & 0 deletions internal/app/helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"io/fs"
"os"
"path/filepath"
"regexp"
"strings"
"testing"
"time"
Expand Down Expand Up @@ -174,3 +175,9 @@ func initAndApplyModuleA(t *testing.T, tm *teatest.TestModel) {
return strings.Contains(s, "Apply complete! Resources: 10 added, 0 changed, 0 destroyed.")
})
}

func matchPattern(t *testing.T, pattern string, s string) bool {
matched, err := regexp.MatchString(pattern, s)
require.NoError(t, err)
return matched
}
121 changes: 121 additions & 0 deletions internal/app/module_list_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,124 @@ func TestModuleList_Reload(t *testing.T) {
return strings.Contains(s, "reloaded modules: added 0; removed 0")
})
}

// TestModuleList_CreateRun demonstrates a user selecting multiple modules and
// then attempting to create a run on each. Pug should de-select those
// selections which are not initialized / have no current workspace.
func TestModuleList_CreateRun(t *testing.T) {
tm := setup(t)

// Expect message to inform user that modules have been loaded
waitFor(t, tm, func(s string) bool {
return strings.Contains(s, "loaded 3 modules")
})

// Attempt to create run on uninitialized module
tm.Type("p")

// Expect error message
waitFor(t, tm, func(s string) bool {
return matchPattern(t, "Error:.*module does not have a current workspace", s)
})

// Select all modules and init
tm.Send(tea.KeyMsg{
Type: tea.KeyCtrlA,
})
tm.Type("i")

// Wait for each module to be initialized, and to have its current workspace
// set (should be "default")
waitFor(t, tm, func(s string) bool {
return strings.Count(s, "default") == 3
})

// Create a run on two modules, but not on the last one ("c")
tm.Type("s")
tm.Send(tea.KeyMsg{Type: tea.KeyDown})
tm.Type("s")
tm.Type("p")
waitFor(t, tm, func(s string) bool {
return strings.Count(s, "planned") == 2
})

}

// TestModuleList_ApplyCurrentRun demonstrates a user selecting
// multiple modules and then attempting to apply the latest run on each. Pug
// should de-select those selections which have no current run in a planned
// state.
func TestModuleList_ApplyCurrentRun(t *testing.T) {
tm := setup(t)

// Expect message to inform user that modules have been loaded
waitFor(t, tm, func(s string) bool {
return strings.Contains(s, "loaded 3 modules")
})

// Attempt to apply uninitialized module
tm.Type("a")

// Expect error message
waitFor(t, tm, func(s string) bool {
return matchPattern(t, "Error:.*module does not have a current run", s)
})

// Select all modules and init
tm.Send(tea.KeyMsg{
Type: tea.KeyCtrlA,
})
tm.Type("i")

// Wait for each module to be initialized, and to have its current workspace
// set (should be "default")
waitFor(t, tm, func(s string) bool {
return strings.Count(s, "default") == 3
})

// Attempt to apply initialized module but has no plan
tm.Type("a")

// Expect error message
waitFor(t, tm, func(s string) bool {
return matchPattern(t, "Error:.*module does not have a current run", s)
})

// Attempt to apply multiple modules but none have a plan
tm.Type("s")
tm.Send(tea.KeyMsg{Type: tea.KeyDown})
tm.Type("s")
tm.Send(tea.KeyMsg{Type: tea.KeyDown})
tm.Type("s")
tm.Type("a")

// Expect error message
waitFor(t, tm, func(s string) bool {
return matchPattern(t, "Error:.*no rows are applicable to the given action", s)
})

// Create a plan on two modules, but not on the last one ("c")
tm.Type("s")
tm.Send(tea.KeyMsg{Type: tea.KeyUp})
tm.Type("s")
tm.Type("p")
waitFor(t, tm, func(s string) bool {
return strings.Count(s, "planned") == 2
})

// Attempt to apply all modules
tm.Send(tea.KeyMsg{Type: tea.KeyCtrlA})
tm.Type("a")

// Expect one module ("C") to have been de-selected, and the apply to be
// invoked only on two modules ("A", and "B")
waitFor(t, tm, func(s string) bool {
return strings.Contains(s, "Apply 2 runs (y/N)?")
})
tm.Type("y")

// Expect two modules to be applied
waitFor(t, tm, func(s string) bool {
return strings.Count(s, "applied") == 2
})
}
Loading

0 comments on commit b2b9902

Please sign in to comment.