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  }