pulumi/pkg/backend/diy/backend_legacy_test.go

374 lines
12 KiB
Go
Raw Permalink Normal View History

package diy
import (
"context"
"encoding/json"
"os"
"path"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/pulumi/pulumi/pkg/v3/backend"
"github.com/pulumi/pulumi/pkg/v3/resource/deploy"
"github.com/pulumi/pulumi/pkg/v3/resource/stack"
"github.com/pulumi/pulumi/pkg/v3/secrets/b64"
"github.com/pulumi/pulumi/sdk/v3/go/common/apitype"
"github.com/pulumi/pulumi/sdk/v3/go/common/encoding"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
"github.com/pulumi/pulumi/sdk/v3/go/common/testing/diagtest"
)
// This file contains copies of old backend tests
// that were upgraded to run with project support.
// This duplicates those tests to run with legacy, non-project state,
// validating that the legacy behavior is preserved.
//nolint:paralleltest // mutates environment variables
func TestListStacksWithMultiplePassphrases_legacy(t *testing.T) {
// Login to a temp dir diy backend
tmpDir := markLegacyStore(t, t.TempDir())
ctx := context.Background()
b, err := New(ctx, diagtest.LogSink(t), "file://"+filepath.ToSlash(tmpDir), nil)
assert.NoError(t, err)
// Create stack "a" and import a checkpoint with a secret
aStackRef, err := b.ParseStackReference("a")
assert.NoError(t, err)
aStack, err := b.CreateStack(ctx, aStackRef, "", nil)
assert.NoError(t, err)
assert.NotNil(t, aStack)
defer func() {
t.Setenv("PULUMI_CONFIG_PASSPHRASE", "abc123")
_, err := b.RemoveStack(ctx, aStack, true)
assert.NoError(t, err)
}()
deployment, err := makeUntypedDeployment("a", "abc123",
"v1:4iF78gb0nF0=:v1:Co6IbTWYs/UdrjgY:FSrAWOFZnj9ealCUDdJL7LrUKXX9BA==")
assert.NoError(t, err)
t.Setenv("PULUMI_CONFIG_PASSPHRASE", "abc123")
err = b.ImportDeployment(ctx, aStack, deployment)
assert.NoError(t, err)
// Create stack "b" and import a checkpoint with a secret
bStackRef, err := b.ParseStackReference("b")
assert.NoError(t, err)
bStack, err := b.CreateStack(ctx, bStackRef, "", nil)
assert.NoError(t, err)
assert.NotNil(t, bStack)
defer func() {
t.Setenv("PULUMI_CONFIG_PASSPHRASE", "123abc")
_, err := b.RemoveStack(ctx, bStack, true)
assert.NoError(t, err)
}()
deployment, err = makeUntypedDeployment("b", "123abc",
"v1:C7H2a7/Ietk=:v1:yfAd1zOi6iY9DRIB:dumdsr+H89VpHIQWdB01XEFqYaYjAg==")
assert.NoError(t, err)
t.Setenv("PULUMI_CONFIG_PASSPHRASE", "123abc")
err = b.ImportDeployment(ctx, bStack, deployment)
assert.NoError(t, err)
// Remove the config passphrase so that we can no longer deserialize the checkpoints
err = os.Unsetenv("PULUMI_CONFIG_PASSPHRASE")
assert.NoError(t, err)
// Ensure that we can list the stacks we created even without a passphrase
stacks, outContToken, err := b.ListStacks(ctx, backend.ListStacksFilter{}, nil /* inContToken */)
assert.NoError(t, err)
assert.Nil(t, outContToken)
assert.Len(t, stacks, 2)
for _, stack := range stacks {
assert.NotNil(t, stack.ResourceCount())
assert.Equal(t, 1, *stack.ResourceCount())
}
}
func TestDrillError_legacy(t *testing.T) {
t.Parallel()
// Login to a temp dir diy backend
tmpDir := markLegacyStore(t, t.TempDir())
ctx := context.Background()
b, err := New(ctx, diagtest.LogSink(t), "file://"+filepath.ToSlash(tmpDir), nil)
assert.NoError(t, err)
// Get a non-existent stack and expect a nil error because it won't be found.
stackRef, err := b.ParseStackReference("dev")
if err != nil {
t.Fatalf("unexpected error %v when parsing stack reference", err)
}
_, err = b.GetStack(ctx, stackRef)
assert.NoError(t, err)
}
func TestCancel_legacy(t *testing.T) {
t.Parallel()
// Login to a temp dir diy backend
tmpDir := markLegacyStore(t, t.TempDir())
ctx := context.Background()
b, err := New(ctx, diagtest.LogSink(t), "file://"+filepath.ToSlash(tmpDir), nil)
assert.NoError(t, err)
// Check that trying to cancel a stack that isn't created yet doesn't error
aStackRef, err := b.ParseStackReference("a")
assert.NoError(t, err)
err = b.CancelCurrentUpdate(ctx, aStackRef)
assert.NoError(t, err)
// Check that trying to cancel a stack that isn't locked doesn't error
aStack, err := b.CreateStack(ctx, aStackRef, "", nil)
assert.NoError(t, err)
assert.NotNil(t, aStack)
err = b.CancelCurrentUpdate(ctx, aStackRef)
assert.NoError(t, err)
// Locking and lock checks are only part of the internal interface
lb, ok := b.(*diyBackend)
assert.True(t, ok)
assert.NotNil(t, lb)
// Lock the stack and check CancelCurrentUpdate deletes the lock file
err = lb.Lock(ctx, aStackRef)
assert.NoError(t, err)
// check the lock file exists
lockExists, err := lb.bucket.Exists(ctx, lb.lockPath(aStackRef))
assert.NoError(t, err)
assert.True(t, lockExists)
// Call CancelCurrentUpdate
err = lb.CancelCurrentUpdate(ctx, aStackRef)
assert.NoError(t, err)
// Now check the lock file no longer exists
lockExists, err = lb.bucket.Exists(ctx, lb.lockPath(aStackRef))
assert.NoError(t, err)
assert.False(t, lockExists)
// Make another diy backend which will have a different lockId
ob, err := New(ctx, diagtest.LogSink(t), "file://"+filepath.ToSlash(tmpDir), nil)
assert.NoError(t, err)
otherBackend, ok := ob.(*diyBackend)
assert.True(t, ok)
assert.NotNil(t, lb)
// Lock the stack with this new backend, then check that checkForLocks on the first backend now errors
err = otherBackend.Lock(ctx, aStackRef)
assert.NoError(t, err)
err = lb.checkForLock(ctx, aStackRef)
assert.Error(t, err)
// Now call CancelCurrentUpdate and check that checkForLocks no longer errors
err = lb.CancelCurrentUpdate(ctx, aStackRef)
assert.NoError(t, err)
err = lb.checkForLock(ctx, aStackRef)
assert.NoError(t, err)
}
func TestRemoveMakesBackups_legacy(t *testing.T) {
t.Parallel()
// Login to a temp dir diy backend
tmpDir := markLegacyStore(t, t.TempDir())
ctx := context.Background()
b, err := New(ctx, diagtest.LogSink(t), "file://"+filepath.ToSlash(tmpDir), nil)
assert.NoError(t, err)
// Grab the bucket interface to test with
lb, ok := b.(*diyBackend)
assert.True(t, ok)
assert.NotNil(t, lb)
// Check that creating a new stack doesn't make a backup file
aStackRef, err := lb.parseStackReference("a")
assert.NoError(t, err)
aStack, err := b.CreateStack(ctx, aStackRef, "", nil)
assert.NoError(t, err)
assert.NotNil(t, aStack)
// Check the stack file now exists, but the backup file doesn't
stackFileExists, err := lb.bucket.Exists(ctx, lb.stackPath(ctx, aStackRef))
assert.NoError(t, err)
assert.True(t, stackFileExists)
backupFileExists, err := lb.bucket.Exists(ctx, lb.stackPath(ctx, aStackRef)+".bak")
assert.NoError(t, err)
assert.False(t, backupFileExists)
// Now remove the stack
removed, err := b.RemoveStack(ctx, aStack, false)
assert.NoError(t, err)
assert.False(t, removed)
// Check the stack file is now gone, but the backup file exists
stackFileExists, err = lb.bucket.Exists(ctx, lb.stackPath(ctx, aStackRef))
assert.NoError(t, err)
assert.False(t, stackFileExists)
backupFileExists, err = lb.bucket.Exists(ctx, lb.stackPath(ctx, aStackRef)+".bak")
assert.NoError(t, err)
assert.True(t, backupFileExists)
}
func TestRenameWorks_legacy(t *testing.T) {
t.Parallel()
// Login to a temp dir diy backend
tmpDir := markLegacyStore(t, t.TempDir())
ctx := context.Background()
b, err := New(ctx, diagtest.LogSink(t), "file://"+filepath.ToSlash(tmpDir), nil)
assert.NoError(t, err)
// Grab the bucket interface to test with
lb, ok := b.(*diyBackend)
assert.True(t, ok)
assert.NotNil(t, lb)
// Create a new stack
aStackRef, err := lb.parseStackReference("a")
assert.NoError(t, err)
aStack, err := b.CreateStack(ctx, aStackRef, "", nil)
assert.NoError(t, err)
assert.NotNil(t, aStack)
// Check the stack file now exists
stackFileExists, err := lb.bucket.Exists(ctx, lb.stackPath(ctx, aStackRef))
assert.NoError(t, err)
assert.True(t, stackFileExists)
// Fake up some history
err = lb.addToHistory(ctx, aStackRef, backend.UpdateInfo{Kind: apitype.DestroyUpdate})
assert.NoError(t, err)
// And pollute the history folder
err = lb.bucket.WriteAll(ctx, path.Join(aStackRef.HistoryDir(), "randomfile.txt"), []byte{0, 13}, nil)
assert.NoError(t, err)
// Rename the stack
bStackRefI, err := b.RenameStack(ctx, aStack, "b")
assert.NoError(t, err)
assert.Equal(t, "b", bStackRefI.String())
bStackRef := bStackRefI.(*diyBackendReference)
// Check the new stack file now exists and the old one is gone
stackFileExists, err = lb.bucket.Exists(ctx, lb.stackPath(ctx, bStackRef))
assert.NoError(t, err)
assert.True(t, stackFileExists)
stackFileExists, err = lb.bucket.Exists(ctx, lb.stackPath(ctx, aStackRef))
assert.NoError(t, err)
assert.False(t, stackFileExists)
// Rename again
bStack, err := b.GetStack(ctx, bStackRef)
assert.NoError(t, err)
cStackRefI, err := b.RenameStack(ctx, bStack, "c")
assert.NoError(t, err)
assert.Equal(t, "c", cStackRefI.String())
cStackRef := cStackRefI.(*diyBackendReference)
// Check the new stack file now exists and the old one is gone
stackFileExists, err = lb.bucket.Exists(ctx, lb.stackPath(ctx, cStackRef))
assert.NoError(t, err)
assert.True(t, stackFileExists)
stackFileExists, err = lb.bucket.Exists(ctx, lb.stackPath(ctx, bStackRef))
assert.NoError(t, err)
assert.False(t, stackFileExists)
// Check we can still get the history
history, err := b.GetHistory(ctx, cStackRef, 10, 0)
assert.NoError(t, err)
assert.Len(t, history, 1)
assert.Equal(t, apitype.DestroyUpdate, history[0].Kind)
}
// Regression test for https://github.com/pulumi/pulumi/issues/10439
func TestHtmlEscaping_legacy(t *testing.T) {
t.Parallel()
sm := b64.NewBase64SecretsManager()
resources := []*resource.State{
{
URN: resource.NewURN("a", "proj", "d:e:f", "a:b:c", "name"),
Type: "a:b:c",
Inputs: resource.PropertyMap{
resource.PropertyKey("html"): resource.NewStringProperty("<html@tags>"),
},
},
}
snap := deploy.NewSnapshot(deploy.Manifest{}, sm, resources, nil)
Lift context parameter to SerializeDeployment/Resource/Operations/Properties (#15929) <!--- Thanks so much for your contribution! If this is your first time contributing, please ensure that you have read the [CONTRIBUTING](https://github.com/pulumi/pulumi/blob/master/CONTRIBUTING.md) documentation. --> # Description <!--- Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. --> SerializePropertyValue needed a `context.Context` object to pass to the `config.Encrypter`. It was using `context.TODO()`, this change instead accepts a context on the parameters and lifts that up to SerializeProperties, SerializeResource, SerializeOperation, and SerializeDeployment. There were a few call sites for those methods that already had a context on hand, and they now pass that context. The other calls sites now use `context.TODO()`, we should continue to iterate in this area to ensure everywhere that needs a context has one passed in. ## Checklist - [x] I have run `make tidy` to update any new dependencies - [x] I have run `make lint` to verify my code passes the lint check - [ ] I have formatted my code using `gofumpt` <!--- Please provide details if the checkbox below is to be left unchecked. --> - [ ] I have added tests that prove my fix is effective or that my feature works <!--- User-facing changes require a CHANGELOG entry. --> - [ ] I have run `make changelog` and committed the `changelog/pending/<file>` documenting my change <!-- If the change(s) in this PR is a modification of an existing call to the Pulumi Cloud, then the service should honor older versions of the CLI where this change would not exist. You must then bump the API version in /pkg/backend/httpstate/client/api.go, as well as add it to the service. --> - [ ] Yes, there are changes in this PR that warrants bumping the Pulumi Cloud API version <!-- @Pulumi employees: If yes, you must submit corresponding changes in the service repo. -->
2024-04-15 07:45:46 +00:00
ctx := context.Background()
Lift context parameter to SerializeDeployment/Resource/Operations/Properties (#15929) <!--- Thanks so much for your contribution! If this is your first time contributing, please ensure that you have read the [CONTRIBUTING](https://github.com/pulumi/pulumi/blob/master/CONTRIBUTING.md) documentation. --> # Description <!--- Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. --> SerializePropertyValue needed a `context.Context` object to pass to the `config.Encrypter`. It was using `context.TODO()`, this change instead accepts a context on the parameters and lifts that up to SerializeProperties, SerializeResource, SerializeOperation, and SerializeDeployment. There were a few call sites for those methods that already had a context on hand, and they now pass that context. The other calls sites now use `context.TODO()`, we should continue to iterate in this area to ensure everywhere that needs a context has one passed in. ## Checklist - [x] I have run `make tidy` to update any new dependencies - [x] I have run `make lint` to verify my code passes the lint check - [ ] I have formatted my code using `gofumpt` <!--- Please provide details if the checkbox below is to be left unchecked. --> - [ ] I have added tests that prove my fix is effective or that my feature works <!--- User-facing changes require a CHANGELOG entry. --> - [ ] I have run `make changelog` and committed the `changelog/pending/<file>` documenting my change <!-- If the change(s) in this PR is a modification of an existing call to the Pulumi Cloud, then the service should honor older versions of the CLI where this change would not exist. You must then bump the API version in /pkg/backend/httpstate/client/api.go, as well as add it to the service. --> - [ ] Yes, there are changes in this PR that warrants bumping the Pulumi Cloud API version <!-- @Pulumi employees: If yes, you must submit corresponding changes in the service repo. -->
2024-04-15 07:45:46 +00:00
sdep, err := stack.SerializeDeployment(ctx, snap, false /* showSecrets */)
assert.NoError(t, err)
data, err := encoding.JSON.Marshal(sdep)
assert.NoError(t, err)
// Ensure data has the string contents "<html@tags>"", not "\u003chtml\u0026tags\u003e"
// ImportDeployment below should not modify the data
assert.Contains(t, string(data), "<html@tags>")
udep := &apitype.UntypedDeployment{
Version: 3,
Deployment: json.RawMessage(data),
}
// Login to a temp dir diy backend
tmpDir := markLegacyStore(t, t.TempDir())
b, err := New(ctx, diagtest.LogSink(t), "file://"+filepath.ToSlash(tmpDir), nil)
assert.NoError(t, err)
// Create stack "a" and import a checkpoint with a secret
aStackRef, err := b.ParseStackReference("a")
assert.NoError(t, err)
aStack, err := b.CreateStack(ctx, aStackRef, "", nil)
assert.NoError(t, err)
assert.NotNil(t, aStack)
err = b.ImportDeployment(ctx, aStack, udep)
assert.NoError(t, err)
// Ensure the file has the string contents "<html@tags>"", not "\u003chtml\u0026tags\u003e"
// Grab the bucket interface to read the file with
lb, ok := b.(*diyBackend)
assert.True(t, ok)
assert.NotNil(t, lb)
chkpath := lb.stackPath(ctx, aStackRef.(*diyBackendReference))
bytes, err := lb.bucket.ReadAll(context.Background(), chkpath)
assert.NoError(t, err)
state := string(bytes)
assert.Contains(t, state, "<html@tags>")
}
func TestDIYBackendRejectsStackInitOptions_legacy(t *testing.T) {
t.Parallel()
// Here, we provide options that illegally specify a team on a
// backend that does not support teams. We expect this to create
// an error later when we call CreateStack.
illegalOptions := &backend.CreateStackOptions{Teams: []string{"red-team"}}
// • Create a mock diy backend
tmpDir := markLegacyStore(t, t.TempDir())
Enable perfsprint linter (#14813) <!--- Thanks so much for your contribution! If this is your first time contributing, please ensure that you have read the [CONTRIBUTING](https://github.com/pulumi/pulumi/blob/master/CONTRIBUTING.md) documentation. --> # Description <!--- Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. --> Prompted by a comment in another review: https://github.com/pulumi/pulumi/pull/14654#discussion_r1419995945 This lints that we don't use `fmt.Errorf` when `errors.New` will suffice, it also covers a load of other cases where `Sprintf` is sub-optimal. Most of these edits were made by running `perfsprint --fix`. ## Checklist - [x] I have run `make tidy` to update any new dependencies - [x] I have run `make lint` to verify my code passes the lint check - [x] I have formatted my code using `gofumpt` <!--- Please provide details if the checkbox below is to be left unchecked. --> - [ ] I have added tests that prove my fix is effective or that my feature works <!--- User-facing changes require a CHANGELOG entry. --> - [ ] I have run `make changelog` and committed the `changelog/pending/<file>` documenting my change <!-- If the change(s) in this PR is a modification of an existing call to the Pulumi Cloud, then the service should honor older versions of the CLI where this change would not exist. You must then bump the API version in /pkg/backend/httpstate/client/api.go, as well as add it to the service. --> - [ ] Yes, there are changes in this PR that warrants bumping the Pulumi Cloud API version <!-- @Pulumi employees: If yes, you must submit corresponding changes in the service repo. -->
2023-12-12 12:19:42 +00:00
dirURI := "file://" + filepath.ToSlash(tmpDir)
diy, err := New(context.Background(), diagtest.LogSink(t), dirURI, nil)
assert.NoError(t, err)
ctx := context.Background()
// • Simulate `pulumi stack init`, passing non-nil init options
fakeStackRef, err := diy.ParseStackReference("foobar")
assert.NoError(t, err)
_, err = diy.CreateStack(ctx, fakeStackRef, "", illegalOptions)
assert.ErrorIs(t, err, backend.ErrTeamsNotSupported)
}
// markLegacyStore marks the given directory as a legacy store.
// This is done by dropping a single file into the bookkeeping directory.
// ensurePulumiMeta will treat this as a legacy store if the directory exists.
//
// Returns the directory that was marked.
func markLegacyStore(t *testing.T, dir string) string {
metaPath := filepath.Join(dir, pulumiMetaPath)
require.NoError(t, os.MkdirAll(filepath.Dir(metaPath), 0o755))
require.NoError(t, os.WriteFile(metaPath, []byte(`version: 0`), 0o600))
return dir
}