pulumi/sdk/go/auto/remote_workspace_test.go

348 lines
9.7 KiB
Go

// Copyright 2016-2022, Pulumi Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package auto
import (
"context"
"fmt"
"os"
"os/exec"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/pulumi/pulumi/sdk/v3/go/auto/events"
"github.com/pulumi/pulumi/sdk/v3/go/auto/optremotepreview"
"github.com/pulumi/pulumi/sdk/v3/go/common/apitype"
ptesting "github.com/pulumi/pulumi/sdk/v3/go/common/testing"
)
const (
remoteTestRepo = "https://github.com/pulumi/test-repo.git"
remoteTestRepoBranch = "refs/heads/master"
)
func testRemoteStackGitSourceErrors(t *testing.T, fn func(ctx context.Context, stackName string, repo GitRepo,
opts ...RemoteWorkspaceOption) (RemoteStack, error),
) {
ctx := context.Background()
const stack = "owner/project/stack"
tests := map[string]struct {
stack string
repo GitRepo
executorImage *ExecutorImage
err string
inheritSettings bool
}{
"stack empty": {
stack: "",
err: `stack name "" must be fully qualified`,
},
"stack just name": {
stack: "name",
err: `stack name "name" must be fully qualified`,
},
"stack just name & owner": {
stack: "owner/name",
err: `stack name "owner/name" must be fully qualified`,
},
"stack just sep": {
stack: "/",
err: `stack name "/" must be fully qualified`,
},
"stack just two seps": {
stack: "//",
err: `stack name "//" must be fully qualified`,
},
"stack just three seps": {
stack: "///",
err: `stack name "///" must be fully qualified`,
},
"stack invalid": {
stack: "owner/project/stack/wat",
err: `stack name "owner/project/stack/wat" must be fully qualified`,
},
"repo setup": {
stack: stack,
repo: GitRepo{Setup: func(context.Context, Workspace) error { return nil }},
inheritSettings: true,
err: "repo.Setup cannot be used with remote workspaces",
},
"no url": {
stack: stack,
repo: GitRepo{},
err: "repo.URL is required if RemoteInheritSettings(true) is not set",
},
"no branch or commit": {
stack: stack,
repo: GitRepo{URL: remoteTestRepo},
err: "either repo.Branch or repo.CommitHash is required if RemoteInheritSettings(true) is not set",
},
"both branch and commit": {
stack: stack,
repo: GitRepo{URL: remoteTestRepo, Branch: "branch", CommitHash: "commit"},
err: "repo.Branch and repo.CommitHash cannot both be specified",
},
"both ssh private key and path": {
stack: stack,
repo: GitRepo{
URL: remoteTestRepo,
Branch: "branch",
Auth: &GitAuth{SSHPrivateKey: "key", SSHPrivateKeyPath: "path"},
},
err: "repo.Auth.SSHPrivateKey and repo.Auth.SSHPrivateKeyPath cannot both be specified",
},
"executor creds with no image": {
stack: stack,
repo: GitRepo{
URL: remoteTestRepo,
Branch: "branch",
},
executorImage: &ExecutorImage{
Credentials: &DockerImageCredentials{
Username: "user",
Password: "password",
},
},
err: "executorImage.Image cannot be empty",
},
"executor image with username and no password": {
stack: stack,
repo: GitRepo{
URL: remoteTestRepo,
Branch: "branch",
},
executorImage: &ExecutorImage{
Image: "image",
Credentials: &DockerImageCredentials{
Username: "username",
},
},
err: "executorImage.Credentials.Password cannot be empty",
},
"executor image with password and no username": {
stack: stack,
repo: GitRepo{
URL: remoteTestRepo,
Branch: "branch",
},
executorImage: &ExecutorImage{
Image: "image",
Credentials: &DockerImageCredentials{
Password: "password",
},
},
err: "executorImage.Credentials.Username cannot be empty",
},
}
for name, tc := range tests {
tc := tc
t.Run(name, func(t *testing.T) {
t.Parallel()
_, err := fn(ctx, tc.stack, tc.repo, RemoteExecutorImage(tc.executorImage),
RemoteInheritSettings(tc.inheritSettings))
assert.EqualError(t, err, tc.err)
})
}
}
// fetchCommitHash runs `git ls-remote URL branch` to determine the latest commit for the given repo
// URL and branch.
func fetchCommitHash(url, branch string) (string, error) {
cmd := exec.Command("git", "ls-remote", url, branch)
output, err := cmd.Output()
if err != nil {
return "", fmt.Errorf("git ls-remote: %w", err)
}
out := strings.TrimSpace(string(output))
fields := strings.Fields(out)
if len(fields) == 0 {
return "", fmt.Errorf("could not determine commit hash from %q", out)
}
return fields[0], nil
}
func testRemoteStackGitSource(
t *testing.T,
fn func(ctx context.Context, stackName string, repo GitRepo, opts ...RemoteWorkspaceOption) (RemoteStack, error),
useCommitHash bool,
useExecutorImage bool,
) {
// This test requires the service with access to Pulumi Deployments.
// Set PULUMI_ACCESS_TOKEN to an access token with access to Pulumi Deployments
// and set PULUMI_TEST_DEPLOYMENTS_API to any value to enable the test.
if os.Getenv("PULUMI_ACCESS_TOKEN") == "" {
t.Skipf("Skipping: PULUMI_ACCESS_TOKEN is not set")
}
if os.Getenv("PULUMI_TEST_DEPLOYMENTS_API") == "" {
t.Skipf("Skipping: PULUMI_TEST_DEPLOYMENTS_API is not set")
}
ctx := context.Background()
pName := "go_remote_proj"
sName := ptesting.RandomStackName()
stackName := FullyQualifiedStackName(pulumiOrg, pName, sName)
repo := GitRepo{
URL: remoteTestRepo,
ProjectPath: "goproj",
}
var executorImage *ExecutorImage
if useCommitHash {
commitHash, err := fetchCommitHash(remoteTestRepo, remoteTestRepoBranch)
require.NoError(t, err)
repo.CommitHash = commitHash
} else {
repo.Branch = remoteTestRepoBranch
}
if useExecutorImage {
executorImage = &ExecutorImage{
Image: "pulumi/pulumi",
}
}
// initialize
s, err := fn(ctx, stackName, repo,
RemotePreRunCommands(
"pulumi config set bar abc --stack "+stackName,
"pulumi config set --secret buzz secret --stack "+stackName),
RemoteSkipInstallDependencies(true),
RemoteExecutorImage(executorImage),
)
if err != nil {
t.Errorf("failed to initialize stack, err: %v", err)
t.FailNow()
}
defer func() {
// -- pulumi stack rm --
err = s.stack.Workspace().RemoveStack(ctx, s.Name())
assert.Nil(t, err, "failed to remove stack. Resources have leaked.")
}()
// -- pulumi up --
res, err := s.Up(ctx)
if err != nil {
t.Errorf("up failed, err: %v", err)
t.FailNow()
}
assert.Equal(t, 3, len(res.Outputs), "expected two plain outputs")
assert.Equal(t, "foo", res.Outputs["exp_static"].Value)
assert.False(t, res.Outputs["exp_static"].Secret)
assert.Equal(t, "abc", res.Outputs["exp_cfg"].Value)
assert.False(t, res.Outputs["exp_cfg"].Secret)
assert.Equal(t, "secret", res.Outputs["exp_secret"].Value)
assert.True(t, res.Outputs["exp_secret"].Secret)
assert.Equal(t, "update", res.Summary.Kind)
assert.Equal(t, "succeeded", res.Summary.Result)
// -- pulumi preview --
var previewEvents []events.EngineEvent
prevCh := make(chan events.EngineEvent)
wg := collectEvents(prevCh, &previewEvents)
prev, err := s.Preview(ctx, optremotepreview.EventStreams(prevCh))
if err != nil {
t.Errorf("preview failed, err: %v", err)
t.FailNow()
}
wg.Wait()
assert.Equal(t, 1, prev.ChangeSummary[apitype.OpSame])
steps := countSteps(previewEvents)
assert.Equal(t, 1, steps)
// -- pulumi refresh --
ref, err := s.Refresh(ctx)
if err != nil {
t.Errorf("refresh failed, err: %v", err)
t.FailNow()
}
assert.Equal(t, "refresh", ref.Summary.Kind)
assert.Equal(t, "succeeded", ref.Summary.Result)
// -- pulumi destroy --
dRes, err := s.Destroy(ctx)
if err != nil {
t.Errorf("destroy failed, err: %v", err)
t.FailNow()
}
assert.Equal(t, "destroy", dRes.Summary.Kind)
assert.Equal(t, "succeeded", dRes.Summary.Result)
}
func TestSelectRemoteStackGitSourceErrors(t *testing.T) {
t.Parallel()
testRemoteStackGitSourceErrors(t, SelectRemoteStackGitSource)
}
func TestNewRemoteStackGitSourceErrors(t *testing.T) {
t.Parallel()
testRemoteStackGitSourceErrors(t, NewRemoteStackGitSource)
}
func TestNewRemoteStackGitSource(t *testing.T) {
t.Parallel()
testRemoteStackGitSource(t, NewRemoteStackGitSource, true /*useCommitHash*/, false /*useExecutorImage*/)
}
func TestUpsertRemoteStackGitSourceErrors(t *testing.T) {
t.Parallel()
testRemoteStackGitSourceErrors(t, UpsertRemoteStackGitSource)
}
func TestUpsertRemoteStackGitSource(t *testing.T) {
t.Parallel()
testRemoteStackGitSource(t, UpsertRemoteStackGitSource, false /*useCommitHash*/, true /*useExecutorImage*/)
}
func TestIsFullyQualifiedStackName(t *testing.T) {
t.Parallel()
tests := []struct {
name string
input string
expected bool
}{
{name: "fully qualified", input: "owner/project/stack", expected: true},
{name: "empty", input: "", expected: false},
{name: "name", input: "name", expected: false},
{name: "name & owner", input: "owner/name", expected: false},
{name: "sep", input: "/", expected: false},
{name: "two seps", input: "//", expected: false},
{name: "three seps", input: "///", expected: false},
{name: "invalid", input: "owner/project/stack/wat", expected: false},
}
for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
actual := isFullyQualifiedStackName(tc.input)
assert.Equal(t, tc.expected, actual)
})
}
}