github.com/opentofu/opentofu@v1.7.1/internal/command/workspace_command_test.go (about) 1 // Copyright (c) The OpenTofu Authors 2 // SPDX-License-Identifier: MPL-2.0 3 // Copyright (c) 2023 HashiCorp, Inc. 4 // SPDX-License-Identifier: MPL-2.0 5 6 package command 7 8 import ( 9 "os" 10 "path/filepath" 11 "strings" 12 "testing" 13 14 "github.com/mitchellh/cli" 15 "github.com/opentofu/opentofu/internal/addrs" 16 "github.com/opentofu/opentofu/internal/backend" 17 "github.com/opentofu/opentofu/internal/backend/local" 18 "github.com/opentofu/opentofu/internal/backend/remote-state/inmem" 19 "github.com/opentofu/opentofu/internal/encryption" 20 "github.com/opentofu/opentofu/internal/states" 21 "github.com/opentofu/opentofu/internal/states/statemgr" 22 23 legacy "github.com/opentofu/opentofu/internal/legacy/tofu" 24 ) 25 26 func TestWorkspace_createAndChange(t *testing.T) { 27 // Create a temporary working directory that is empty 28 td := t.TempDir() 29 os.MkdirAll(td, 0755) 30 defer testChdir(t, td)() 31 32 newCmd := &WorkspaceNewCommand{} 33 34 current, _ := newCmd.Workspace() 35 if current != backend.DefaultStateName { 36 t.Fatal("current workspace should be 'default'") 37 } 38 39 args := []string{"test"} 40 ui := new(cli.MockUi) 41 view, _ := testView(t) 42 newCmd.Meta = Meta{Ui: ui, View: view} 43 if code := newCmd.Run(args); code != 0 { 44 t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter) 45 } 46 47 current, _ = newCmd.Workspace() 48 if current != "test" { 49 t.Fatalf("current workspace should be 'test', got %q", current) 50 } 51 52 selCmd := &WorkspaceSelectCommand{} 53 args = []string{backend.DefaultStateName} 54 ui = new(cli.MockUi) 55 selCmd.Meta = Meta{Ui: ui, View: view} 56 if code := selCmd.Run(args); code != 0 { 57 t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter) 58 } 59 60 current, _ = newCmd.Workspace() 61 if current != backend.DefaultStateName { 62 t.Fatal("current workspace should be 'default'") 63 } 64 65 } 66 67 // Create some workspaces and test the list output. 68 // This also ensures we switch to the correct env after each call 69 func TestWorkspace_createAndList(t *testing.T) { 70 // Create a temporary working directory that is empty 71 td := t.TempDir() 72 os.MkdirAll(td, 0755) 73 defer testChdir(t, td)() 74 75 // make sure a vars file doesn't interfere 76 err := os.WriteFile( 77 DefaultVarsFilename, 78 []byte(`foo = "bar"`), 79 0644, 80 ) 81 if err != nil { 82 t.Fatal(err) 83 } 84 85 envs := []string{"test_a", "test_b", "test_c"} 86 87 // create multiple workspaces 88 for _, env := range envs { 89 ui := new(cli.MockUi) 90 view, _ := testView(t) 91 newCmd := &WorkspaceNewCommand{ 92 Meta: Meta{Ui: ui, View: view}, 93 } 94 if code := newCmd.Run([]string{env}); code != 0 { 95 t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter) 96 } 97 } 98 99 listCmd := &WorkspaceListCommand{} 100 ui := new(cli.MockUi) 101 view, _ := testView(t) 102 listCmd.Meta = Meta{Ui: ui, View: view} 103 104 if code := listCmd.Run(nil); code != 0 { 105 t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter) 106 } 107 108 actual := strings.TrimSpace(ui.OutputWriter.String()) 109 expected := "default\n test_a\n test_b\n* test_c" 110 111 if actual != expected { 112 t.Fatalf("\nexpected: %q\nactual: %q", expected, actual) 113 } 114 } 115 116 // Create some workspaces and test the show output. 117 func TestWorkspace_createAndShow(t *testing.T) { 118 // Create a temporary working directory that is empty 119 td := t.TempDir() 120 os.MkdirAll(td, 0755) 121 defer testChdir(t, td)() 122 123 // make sure a vars file doesn't interfere 124 err := os.WriteFile( 125 DefaultVarsFilename, 126 []byte(`foo = "bar"`), 127 0644, 128 ) 129 if err != nil { 130 t.Fatal(err) 131 } 132 133 // make sure current workspace show outputs "default" 134 showCmd := &WorkspaceShowCommand{} 135 ui := new(cli.MockUi) 136 view, _ := testView(t) 137 showCmd.Meta = Meta{Ui: ui, View: view} 138 139 if code := showCmd.Run(nil); code != 0 { 140 t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter) 141 } 142 143 actual := strings.TrimSpace(ui.OutputWriter.String()) 144 expected := "default" 145 146 if actual != expected { 147 t.Fatalf("\nexpected: %q\nactual: %q", expected, actual) 148 } 149 150 newCmd := &WorkspaceNewCommand{} 151 152 env := []string{"test_a"} 153 154 // create test_a workspace 155 ui = new(cli.MockUi) 156 newCmd.Meta = Meta{Ui: ui, View: view} 157 if code := newCmd.Run(env); code != 0 { 158 t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter) 159 } 160 161 selCmd := &WorkspaceSelectCommand{} 162 ui = new(cli.MockUi) 163 selCmd.Meta = Meta{Ui: ui, View: view} 164 if code := selCmd.Run(env); code != 0 { 165 t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter) 166 } 167 168 showCmd = &WorkspaceShowCommand{} 169 ui = new(cli.MockUi) 170 showCmd.Meta = Meta{Ui: ui, View: view} 171 172 if code := showCmd.Run(nil); code != 0 { 173 t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter) 174 } 175 176 actual = strings.TrimSpace(ui.OutputWriter.String()) 177 expected = "test_a" 178 179 if actual != expected { 180 t.Fatalf("\nexpected: %q\nactual: %q", expected, actual) 181 } 182 } 183 184 // Don't allow names that aren't URL safe 185 func TestWorkspace_createInvalid(t *testing.T) { 186 // Create a temporary working directory that is empty 187 td := t.TempDir() 188 os.MkdirAll(td, 0755) 189 defer testChdir(t, td)() 190 191 envs := []string{"test_a*", "test_b/foo", "../../../test_c", "好_d"} 192 193 // create multiple workspaces 194 for _, env := range envs { 195 ui := new(cli.MockUi) 196 view, _ := testView(t) 197 newCmd := &WorkspaceNewCommand{ 198 Meta: Meta{Ui: ui, View: view}, 199 } 200 if code := newCmd.Run([]string{env}); code == 0 { 201 t.Fatalf("expected failure: \n%s", ui.OutputWriter) 202 } 203 } 204 205 // list workspaces to make sure none were created 206 listCmd := &WorkspaceListCommand{} 207 ui := new(cli.MockUi) 208 view, _ := testView(t) 209 listCmd.Meta = Meta{Ui: ui, View: view} 210 211 if code := listCmd.Run(nil); code != 0 { 212 t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter) 213 } 214 215 actual := strings.TrimSpace(ui.OutputWriter.String()) 216 expected := "* default" 217 218 if actual != expected { 219 t.Fatalf("\nexpected: %q\nactual: %q", expected, actual) 220 } 221 } 222 223 func TestWorkspace_createWithState(t *testing.T) { 224 td := t.TempDir() 225 testCopyDir(t, testFixturePath("inmem-backend"), td) 226 defer testChdir(t, td)() 227 defer inmem.Reset() 228 229 // init the backend 230 ui := new(cli.MockUi) 231 view, _ := testView(t) 232 initCmd := &InitCommand{ 233 Meta: Meta{Ui: ui, View: view}, 234 } 235 if code := initCmd.Run([]string{}); code != 0 { 236 t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) 237 } 238 239 originalState := states.BuildState(func(s *states.SyncState) { 240 s.SetResourceInstanceCurrent( 241 addrs.Resource{ 242 Mode: addrs.ManagedResourceMode, 243 Type: "test_instance", 244 Name: "foo", 245 }.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance), 246 &states.ResourceInstanceObjectSrc{ 247 AttrsJSON: []byte(`{"id":"bar"}`), 248 Status: states.ObjectReady, 249 }, 250 addrs.AbsProviderConfig{ 251 Provider: addrs.NewDefaultProvider("test"), 252 Module: addrs.RootModule, 253 }, 254 ) 255 }) 256 257 err := statemgr.WriteAndPersist(statemgr.NewFilesystem("test.tfstate", encryption.StateEncryptionDisabled()), originalState, nil) 258 if err != nil { 259 t.Fatal(err) 260 } 261 262 workspace := "test_workspace" 263 264 args := []string{"-state", "test.tfstate", workspace} 265 ui = new(cli.MockUi) 266 newCmd := &WorkspaceNewCommand{ 267 Meta: Meta{Ui: ui, View: view}, 268 } 269 if code := newCmd.Run(args); code != 0 { 270 t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter) 271 } 272 273 newPath := filepath.Join(local.DefaultWorkspaceDir, "test", DefaultStateFilename) 274 envState := statemgr.NewFilesystem(newPath, encryption.StateEncryptionDisabled()) 275 err = envState.RefreshState() 276 if err != nil { 277 t.Fatal(err) 278 } 279 280 b := backend.TestBackendConfig(t, inmem.New(encryption.StateEncryptionDisabled()), nil) 281 sMgr, err := b.StateMgr(workspace) 282 if err != nil { 283 t.Fatal(err) 284 } 285 286 newState := sMgr.State() 287 288 if got, want := newState.String(), originalState.String(); got != want { 289 t.Fatalf("states not equal\ngot: %s\nwant: %s", got, want) 290 } 291 } 292 293 func TestWorkspace_delete(t *testing.T) { 294 td := t.TempDir() 295 os.MkdirAll(td, 0755) 296 defer testChdir(t, td)() 297 298 // create the workspace directories 299 if err := os.MkdirAll(filepath.Join(local.DefaultWorkspaceDir, "test"), 0755); err != nil { 300 t.Fatal(err) 301 } 302 303 // create the workspace file 304 if err := os.MkdirAll(DefaultDataDir, 0755); err != nil { 305 t.Fatal(err) 306 } 307 if err := os.WriteFile(filepath.Join(DefaultDataDir, local.DefaultWorkspaceFile), []byte("test"), 0644); err != nil { 308 t.Fatal(err) 309 } 310 311 ui := new(cli.MockUi) 312 view, _ := testView(t) 313 delCmd := &WorkspaceDeleteCommand{ 314 Meta: Meta{Ui: ui, View: view}, 315 } 316 317 current, _ := delCmd.Workspace() 318 if current != "test" { 319 t.Fatal("wrong workspace:", current) 320 } 321 322 // we can't delete our current workspace 323 args := []string{"test"} 324 if code := delCmd.Run(args); code == 0 { 325 t.Fatal("expected error deleting current workspace") 326 } 327 328 // change back to default 329 if err := delCmd.SetWorkspace(backend.DefaultStateName); err != nil { 330 t.Fatal(err) 331 } 332 333 // try the delete again 334 ui = new(cli.MockUi) 335 delCmd.Meta.Ui = ui 336 if code := delCmd.Run(args); code != 0 { 337 t.Fatalf("error deleting workspace: %s", ui.ErrorWriter) 338 } 339 340 current, _ = delCmd.Workspace() 341 if current != backend.DefaultStateName { 342 t.Fatalf("wrong workspace: %q", current) 343 } 344 } 345 346 func TestWorkspace_deleteInvalid(t *testing.T) { 347 td := t.TempDir() 348 os.MkdirAll(td, 0755) 349 defer testChdir(t, td)() 350 351 // choose an invalid workspace name 352 workspace := "test workspace" 353 path := filepath.Join(local.DefaultWorkspaceDir, workspace) 354 355 // create the workspace directories 356 if err := os.MkdirAll(path, 0755); err != nil { 357 t.Fatal(err) 358 } 359 360 ui := new(cli.MockUi) 361 view, _ := testView(t) 362 delCmd := &WorkspaceDeleteCommand{ 363 Meta: Meta{Ui: ui, View: view}, 364 } 365 366 // delete the workspace 367 if code := delCmd.Run([]string{workspace}); code != 0 { 368 t.Fatalf("error deleting workspace: %s", ui.ErrorWriter) 369 } 370 371 if _, err := os.Stat(path); err == nil { 372 t.Fatalf("should have deleted workspace, but %s still exists", path) 373 } else if !os.IsNotExist(err) { 374 t.Fatalf("unexpected error for workspace path: %s", err) 375 } 376 } 377 378 func TestWorkspace_deleteWithState(t *testing.T) { 379 td := t.TempDir() 380 os.MkdirAll(td, 0755) 381 defer testChdir(t, td)() 382 383 // create the workspace directories 384 if err := os.MkdirAll(filepath.Join(local.DefaultWorkspaceDir, "test"), 0755); err != nil { 385 t.Fatal(err) 386 } 387 388 // create a non-empty state 389 originalState := &legacy.State{ 390 Modules: []*legacy.ModuleState{ 391 { 392 Path: []string{"root"}, 393 Resources: map[string]*legacy.ResourceState{ 394 "test_instance.foo": { 395 Type: "test_instance", 396 Primary: &legacy.InstanceState{ 397 ID: "bar", 398 }, 399 }, 400 }, 401 }, 402 }, 403 } 404 405 f, err := os.Create(filepath.Join(local.DefaultWorkspaceDir, "test", "terraform.tfstate")) 406 if err != nil { 407 t.Fatal(err) 408 } 409 defer f.Close() 410 if err := legacy.WriteState(originalState, f); err != nil { 411 t.Fatal(err) 412 } 413 414 ui := cli.NewMockUi() 415 view, _ := testView(t) 416 delCmd := &WorkspaceDeleteCommand{ 417 Meta: Meta{Ui: ui, View: view}, 418 } 419 args := []string{"test"} 420 if code := delCmd.Run(args); code == 0 { 421 t.Fatalf("expected failure without -force.\noutput: %s", ui.OutputWriter) 422 } 423 gotStderr := ui.ErrorWriter.String() 424 if want, got := `Workspace "test" is currently tracking the following resource instances`, gotStderr; !strings.Contains(got, want) { 425 t.Errorf("missing expected error message\nwant substring: %s\ngot:\n%s", want, got) 426 } 427 if want, got := `- test_instance.foo`, gotStderr; !strings.Contains(got, want) { 428 t.Errorf("error message doesn't mention the remaining instance\nwant substring: %s\ngot:\n%s", want, got) 429 } 430 431 ui = new(cli.MockUi) 432 delCmd.Meta.Ui = ui 433 434 args = []string{"-force", "test"} 435 if code := delCmd.Run(args); code != 0 { 436 t.Fatalf("failure: %s", ui.ErrorWriter) 437 } 438 439 if _, err := os.Stat(filepath.Join(local.DefaultWorkspaceDir, "test")); !os.IsNotExist(err) { 440 t.Fatal("env 'test' still exists!") 441 } 442 } 443 444 func TestWorkspace_selectWithOrCreate(t *testing.T) { 445 // Create a temporary working directory that is empty 446 td := t.TempDir() 447 os.MkdirAll(td, 0755) 448 defer testChdir(t, td)() 449 450 selectCmd := &WorkspaceSelectCommand{} 451 452 current, _ := selectCmd.Workspace() 453 if current != backend.DefaultStateName { 454 t.Fatal("current workspace should be 'default'") 455 } 456 457 args := []string{"-or-create", "test"} 458 ui := new(cli.MockUi) 459 view, _ := testView(t) 460 selectCmd.Meta = Meta{Ui: ui, View: view} 461 if code := selectCmd.Run(args); code != 0 { 462 t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter) 463 } 464 465 current, _ = selectCmd.Workspace() 466 if current != "test" { 467 t.Fatalf("current workspace should be 'test', got %q", current) 468 } 469 470 }