2024-01-30 15:53:10 +00:00
|
|
|
package diy
|
2019-07-25 14:58:19 +00:00
|
|
|
|
|
|
|
import (
|
2023-03-23 18:49:54 +00:00
|
|
|
"bytes"
|
2021-07-28 01:37:25 +00:00
|
|
|
"context"
|
|
|
|
"encoding/json"
|
2023-04-04 22:20:50 +00:00
|
|
|
"errors"
|
2023-03-17 19:39:45 +00:00
|
|
|
"fmt"
|
2023-03-23 18:49:54 +00:00
|
|
|
"io"
|
2021-07-28 01:37:25 +00:00
|
|
|
"os"
|
2022-05-25 08:10:36 +00:00
|
|
|
"path"
|
2019-07-25 14:58:19 +00:00
|
|
|
"path/filepath"
|
2023-03-24 21:24:06 +00:00
|
|
|
"regexp"
|
2019-07-25 14:58:19 +00:00
|
|
|
"runtime"
|
2023-02-10 12:24:28 +00:00
|
|
|
"sync"
|
2019-07-25 14:58:19 +00:00
|
|
|
"testing"
|
This commit adds the `Created` and `Modified` timestamps to pulumi state that are optional.
`Created`: Created tracks when the remote resource was first added to state by pulumi. Checkpoints prior to early 2023 do not include this. (Create, Import)
`Modified`: Modified tracks when the resource state was last altered. Checkpoints prior to early 2023 do not include this. (Create, Import, Read, Refresh, Update)
When serialized they will follow RFC3339 with nanoseconds captured by a test case.
https://pkg.go.dev/time#RFC3339
Note: Older versions of pulumi may strip these fields when modifying the state.
For future expansion, when we inevitably need to track other timestamps, we'll add a new "operationTimestamps" field (or something similarly named that clarified these are timestamps of the actual Pulumi operations).
operationTimestamps: {
created: ...,
updated: ...,
imported: ...,
}
Fixes https://github.com/pulumi/pulumi/issues/12022
2023-02-06 20:39:11 +00:00
|
|
|
"time"
|
2019-07-25 14:58:19 +00:00
|
|
|
|
|
|
|
"github.com/stretchr/testify/assert"
|
filestate: Track a state metadata file (.pulumi/Pulumi.yaml)
We want the filestate backend to support project-scoped stacks,
but we can't make the change as-is because it would break old states
with new CLIs.
To differentiate between old and new states,
we've decided to introduce the concept of state metadata.
This is a file under the path .pulumi/Pulumi.yaml
that tracks metadata necessary for the filestate backend to operate.
Initially, this contains just one field: `version`,
with the initial value of 0 representing non-project or "legacy mode".
This changes the filestate layout to track such a file,
creating it if it doesn't exist with the default value of 0.
In a future change, we'll introduce "version 1",
which adds support for project-scoped stacks.
If we ever need to make breaking changes to the layout,
the version in this file will help the CLI decide
whether it's allowed to handle that state bucket
without corrupting it.
Note that this differs slightly
from the initial implementation of this functionality in #12134.
Particularly, this idempotently ensures that a Pulumi.yaml exists,
allowing `version: 0` to indicate legacy mode,
versus the original implementation that treated absence of the file
in a non-empty bucket as legacy mode.
This drops the bucket.IsAccessible check from filestate.New
because accessibility is now verified
when we try to read the metadata file.
Extracted from #12437
2023-03-22 19:14:05 +00:00
|
|
|
"github.com/stretchr/testify/require"
|
2020-10-02 21:09:34 +00:00
|
|
|
user "github.com/tweekmonster/luser"
|
filestate: Track a state metadata file (.pulumi/Pulumi.yaml)
We want the filestate backend to support project-scoped stacks,
but we can't make the change as-is because it would break old states
with new CLIs.
To differentiate between old and new states,
we've decided to introduce the concept of state metadata.
This is a file under the path .pulumi/Pulumi.yaml
that tracks metadata necessary for the filestate backend to operate.
Initially, this contains just one field: `version`,
with the initial value of 0 representing non-project or "legacy mode".
This changes the filestate layout to track such a file,
creating it if it doesn't exist with the default value of 0.
In a future change, we'll introduce "version 1",
which adds support for project-scoped stacks.
If we ever need to make breaking changes to the layout,
the version in this file will help the CLI decide
whether it's allowed to handle that state bucket
without corrupting it.
Note that this differs slightly
from the initial implementation of this functionality in #12134.
Particularly, this idempotently ensures that a Pulumi.yaml exists,
allowing `version: 0` to indicate legacy mode,
versus the original implementation that treated absence of the file
in a non-empty bucket as legacy mode.
This drops the bucket.IsAccessible check from filestate.New
because accessibility is now verified
when we try to read the metadata file.
Extracted from #12437
2023-03-22 19:14:05 +00:00
|
|
|
"gocloud.dev/blob/fileblob"
|
2020-10-02 21:09:34 +00:00
|
|
|
|
2021-07-28 01:37:25 +00:00
|
|
|
"github.com/pulumi/pulumi/pkg/v3/backend"
|
2021-03-17 13:20:05 +00:00
|
|
|
"github.com/pulumi/pulumi/pkg/v3/operations"
|
|
|
|
"github.com/pulumi/pulumi/pkg/v3/resource/deploy"
|
2021-07-28 01:37:25 +00:00
|
|
|
"github.com/pulumi/pulumi/pkg/v3/resource/stack"
|
2022-08-19 12:27:34 +00:00
|
|
|
"github.com/pulumi/pulumi/pkg/v3/secrets/b64"
|
2021-07-28 01:37:25 +00:00
|
|
|
"github.com/pulumi/pulumi/pkg/v3/secrets/passphrase"
|
|
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/apitype"
|
2023-03-23 18:49:54 +00:00
|
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/diag"
|
|
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/diag/colors"
|
2023-01-11 11:24:10 +00:00
|
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/encoding"
|
2023-10-18 10:52:54 +00:00
|
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/env"
|
2021-07-28 01:37:25 +00:00
|
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/resource"
|
2021-03-17 13:20:05 +00:00
|
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/resource/config"
|
2023-03-13 19:54:04 +00:00
|
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/testing/diagtest"
|
2023-04-04 22:20:50 +00:00
|
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/testing/iotest"
|
2021-07-28 01:37:25 +00:00
|
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/tokens"
|
2024-04-04 10:11:46 +00:00
|
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/util/cmdutil"
|
2023-02-10 12:24:28 +00:00
|
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/workspace"
|
2019-07-25 14:58:19 +00:00
|
|
|
)
|
|
|
|
|
2024-04-04 10:11:46 +00:00
|
|
|
//nolint:paralleltest // mutates global configuration
|
|
|
|
func TestEnabledFullyQualifiedStackNames(t *testing.T) {
|
|
|
|
// Arrange
|
|
|
|
tmpDir := t.TempDir()
|
|
|
|
ctx := context.Background()
|
|
|
|
b, err := New(ctx, diagtest.LogSink(t), "file://"+filepath.ToSlash(tmpDir), nil)
|
|
|
|
assert.NoError(t, err)
|
|
|
|
|
|
|
|
stackName := "organization/project-12345/stack-67890"
|
|
|
|
ref, err := b.ParseStackReference(stackName)
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
s, err := b.CreateStack(ctx, ref, "", nil)
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
previous := cmdutil.FullyQualifyStackNames
|
|
|
|
expected := stackName
|
|
|
|
|
|
|
|
// Act
|
|
|
|
cmdutil.FullyQualifyStackNames = true
|
|
|
|
defer func() { cmdutil.FullyQualifyStackNames = previous }()
|
|
|
|
|
|
|
|
actual := s.Ref().String()
|
|
|
|
|
|
|
|
// Assert
|
|
|
|
assert.Equal(t, expected, actual)
|
|
|
|
}
|
|
|
|
|
|
|
|
//nolint:paralleltest // mutates global configuration
|
|
|
|
func TestDisabledFullyQualifiedStackNames(t *testing.T) {
|
|
|
|
// Arrange
|
|
|
|
// Create a new project
|
|
|
|
projectDir := t.TempDir()
|
|
|
|
pyaml := filepath.Join(projectDir, "Pulumi.yaml")
|
|
|
|
err := os.WriteFile(pyaml, []byte("name: project-12345\nruntime: test"), 0o600)
|
|
|
|
require.NoError(t, err)
|
|
|
|
proj, err := workspace.LoadProject(pyaml)
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
chdir(t, projectDir)
|
|
|
|
|
|
|
|
tmpDir := t.TempDir()
|
|
|
|
ctx := context.Background()
|
|
|
|
b, err := New(ctx, diagtest.LogSink(t), "file://"+filepath.ToSlash(tmpDir), proj)
|
|
|
|
assert.NoError(t, err)
|
|
|
|
|
|
|
|
stackName := "organization/project-12345/stack-67890"
|
|
|
|
ref, err := b.ParseStackReference(stackName)
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
s, err := b.CreateStack(ctx, ref, "", nil)
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
previous := cmdutil.FullyQualifyStackNames
|
|
|
|
expected := "stack-67890"
|
|
|
|
|
|
|
|
// Act
|
|
|
|
cmdutil.FullyQualifyStackNames = false
|
|
|
|
defer func() { cmdutil.FullyQualifyStackNames = previous }()
|
|
|
|
|
|
|
|
actual := s.Ref().String()
|
|
|
|
|
|
|
|
// Assert
|
|
|
|
assert.Equal(t, expected, actual)
|
|
|
|
}
|
|
|
|
|
2019-07-25 14:58:19 +00:00
|
|
|
func TestMassageBlobPath(t *testing.T) {
|
2022-03-04 08:17:41 +00:00
|
|
|
t.Parallel()
|
|
|
|
|
2019-07-25 14:58:19 +00:00
|
|
|
testMassagePath := func(t *testing.T, s string, want string) {
|
add no_tmp_dir query parameter to file:// URLs (#15375)
In gocloud.dev 0.34.0 the behaviour for file:// URLs changed, making
pulumi fail when the state is supposed to be written to a mounted
directory. See also https://github.com/pulumi/pulumi/issues/15352.
Restore the old behaviour by adding a `no_tmp_dir=true` flag to the URL,
unless the user provided it themselves. We also allow users to pass
`no_tmp_dir=false` in the parameters, which will opt in to the new
behaviour.
This actually implements option 2 from the linked issue since it was
easy enough to do, though I don't expect may users to make use of it,
since I don't think a lot of people care about the contents of whatever
directory the state is stored in anyway, so additional temp files there
don't really matter.
Fixes https://github.com/pulumi/pulumi/issues/15352
## 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. -->
- [x] I have added tests that prove my fix is effective or that my
feature works
<!---
User-facing changes require a CHANGELOG entry.
-->
- [x] 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. -->
---------
Co-authored-by: Justin Van Patten <jvp@justinvp.com>
2024-02-05 13:40:31 +00:00
|
|
|
t.Helper()
|
|
|
|
|
2019-07-25 14:58:19 +00:00
|
|
|
massaged, err := massageBlobPath(s)
|
|
|
|
assert.NoError(t, err)
|
|
|
|
assert.Equal(t, want, massaged,
|
|
|
|
"massageBlobPath(%s) didn't return expected result.\nWant: %q\nGot: %q", s, want, massaged)
|
|
|
|
}
|
|
|
|
|
|
|
|
// URLs not prefixed with "file://" are kept as-is. Also why we add FilePathPrefix as a prefix for other tests.
|
|
|
|
t.Run("NonFilePrefixed", func(t *testing.T) {
|
2022-03-04 08:17:41 +00:00
|
|
|
t.Parallel()
|
|
|
|
|
2019-07-25 14:58:19 +00:00
|
|
|
testMassagePath(t, "asdf-123", "asdf-123")
|
|
|
|
})
|
|
|
|
|
add no_tmp_dir query parameter to file:// URLs (#15375)
In gocloud.dev 0.34.0 the behaviour for file:// URLs changed, making
pulumi fail when the state is supposed to be written to a mounted
directory. See also https://github.com/pulumi/pulumi/issues/15352.
Restore the old behaviour by adding a `no_tmp_dir=true` flag to the URL,
unless the user provided it themselves. We also allow users to pass
`no_tmp_dir=false` in the parameters, which will opt in to the new
behaviour.
This actually implements option 2 from the linked issue since it was
easy enough to do, though I don't expect may users to make use of it,
since I don't think a lot of people care about the contents of whatever
directory the state is stored in anyway, so additional temp files there
don't really matter.
Fixes https://github.com/pulumi/pulumi/issues/15352
## 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. -->
- [x] I have added tests that prove my fix is effective or that my
feature works
<!---
User-facing changes require a CHANGELOG entry.
-->
- [x] 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. -->
---------
Co-authored-by: Justin Van Patten <jvp@justinvp.com>
2024-02-05 13:40:31 +00:00
|
|
|
noTmpDirSuffix := "?no_tmp_dir=true"
|
|
|
|
|
2019-07-25 14:58:19 +00:00
|
|
|
// The home directory is converted into the user's actual home directory.
|
|
|
|
// Which requires even more tweaks to work on Windows.
|
|
|
|
t.Run("PrefixedWithTilde", func(t *testing.T) {
|
2022-03-04 08:17:41 +00:00
|
|
|
t.Parallel()
|
|
|
|
|
2019-07-25 14:58:19 +00:00
|
|
|
usr, err := user.Current()
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("Unable to get current user: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
homeDir := usr.HomeDir
|
|
|
|
|
|
|
|
// When running on Windows, the "home directory" takes on a different meaning.
|
|
|
|
if runtime.GOOS == "windows" {
|
|
|
|
t.Logf("Running on %v", runtime.GOOS)
|
|
|
|
|
|
|
|
t.Run("NormalizeDirSeparator", func(t *testing.T) {
|
2022-03-04 08:17:41 +00:00
|
|
|
t.Parallel()
|
|
|
|
|
add no_tmp_dir query parameter to file:// URLs (#15375)
In gocloud.dev 0.34.0 the behaviour for file:// URLs changed, making
pulumi fail when the state is supposed to be written to a mounted
directory. See also https://github.com/pulumi/pulumi/issues/15352.
Restore the old behaviour by adding a `no_tmp_dir=true` flag to the URL,
unless the user provided it themselves. We also allow users to pass
`no_tmp_dir=false` in the parameters, which will opt in to the new
behaviour.
This actually implements option 2 from the linked issue since it was
easy enough to do, though I don't expect may users to make use of it,
since I don't think a lot of people care about the contents of whatever
directory the state is stored in anyway, so additional temp files there
don't really matter.
Fixes https://github.com/pulumi/pulumi/issues/15352
## 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. -->
- [x] I have added tests that prove my fix is effective or that my
feature works
<!---
User-facing changes require a CHANGELOG entry.
-->
- [x] 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. -->
---------
Co-authored-by: Justin Van Patten <jvp@justinvp.com>
2024-02-05 13:40:31 +00:00
|
|
|
testMassagePath(t, FilePathPrefix+`C:\Users\steve\`, FilePathPrefix+"/C:/Users/steve"+noTmpDirSuffix)
|
2019-07-25 14:58:19 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
newHomeDir := "/" + filepath.ToSlash(homeDir)
|
|
|
|
t.Logf("Changed homeDir to expect from %q to %q", homeDir, newHomeDir)
|
|
|
|
homeDir = newHomeDir
|
|
|
|
}
|
|
|
|
|
add no_tmp_dir query parameter to file:// URLs (#15375)
In gocloud.dev 0.34.0 the behaviour for file:// URLs changed, making
pulumi fail when the state is supposed to be written to a mounted
directory. See also https://github.com/pulumi/pulumi/issues/15352.
Restore the old behaviour by adding a `no_tmp_dir=true` flag to the URL,
unless the user provided it themselves. We also allow users to pass
`no_tmp_dir=false` in the parameters, which will opt in to the new
behaviour.
This actually implements option 2 from the linked issue since it was
easy enough to do, though I don't expect may users to make use of it,
since I don't think a lot of people care about the contents of whatever
directory the state is stored in anyway, so additional temp files there
don't really matter.
Fixes https://github.com/pulumi/pulumi/issues/15352
## 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. -->
- [x] I have added tests that prove my fix is effective or that my
feature works
<!---
User-facing changes require a CHANGELOG entry.
-->
- [x] 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. -->
---------
Co-authored-by: Justin Van Patten <jvp@justinvp.com>
2024-02-05 13:40:31 +00:00
|
|
|
testMassagePath(t, FilePathPrefix+"~", FilePathPrefix+homeDir+noTmpDirSuffix)
|
|
|
|
testMassagePath(t, FilePathPrefix+"~/alpha/beta", FilePathPrefix+homeDir+"/alpha/beta"+noTmpDirSuffix)
|
2019-07-25 14:58:19 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
t.Run("MakeAbsolute", func(t *testing.T) {
|
2022-03-04 08:17:41 +00:00
|
|
|
t.Parallel()
|
|
|
|
|
2019-07-25 14:58:19 +00:00
|
|
|
// Run the expected result through filepath.Abs, since on Windows we expect "C:\1\2".
|
|
|
|
expected := "/1/2"
|
|
|
|
abs, err := filepath.Abs(expected)
|
|
|
|
assert.NoError(t, err)
|
|
|
|
|
|
|
|
expected = filepath.ToSlash(abs)
|
|
|
|
if expected[0] != '/' {
|
|
|
|
expected = "/" + expected // A leading slash is added on Windows.
|
|
|
|
}
|
|
|
|
|
add no_tmp_dir query parameter to file:// URLs (#15375)
In gocloud.dev 0.34.0 the behaviour for file:// URLs changed, making
pulumi fail when the state is supposed to be written to a mounted
directory. See also https://github.com/pulumi/pulumi/issues/15352.
Restore the old behaviour by adding a `no_tmp_dir=true` flag to the URL,
unless the user provided it themselves. We also allow users to pass
`no_tmp_dir=false` in the parameters, which will opt in to the new
behaviour.
This actually implements option 2 from the linked issue since it was
easy enough to do, though I don't expect may users to make use of it,
since I don't think a lot of people care about the contents of whatever
directory the state is stored in anyway, so additional temp files there
don't really matter.
Fixes https://github.com/pulumi/pulumi/issues/15352
## 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. -->
- [x] I have added tests that prove my fix is effective or that my
feature works
<!---
User-facing changes require a CHANGELOG entry.
-->
- [x] 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. -->
---------
Co-authored-by: Justin Van Patten <jvp@justinvp.com>
2024-02-05 13:40:31 +00:00
|
|
|
testMassagePath(t, FilePathPrefix+"/1/2/3/../4/..", FilePathPrefix+expected+noTmpDirSuffix)
|
|
|
|
})
|
|
|
|
|
|
|
|
t.Run("AlreadySuffixedWithNoTmpDir", func(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
testMassagePath(t, FilePathPrefix+"/1?no_tmp_dir=yes", FilePathPrefix+"/1?no_tmp_dir=yes")
|
|
|
|
})
|
|
|
|
|
|
|
|
t.Run("AlreadySuffixedWithOtherQuery", func(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
testMassagePath(t, FilePathPrefix+"/1?foo=bar", FilePathPrefix+"/1?foo=bar&no_tmp_dir=true")
|
|
|
|
})
|
|
|
|
|
|
|
|
t.Run("NoTmpDirFalseStripped", func(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
testMassagePath(t, FilePathPrefix+"/1?no_tmp_dir=false", FilePathPrefix+"/1")
|
|
|
|
testMassagePath(t, FilePathPrefix+"/1?foo=bar&no_tmp_dir=false", FilePathPrefix+"/1?foo=bar")
|
2019-07-25 14:58:19 +00:00
|
|
|
})
|
|
|
|
}
|
2020-10-02 21:09:34 +00:00
|
|
|
|
|
|
|
func TestGetLogsForTargetWithNoSnapshot(t *testing.T) {
|
2022-03-04 08:17:41 +00:00
|
|
|
t.Parallel()
|
|
|
|
|
2020-10-02 21:09:34 +00:00
|
|
|
target := &deploy.Target{
|
Add tokens.StackName (#14487)
<!---
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. -->
This adds a new type `tokens.StackName` which is a relatively strongly
typed container for a stack name. The only weakly typed aspect of it is
Go will always allow the "zero" value to be created for a struct, which
for a stack name is the empty string which is invalid. To prevent
introducing unexpected empty strings when working with stack names the
`String()` method will panic for zero initialized stack names.
Apart from the zero value, all other instances of `StackName` are via
`ParseStackName` which returns a descriptive error if the string is not
valid.
This PR only updates "pkg/" to use this type. There are a number of
places in "sdk/" which could do with this type as well, but there's no
harm in doing a staggered roll out, and some parts of "sdk/" are user
facing and will probably have to stay on the current `tokens.Name` and
`tokens.QName` types.
There are two places in the system where we panic on invalid stack
names, both in the http backend. This _should_ be fine as we've had long
standing validation that stacks created in the service are valid stack
names.
Just in case people have managed to introduce invalid stack names, there
is the `PULUMI_DISABLE_VALIDATION` environment variable which will turn
off the validation _and_ panicing for stack names. Users can use that to
temporarily disable the validation and continue working, but it should
only be seen as a temporary measure. If they have invalid names they
should rename them, or if they think they should be valid raise an issue
with us to change the validation code.
## 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. -->
- [x] 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-11-15 07:44:54 +00:00
|
|
|
Name: tokens.MustParseStackName("test"),
|
2020-10-02 21:09:34 +00:00
|
|
|
Config: config.Map{},
|
|
|
|
Decrypter: config.NopDecrypter,
|
|
|
|
Snapshot: nil,
|
|
|
|
}
|
|
|
|
query := operations.LogQuery{}
|
|
|
|
res, err := GetLogsForTarget(target, query)
|
|
|
|
assert.NoError(t, err)
|
|
|
|
assert.Nil(t, res)
|
|
|
|
}
|
2021-07-28 01:37:25 +00:00
|
|
|
|
2023-11-20 08:59:00 +00:00
|
|
|
func makeUntypedDeployment(name string, phrase, state string) (*apitype.UntypedDeployment, error) {
|
This commit adds the `Created` and `Modified` timestamps to pulumi state that are optional.
`Created`: Created tracks when the remote resource was first added to state by pulumi. Checkpoints prior to early 2023 do not include this. (Create, Import)
`Modified`: Modified tracks when the resource state was last altered. Checkpoints prior to early 2023 do not include this. (Create, Import, Read, Refresh, Update)
When serialized they will follow RFC3339 with nanoseconds captured by a test case.
https://pkg.go.dev/time#RFC3339
Note: Older versions of pulumi may strip these fields when modifying the state.
For future expansion, when we inevitably need to track other timestamps, we'll add a new "operationTimestamps" field (or something similarly named that clarified these are timestamps of the actual Pulumi operations).
operationTimestamps: {
created: ...,
updated: ...,
imported: ...,
}
Fixes https://github.com/pulumi/pulumi/issues/12022
2023-02-06 20:39:11 +00:00
|
|
|
return makeUntypedDeploymentTimestamp(name, phrase, state, nil, nil)
|
|
|
|
}
|
|
|
|
|
|
|
|
func makeUntypedDeploymentTimestamp(
|
2023-11-20 08:59:00 +00:00
|
|
|
name string,
|
This commit adds the `Created` and `Modified` timestamps to pulumi state that are optional.
`Created`: Created tracks when the remote resource was first added to state by pulumi. Checkpoints prior to early 2023 do not include this. (Create, Import)
`Modified`: Modified tracks when the resource state was last altered. Checkpoints prior to early 2023 do not include this. (Create, Import, Read, Refresh, Update)
When serialized they will follow RFC3339 with nanoseconds captured by a test case.
https://pkg.go.dev/time#RFC3339
Note: Older versions of pulumi may strip these fields when modifying the state.
For future expansion, when we inevitably need to track other timestamps, we'll add a new "operationTimestamps" field (or something similarly named that clarified these are timestamps of the actual Pulumi operations).
operationTimestamps: {
created: ...,
updated: ...,
imported: ...,
}
Fixes https://github.com/pulumi/pulumi/issues/12022
2023-02-06 20:39:11 +00:00
|
|
|
phrase, state string,
|
|
|
|
created, modified *time.Time,
|
|
|
|
) (*apitype.UntypedDeployment, error) {
|
2023-08-31 08:56:38 +00:00
|
|
|
sm, err := passphrase.GetPassphraseSecretsManager(phrase, state)
|
2021-07-28 01:37:25 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
resources := []*resource.State{
|
|
|
|
{
|
|
|
|
URN: resource.NewURN("a", "proj", "d:e:f", "a:b:c", name),
|
|
|
|
Type: "a:b:c",
|
|
|
|
Inputs: resource.PropertyMap{
|
|
|
|
resource.PropertyKey("secret"): resource.MakeSecret(resource.NewStringProperty("s3cr3t")),
|
|
|
|
},
|
This commit adds the `Created` and `Modified` timestamps to pulumi state that are optional.
`Created`: Created tracks when the remote resource was first added to state by pulumi. Checkpoints prior to early 2023 do not include this. (Create, Import)
`Modified`: Modified tracks when the resource state was last altered. Checkpoints prior to early 2023 do not include this. (Create, Import, Read, Refresh, Update)
When serialized they will follow RFC3339 with nanoseconds captured by a test case.
https://pkg.go.dev/time#RFC3339
Note: Older versions of pulumi may strip these fields when modifying the state.
For future expansion, when we inevitably need to track other timestamps, we'll add a new "operationTimestamps" field (or something similarly named that clarified these are timestamps of the actual Pulumi operations).
operationTimestamps: {
created: ...,
updated: ...,
imported: ...,
}
Fixes https://github.com/pulumi/pulumi/issues/12022
2023-02-06 20:39:11 +00:00
|
|
|
Created: created,
|
|
|
|
Modified: modified,
|
2021-07-28 01:37:25 +00:00
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
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()
|
2021-07-28 01:37:25 +00:00
|
|
|
|
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 */)
|
2021-07-28 01:37:25 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
data, err := json.Marshal(sdep)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return &apitype.UntypedDeployment{
|
|
|
|
Version: 3,
|
|
|
|
Deployment: json.RawMessage(data),
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
2022-03-04 08:17:41 +00:00
|
|
|
//nolint:paralleltest // mutates environment variables
|
2021-07-28 01:37:25 +00:00
|
|
|
func TestListStacksWithMultiplePassphrases(t *testing.T) {
|
2024-01-30 15:53:10 +00:00
|
|
|
// Login to a temp dir diy backend
|
2022-12-03 07:17:08 +00:00
|
|
|
tmpDir := t.TempDir()
|
2021-07-28 01:37:25 +00:00
|
|
|
ctx := context.Background()
|
2023-02-10 12:24:28 +00:00
|
|
|
b, err := New(ctx, diagtest.LogSink(t), "file://"+filepath.ToSlash(tmpDir), nil)
|
|
|
|
assert.NoError(t, err)
|
2021-07-28 01:37:25 +00:00
|
|
|
|
|
|
|
// Create stack "a" and import a checkpoint with a secret
|
2023-02-10 12:24:28 +00:00
|
|
|
aStackRef, err := b.ParseStackReference("organization/project/a")
|
2021-07-28 01:37:25 +00:00
|
|
|
assert.NoError(t, err)
|
2023-03-03 20:32:42 +00:00
|
|
|
aStack, err := b.CreateStack(ctx, aStackRef, "", nil)
|
2021-07-28 01:37:25 +00:00
|
|
|
assert.NoError(t, err)
|
|
|
|
assert.NotNil(t, aStack)
|
|
|
|
defer func() {
|
2022-07-24 09:41:44 +00:00
|
|
|
t.Setenv("PULUMI_CONFIG_PASSPHRASE", "abc123")
|
2021-07-28 01:37:25 +00:00
|
|
|
_, 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)
|
2022-07-24 09:41:44 +00:00
|
|
|
t.Setenv("PULUMI_CONFIG_PASSPHRASE", "abc123")
|
2021-07-28 01:37:25 +00:00
|
|
|
err = b.ImportDeployment(ctx, aStack, deployment)
|
|
|
|
assert.NoError(t, err)
|
|
|
|
|
|
|
|
// Create stack "b" and import a checkpoint with a secret
|
2023-02-10 12:24:28 +00:00
|
|
|
bStackRef, err := b.ParseStackReference("organization/project/b")
|
2021-07-28 01:37:25 +00:00
|
|
|
assert.NoError(t, err)
|
2023-03-03 20:32:42 +00:00
|
|
|
bStack, err := b.CreateStack(ctx, bStackRef, "", nil)
|
2021-07-28 01:37:25 +00:00
|
|
|
assert.NoError(t, err)
|
|
|
|
assert.NotNil(t, bStack)
|
|
|
|
defer func() {
|
2022-07-24 09:41:44 +00:00
|
|
|
t.Setenv("PULUMI_CONFIG_PASSPHRASE", "123abc")
|
2021-07-28 01:37:25 +00:00
|
|
|
_, 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)
|
2022-07-24 09:41:44 +00:00
|
|
|
t.Setenv("PULUMI_CONFIG_PASSPHRASE", "123abc")
|
2021-07-28 01:37:25 +00:00
|
|
|
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
|
2021-07-29 20:37:17 +00:00
|
|
|
stacks, outContToken, err := b.ListStacks(ctx, backend.ListStacksFilter{}, nil /* inContToken */)
|
2021-07-28 01:37:25 +00:00
|
|
|
assert.NoError(t, err)
|
2021-07-29 20:37:17 +00:00
|
|
|
assert.Nil(t, outContToken)
|
2021-07-28 01:37:25 +00:00
|
|
|
assert.Len(t, stacks, 2)
|
|
|
|
for _, stack := range stacks {
|
|
|
|
assert.NotNil(t, stack.ResourceCount())
|
|
|
|
assert.Equal(t, 1, *stack.ResourceCount())
|
|
|
|
}
|
|
|
|
}
|
2021-11-19 20:21:37 +00:00
|
|
|
|
|
|
|
func TestDrillError(t *testing.T) {
|
2022-03-04 08:17:41 +00:00
|
|
|
t.Parallel()
|
|
|
|
|
2024-01-30 15:53:10 +00:00
|
|
|
// Login to a temp dir diy backend
|
2022-12-03 07:17:08 +00:00
|
|
|
tmpDir := t.TempDir()
|
2021-11-19 20:21:37 +00:00
|
|
|
ctx := context.Background()
|
2023-02-10 12:24:28 +00:00
|
|
|
b, err := New(ctx, diagtest.LogSink(t), "file://"+filepath.ToSlash(tmpDir), nil)
|
|
|
|
assert.NoError(t, err)
|
2021-11-19 20:21:37 +00:00
|
|
|
|
|
|
|
// Get a non-existent stack and expect a nil error because it won't be found.
|
2023-02-10 12:24:28 +00:00
|
|
|
stackRef, err := b.ParseStackReference("organization/project/dev")
|
2021-11-19 20:21:37 +00:00
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("unexpected error %v when parsing stack reference", err)
|
|
|
|
}
|
|
|
|
_, err = b.GetStack(ctx, stackRef)
|
2023-10-13 09:46:07 +00:00
|
|
|
assert.NoError(t, err)
|
2021-11-19 20:21:37 +00:00
|
|
|
}
|
2022-03-03 17:07:05 +00:00
|
|
|
|
|
|
|
func TestCancel(t *testing.T) {
|
2022-03-04 08:17:41 +00:00
|
|
|
t.Parallel()
|
|
|
|
|
2024-01-30 15:53:10 +00:00
|
|
|
// Login to a temp dir diy backend
|
2022-12-03 07:17:08 +00:00
|
|
|
tmpDir := t.TempDir()
|
2022-03-03 17:07:05 +00:00
|
|
|
ctx := context.Background()
|
2023-02-10 12:24:28 +00:00
|
|
|
b, err := New(ctx, diagtest.LogSink(t), "file://"+filepath.ToSlash(tmpDir), nil)
|
|
|
|
assert.NoError(t, err)
|
2022-03-03 17:07:05 +00:00
|
|
|
|
|
|
|
// Check that trying to cancel a stack that isn't created yet doesn't error
|
2023-02-10 12:24:28 +00:00
|
|
|
aStackRef, err := b.ParseStackReference("organization/project/a")
|
2022-03-03 17:07:05 +00:00
|
|
|
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
|
2023-03-03 20:32:42 +00:00
|
|
|
aStack, err := b.CreateStack(ctx, aStackRef, "", nil)
|
2022-03-03 17:07:05 +00:00
|
|
|
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
|
2024-01-30 15:53:10 +00:00
|
|
|
lb, ok := b.(*diyBackend)
|
2022-03-03 17:07:05 +00:00
|
|
|
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
|
2023-02-10 12:24:28 +00:00
|
|
|
lockExists, err := lb.bucket.Exists(ctx, lb.lockPath(aStackRef))
|
2022-03-03 17:07:05 +00:00
|
|
|
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
|
2023-02-10 12:24:28 +00:00
|
|
|
lockExists, err = lb.bucket.Exists(ctx, lb.lockPath(aStackRef))
|
2022-03-03 17:07:05 +00:00
|
|
|
assert.NoError(t, err)
|
|
|
|
assert.False(t, lockExists)
|
|
|
|
|
2024-01-30 15:53:10 +00:00
|
|
|
// Make another diy backend which will have a different lockId
|
2023-02-10 12:24:28 +00:00
|
|
|
ob, err := New(ctx, diagtest.LogSink(t), "file://"+filepath.ToSlash(tmpDir), nil)
|
2022-03-03 17:07:05 +00:00
|
|
|
assert.NoError(t, err)
|
2024-01-30 15:53:10 +00:00
|
|
|
otherBackend, ok := ob.(*diyBackend)
|
2022-03-03 17:07:05 +00:00
|
|
|
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)
|
|
|
|
}
|
2022-04-26 18:44:00 +00:00
|
|
|
|
|
|
|
func TestRemoveMakesBackups(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
|
2024-01-30 15:53:10 +00:00
|
|
|
// Login to a temp dir diy backend
|
2022-12-03 07:17:08 +00:00
|
|
|
tmpDir := t.TempDir()
|
2022-04-26 18:44:00 +00:00
|
|
|
ctx := context.Background()
|
2023-02-10 12:24:28 +00:00
|
|
|
b, err := New(ctx, diagtest.LogSink(t), "file://"+filepath.ToSlash(tmpDir), nil)
|
|
|
|
assert.NoError(t, err)
|
2022-04-26 18:44:00 +00:00
|
|
|
|
|
|
|
// Grab the bucket interface to test with
|
2024-01-30 15:53:10 +00:00
|
|
|
lb, ok := b.(*diyBackend)
|
2022-04-26 18:44:00 +00:00
|
|
|
assert.True(t, ok)
|
|
|
|
assert.NotNil(t, lb)
|
|
|
|
|
|
|
|
// Check that creating a new stack doesn't make a backup file
|
2023-02-10 12:24:28 +00:00
|
|
|
aStackRef, err := lb.parseStackReference("organization/project/a")
|
2022-04-26 18:44:00 +00:00
|
|
|
assert.NoError(t, err)
|
2023-03-03 20:32:42 +00:00
|
|
|
aStack, err := b.CreateStack(ctx, aStackRef, "", nil)
|
2022-04-26 18:44:00 +00:00
|
|
|
assert.NoError(t, err)
|
|
|
|
assert.NotNil(t, aStack)
|
|
|
|
|
|
|
|
// Check the stack file now exists, but the backup file doesn't
|
2023-04-03 21:36:44 +00:00
|
|
|
stackFileExists, err := lb.bucket.Exists(ctx, lb.stackPath(ctx, aStackRef))
|
2022-04-26 18:44:00 +00:00
|
|
|
assert.NoError(t, err)
|
|
|
|
assert.True(t, stackFileExists)
|
2023-04-03 21:36:44 +00:00
|
|
|
backupFileExists, err := lb.bucket.Exists(ctx, lb.stackPath(ctx, aStackRef)+".bak")
|
2022-04-26 18:44:00 +00:00
|
|
|
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
|
2023-04-03 21:36:44 +00:00
|
|
|
stackFileExists, err = lb.bucket.Exists(ctx, lb.stackPath(ctx, aStackRef))
|
2022-04-26 18:44:00 +00:00
|
|
|
assert.NoError(t, err)
|
|
|
|
assert.False(t, stackFileExists)
|
2023-04-03 21:36:44 +00:00
|
|
|
backupFileExists, err = lb.bucket.Exists(ctx, lb.stackPath(ctx, aStackRef)+".bak")
|
2022-04-26 18:44:00 +00:00
|
|
|
assert.NoError(t, err)
|
|
|
|
assert.True(t, backupFileExists)
|
|
|
|
}
|
2022-05-25 08:10:36 +00:00
|
|
|
|
|
|
|
func TestRenameWorks(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
|
2024-01-30 15:53:10 +00:00
|
|
|
// Login to a temp dir diy backend
|
2022-12-03 07:17:08 +00:00
|
|
|
tmpDir := t.TempDir()
|
2022-05-25 08:10:36 +00:00
|
|
|
ctx := context.Background()
|
2023-02-10 12:24:28 +00:00
|
|
|
b, err := New(ctx, diagtest.LogSink(t), "file://"+filepath.ToSlash(tmpDir), nil)
|
|
|
|
assert.NoError(t, err)
|
2022-05-25 08:10:36 +00:00
|
|
|
|
|
|
|
// Grab the bucket interface to test with
|
2024-01-30 15:53:10 +00:00
|
|
|
lb, ok := b.(*diyBackend)
|
2022-05-25 08:10:36 +00:00
|
|
|
assert.True(t, ok)
|
|
|
|
assert.NotNil(t, lb)
|
|
|
|
|
|
|
|
// Create a new stack
|
2023-02-10 12:24:28 +00:00
|
|
|
aStackRef, err := lb.parseStackReference("organization/project/a")
|
2022-05-25 08:10:36 +00:00
|
|
|
assert.NoError(t, err)
|
2023-03-03 20:32:42 +00:00
|
|
|
aStack, err := b.CreateStack(ctx, aStackRef, "", nil)
|
2022-05-25 08:10:36 +00:00
|
|
|
assert.NoError(t, err)
|
|
|
|
assert.NotNil(t, aStack)
|
|
|
|
|
|
|
|
// Check the stack file now exists
|
2023-04-03 21:36:44 +00:00
|
|
|
stackFileExists, err := lb.bucket.Exists(ctx, lb.stackPath(ctx, aStackRef))
|
2022-05-25 08:10:36 +00:00
|
|
|
assert.NoError(t, err)
|
|
|
|
assert.True(t, stackFileExists)
|
|
|
|
|
|
|
|
// Fake up some history
|
2023-04-03 21:36:44 +00:00
|
|
|
err = lb.addToHistory(ctx, aStackRef, backend.UpdateInfo{Kind: apitype.DestroyUpdate})
|
2022-05-25 08:10:36 +00:00
|
|
|
assert.NoError(t, err)
|
|
|
|
// And pollute the history folder
|
filestate: Introduce referenceStore to control layout
Adds a referenceStore abstraction to control the layout of the storage,
and a legacyReferenceStore implementation based on the current layout
(that does not support projects).
This allows us to move code to determine file paths of stacks, their
histories, and their backups, all into a single component that we can
swap out for project support.
localBackendReferences keep track of the referenceStore that built them.
The primary reason for this is that when we add support for migrating a
stack state from legacy to project mode, `backend.store` will become
mutable.
For references created before the store for a backend was changed, we
still need to be able to access their original file paths, so we need to
hold onto the original referenceStore.
However, as a side-effect of this,
it's more convenient to acess paths from `ref.Foo()` rather than
`backend.foo(ref)` or `backend.store.Foo(ref)`.
In the future, we may also move stackPath to the store,
since right now the .json/.json.gz logic is duplicated in a couple
places.
Extracted from #12134
2023-03-14 00:49:05 +00:00
|
|
|
err = lb.bucket.WriteAll(ctx, path.Join(aStackRef.HistoryDir(), "randomfile.txt"), []byte{0, 13}, nil)
|
2022-05-25 08:10:36 +00:00
|
|
|
assert.NoError(t, err)
|
|
|
|
|
|
|
|
// Rename the stack
|
2023-02-10 12:24:28 +00:00
|
|
|
bStackRefI, err := b.RenameStack(ctx, aStack, "organization/project/b")
|
2022-05-25 08:10:36 +00:00
|
|
|
assert.NoError(t, err)
|
2023-02-10 12:24:28 +00:00
|
|
|
assert.Equal(t, "organization/project/b", bStackRefI.String())
|
2024-01-30 15:53:10 +00:00
|
|
|
bStackRef := bStackRefI.(*diyBackendReference)
|
2022-05-25 08:10:36 +00:00
|
|
|
|
|
|
|
// Check the new stack file now exists and the old one is gone
|
2023-04-03 21:36:44 +00:00
|
|
|
stackFileExists, err = lb.bucket.Exists(ctx, lb.stackPath(ctx, bStackRef))
|
2022-05-25 08:10:36 +00:00
|
|
|
assert.NoError(t, err)
|
|
|
|
assert.True(t, stackFileExists)
|
2023-04-03 21:36:44 +00:00
|
|
|
stackFileExists, err = lb.bucket.Exists(ctx, lb.stackPath(ctx, aStackRef))
|
2022-05-25 08:10:36 +00:00
|
|
|
assert.NoError(t, err)
|
|
|
|
assert.False(t, stackFileExists)
|
|
|
|
|
|
|
|
// Rename again
|
|
|
|
bStack, err := b.GetStack(ctx, bStackRef)
|
|
|
|
assert.NoError(t, err)
|
2023-02-10 12:24:28 +00:00
|
|
|
cStackRefI, err := b.RenameStack(ctx, bStack, "organization/project/c")
|
2022-05-25 08:10:36 +00:00
|
|
|
assert.NoError(t, err)
|
2023-02-10 12:24:28 +00:00
|
|
|
assert.Equal(t, "organization/project/c", cStackRefI.String())
|
2024-01-30 15:53:10 +00:00
|
|
|
cStackRef := cStackRefI.(*diyBackendReference)
|
2022-05-25 08:10:36 +00:00
|
|
|
|
|
|
|
// Check the new stack file now exists and the old one is gone
|
2023-04-03 21:36:44 +00:00
|
|
|
stackFileExists, err = lb.bucket.Exists(ctx, lb.stackPath(ctx, cStackRef))
|
2022-05-25 08:10:36 +00:00
|
|
|
assert.NoError(t, err)
|
|
|
|
assert.True(t, stackFileExists)
|
2023-04-03 21:36:44 +00:00
|
|
|
stackFileExists, err = lb.bucket.Exists(ctx, lb.stackPath(ctx, bStackRef))
|
2022-05-25 08:10:36 +00:00
|
|
|
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)
|
|
|
|
}
|
2022-07-20 13:40:51 +00:00
|
|
|
|
2024-05-16 15:09:32 +00:00
|
|
|
func TestRenamePreservesIntegrity(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
// Arrange.
|
|
|
|
tmpDir := t.TempDir()
|
|
|
|
ctx := context.Background()
|
|
|
|
b, err := New(ctx, diagtest.LogSink(t), "file://"+filepath.ToSlash(tmpDir), nil)
|
|
|
|
assert.NoError(t, err)
|
|
|
|
|
|
|
|
stackRef, err := b.ParseStackReference("organization/project/a")
|
|
|
|
assert.NoError(t, err)
|
|
|
|
stk, err := b.CreateStack(ctx, stackRef, "", nil)
|
|
|
|
assert.NoError(t, err)
|
|
|
|
assert.NotNil(t, stk)
|
|
|
|
|
|
|
|
rBase := &resource.State{
|
|
|
|
URN: resource.NewURN("a", "proj", "d:e:f", "a:b:c", "base"),
|
|
|
|
Type: "a:b:c",
|
|
|
|
Inputs: resource.PropertyMap{
|
|
|
|
resource.PropertyKey("p"): resource.NewStringProperty("v"),
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
rDependency := &resource.State{
|
|
|
|
URN: resource.NewURN("a", "proj", "d:e:f", "a:b:c", "dependency"),
|
|
|
|
Type: "a:b:c",
|
|
|
|
Inputs: resource.PropertyMap{
|
|
|
|
resource.PropertyKey("p"): resource.NewStringProperty("v"),
|
|
|
|
},
|
|
|
|
Dependencies: []resource.URN{rBase.URN},
|
|
|
|
}
|
|
|
|
|
|
|
|
rPropertyDependency := &resource.State{
|
|
|
|
URN: resource.NewURN("a", "proj", "d:e:f", "a:b:c", "property-dependency"),
|
|
|
|
Type: "a:b:c",
|
|
|
|
Inputs: resource.PropertyMap{
|
|
|
|
resource.PropertyKey("p"): resource.NewStringProperty("v"),
|
|
|
|
},
|
|
|
|
PropertyDependencies: map[resource.PropertyKey][]resource.URN{
|
|
|
|
resource.PropertyKey("p"): {rBase.URN},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
rDeletedWith := &resource.State{
|
|
|
|
URN: resource.NewURN("a", "proj", "d:e:f", "a:b:c", "deleted-with"),
|
|
|
|
Type: "a:b:c",
|
|
|
|
Inputs: resource.PropertyMap{
|
|
|
|
resource.PropertyKey("p"): resource.NewStringProperty("v"),
|
|
|
|
},
|
|
|
|
DeletedWith: rBase.URN,
|
|
|
|
}
|
|
|
|
|
|
|
|
rParent := &resource.State{
|
|
|
|
URN: resource.NewURN("a", "proj", "d:e:f", "a:b:c", "parent"),
|
|
|
|
Type: "a:b:c",
|
|
|
|
Inputs: resource.PropertyMap{
|
|
|
|
resource.PropertyKey("p"): resource.NewStringProperty("v"),
|
|
|
|
},
|
|
|
|
Parent: rBase.URN,
|
|
|
|
}
|
|
|
|
|
|
|
|
resources := []*resource.State{
|
|
|
|
rBase,
|
|
|
|
rDependency,
|
|
|
|
rPropertyDependency,
|
|
|
|
rDeletedWith,
|
|
|
|
rParent,
|
|
|
|
}
|
|
|
|
|
|
|
|
snap := deploy.NewSnapshot(deploy.Manifest{}, nil, resources, nil)
|
|
|
|
ctx = context.Background()
|
|
|
|
|
|
|
|
sdep, err := stack.SerializeDeployment(ctx, snap, false)
|
|
|
|
assert.NoError(t, err)
|
|
|
|
|
|
|
|
data, err := encoding.JSON.Marshal(sdep)
|
|
|
|
assert.NoError(t, err)
|
|
|
|
|
|
|
|
err = b.ImportDeployment(ctx, stk, &apitype.UntypedDeployment{
|
|
|
|
Version: 3,
|
|
|
|
Deployment: json.RawMessage(data),
|
|
|
|
})
|
|
|
|
assert.NoError(t, err)
|
|
|
|
|
|
|
|
err = snap.VerifyIntegrity()
|
|
|
|
assert.NoError(t, err)
|
|
|
|
|
|
|
|
// Act.
|
|
|
|
renamedStackRef, err := b.RenameStack(ctx, stk, "organization/project/a-renamed")
|
|
|
|
assert.NoError(t, err)
|
|
|
|
|
|
|
|
// Assert.
|
|
|
|
renamedStk, err := b.GetStack(ctx, renamedStackRef)
|
|
|
|
assert.NoError(t, err)
|
|
|
|
assert.NotNil(t, renamedStk)
|
|
|
|
|
|
|
|
renamedSnap, err := renamedStk.Snapshot(ctx, nil)
|
|
|
|
assert.NoError(t, err)
|
|
|
|
|
|
|
|
err = renamedSnap.VerifyIntegrity()
|
|
|
|
assert.NoError(t, err)
|
|
|
|
}
|
|
|
|
|
2023-05-29 11:29:08 +00:00
|
|
|
func TestRenameProjectWorks(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
|
2024-01-30 15:53:10 +00:00
|
|
|
// Login to a temp dir diy backend
|
2023-05-29 11:29:08 +00:00
|
|
|
tmpDir := 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
|
2024-01-30 15:53:10 +00:00
|
|
|
lb, ok := b.(*diyBackend)
|
2023-05-29 11:29:08 +00:00
|
|
|
assert.True(t, ok)
|
|
|
|
assert.NotNil(t, lb)
|
|
|
|
|
|
|
|
// Create a new stack
|
|
|
|
aStackRef, err := lb.parseStackReference("organization/project/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 project and stack
|
|
|
|
bStackRefI, err := b.RenameStack(ctx, aStack, "organization/newProject/b")
|
|
|
|
assert.NoError(t, err)
|
|
|
|
assert.Equal(t, "organization/newProject/b", bStackRefI.String())
|
2024-01-30 15:53:10 +00:00
|
|
|
bStackRef := bStackRefI.(*diyBackendReference)
|
2023-05-29 11:29:08 +00:00
|
|
|
|
|
|
|
// 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)
|
|
|
|
|
|
|
|
// Check we can still get the history
|
|
|
|
history, err := b.GetHistory(ctx, bStackRef, 10, 0)
|
|
|
|
assert.NoError(t, err)
|
|
|
|
assert.Len(t, history, 1)
|
|
|
|
assert.Equal(t, apitype.DestroyUpdate, history[0].Kind)
|
|
|
|
}
|
|
|
|
|
2022-07-20 13:40:51 +00:00
|
|
|
func TestLoginToNonExistingFolderFails(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
fakeDir := "file://" + filepath.ToSlash(os.TempDir()) + "/non-existing"
|
2023-02-10 12:24:28 +00:00
|
|
|
ctx := context.Background()
|
|
|
|
b, err := New(ctx, diagtest.LogSink(t), fakeDir, nil)
|
2022-07-20 13:40:51 +00:00
|
|
|
assert.Error(t, err)
|
|
|
|
assert.Nil(t, b)
|
|
|
|
}
|
2022-08-17 09:50:48 +00:00
|
|
|
|
|
|
|
// TestParseEmptyStackFails demonstrates that ParseStackReference returns
|
|
|
|
// an error when the stack name is the empty string.TestParseEmptyStackFails
|
|
|
|
func TestParseEmptyStackFails(t *testing.T) {
|
|
|
|
t.Parallel()
|
filestate: Introduce referenceStore to control layout
Adds a referenceStore abstraction to control the layout of the storage,
and a legacyReferenceStore implementation based on the current layout
(that does not support projects).
This allows us to move code to determine file paths of stacks, their
histories, and their backups, all into a single component that we can
swap out for project support.
localBackendReferences keep track of the referenceStore that built them.
The primary reason for this is that when we add support for migrating a
stack state from legacy to project mode, `backend.store` will become
mutable.
For references created before the store for a backend was changed, we
still need to be able to access their original file paths, so we need to
hold onto the original referenceStore.
However, as a side-effect of this,
it's more convenient to acess paths from `ref.Foo()` rather than
`backend.foo(ref)` or `backend.store.Foo(ref)`.
In the future, we may also move stackPath to the store,
since right now the .json/.json.gz logic is duplicated in a couple
places.
Extracted from #12134
2023-03-14 00:49:05 +00:00
|
|
|
tmpDir := t.TempDir()
|
|
|
|
ctx := context.Background()
|
|
|
|
b, err := New(ctx, diagtest.LogSink(t), "file://"+filepath.ToSlash(tmpDir), nil)
|
|
|
|
assert.NoError(t, err)
|
|
|
|
|
|
|
|
_, err = b.ParseStackReference("")
|
2022-08-17 09:50:48 +00:00
|
|
|
assert.Error(t, err)
|
|
|
|
}
|
2022-08-19 12:27:34 +00:00
|
|
|
|
|
|
|
// Regression test for https://github.com/pulumi/pulumi/issues/10439
|
|
|
|
func TestHtmlEscaping(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()
|
2022-08-19 12:27:34 +00:00
|
|
|
|
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 */)
|
2022-08-19 12:27:34 +00:00
|
|
|
assert.NoError(t, err)
|
|
|
|
|
2023-01-11 11:24:10 +00:00
|
|
|
data, err := encoding.JSON.Marshal(sdep)
|
2022-08-19 12:27:34 +00:00
|
|
|
assert.NoError(t, err)
|
|
|
|
|
2023-01-11 11:24:10 +00:00
|
|
|
// 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>")
|
|
|
|
|
2022-08-19 12:27:34 +00:00
|
|
|
udep := &apitype.UntypedDeployment{
|
|
|
|
Version: 3,
|
|
|
|
Deployment: json.RawMessage(data),
|
|
|
|
}
|
|
|
|
|
2024-01-30 15:53:10 +00:00
|
|
|
// Login to a temp dir diy backend
|
2022-12-03 07:17:08 +00:00
|
|
|
tmpDir := t.TempDir()
|
2023-02-10 12:24:28 +00:00
|
|
|
b, err := New(ctx, diagtest.LogSink(t), "file://"+filepath.ToSlash(tmpDir), nil)
|
|
|
|
assert.NoError(t, err)
|
2022-08-19 12:27:34 +00:00
|
|
|
|
|
|
|
// Create stack "a" and import a checkpoint with a secret
|
2023-02-10 12:24:28 +00:00
|
|
|
aStackRef, err := b.ParseStackReference("organization/project/a")
|
2022-08-19 12:27:34 +00:00
|
|
|
assert.NoError(t, err)
|
2023-03-03 20:32:42 +00:00
|
|
|
aStack, err := b.CreateStack(ctx, aStackRef, "", nil)
|
2022-08-19 12:27:34 +00:00
|
|
|
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
|
2024-01-30 15:53:10 +00:00
|
|
|
lb, ok := b.(*diyBackend)
|
2022-08-19 12:27:34 +00:00
|
|
|
assert.True(t, ok)
|
|
|
|
assert.NotNil(t, lb)
|
|
|
|
|
2024-01-30 15:53:10 +00:00
|
|
|
chkpath := lb.stackPath(ctx, aStackRef.(*diyBackendReference))
|
2022-08-19 12:27:34 +00:00
|
|
|
bytes, err := lb.bucket.ReadAll(context.Background(), chkpath)
|
|
|
|
assert.NoError(t, err)
|
|
|
|
state := string(bytes)
|
|
|
|
assert.Contains(t, state, "<html@tags>")
|
|
|
|
}
|
2023-03-17 19:39:45 +00:00
|
|
|
|
2024-01-30 15:53:10 +00:00
|
|
|
func TestDIYBackendRejectsStackInitOptions(t *testing.T) {
|
2023-03-17 19:39:45 +00:00
|
|
|
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.
|
2023-03-20 21:12:17 +00:00
|
|
|
illegalOptions := &backend.CreateStackOptions{Teams: []string{"red-team"}}
|
2023-03-17 19:39:45 +00:00
|
|
|
|
2024-01-30 15:53:10 +00:00
|
|
|
// • Create a mock diy backend
|
2023-03-17 19:39:45 +00:00
|
|
|
tmpDir := t.TempDir()
|
2023-12-12 12:19:42 +00:00
|
|
|
dirURI := "file://" + filepath.ToSlash(tmpDir)
|
2024-01-30 15:53:10 +00:00
|
|
|
diy, err := New(context.Background(), diagtest.LogSink(t), dirURI, nil)
|
2023-03-17 19:39:45 +00:00
|
|
|
assert.NoError(t, err)
|
|
|
|
ctx := context.Background()
|
|
|
|
|
|
|
|
// • Simulate `pulumi stack init`, passing non-nil init options
|
2024-01-30 15:53:10 +00:00
|
|
|
fakeStackRef, err := diy.ParseStackReference("organization/b/foobar")
|
2023-03-17 19:39:45 +00:00
|
|
|
assert.NoError(t, err)
|
2024-01-30 15:53:10 +00:00
|
|
|
_, err = diy.CreateStack(ctx, fakeStackRef, "", illegalOptions)
|
2023-03-20 21:30:53 +00:00
|
|
|
assert.ErrorIs(t, err, backend.ErrTeamsNotSupported)
|
2023-03-17 19:39:45 +00:00
|
|
|
}
|
filestate: Track a state metadata file (.pulumi/Pulumi.yaml)
We want the filestate backend to support project-scoped stacks,
but we can't make the change as-is because it would break old states
with new CLIs.
To differentiate between old and new states,
we've decided to introduce the concept of state metadata.
This is a file under the path .pulumi/Pulumi.yaml
that tracks metadata necessary for the filestate backend to operate.
Initially, this contains just one field: `version`,
with the initial value of 0 representing non-project or "legacy mode".
This changes the filestate layout to track such a file,
creating it if it doesn't exist with the default value of 0.
In a future change, we'll introduce "version 1",
which adds support for project-scoped stacks.
If we ever need to make breaking changes to the layout,
the version in this file will help the CLI decide
whether it's allowed to handle that state bucket
without corrupting it.
Note that this differs slightly
from the initial implementation of this functionality in #12134.
Particularly, this idempotently ensures that a Pulumi.yaml exists,
allowing `version: 0` to indicate legacy mode,
versus the original implementation that treated absence of the file
in a non-empty bucket as legacy mode.
This drops the bucket.IsAccessible check from filestate.New
because accessibility is now verified
when we try to read the metadata file.
Extracted from #12437
2023-03-22 19:14:05 +00:00
|
|
|
|
2023-02-10 12:24:28 +00:00
|
|
|
func TestLegacyFolderStructure(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
// Make a dummy stack file in the legacy location
|
|
|
|
tmpDir := t.TempDir()
|
|
|
|
err := os.MkdirAll(path.Join(tmpDir, ".pulumi", "stacks"), os.ModePerm)
|
|
|
|
require.NoError(t, err)
|
|
|
|
err = os.WriteFile(path.Join(tmpDir, ".pulumi", "stacks", "a.json"), []byte("{}"), os.ModePerm)
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
2024-01-30 15:53:10 +00:00
|
|
|
// Login to a temp dir diy backend
|
2023-02-10 12:24:28 +00:00
|
|
|
ctx := context.Background()
|
|
|
|
b, err := New(ctx, diagtest.LogSink(t), "file://"+filepath.ToSlash(tmpDir), nil)
|
|
|
|
require.NoError(t, err)
|
|
|
|
// Check the backend says it's NOT in project mode
|
2024-01-30 15:53:10 +00:00
|
|
|
lb, ok := b.(*diyBackend)
|
2023-02-10 12:24:28 +00:00
|
|
|
assert.True(t, ok)
|
|
|
|
assert.NotNil(t, lb)
|
|
|
|
assert.IsType(t, &legacyReferenceStore{}, lb.store)
|
|
|
|
|
|
|
|
// Check that list stack shows that stack
|
|
|
|
stacks, token, err := b.ListStacks(ctx, backend.ListStacksFilter{}, nil /* inContToken */)
|
|
|
|
assert.NoError(t, err)
|
|
|
|
assert.Nil(t, token)
|
|
|
|
assert.Len(t, stacks, 1)
|
|
|
|
assert.Equal(t, "a", stacks[0].Name().String())
|
|
|
|
|
|
|
|
// Create a new non-project stack
|
|
|
|
bRef, err := b.ParseStackReference("b")
|
|
|
|
assert.NoError(t, err)
|
|
|
|
assert.Equal(t, "b", bRef.String())
|
|
|
|
bStack, err := b.CreateStack(ctx, bRef, "", nil)
|
|
|
|
assert.NoError(t, err)
|
|
|
|
assert.Equal(t, "b", bStack.Ref().String())
|
|
|
|
assert.FileExists(t, path.Join(tmpDir, ".pulumi", "stacks", "b.json"))
|
|
|
|
}
|
|
|
|
|
2023-05-22 13:13:13 +00:00
|
|
|
func TestListStacksFilter(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
|
2024-01-30 15:53:10 +00:00
|
|
|
// Login to a temp dir diy backend
|
2023-05-22 13:13:13 +00:00
|
|
|
ctx := context.Background()
|
|
|
|
tmpDir := t.TempDir()
|
|
|
|
b, err := New(ctx, diagtest.LogSink(t), "file://"+filepath.ToSlash(tmpDir), nil)
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
// Create two different project stack
|
|
|
|
aRef, err := b.ParseStackReference("organization/proj1/a")
|
|
|
|
assert.NoError(t, err)
|
|
|
|
_, err = b.CreateStack(ctx, aRef, "", nil)
|
|
|
|
assert.NoError(t, err)
|
|
|
|
|
|
|
|
bRef, err := b.ParseStackReference("organization/proj2/b")
|
|
|
|
assert.NoError(t, err)
|
|
|
|
_, err = b.CreateStack(ctx, bRef, "", nil)
|
|
|
|
assert.NoError(t, err)
|
|
|
|
|
|
|
|
// Check that list stack with a filter only shows one stack
|
|
|
|
filter := "proj1"
|
|
|
|
stacks, token, err := b.ListStacks(ctx, backend.ListStacksFilter{
|
|
|
|
Project: &filter,
|
|
|
|
}, nil /* inContToken */)
|
|
|
|
assert.NoError(t, err)
|
|
|
|
assert.Nil(t, token)
|
|
|
|
assert.Len(t, stacks, 1)
|
|
|
|
assert.Equal(t, "organization/proj1/a", stacks[0].Name().String())
|
|
|
|
}
|
|
|
|
|
2023-02-10 12:24:28 +00:00
|
|
|
func TestOptIntoLegacyFolderStructure(t *testing.T) {
|
2023-03-24 21:24:06 +00:00
|
|
|
t.Parallel()
|
2023-02-10 12:24:28 +00:00
|
|
|
|
|
|
|
tmpDir := t.TempDir()
|
|
|
|
ctx := context.Background()
|
2023-10-18 10:52:54 +00:00
|
|
|
s := make(env.MapStore)
|
2024-01-30 15:53:10 +00:00
|
|
|
s[env.DIYBackendLegacyLayout.Var().Name()] = "true"
|
|
|
|
b, err := newDIYBackend(ctx, diagtest.LogSink(t), "file://"+filepath.ToSlash(tmpDir), nil,
|
|
|
|
&diyBackendOptions{Env: env.NewEnv(s)},
|
2023-03-24 21:24:06 +00:00
|
|
|
)
|
2023-02-10 12:24:28 +00:00
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
// Verify that a new stack is created in the legacy location.
|
|
|
|
foo, err := b.ParseStackReference("foo")
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
_, err = b.CreateStack(ctx, foo, "", nil)
|
|
|
|
require.NoError(t, err)
|
|
|
|
assert.FileExists(t, filepath.Join(tmpDir, ".pulumi", "stacks", "foo.json"))
|
|
|
|
}
|
|
|
|
|
2023-02-10 12:24:28 +00:00
|
|
|
// Verifies that the StackReference.String method
|
|
|
|
// takes the current project name into account,
|
|
|
|
// even if the current project name changes
|
|
|
|
// after the stack reference is created.
|
|
|
|
func TestStackReferenceString_currentProjectChange(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
dir := t.TempDir()
|
|
|
|
ctx := context.Background()
|
|
|
|
|
|
|
|
b, err := New(ctx, diagtest.LogSink(t), "file://"+filepath.ToSlash(dir), nil)
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
foo, err := b.ParseStackReference("organization/proj1/foo")
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
bar, err := b.ParseStackReference("organization/proj2/bar")
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
assert.Equal(t, "organization/proj1/foo", foo.String())
|
|
|
|
assert.Equal(t, "organization/proj2/bar", bar.String())
|
|
|
|
|
|
|
|
// Change the current project name
|
|
|
|
b.SetCurrentProject(&workspace.Project{Name: "proj1"})
|
|
|
|
|
|
|
|
assert.Equal(t, "foo", foo.String())
|
|
|
|
assert.Equal(t, "organization/proj2/bar", bar.String())
|
|
|
|
}
|
|
|
|
|
|
|
|
// Verifies that there's no data race in calling StackReference.String
|
2024-01-30 15:53:10 +00:00
|
|
|
// and diyBackend.SetCurrentProject concurrently.
|
2023-02-10 12:24:28 +00:00
|
|
|
func TestStackReferenceString_currentProjectChange_race(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
const N = 1000
|
|
|
|
|
|
|
|
dir := t.TempDir()
|
|
|
|
ctx := context.Background()
|
|
|
|
|
|
|
|
b, err := New(ctx, diagtest.LogSink(t), "file://"+filepath.ToSlash(dir), nil)
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
projects := make([]*workspace.Project, N)
|
|
|
|
refs := make([]backend.StackReference, N)
|
|
|
|
for i := 0; i < N; i++ {
|
|
|
|
name := fmt.Sprintf("proj%d", i)
|
|
|
|
projects[i] = &workspace.Project{Name: tokens.PackageName(name)}
|
|
|
|
refs[i], err = b.ParseStackReference(fmt.Sprintf("organization/%v/foo", name))
|
|
|
|
require.NoError(t, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// To exercise this data race, we'll have two goroutines.
|
|
|
|
// One goroutine will call StackReference.String repeatedly
|
|
|
|
// on all the stack references,
|
2024-01-30 15:53:10 +00:00
|
|
|
// and the other goroutine will call diyBackend.SetCurrentProject
|
2023-02-10 12:24:28 +00:00
|
|
|
// with all the projects.
|
|
|
|
|
|
|
|
var wg sync.WaitGroup
|
|
|
|
ready := make(chan struct{}) // both goroutines wait on this
|
|
|
|
|
|
|
|
wg.Add(1)
|
|
|
|
go func() {
|
|
|
|
defer wg.Done()
|
|
|
|
<-ready
|
|
|
|
for i := 0; i < N; i++ {
|
|
|
|
_ = refs[i].String()
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
|
|
|
wg.Add(1)
|
|
|
|
go func() {
|
|
|
|
defer wg.Done()
|
|
|
|
<-ready
|
|
|
|
for i := 0; i < N; i++ {
|
|
|
|
b.SetCurrentProject(projects[i])
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
|
|
|
close(ready) // start racing
|
|
|
|
wg.Wait()
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestProjectFolderStructure(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
|
2024-01-30 15:53:10 +00:00
|
|
|
// Login to a temp dir diy backend
|
2023-06-22 12:33:03 +00:00
|
|
|
|
|
|
|
// Make a dummy file in the legacy location which isn't a stack file, we should still automatically turn
|
|
|
|
// this into project mode.
|
2023-02-10 12:24:28 +00:00
|
|
|
tmpDir := t.TempDir()
|
2023-06-22 12:33:03 +00:00
|
|
|
err := os.MkdirAll(path.Join(tmpDir, ".pulumi", "plugins"), os.ModePerm)
|
|
|
|
require.NoError(t, err)
|
|
|
|
err = os.MkdirAll(path.Join(tmpDir, ".pulumi", "stacks"), os.ModePerm)
|
|
|
|
require.NoError(t, err)
|
|
|
|
err = os.WriteFile(path.Join(tmpDir, ".pulumi", "stacks", "a.txt"), []byte("{}"), os.ModePerm)
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
2023-02-10 12:24:28 +00:00
|
|
|
ctx := context.Background()
|
|
|
|
b, err := New(ctx, diagtest.LogSink(t), "file://"+filepath.ToSlash(tmpDir), nil)
|
|
|
|
assert.NoError(t, err)
|
|
|
|
|
|
|
|
// Check the backend says it's in project mode
|
2024-01-30 15:53:10 +00:00
|
|
|
lb, ok := b.(*diyBackend)
|
2023-02-10 12:24:28 +00:00
|
|
|
assert.True(t, ok)
|
|
|
|
assert.NotNil(t, lb)
|
|
|
|
assert.IsType(t, &projectReferenceStore{}, lb.store)
|
|
|
|
|
|
|
|
// Make a dummy stack file in the new project location
|
|
|
|
err = os.MkdirAll(path.Join(tmpDir, ".pulumi", "stacks", "testproj"), os.ModePerm)
|
|
|
|
assert.NoError(t, err)
|
|
|
|
err = os.WriteFile(path.Join(tmpDir, ".pulumi", "stacks", "testproj", "a.json"), []byte("{}"), os.ModePerm)
|
|
|
|
assert.NoError(t, err)
|
|
|
|
|
|
|
|
// Check that testproj is reported as existing
|
2023-06-21 15:46:54 +00:00
|
|
|
exists, err := b.DoesProjectExist(ctx, "", "testproj")
|
2023-02-10 12:24:28 +00:00
|
|
|
assert.NoError(t, err)
|
|
|
|
assert.True(t, exists)
|
|
|
|
|
|
|
|
// Check that list stack shows that stack
|
|
|
|
stacks, token, err := b.ListStacks(ctx, backend.ListStacksFilter{}, nil /* inContToken */)
|
|
|
|
assert.NoError(t, err)
|
|
|
|
assert.Nil(t, token)
|
|
|
|
assert.Len(t, stacks, 1)
|
|
|
|
assert.Equal(t, "organization/testproj/a", stacks[0].Name().String())
|
|
|
|
|
|
|
|
// Create a new project stack
|
|
|
|
bRef, err := b.ParseStackReference("organization/testproj/b")
|
|
|
|
assert.NoError(t, err)
|
|
|
|
assert.Equal(t, "organization/testproj/b", bRef.String())
|
|
|
|
bStack, err := b.CreateStack(ctx, bRef, "", nil)
|
|
|
|
assert.NoError(t, err)
|
|
|
|
assert.Equal(t, "organization/testproj/b", bStack.Ref().String())
|
|
|
|
assert.FileExists(t, path.Join(tmpDir, ".pulumi", "stacks", "testproj", "b.json"))
|
|
|
|
}
|
|
|
|
|
|
|
|
func chdir(t *testing.T, dir string) {
|
|
|
|
cwd, err := os.Getwd()
|
|
|
|
require.NoError(t, err)
|
|
|
|
require.NoError(t, os.Chdir(dir)) // Set directory
|
|
|
|
t.Cleanup(func() {
|
|
|
|
require.NoError(t, os.Chdir(cwd)) // Restore directory
|
|
|
|
restoredDir, err := os.Getwd()
|
|
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, cwd, restoredDir)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
//nolint:paralleltest // mutates cwd
|
|
|
|
func TestProjectNameMustMatch(t *testing.T) {
|
|
|
|
// Create a new project
|
|
|
|
projectDir := t.TempDir()
|
|
|
|
pyaml := filepath.Join(projectDir, "Pulumi.yaml")
|
|
|
|
err := os.WriteFile(pyaml, []byte("name: my-project\nruntime: test"), 0o600)
|
|
|
|
require.NoError(t, err)
|
|
|
|
proj, err := workspace.LoadProject(pyaml)
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
chdir(t, projectDir)
|
|
|
|
|
2024-01-30 15:53:10 +00:00
|
|
|
// Login to a temp dir diy backend
|
2023-02-10 12:24:28 +00:00
|
|
|
tmpDir := t.TempDir()
|
|
|
|
ctx := context.Background()
|
|
|
|
b, err := New(ctx, diagtest.LogSink(t), "file://"+filepath.ToSlash(tmpDir), proj)
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
// Create a new implicit-project stack
|
|
|
|
aRef, err := b.ParseStackReference("a")
|
|
|
|
assert.NoError(t, err)
|
|
|
|
assert.Equal(t, "a", aRef.String())
|
|
|
|
aStack, err := b.CreateStack(ctx, aRef, "", nil)
|
|
|
|
assert.NoError(t, err)
|
|
|
|
assert.Equal(t, "a", aStack.Ref().String())
|
|
|
|
assert.FileExists(t, path.Join(tmpDir, ".pulumi", "stacks", "my-project", "a.json"))
|
|
|
|
|
|
|
|
// Create a new project stack with the wrong project name
|
|
|
|
bRef, err := b.ParseStackReference("organization/not-my-project/b")
|
|
|
|
assert.NoError(t, err)
|
|
|
|
assert.Equal(t, "organization/not-my-project/b", bRef.String())
|
|
|
|
bStack, err := b.CreateStack(ctx, bRef, "", nil)
|
|
|
|
assert.Error(t, err)
|
|
|
|
assert.Nil(t, bStack)
|
|
|
|
|
|
|
|
// Create a new project stack with the right project name
|
|
|
|
cRef, err := b.ParseStackReference("organization/my-project/c")
|
|
|
|
assert.NoError(t, err)
|
|
|
|
assert.Equal(t, "c", cRef.String())
|
|
|
|
cStack, err := b.CreateStack(ctx, cRef, "", nil)
|
|
|
|
assert.NoError(t, err)
|
|
|
|
assert.Equal(t, "c", cStack.Ref().String())
|
|
|
|
assert.FileExists(t, path.Join(tmpDir, ".pulumi", "stacks", "my-project", "c.json"))
|
|
|
|
}
|
|
|
|
|
2023-03-23 18:49:54 +00:00
|
|
|
func TestNew_legacyFileWarning(t *testing.T) {
|
2023-03-24 21:24:06 +00:00
|
|
|
t.Parallel()
|
|
|
|
|
2023-03-23 18:49:54 +00:00
|
|
|
// Verifies the names of files printed in warnings
|
|
|
|
// when legacy files are found while running in project mode.
|
|
|
|
|
|
|
|
tests := []struct {
|
|
|
|
desc string
|
|
|
|
files map[string]string
|
2023-10-18 10:52:54 +00:00
|
|
|
env env.MapStore
|
2023-03-23 18:49:54 +00:00
|
|
|
wantOut string
|
|
|
|
}{
|
|
|
|
{
|
|
|
|
desc: "no legacy stacks",
|
|
|
|
files: map[string]string{
|
|
|
|
// Should ignore non-stack files.
|
|
|
|
".pulumi/foo/extraneous_file": "",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
desc: "legacy stacks",
|
|
|
|
files: map[string]string{
|
|
|
|
".pulumi/stacks/a.json": "{}",
|
|
|
|
".pulumi/stacks/b.json": "{}",
|
|
|
|
".pulumi/stacks/c.json.bak": "{}", // should ignore backup files
|
|
|
|
},
|
|
|
|
wantOut: "warning: Found legacy stack files in state store:\n" +
|
|
|
|
" - a\n" +
|
|
|
|
" - b\n" +
|
2023-02-10 12:24:28 +00:00
|
|
|
"Please run 'pulumi state upgrade' to migrate them to the new format.\n" +
|
2024-01-30 09:00:15 +00:00
|
|
|
"Set PULUMI_DIY_BACKEND_NO_LEGACY_WARNING=1 to disable this warning.\n",
|
2023-03-23 18:49:54 +00:00
|
|
|
},
|
|
|
|
{
|
|
|
|
desc: "warning opt-out",
|
|
|
|
files: map[string]string{
|
|
|
|
".pulumi/stacks/a.json": "{}",
|
|
|
|
".pulumi/stacks/b.json": "{}",
|
|
|
|
},
|
|
|
|
env: map[string]string{
|
2024-01-30 09:00:15 +00:00
|
|
|
"PULUMI_DIY_BACKEND_NO_LEGACY_WARNING": "true",
|
2023-03-23 18:49:54 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, tt := range tests {
|
2023-03-24 21:24:06 +00:00
|
|
|
tt := tt
|
2023-03-23 18:49:54 +00:00
|
|
|
t.Run(tt.desc, func(t *testing.T) {
|
2023-03-24 21:24:06 +00:00
|
|
|
t.Parallel()
|
2023-03-23 18:49:54 +00:00
|
|
|
|
|
|
|
stateDir := t.TempDir()
|
|
|
|
bucket, err := fileblob.OpenBucket(stateDir, nil)
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
ctx := context.Background()
|
|
|
|
require.NoError(t,
|
|
|
|
bucket.WriteAll(ctx, ".pulumi/meta.yaml", []byte("version: 1"), nil),
|
|
|
|
"write meta.yaml")
|
|
|
|
|
|
|
|
for path, contents := range tt.files {
|
|
|
|
require.NoError(t, bucket.WriteAll(ctx, path, []byte(contents), nil),
|
|
|
|
"write %q", path)
|
|
|
|
}
|
|
|
|
|
|
|
|
var buff bytes.Buffer
|
|
|
|
sink := diag.DefaultSink(io.Discard, &buff, diag.FormatOptions{Color: colors.Never})
|
2023-03-24 21:24:06 +00:00
|
|
|
|
2024-01-30 15:53:10 +00:00
|
|
|
_, err = newDIYBackend(ctx, sink, "file://"+filepath.ToSlash(stateDir), nil,
|
|
|
|
&diyBackendOptions{Env: env.NewEnv(tt.env)})
|
2023-03-23 18:49:54 +00:00
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
assert.Equal(t, tt.wantOut, buff.String())
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-10 12:24:28 +00:00
|
|
|
func TestLegacyUpgrade(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
// Make a dummy stack file in the legacy location
|
|
|
|
tmpDir := t.TempDir()
|
|
|
|
err := os.MkdirAll(path.Join(tmpDir, ".pulumi", "stacks"), os.ModePerm)
|
|
|
|
require.NoError(t, err)
|
|
|
|
err = os.WriteFile(path.Join(tmpDir, ".pulumi", "stacks", "a.json"), []byte(`{
|
|
|
|
"latest": {
|
|
|
|
"resources": [
|
|
|
|
{
|
|
|
|
"type": "package:module:resource",
|
|
|
|
"urn": "urn:pulumi:stack::project::package:module:resource::name"
|
|
|
|
}
|
|
|
|
]
|
|
|
|
}
|
|
|
|
}`), os.ModePerm)
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
var output bytes.Buffer
|
|
|
|
sink := diag.DefaultSink(&output, &output, diag.FormatOptions{Color: colors.Never})
|
|
|
|
|
2024-01-30 15:53:10 +00:00
|
|
|
// Login to a temp dir diy backend
|
2023-02-10 12:24:28 +00:00
|
|
|
ctx := context.Background()
|
|
|
|
b, err := New(ctx, sink, "file://"+filepath.ToSlash(tmpDir), nil)
|
|
|
|
require.NoError(t, err)
|
|
|
|
// Check the backend says it's NOT in project mode
|
2024-01-30 15:53:10 +00:00
|
|
|
lb, ok := b.(*diyBackend)
|
2023-02-10 12:24:28 +00:00
|
|
|
assert.True(t, ok)
|
|
|
|
assert.NotNil(t, lb)
|
|
|
|
assert.IsType(t, &legacyReferenceStore{}, lb.store)
|
|
|
|
|
2023-04-04 22:19:35 +00:00
|
|
|
err = lb.Upgrade(ctx, nil /* opts */)
|
2023-02-10 12:24:28 +00:00
|
|
|
require.NoError(t, err)
|
|
|
|
assert.IsType(t, &projectReferenceStore{}, lb.store)
|
|
|
|
|
|
|
|
assert.Contains(t, output.String(), "Upgraded 1 stack(s) to project mode")
|
|
|
|
|
|
|
|
// Check that a has been moved
|
|
|
|
aStackRef, err := lb.parseStackReference("organization/project/a")
|
|
|
|
require.NoError(t, err)
|
2023-04-03 21:36:44 +00:00
|
|
|
stackFileExists, err := lb.bucket.Exists(ctx, lb.stackPath(ctx, aStackRef))
|
2023-02-10 12:24:28 +00:00
|
|
|
require.NoError(t, err)
|
|
|
|
assert.True(t, stackFileExists)
|
|
|
|
|
|
|
|
// Write b.json and upgrade again
|
|
|
|
err = os.WriteFile(path.Join(tmpDir, ".pulumi", "stacks", "b.json"), []byte(`{
|
|
|
|
"latest": {
|
|
|
|
"resources": [
|
|
|
|
{
|
|
|
|
"type": "package:module:resource",
|
|
|
|
"urn": "urn:pulumi:stack::other-project::package:module:resource::name"
|
|
|
|
}
|
|
|
|
]
|
|
|
|
}
|
|
|
|
}`), os.ModePerm)
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
2023-04-04 22:19:35 +00:00
|
|
|
err = lb.Upgrade(ctx, nil /* opts */)
|
2023-02-10 12:24:28 +00:00
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
// Check that b has been moved
|
|
|
|
bStackRef, err := lb.parseStackReference("organization/other-project/b")
|
|
|
|
require.NoError(t, err)
|
2023-04-03 21:36:44 +00:00
|
|
|
stackFileExists, err = lb.bucket.Exists(ctx, lb.stackPath(ctx, bStackRef))
|
2023-02-10 12:24:28 +00:00
|
|
|
require.NoError(t, err)
|
|
|
|
assert.True(t, stackFileExists)
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestLegacyUpgrade_partial(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
// Verifies that we can upgrade a subset of stacks.
|
|
|
|
|
|
|
|
stateDir := t.TempDir()
|
|
|
|
bucket, err := fileblob.OpenBucket(stateDir, nil)
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
ctx := context.Background()
|
|
|
|
require.NoError(t,
|
|
|
|
bucket.WriteAll(ctx, ".pulumi/stacks/foo.json", []byte(`{
|
|
|
|
"latest": {
|
|
|
|
"resources": [
|
|
|
|
{
|
|
|
|
"type": "package:module:resource",
|
|
|
|
"urn": "urn:pulumi:stack::project::package:module:resource::name"
|
|
|
|
}
|
|
|
|
]
|
|
|
|
}
|
|
|
|
}`), nil))
|
|
|
|
require.NoError(t,
|
|
|
|
// no resources, can't guess project name
|
|
|
|
bucket.WriteAll(ctx, ".pulumi/stacks/bar.json",
|
|
|
|
[]byte(`{"latest": {"resources": []}}`), nil))
|
|
|
|
|
|
|
|
var buff bytes.Buffer
|
|
|
|
sink := diag.DefaultSink(io.Discard, &buff, diag.FormatOptions{Color: colors.Never})
|
|
|
|
b, err := New(ctx, sink, "file://"+filepath.ToSlash(stateDir), nil)
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
2023-04-04 22:19:35 +00:00
|
|
|
require.NoError(t, b.Upgrade(ctx, nil /* opts */))
|
2023-05-20 00:45:54 +00:00
|
|
|
assert.Contains(t, buff.String(), `Skipping stack "bar": no project name found`)
|
2023-02-10 12:24:28 +00:00
|
|
|
|
|
|
|
exists, err := bucket.Exists(ctx, ".pulumi/stacks/project/foo.json")
|
|
|
|
require.NoError(t, err)
|
|
|
|
assert.True(t, exists, "foo was not migrated")
|
|
|
|
|
|
|
|
ref, err := b.ParseStackReference("organization/project/foo")
|
|
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, tokens.QName("organization/project/foo"), ref.FullyQualifiedName())
|
|
|
|
}
|
|
|
|
|
2023-04-04 22:20:50 +00:00
|
|
|
// When a stack project could not be determined,
|
|
|
|
// we should fill it in with ProjectsForDetachedStacks.
|
|
|
|
func TestLegacyUpgrade_ProjectsForDetachedStacks(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
stateDir := t.TempDir()
|
|
|
|
bucket, err := fileblob.OpenBucket(stateDir, nil)
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
// Write a few empty stacks.
|
|
|
|
// These stacks have no resources, so we can't guess the project name.
|
|
|
|
ctx := context.Background()
|
|
|
|
for _, stack := range []string{"foo", "bar", "baz"} {
|
|
|
|
statePath := path.Join(".pulumi", "stacks", stack+".json")
|
|
|
|
require.NoError(t,
|
|
|
|
bucket.WriteAll(ctx, statePath,
|
|
|
|
[]byte(`{"latest": {"resources": []}}`), nil),
|
|
|
|
"write stack %s", stack)
|
|
|
|
}
|
|
|
|
|
|
|
|
var stderr bytes.Buffer
|
|
|
|
sink := diag.DefaultSink(io.Discard, &stderr, diag.FormatOptions{Color: colors.Never})
|
|
|
|
b, err := New(ctx, sink, "file://"+filepath.ToSlash(stateDir), nil)
|
|
|
|
require.NoError(t, err, "initialize backend")
|
|
|
|
|
|
|
|
// For the first two stacks, we'll return project names to upgrade them.
|
|
|
|
// For the third stack, we will not set a project name, and it should be skipped.
|
|
|
|
err = b.Upgrade(ctx, &UpgradeOptions{
|
Add tokens.StackName (#14487)
<!---
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. -->
This adds a new type `tokens.StackName` which is a relatively strongly
typed container for a stack name. The only weakly typed aspect of it is
Go will always allow the "zero" value to be created for a struct, which
for a stack name is the empty string which is invalid. To prevent
introducing unexpected empty strings when working with stack names the
`String()` method will panic for zero initialized stack names.
Apart from the zero value, all other instances of `StackName` are via
`ParseStackName` which returns a descriptive error if the string is not
valid.
This PR only updates "pkg/" to use this type. There are a number of
places in "sdk/" which could do with this type as well, but there's no
harm in doing a staggered roll out, and some parts of "sdk/" are user
facing and will probably have to stay on the current `tokens.Name` and
`tokens.QName` types.
There are two places in the system where we panic on invalid stack
names, both in the http backend. This _should_ be fine as we've had long
standing validation that stacks created in the service are valid stack
names.
Just in case people have managed to introduce invalid stack names, there
is the `PULUMI_DISABLE_VALIDATION` environment variable which will turn
off the validation _and_ panicing for stack names. Users can use that to
temporarily disable the validation and continue working, but it should
only be seen as a temporary measure. If they have invalid names they
should rename them, or if they think they should be valid raise an issue
with us to change the validation code.
## 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. -->
- [x] 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-11-15 07:44:54 +00:00
|
|
|
ProjectsForDetachedStacks: func(stacks []tokens.StackName) (projects []tokens.Name, err error) {
|
|
|
|
assert.ElementsMatch(t, []tokens.StackName{
|
|
|
|
tokens.MustParseStackName("foo"),
|
|
|
|
tokens.MustParseStackName("bar"),
|
|
|
|
tokens.MustParseStackName("baz"),
|
|
|
|
}, stacks)
|
2023-04-04 22:20:50 +00:00
|
|
|
|
|
|
|
projects = make([]tokens.Name, len(stacks))
|
|
|
|
for idx, stack := range stacks {
|
Add tokens.StackName (#14487)
<!---
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. -->
This adds a new type `tokens.StackName` which is a relatively strongly
typed container for a stack name. The only weakly typed aspect of it is
Go will always allow the "zero" value to be created for a struct, which
for a stack name is the empty string which is invalid. To prevent
introducing unexpected empty strings when working with stack names the
`String()` method will panic for zero initialized stack names.
Apart from the zero value, all other instances of `StackName` are via
`ParseStackName` which returns a descriptive error if the string is not
valid.
This PR only updates "pkg/" to use this type. There are a number of
places in "sdk/" which could do with this type as well, but there's no
harm in doing a staggered roll out, and some parts of "sdk/" are user
facing and will probably have to stay on the current `tokens.Name` and
`tokens.QName` types.
There are two places in the system where we panic on invalid stack
names, both in the http backend. This _should_ be fine as we've had long
standing validation that stacks created in the service are valid stack
names.
Just in case people have managed to introduce invalid stack names, there
is the `PULUMI_DISABLE_VALIDATION` environment variable which will turn
off the validation _and_ panicing for stack names. Users can use that to
temporarily disable the validation and continue working, but it should
only be seen as a temporary measure. If they have invalid names they
should rename them, or if they think they should be valid raise an issue
with us to change the validation code.
## 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. -->
- [x] 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-11-15 07:44:54 +00:00
|
|
|
switch stack.String() {
|
2023-04-04 22:20:50 +00:00
|
|
|
case "foo":
|
|
|
|
projects[idx] = "proj1"
|
|
|
|
case "bar":
|
|
|
|
projects[idx] = "proj2"
|
|
|
|
case "baz":
|
|
|
|
// Leave baz detached.
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return projects, nil
|
|
|
|
},
|
|
|
|
})
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
for _, stack := range []string{"foo", "bar"} {
|
|
|
|
assert.NotContains(t, stderr.String(), fmt.Sprintf("Skipping stack %q", stack))
|
|
|
|
}
|
|
|
|
assert.Contains(t, stderr.String(), fmt.Sprintf("Skipping stack %q", "baz"))
|
|
|
|
|
|
|
|
wantFiles := []string{
|
|
|
|
".pulumi/stacks/proj1/foo.json",
|
|
|
|
".pulumi/stacks/proj2/bar.json",
|
|
|
|
".pulumi/stacks/baz.json",
|
|
|
|
}
|
|
|
|
for _, file := range wantFiles {
|
|
|
|
exists, err := bucket.Exists(ctx, file)
|
|
|
|
require.NoError(t, err, "exists(%q)", file)
|
|
|
|
assert.True(t, exists, "file %q must exist", file)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// When a stack project could not be determined
|
|
|
|
// and ProjectsForDetachedStacks returns an error,
|
|
|
|
// the upgrade should fail.
|
|
|
|
func TestLegacyUpgrade_ProjectsForDetachedStacks_error(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
stateDir := t.TempDir()
|
|
|
|
bucket, err := fileblob.OpenBucket(stateDir, nil)
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
|
|
|
// We have one stack with a guessable project name, and one without.
|
|
|
|
// If ProjectsForDetachedStacks returns an error, the upgrade should
|
|
|
|
// fail for both because the user likely cancelled the upgrade.
|
|
|
|
require.NoError(t,
|
|
|
|
bucket.WriteAll(ctx, ".pulumi/stacks/foo.json", []byte(`{
|
|
|
|
"latest": {
|
|
|
|
"resources": [
|
|
|
|
{
|
|
|
|
"type": "package:module:resource",
|
|
|
|
"urn": "urn:pulumi:stack::project::package:module:resource::name"
|
|
|
|
}
|
|
|
|
]
|
|
|
|
}
|
|
|
|
}`), nil))
|
|
|
|
require.NoError(t,
|
|
|
|
bucket.WriteAll(ctx, ".pulumi/stacks/bar.json",
|
|
|
|
[]byte(`{"latest": {"resources": []}}`), nil))
|
|
|
|
|
|
|
|
sink := diag.DefaultSink(io.Discard, iotest.LogWriter(t), diag.FormatOptions{Color: colors.Never})
|
|
|
|
b, err := New(ctx, sink, "file://"+filepath.ToSlash(stateDir), nil)
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
giveErr := errors.New("canceled operation")
|
|
|
|
err = b.Upgrade(ctx, &UpgradeOptions{
|
Add tokens.StackName (#14487)
<!---
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. -->
This adds a new type `tokens.StackName` which is a relatively strongly
typed container for a stack name. The only weakly typed aspect of it is
Go will always allow the "zero" value to be created for a struct, which
for a stack name is the empty string which is invalid. To prevent
introducing unexpected empty strings when working with stack names the
`String()` method will panic for zero initialized stack names.
Apart from the zero value, all other instances of `StackName` are via
`ParseStackName` which returns a descriptive error if the string is not
valid.
This PR only updates "pkg/" to use this type. There are a number of
places in "sdk/" which could do with this type as well, but there's no
harm in doing a staggered roll out, and some parts of "sdk/" are user
facing and will probably have to stay on the current `tokens.Name` and
`tokens.QName` types.
There are two places in the system where we panic on invalid stack
names, both in the http backend. This _should_ be fine as we've had long
standing validation that stacks created in the service are valid stack
names.
Just in case people have managed to introduce invalid stack names, there
is the `PULUMI_DISABLE_VALIDATION` environment variable which will turn
off the validation _and_ panicing for stack names. Users can use that to
temporarily disable the validation and continue working, but it should
only be seen as a temporary measure. If they have invalid names they
should rename them, or if they think they should be valid raise an issue
with us to change the validation code.
## 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. -->
- [x] 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-11-15 07:44:54 +00:00
|
|
|
ProjectsForDetachedStacks: func(stacks []tokens.StackName) (projects []tokens.Name, err error) {
|
|
|
|
assert.Equal(t, []tokens.StackName{
|
|
|
|
tokens.MustParseStackName("bar"),
|
|
|
|
}, stacks)
|
2023-04-04 22:20:50 +00:00
|
|
|
return nil, giveErr
|
|
|
|
},
|
|
|
|
})
|
|
|
|
require.Error(t, err)
|
|
|
|
assert.ErrorIs(t, err, giveErr)
|
|
|
|
|
|
|
|
wantFiles := []string{
|
|
|
|
".pulumi/stacks/foo.json",
|
|
|
|
".pulumi/stacks/bar.json",
|
|
|
|
}
|
|
|
|
for _, file := range wantFiles {
|
|
|
|
exists, err := bucket.Exists(ctx, file)
|
|
|
|
require.NoError(t, err, "exists(%q)", file)
|
|
|
|
assert.True(t, exists, "file %q must exist", file)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-03-28 19:06:03 +00:00
|
|
|
// If an upgrade failed because we couldn't write the meta.yaml,
|
|
|
|
// the stacks should be left in legacy mode.
|
|
|
|
func TestLegacyUpgrade_writeMetaError(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
stateDir := t.TempDir()
|
|
|
|
bucket, err := fileblob.OpenBucket(stateDir, nil)
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
ctx := context.Background()
|
|
|
|
require.NoError(t,
|
|
|
|
bucket.WriteAll(ctx, ".pulumi/stacks/foo.json", []byte(`{
|
|
|
|
"latest": {
|
|
|
|
"resources": [
|
|
|
|
{
|
|
|
|
"type": "package:module:resource",
|
|
|
|
"urn": "urn:pulumi:stack::project::package:module:resource::name"
|
|
|
|
}
|
|
|
|
]
|
|
|
|
}
|
|
|
|
}`), nil))
|
|
|
|
|
|
|
|
// To prevent a write to meta.yaml, we'll create a directory with that name.
|
|
|
|
// The system will reject creating a file with the same name.
|
|
|
|
require.NoError(t, os.MkdirAll(filepath.Join(stateDir, ".pulumi", "meta.yaml"), 0o755))
|
|
|
|
|
|
|
|
var buff bytes.Buffer
|
|
|
|
sink := diag.DefaultSink(io.Discard, &buff, diag.FormatOptions{Color: colors.Never})
|
|
|
|
b, err := New(ctx, sink, "file://"+filepath.ToSlash(stateDir), nil)
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
2023-04-04 22:19:35 +00:00
|
|
|
require.Error(t, b.Upgrade(ctx, nil /* opts */))
|
2023-03-28 19:06:03 +00:00
|
|
|
|
|
|
|
stderr := buff.String()
|
|
|
|
assert.Contains(t, stderr, "error: Could not write new state metadata file")
|
|
|
|
assert.Contains(t, stderr, "Please verify that the storage is writable")
|
|
|
|
|
|
|
|
assert.FileExists(t, filepath.Join(stateDir, ".pulumi", "stacks", "foo.json"),
|
|
|
|
"foo.json should not have been upgraded")
|
|
|
|
}
|
|
|
|
|
filestate: Track a state metadata file (.pulumi/Pulumi.yaml)
We want the filestate backend to support project-scoped stacks,
but we can't make the change as-is because it would break old states
with new CLIs.
To differentiate between old and new states,
we've decided to introduce the concept of state metadata.
This is a file under the path .pulumi/Pulumi.yaml
that tracks metadata necessary for the filestate backend to operate.
Initially, this contains just one field: `version`,
with the initial value of 0 representing non-project or "legacy mode".
This changes the filestate layout to track such a file,
creating it if it doesn't exist with the default value of 0.
In a future change, we'll introduce "version 1",
which adds support for project-scoped stacks.
If we ever need to make breaking changes to the layout,
the version in this file will help the CLI decide
whether it's allowed to handle that state bucket
without corrupting it.
Note that this differs slightly
from the initial implementation of this functionality in #12134.
Particularly, this idempotently ensures that a Pulumi.yaml exists,
allowing `version: 0` to indicate legacy mode,
versus the original implementation that treated absence of the file
in a non-empty bucket as legacy mode.
This drops the bucket.IsAccessible check from filestate.New
because accessibility is now verified
when we try to read the metadata file.
Extracted from #12437
2023-03-22 19:14:05 +00:00
|
|
|
func TestNew_unsupportedStoreVersion(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
// Verifies that we fail to initialize a backend if the store version is
|
|
|
|
// newer than the CLI version.
|
|
|
|
|
|
|
|
stateDir := t.TempDir()
|
|
|
|
bucket, err := fileblob.OpenBucket(stateDir, nil)
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
2023-03-28 17:52:53 +00:00
|
|
|
// Set up a meta.yaml "from the future".
|
filestate: Track a state metadata file (.pulumi/Pulumi.yaml)
We want the filestate backend to support project-scoped stacks,
but we can't make the change as-is because it would break old states
with new CLIs.
To differentiate between old and new states,
we've decided to introduce the concept of state metadata.
This is a file under the path .pulumi/Pulumi.yaml
that tracks metadata necessary for the filestate backend to operate.
Initially, this contains just one field: `version`,
with the initial value of 0 representing non-project or "legacy mode".
This changes the filestate layout to track such a file,
creating it if it doesn't exist with the default value of 0.
In a future change, we'll introduce "version 1",
which adds support for project-scoped stacks.
If we ever need to make breaking changes to the layout,
the version in this file will help the CLI decide
whether it's allowed to handle that state bucket
without corrupting it.
Note that this differs slightly
from the initial implementation of this functionality in #12134.
Particularly, this idempotently ensures that a Pulumi.yaml exists,
allowing `version: 0` to indicate legacy mode,
versus the original implementation that treated absence of the file
in a non-empty bucket as legacy mode.
This drops the bucket.IsAccessible check from filestate.New
because accessibility is now verified
when we try to read the metadata file.
Extracted from #12437
2023-03-22 19:14:05 +00:00
|
|
|
ctx := context.Background()
|
|
|
|
require.NoError(t,
|
2023-03-28 17:52:53 +00:00
|
|
|
bucket.WriteAll(ctx, ".pulumi/meta.yaml", []byte("version: 999999999"), nil))
|
filestate: Track a state metadata file (.pulumi/Pulumi.yaml)
We want the filestate backend to support project-scoped stacks,
but we can't make the change as-is because it would break old states
with new CLIs.
To differentiate between old and new states,
we've decided to introduce the concept of state metadata.
This is a file under the path .pulumi/Pulumi.yaml
that tracks metadata necessary for the filestate backend to operate.
Initially, this contains just one field: `version`,
with the initial value of 0 representing non-project or "legacy mode".
This changes the filestate layout to track such a file,
creating it if it doesn't exist with the default value of 0.
In a future change, we'll introduce "version 1",
which adds support for project-scoped stacks.
If we ever need to make breaking changes to the layout,
the version in this file will help the CLI decide
whether it's allowed to handle that state bucket
without corrupting it.
Note that this differs slightly
from the initial implementation of this functionality in #12134.
Particularly, this idempotently ensures that a Pulumi.yaml exists,
allowing `version: 0` to indicate legacy mode,
versus the original implementation that treated absence of the file
in a non-empty bucket as legacy mode.
This drops the bucket.IsAccessible check from filestate.New
because accessibility is now verified
when we try to read the metadata file.
Extracted from #12437
2023-03-22 19:14:05 +00:00
|
|
|
|
|
|
|
_, err = New(ctx, diagtest.LogSink(t), "file://"+filepath.ToSlash(stateDir), nil)
|
|
|
|
assert.ErrorContains(t, err, "state store unsupported")
|
2023-03-28 17:52:53 +00:00
|
|
|
assert.ErrorContains(t, err, "'meta.yaml' version (999999999) is not supported")
|
filestate: Track a state metadata file (.pulumi/Pulumi.yaml)
We want the filestate backend to support project-scoped stacks,
but we can't make the change as-is because it would break old states
with new CLIs.
To differentiate between old and new states,
we've decided to introduce the concept of state metadata.
This is a file under the path .pulumi/Pulumi.yaml
that tracks metadata necessary for the filestate backend to operate.
Initially, this contains just one field: `version`,
with the initial value of 0 representing non-project or "legacy mode".
This changes the filestate layout to track such a file,
creating it if it doesn't exist with the default value of 0.
In a future change, we'll introduce "version 1",
which adds support for project-scoped stacks.
If we ever need to make breaking changes to the layout,
the version in this file will help the CLI decide
whether it's allowed to handle that state bucket
without corrupting it.
Note that this differs slightly
from the initial implementation of this functionality in #12134.
Particularly, this idempotently ensures that a Pulumi.yaml exists,
allowing `version: 0` to indicate legacy mode,
versus the original implementation that treated absence of the file
in a non-empty bucket as legacy mode.
This drops the bucket.IsAccessible check from filestate.New
because accessibility is now verified
when we try to read the metadata file.
Extracted from #12437
2023-03-22 19:14:05 +00:00
|
|
|
}
|
This commit adds the `Created` and `Modified` timestamps to pulumi state that are optional.
`Created`: Created tracks when the remote resource was first added to state by pulumi. Checkpoints prior to early 2023 do not include this. (Create, Import)
`Modified`: Modified tracks when the resource state was last altered. Checkpoints prior to early 2023 do not include this. (Create, Import, Read, Refresh, Update)
When serialized they will follow RFC3339 with nanoseconds captured by a test case.
https://pkg.go.dev/time#RFC3339
Note: Older versions of pulumi may strip these fields when modifying the state.
For future expansion, when we inevitably need to track other timestamps, we'll add a new "operationTimestamps" field (or something similarly named that clarified these are timestamps of the actual Pulumi operations).
operationTimestamps: {
created: ...,
updated: ...,
imported: ...,
}
Fixes https://github.com/pulumi/pulumi/issues/12022
2023-02-06 20:39:11 +00:00
|
|
|
|
|
|
|
// TestSerializeTimestampRFC3339 captures our expectations that Created and Modified will be serialized to
|
|
|
|
// RFC3339.
|
|
|
|
func TestSerializeTimestampRFC3339(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
created := time.Now().UTC()
|
|
|
|
modified := created.Add(time.Hour)
|
|
|
|
|
|
|
|
deployment, err := makeUntypedDeploymentTimestamp("b", "123abc",
|
|
|
|
"v1:C7H2a7/Ietk=:v1:yfAd1zOi6iY9DRIB:dumdsr+H89VpHIQWdB01XEFqYaYjAg==", &created, &modified)
|
|
|
|
assert.NoError(t, err)
|
|
|
|
|
|
|
|
createdStr := created.Format(time.RFC3339Nano)
|
|
|
|
modifiedStr := modified.Format(time.RFC3339Nano)
|
|
|
|
assert.Contains(t, string(deployment.Deployment), createdStr)
|
|
|
|
assert.Contains(t, string(deployment.Deployment), modifiedStr)
|
|
|
|
}
|
2023-03-23 22:47:50 +00:00
|
|
|
|
|
|
|
func TestUpgrade_manyFailures(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
const (
|
|
|
|
numStacks = 100
|
|
|
|
badStackBody = `{"latest": {"resources": []}}`
|
|
|
|
)
|
|
|
|
|
|
|
|
tmpDir := t.TempDir()
|
|
|
|
|
|
|
|
bucket, err := fileblob.OpenBucket(tmpDir, nil)
|
|
|
|
require.NoError(t, err)
|
|
|
|
ctx := context.Background()
|
|
|
|
for i := 0; i < numStacks; i++ {
|
|
|
|
stackPath := path.Join(".pulumi", "stacks", fmt.Sprintf("stack-%d.json", i))
|
|
|
|
require.NoError(t, bucket.WriteAll(ctx, stackPath, []byte(badStackBody), nil))
|
|
|
|
}
|
|
|
|
|
|
|
|
var output bytes.Buffer
|
|
|
|
sink := diag.DefaultSink(io.Discard, &output, diag.FormatOptions{Color: colors.Never})
|
|
|
|
|
2024-01-30 15:53:10 +00:00
|
|
|
// Login to a temp dir diy backend
|
2023-03-23 22:47:50 +00:00
|
|
|
b, err := New(ctx, sink, "file://"+filepath.ToSlash(tmpDir), nil)
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
2023-04-04 22:19:35 +00:00
|
|
|
require.NoError(t, b.Upgrade(ctx, nil /* opts */))
|
2023-03-23 22:47:50 +00:00
|
|
|
out := output.String()
|
|
|
|
for i := 0; i < numStacks; i++ {
|
|
|
|
assert.Contains(t, out, fmt.Sprintf(`Skipping stack "stack-%d"`, i))
|
|
|
|
}
|
|
|
|
}
|
2023-03-24 21:24:06 +00:00
|
|
|
|
|
|
|
func TestCreateStack_gzip(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
stateDir := t.TempDir()
|
|
|
|
ctx := context.Background()
|
2023-10-18 10:52:54 +00:00
|
|
|
|
|
|
|
s := make(env.MapStore)
|
2024-01-30 15:53:10 +00:00
|
|
|
s[env.DIYBackendGzip.Var().Name()] = "true"
|
2023-10-18 10:52:54 +00:00
|
|
|
|
2024-01-30 15:53:10 +00:00
|
|
|
b, err := newDIYBackend(
|
2023-03-24 21:24:06 +00:00
|
|
|
ctx,
|
|
|
|
diagtest.LogSink(t), "file://"+filepath.ToSlash(stateDir),
|
|
|
|
&workspace.Project{Name: "testproj"},
|
2024-01-30 15:53:10 +00:00
|
|
|
&diyBackendOptions{Env: env.NewEnv(s)},
|
2023-03-24 21:24:06 +00:00
|
|
|
)
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
fooRef, err := b.ParseStackReference("foo")
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
_, err = b.CreateStack(ctx, fooRef, "", nil)
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
2024-01-30 09:00:15 +00:00
|
|
|
// With PULUMI_DIY_BACKEND_GZIP enabled,
|
2023-03-24 21:24:06 +00:00
|
|
|
// we'll store state into gzipped files.
|
|
|
|
assert.FileExists(t, filepath.Join(stateDir, ".pulumi", "stacks", "testproj", "foo.json.gz"))
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestCreateStack_retainCheckpoints(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
|
|
|
|
stateDir := t.TempDir()
|
|
|
|
ctx := context.Background()
|
2023-10-18 10:52:54 +00:00
|
|
|
|
|
|
|
s := make(env.MapStore)
|
2024-01-30 15:53:10 +00:00
|
|
|
s[env.DIYBackendRetainCheckpoints.Var().Name()] = "true"
|
2023-10-18 10:52:54 +00:00
|
|
|
|
2024-01-30 15:53:10 +00:00
|
|
|
b, err := newDIYBackend(
|
2023-03-24 21:24:06 +00:00
|
|
|
ctx,
|
|
|
|
diagtest.LogSink(t), "file://"+filepath.ToSlash(stateDir),
|
|
|
|
&workspace.Project{Name: "testproj"},
|
2024-01-30 15:53:10 +00:00
|
|
|
&diyBackendOptions{Env: env.NewEnv(s)},
|
2023-03-24 21:24:06 +00:00
|
|
|
)
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
fooRef, err := b.ParseStackReference("foo")
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
_, err = b.CreateStack(ctx, fooRef, "", nil)
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
// With PULUMI_RETAIN_CHECKPOINTS enabled,
|
|
|
|
// we'll make copies of files under $orig.$timestamp.
|
|
|
|
// Since we can't predict the timestamp,
|
|
|
|
// we'll just check that there's at least one file
|
|
|
|
// with a timestamp extension.
|
|
|
|
got, err := filepath.Glob(
|
|
|
|
filepath.Join(stateDir, ".pulumi", "stacks", "testproj", "foo.json.*"))
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
checkpointExtRe := regexp.MustCompile(`^\.[0-9]+$`)
|
|
|
|
var found bool
|
|
|
|
for _, f := range got {
|
|
|
|
if checkpointExtRe.MatchString(filepath.Ext(f)) {
|
|
|
|
found = true
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
assert.True(t, found,
|
|
|
|
"file with a timestamp extension not found in %v", got)
|
|
|
|
}
|
2023-12-04 15:12:56 +00:00
|
|
|
|
|
|
|
//nolint:paralleltest // mutates global state
|
|
|
|
func TestDisableIntegrityChecking(t *testing.T) {
|
|
|
|
stateDir := t.TempDir()
|
|
|
|
ctx := context.Background()
|
|
|
|
b, err := New(ctx, diagtest.LogSink(t), "file://"+filepath.ToSlash(stateDir), &workspace.Project{Name: "testproj"})
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
ref, err := b.ParseStackReference("stack")
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
s, err := b.CreateStack(ctx, ref, "", nil)
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
// make up a bad stack
|
|
|
|
deployment := apitype.UntypedDeployment{
|
|
|
|
Version: 3,
|
|
|
|
Deployment: json.RawMessage(`{
|
|
|
|
"resources": [
|
|
|
|
{
|
|
|
|
"urn": "urn:pulumi:stack::proj::type::name1",
|
|
|
|
"type": "type",
|
|
|
|
"parent": "urn:pulumi:stack::proj::type::name2"
|
|
|
|
},
|
|
|
|
{
|
|
|
|
"urn": "urn:pulumi:stack::proj::type::name2",
|
|
|
|
"type": "type"
|
|
|
|
}
|
|
|
|
]
|
|
|
|
}`),
|
|
|
|
}
|
|
|
|
|
|
|
|
// Import deployment doesn't verify the deployment
|
|
|
|
err = b.ImportDeployment(ctx, s, &deployment)
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
backend.DisableIntegrityChecking = false
|
2024-01-05 08:32:56 +00:00
|
|
|
snap, err := s.Snapshot(ctx, b64.Base64SecretsProvider)
|
2023-12-04 15:12:56 +00:00
|
|
|
require.ErrorContains(t, err,
|
|
|
|
"child resource urn:pulumi:stack::proj::type::name1's parent urn:pulumi:stack::proj::type::name2 comes after it")
|
|
|
|
assert.Nil(t, snap)
|
|
|
|
|
|
|
|
backend.DisableIntegrityChecking = true
|
2024-01-05 08:32:56 +00:00
|
|
|
snap, err = s.Snapshot(ctx, b64.Base64SecretsProvider)
|
2023-12-04 15:12:56 +00:00
|
|
|
require.NoError(t, err)
|
|
|
|
assert.NotNil(t, snap)
|
|
|
|
}
|