github.com/myhau/pulumi/pkg/v3@v3.70.2-0.20221116134521-f2775972e587/backend/filestate/backend_test.go (about) 1 package filestate 2 3 import ( 4 "context" 5 "encoding/json" 6 "io/ioutil" 7 "os" 8 "path" 9 "path/filepath" 10 "runtime" 11 "testing" 12 13 "github.com/stretchr/testify/assert" 14 user "github.com/tweekmonster/luser" 15 16 "github.com/pulumi/pulumi/pkg/v3/backend" 17 "github.com/pulumi/pulumi/pkg/v3/operations" 18 "github.com/pulumi/pulumi/pkg/v3/resource/deploy" 19 "github.com/pulumi/pulumi/pkg/v3/resource/stack" 20 "github.com/pulumi/pulumi/pkg/v3/secrets/b64" 21 "github.com/pulumi/pulumi/pkg/v3/secrets/passphrase" 22 "github.com/pulumi/pulumi/sdk/v3/go/common/apitype" 23 "github.com/pulumi/pulumi/sdk/v3/go/common/resource" 24 "github.com/pulumi/pulumi/sdk/v3/go/common/resource/config" 25 "github.com/pulumi/pulumi/sdk/v3/go/common/tokens" 26 "github.com/pulumi/pulumi/sdk/v3/go/common/util/cmdutil" 27 ) 28 29 func TestMassageBlobPath(t *testing.T) { 30 t.Parallel() 31 32 testMassagePath := func(t *testing.T, s string, want string) { 33 massaged, err := massageBlobPath(s) 34 assert.NoError(t, err) 35 assert.Equal(t, want, massaged, 36 "massageBlobPath(%s) didn't return expected result.\nWant: %q\nGot: %q", s, want, massaged) 37 } 38 39 // URLs not prefixed with "file://" are kept as-is. Also why we add FilePathPrefix as a prefix for other tests. 40 t.Run("NonFilePrefixed", func(t *testing.T) { 41 t.Parallel() 42 43 testMassagePath(t, "asdf-123", "asdf-123") 44 }) 45 46 // The home directory is converted into the user's actual home directory. 47 // Which requires even more tweaks to work on Windows. 48 t.Run("PrefixedWithTilde", func(t *testing.T) { 49 t.Parallel() 50 51 usr, err := user.Current() 52 if err != nil { 53 t.Fatalf("Unable to get current user: %v", err) 54 } 55 56 homeDir := usr.HomeDir 57 58 // When running on Windows, the "home directory" takes on a different meaning. 59 if runtime.GOOS == "windows" { 60 t.Logf("Running on %v", runtime.GOOS) 61 62 t.Run("NormalizeDirSeparator", func(t *testing.T) { 63 t.Parallel() 64 65 testMassagePath(t, FilePathPrefix+`C:\Users\steve\`, FilePathPrefix+"/C:/Users/steve") 66 }) 67 68 newHomeDir := "/" + filepath.ToSlash(homeDir) 69 t.Logf("Changed homeDir to expect from %q to %q", homeDir, newHomeDir) 70 homeDir = newHomeDir 71 } 72 73 testMassagePath(t, FilePathPrefix+"~", FilePathPrefix+homeDir) 74 testMassagePath(t, FilePathPrefix+"~/alpha/beta", FilePathPrefix+homeDir+"/alpha/beta") 75 }) 76 77 t.Run("MakeAbsolute", func(t *testing.T) { 78 t.Parallel() 79 80 // Run the expected result through filepath.Abs, since on Windows we expect "C:\1\2". 81 expected := "/1/2" 82 abs, err := filepath.Abs(expected) 83 assert.NoError(t, err) 84 85 expected = filepath.ToSlash(abs) 86 if expected[0] != '/' { 87 expected = "/" + expected // A leading slash is added on Windows. 88 } 89 90 testMassagePath(t, FilePathPrefix+"/1/2/3/../4/..", FilePathPrefix+expected) 91 }) 92 } 93 94 func TestGetLogsForTargetWithNoSnapshot(t *testing.T) { 95 t.Parallel() 96 97 target := &deploy.Target{ 98 Name: "test", 99 Config: config.Map{}, 100 Decrypter: config.NopDecrypter, 101 Snapshot: nil, 102 } 103 query := operations.LogQuery{} 104 res, err := GetLogsForTarget(target, query) 105 assert.NoError(t, err) 106 assert.Nil(t, res) 107 } 108 109 func makeUntypedDeployment(name tokens.QName, phrase, state string) (*apitype.UntypedDeployment, error) { 110 sm, err := passphrase.NewPassphaseSecretsManager(phrase, state) 111 if err != nil { 112 return nil, err 113 } 114 115 resources := []*resource.State{ 116 { 117 URN: resource.NewURN("a", "proj", "d:e:f", "a:b:c", name), 118 Type: "a:b:c", 119 Inputs: resource.PropertyMap{ 120 resource.PropertyKey("secret"): resource.MakeSecret(resource.NewStringProperty("s3cr3t")), 121 }, 122 }, 123 } 124 125 snap := deploy.NewSnapshot(deploy.Manifest{}, sm, resources, nil) 126 127 sdep, err := stack.SerializeDeployment(snap, snap.SecretsManager, false /* showSecrsts */) 128 if err != nil { 129 return nil, err 130 } 131 132 data, err := json.Marshal(sdep) 133 if err != nil { 134 return nil, err 135 } 136 137 return &apitype.UntypedDeployment{ 138 Version: 3, 139 Deployment: json.RawMessage(data), 140 }, nil 141 } 142 143 //nolint:paralleltest // mutates environment variables 144 func TestListStacksWithMultiplePassphrases(t *testing.T) { 145 // Login to a temp dir filestate backend 146 tmpDir, err := ioutil.TempDir("", "filestatebackend") 147 assert.NoError(t, err) 148 b, err := New(cmdutil.Diag(), "file://"+filepath.ToSlash(tmpDir)) 149 assert.NoError(t, err) 150 ctx := context.Background() 151 152 // Create stack "a" and import a checkpoint with a secret 153 aStackRef, err := b.ParseStackReference("a") 154 assert.NoError(t, err) 155 aStack, err := b.CreateStack(ctx, aStackRef, nil) 156 assert.NoError(t, err) 157 assert.NotNil(t, aStack) 158 defer func() { 159 t.Setenv("PULUMI_CONFIG_PASSPHRASE", "abc123") 160 _, err := b.RemoveStack(ctx, aStack, true) 161 assert.NoError(t, err) 162 }() 163 deployment, err := makeUntypedDeployment("a", "abc123", 164 "v1:4iF78gb0nF0=:v1:Co6IbTWYs/UdrjgY:FSrAWOFZnj9ealCUDdJL7LrUKXX9BA==") 165 assert.NoError(t, err) 166 t.Setenv("PULUMI_CONFIG_PASSPHRASE", "abc123") 167 err = b.ImportDeployment(ctx, aStack, deployment) 168 assert.NoError(t, err) 169 170 // Create stack "b" and import a checkpoint with a secret 171 bStackRef, err := b.ParseStackReference("b") 172 assert.NoError(t, err) 173 bStack, err := b.CreateStack(ctx, bStackRef, nil) 174 assert.NoError(t, err) 175 assert.NotNil(t, bStack) 176 defer func() { 177 t.Setenv("PULUMI_CONFIG_PASSPHRASE", "123abc") 178 _, err := b.RemoveStack(ctx, bStack, true) 179 assert.NoError(t, err) 180 }() 181 deployment, err = makeUntypedDeployment("b", "123abc", 182 "v1:C7H2a7/Ietk=:v1:yfAd1zOi6iY9DRIB:dumdsr+H89VpHIQWdB01XEFqYaYjAg==") 183 assert.NoError(t, err) 184 t.Setenv("PULUMI_CONFIG_PASSPHRASE", "123abc") 185 err = b.ImportDeployment(ctx, bStack, deployment) 186 assert.NoError(t, err) 187 188 // Remove the config passphrase so that we can no longer deserialize the checkpoints 189 err = os.Unsetenv("PULUMI_CONFIG_PASSPHRASE") 190 assert.NoError(t, err) 191 192 // Ensure that we can list the stacks we created even without a passphrase 193 stacks, outContToken, err := b.ListStacks(ctx, backend.ListStacksFilter{}, nil /* inContToken */) 194 assert.NoError(t, err) 195 assert.Nil(t, outContToken) 196 assert.Len(t, stacks, 2) 197 for _, stack := range stacks { 198 assert.NotNil(t, stack.ResourceCount()) 199 assert.Equal(t, 1, *stack.ResourceCount()) 200 } 201 202 } 203 204 func TestDrillError(t *testing.T) { 205 t.Parallel() 206 207 // Login to a temp dir filestate backend 208 tmpDir, err := ioutil.TempDir("", "filestatebackend") 209 assert.NoError(t, err) 210 b, err := New(cmdutil.Diag(), "file://"+filepath.ToSlash(tmpDir)) 211 assert.NoError(t, err) 212 ctx := context.Background() 213 214 // Get a non-existent stack and expect a nil error because it won't be found. 215 stackRef, err := b.ParseStackReference("dev") 216 if err != nil { 217 t.Fatalf("unexpected error %v when parsing stack reference", err) 218 } 219 _, err = b.GetStack(ctx, stackRef) 220 assert.Nil(t, err) 221 } 222 223 func TestCancel(t *testing.T) { 224 t.Parallel() 225 226 // Login to a temp dir filestate backend 227 tmpDir, err := ioutil.TempDir("", "filestatebackend") 228 assert.NoError(t, err) 229 b, err := New(cmdutil.Diag(), "file://"+filepath.ToSlash(tmpDir)) 230 assert.NoError(t, err) 231 ctx := context.Background() 232 233 // Check that trying to cancel a stack that isn't created yet doesn't error 234 aStackRef, err := b.ParseStackReference("a") 235 assert.NoError(t, err) 236 err = b.CancelCurrentUpdate(ctx, aStackRef) 237 assert.NoError(t, err) 238 239 // Check that trying to cancel a stack that isn't locked doesn't error 240 aStack, err := b.CreateStack(ctx, aStackRef, nil) 241 assert.NoError(t, err) 242 assert.NotNil(t, aStack) 243 err = b.CancelCurrentUpdate(ctx, aStackRef) 244 assert.NoError(t, err) 245 246 // Locking and lock checks are only part of the internal interface 247 lb, ok := b.(*localBackend) 248 assert.True(t, ok) 249 assert.NotNil(t, lb) 250 251 // Lock the stack and check CancelCurrentUpdate deletes the lock file 252 err = lb.Lock(ctx, aStackRef) 253 assert.NoError(t, err) 254 // check the lock file exists 255 lockExists, err := lb.bucket.Exists(ctx, lb.lockPath(aStackRef.Name())) 256 assert.NoError(t, err) 257 assert.True(t, lockExists) 258 // Call CancelCurrentUpdate 259 err = lb.CancelCurrentUpdate(ctx, aStackRef) 260 assert.NoError(t, err) 261 // Now check the lock file no longer exists 262 lockExists, err = lb.bucket.Exists(ctx, lb.lockPath(aStackRef.Name())) 263 assert.NoError(t, err) 264 assert.False(t, lockExists) 265 266 // Make another filestate backend which will have a different lockId 267 ob, err := New(cmdutil.Diag(), "file://"+filepath.ToSlash(tmpDir)) 268 assert.NoError(t, err) 269 otherBackend, ok := ob.(*localBackend) 270 assert.True(t, ok) 271 assert.NotNil(t, lb) 272 273 // Lock the stack with this new backend, then check that checkForLocks on the first backend now errors 274 err = otherBackend.Lock(ctx, aStackRef) 275 assert.NoError(t, err) 276 err = lb.checkForLock(ctx, aStackRef) 277 assert.Error(t, err) 278 // Now call CancelCurrentUpdate and check that checkForLocks no longer errors 279 err = lb.CancelCurrentUpdate(ctx, aStackRef) 280 assert.NoError(t, err) 281 err = lb.checkForLock(ctx, aStackRef) 282 assert.NoError(t, err) 283 } 284 285 func TestRemoveMakesBackups(t *testing.T) { 286 t.Parallel() 287 288 // Login to a temp dir filestate backend 289 tmpDir, err := ioutil.TempDir("", "filestatebackend") 290 assert.NoError(t, err) 291 b, err := New(cmdutil.Diag(), "file://"+filepath.ToSlash(tmpDir)) 292 assert.NoError(t, err) 293 ctx := context.Background() 294 295 // Grab the bucket interface to test with 296 lb, ok := b.(*localBackend) 297 assert.True(t, ok) 298 assert.NotNil(t, lb) 299 300 // Check that creating a new stack doesn't make a backup file 301 aStackRef, err := b.ParseStackReference("a") 302 assert.NoError(t, err) 303 aStack, err := b.CreateStack(ctx, aStackRef, nil) 304 assert.NoError(t, err) 305 assert.NotNil(t, aStack) 306 307 // Check the stack file now exists, but the backup file doesn't 308 stackFileExists, err := lb.bucket.Exists(ctx, lb.stackPath(aStackRef.Name())) 309 assert.NoError(t, err) 310 assert.True(t, stackFileExists) 311 backupFileExists, err := lb.bucket.Exists(ctx, lb.stackPath(aStackRef.Name())+".bak") 312 assert.NoError(t, err) 313 assert.False(t, backupFileExists) 314 315 // Now remove the stack 316 removed, err := b.RemoveStack(ctx, aStack, false) 317 assert.NoError(t, err) 318 assert.False(t, removed) 319 320 // Check the stack file is now gone, but the backup file exists 321 stackFileExists, err = lb.bucket.Exists(ctx, lb.stackPath(aStackRef.Name())) 322 assert.NoError(t, err) 323 assert.False(t, stackFileExists) 324 backupFileExists, err = lb.bucket.Exists(ctx, lb.stackPath(aStackRef.Name())+".bak") 325 assert.NoError(t, err) 326 assert.True(t, backupFileExists) 327 } 328 329 func TestRenameWorks(t *testing.T) { 330 t.Parallel() 331 332 // Login to a temp dir filestate backend 333 tmpDir, err := ioutil.TempDir("", "filestatebackend") 334 assert.NoError(t, err) 335 b, err := New(cmdutil.Diag(), "file://"+filepath.ToSlash(tmpDir)) 336 assert.NoError(t, err) 337 ctx := context.Background() 338 339 // Grab the bucket interface to test with 340 lb, ok := b.(*localBackend) 341 assert.True(t, ok) 342 assert.NotNil(t, lb) 343 344 // Create a new stack 345 aStackRef, err := b.ParseStackReference("a") 346 assert.NoError(t, err) 347 aStack, err := b.CreateStack(ctx, aStackRef, nil) 348 assert.NoError(t, err) 349 assert.NotNil(t, aStack) 350 351 // Check the stack file now exists 352 stackFileExists, err := lb.bucket.Exists(ctx, lb.stackPath(aStackRef.Name())) 353 assert.NoError(t, err) 354 assert.True(t, stackFileExists) 355 356 // Fake up some history 357 err = lb.addToHistory("a", backend.UpdateInfo{Kind: apitype.DestroyUpdate}) 358 assert.NoError(t, err) 359 // And pollute the history folder 360 err = lb.bucket.WriteAll(ctx, path.Join(lb.historyDirectory("a"), "randomfile.txt"), []byte{0, 13}, nil) 361 assert.NoError(t, err) 362 363 // Rename the stack 364 bStackRef, err := b.RenameStack(ctx, aStack, "b") 365 assert.NoError(t, err) 366 assert.Equal(t, "b", bStackRef.String()) 367 368 // Check the new stack file now exists and the old one is gone 369 stackFileExists, err = lb.bucket.Exists(ctx, lb.stackPath(bStackRef.Name())) 370 assert.NoError(t, err) 371 assert.True(t, stackFileExists) 372 stackFileExists, err = lb.bucket.Exists(ctx, lb.stackPath(aStackRef.Name())) 373 assert.NoError(t, err) 374 assert.False(t, stackFileExists) 375 376 // Rename again 377 bStack, err := b.GetStack(ctx, bStackRef) 378 assert.NoError(t, err) 379 cStackRef, err := b.RenameStack(ctx, bStack, "c") 380 assert.NoError(t, err) 381 assert.Equal(t, "c", cStackRef.String()) 382 383 // Check the new stack file now exists and the old one is gone 384 stackFileExists, err = lb.bucket.Exists(ctx, lb.stackPath(cStackRef.Name())) 385 assert.NoError(t, err) 386 assert.True(t, stackFileExists) 387 stackFileExists, err = lb.bucket.Exists(ctx, lb.stackPath(bStackRef.Name())) 388 assert.NoError(t, err) 389 assert.False(t, stackFileExists) 390 391 // Check we can still get the history 392 history, err := b.GetHistory(ctx, cStackRef, 10, 0) 393 assert.NoError(t, err) 394 assert.Len(t, history, 1) 395 assert.Equal(t, apitype.DestroyUpdate, history[0].Kind) 396 } 397 398 func TestLoginToNonExistingFolderFails(t *testing.T) { 399 t.Parallel() 400 401 fakeDir := "file://" + filepath.ToSlash(os.TempDir()) + "/non-existing" 402 b, err := New(cmdutil.Diag(), fakeDir) 403 assert.Error(t, err) 404 assert.Nil(t, b) 405 } 406 407 // TestParseEmptyStackFails demonstrates that ParseStackReference returns 408 // an error when the stack name is the empty string.TestParseEmptyStackFails 409 func TestParseEmptyStackFails(t *testing.T) { 410 t.Parallel() 411 // ParseStackReference does use the method receiver 412 // (it is a total function disguised as a method.) 413 var b *localBackend 414 var stackName = "" 415 var _, err = b.ParseStackReference(stackName) 416 assert.Error(t, err) 417 } 418 419 // Regression test for https://github.com/pulumi/pulumi/issues/10439 420 func TestHtmlEscaping(t *testing.T) { 421 t.Parallel() 422 423 sm := b64.NewBase64SecretsManager() 424 resources := []*resource.State{ 425 { 426 URN: resource.NewURN("a", "proj", "d:e:f", "a:b:c", "name"), 427 Type: "a:b:c", 428 Inputs: resource.PropertyMap{ 429 resource.PropertyKey("html"): resource.NewStringProperty("<html@tags>"), 430 }, 431 }, 432 } 433 434 snap := deploy.NewSnapshot(deploy.Manifest{}, sm, resources, nil) 435 436 sdep, err := stack.SerializeDeployment(snap, snap.SecretsManager, false /* showSecrsts */) 437 assert.NoError(t, err) 438 439 data, err := json.Marshal(sdep) 440 assert.NoError(t, err) 441 442 udep := &apitype.UntypedDeployment{ 443 Version: 3, 444 Deployment: json.RawMessage(data), 445 } 446 447 // Login to a temp dir filestate backend 448 tmpDir, err := ioutil.TempDir("", "filestatebackend") 449 assert.NoError(t, err) 450 b, err := New(cmdutil.Diag(), "file://"+filepath.ToSlash(tmpDir)) 451 assert.NoError(t, err) 452 ctx := context.Background() 453 454 // Create stack "a" and import a checkpoint with a secret 455 aStackRef, err := b.ParseStackReference("a") 456 assert.NoError(t, err) 457 aStack, err := b.CreateStack(ctx, aStackRef, nil) 458 assert.NoError(t, err) 459 assert.NotNil(t, aStack) 460 err = b.ImportDeployment(ctx, aStack, udep) 461 assert.NoError(t, err) 462 463 // Ensure the file has the string contents "<html@tags>"", not "\u003chtml\u0026tags\u003e" 464 465 // Grab the bucket interface to read the file with 466 lb, ok := b.(*localBackend) 467 assert.True(t, ok) 468 assert.NotNil(t, lb) 469 470 chkpath := lb.stackPath("a") 471 bytes, err := lb.bucket.ReadAll(context.Background(), chkpath) 472 assert.NoError(t, err) 473 state := string(bytes) 474 assert.Contains(t, state, "<html@tags>") 475 }