Skip to content

Commit

Permalink
[cli] Test remote operations
Browse files Browse the repository at this point in the history
Also some changes to validate flags earlier (before getting the backend).
  • Loading branch information
justinvp committed Nov 9, 2022
1 parent b7d2a7f commit 2f1d3fe
Show file tree
Hide file tree
Showing 3 changed files with 224 additions and 32 deletions.
73 changes: 41 additions & 32 deletions pkg/cmd/pulumi/util_remote.go
Original file line number Diff line number Diff line change
Expand Up @@ -215,33 +215,22 @@ func (r *RemoteArgs) applyFlags(cmd *cobra.Command) {
func runDeployment(ctx context.Context, opts display.Options, operation apitype.PulumiOperation, stack, url string,
args RemoteArgs) result.Result {

b, err := currentBackend(ctx, opts)
if err != nil {
return result.FromError(err)
}

// Ensure the cloud backend is being used.
cb, isCloud := b.(httpstate.Backend)
if !isCloud {
return result.FromError(errors.New("the Pulumi service backend must be used for remote operations; " +
"use `pulumi login` without arguments to log into the Pulumi service backend"))
}

stackRef, err := b.ParseStackReference(stack)
if err != nil {
return result.FromError(err)
}

// Validate args.
if url == "" {
return result.FromError(errors.New("the url arg must be specified"))
}
if args.gitCommit != "" && args.gitBranch != "" {
if args.gitBranch != "" && args.gitCommit != "" {
return result.FromError(errors.New("`--remote-git-branch` and `--remote-git-commit` cannot both be specified"))
}
if args.gitCommit == "" && args.gitBranch == "" {
if args.gitBranch == "" && args.gitCommit == "" {
return result.FromError(errors.New("either `--remote-git-branch` or `--remote-git-commit` is required"))
}
if args.gitAuthSSHPrivateKey != "" && args.gitAuthSSHPrivateKeyPath != "" {
return result.FromError(errors.New("`--remote-git-auth-ssh-private-key` and " +
"`--remote-git-auth-ssh-private-key-path` cannot both be specified"))
}

// Parse and validate the environment args.
env := map[string]apitype.SecretValue{}
for i, e := range append(args.envVars, args.secretEnvVars...) {
name, value, err := parseEnv(e)
Expand All @@ -254,26 +243,46 @@ func runDeployment(ctx context.Context, opts display.Options, operation apitype.
}
}

// Read the SSH Private Key from the path, if necessary.
sshPrivateKey := args.gitAuthSSHPrivateKey
if args.gitAuthSSHPrivateKeyPath != "" {
key, err := os.ReadFile(args.gitAuthSSHPrivateKeyPath)
if err != nil {
return result.FromError(fmt.Errorf(
"reading SSH private key path %q: %w", args.gitAuthSSHPrivateKeyPath, err))
}
sshPrivateKey = string(key)
}

b, err := currentBackend(ctx, opts)
if err != nil {
return result.FromError(err)
}

// Ensure the cloud backend is being used.
cb, isCloud := b.(httpstate.Backend)
if !isCloud {
return result.FromError(errors.New("the Pulumi service backend must be used for remote operations; " +
"use `pulumi login` without arguments to log into the Pulumi service backend"))
}

stackRef, err := b.ParseStackReference(stack)
if err != nil {
return result.FromError(err)
}

var gitAuth *apitype.GitAuthConfig
if args.gitAuthAccessToken != "" || args.gitAuthSSHPrivateKey != "" || args.gitAuthSSHPrivateKeyPath != "" ||
args.gitAuthPassword != "" || args.gitAuthUsername != "" {
if args.gitAuthAccessToken != "" || sshPrivateKey != "" || args.gitAuthPassword != "" ||
args.gitAuthUsername != "" {

gitAuth = &apitype.GitAuthConfig{}
switch {
case args.gitAuthAccessToken != "":
gitAuth.PersonalAccessToken = &apitype.SecretValue{Value: args.gitAuthAccessToken, Secret: true}

case args.gitAuthSSHPrivateKey != "" || args.gitAuthSSHPrivateKeyPath != "":
sshAuth := &apitype.SSHAuth{}
if args.gitAuthSSHPrivateKeyPath != "" {
content, err := os.ReadFile(args.gitAuthSSHPrivateKeyPath)
if err != nil {
return result.FromError(fmt.Errorf(
"reading SSH private key path %q: %w", args.gitAuthSSHPrivateKeyPath, err))
}
sshAuth.SSHPrivateKey = apitype.SecretValue{Value: string(content), Secret: true}
} else {
sshAuth.SSHPrivateKey = apitype.SecretValue{Value: args.gitAuthSSHPrivateKey, Secret: true}
case sshPrivateKey != "":
sshAuth := &apitype.SSHAuth{
SSHPrivateKey: apitype.SecretValue{Value: sshPrivateKey, Secret: true},
}
if args.gitAuthPassword != "" {
sshAuth.Password = &apitype.SecretValue{Value: args.gitAuthPassword, Secret: true}
Expand Down
150 changes: 150 additions & 0 deletions tests/remote_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
// 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 tests

import (
"crypto/rand"
"encoding/hex"
"fmt"
"os"
"strings"
"testing"

"github.com/stretchr/testify/assert"

ptesting "github.com/pulumi/pulumi/sdk/v3/go/common/testing"
)

const remoteTestRepo = "https://github.com/pulumi/test-repo.git"

func TestInvalidRemoteFlags(t *testing.T) {
t.Parallel()

commands := []string{"preview", "up", "refresh", "destroy"}

tests := map[string]struct {
args []string
err string
}{
"no url": {
err: "error: must specify remote URL",
},
"no branch or commit": {
args: []string{remoteTestRepo},
err: "error: either `--remote-git-branch` or `--remote-git-commit` is required",
},
"both branch and commit": {
args: []string{remoteTestRepo, "--remote-git-branch", "branch", "--remote-git-commit", "commit"},
err: "error: `--remote-git-branch` and `--remote-git-commit` cannot both be specified",
},
"both ssh private key and path": {
args: []string{remoteTestRepo, "--remote-git-branch", "branch", "--remote-git-auth-ssh-private-key", "key",
"--remote-git-auth-ssh-private-key-path", "path"},
err: "error: `--remote-git-auth-ssh-private-key` and `--remote-git-auth-ssh-private-key-path` " +
"cannot both be specified",
},
"ssh private key path doesn't exist": {
args: []string{remoteTestRepo, "--remote-git-branch", "branch", "--remote-git-auth-ssh-private-key-path",
"doesntexist"},
err: "error: reading SSH private key path",
},
"invalid env": {
args: []string{remoteTestRepo, "--remote-git-branch", "branch", "--remote-env", "invalid"},
err: `expected value of the form "NAME=value": missing "=" in "invalid"`,
},
"empty env name": {
args: []string{remoteTestRepo, "--remote-git-branch", "branch", "--remote-env", "=value"},
err: `error: expected non-empty environment name in "=value"`,
},
"invalid secret env": {
args: []string{remoteTestRepo, "--remote-git-branch", "branch", "--remote-env-secret", "blah"},
err: `expected value of the form "NAME=value": missing "=" in "blah"`,
},
"empty secret env name": {
args: []string{remoteTestRepo, "--remote-git-branch", "branch", "--remote-env-secret", "=value"},
err: `error: expected non-empty environment name in "=value"`,
},
}

for _, command := range commands {
command := command
for name, tc := range tests {
tc := tc
t.Run(command+"_"+name, func(t *testing.T) {
t.Parallel()

e := ptesting.NewEnvironment(t)
defer deleteIfNotFailed(e)

// Remote flags currently require PULUMI_EXPERIMENTAL.
e.Env = append(e.Env, "PULUMI_EXPERIMENTAL=true")

args := []string{command, "--remote"}
_, err := e.RunCommandExpectError("pulumi", append(args, tc.args...)...)
assert.NotEmpty(t, tc.err)
assert.Contains(t, err, tc.err)
})
}
}
}

func TestRemoteLifecycle(t *testing.T) {
// This test requires the service with access to Pulumi Deployments.
// Set PULUMI_ACCESS_TOKEN to an access token with access to Pulumi Deployments,
// set PULUMI_TEST_OWNER to the organization to use for the fully qualified stack,
// 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_OWNER") == "" {
t.Skipf("Skipping: PULUMI_TEST_OWNER is not set")
}
if os.Getenv("PULUMI_TEST_DEPLOYMENTS_API") == "" {
t.Skipf("Skipping: PULUMI_TEST_DEPLOYMENTS_API is not set")
}
t.Parallel()

e := ptesting.NewEnvironment(t)
defer deleteIfNotFailed(e)

// Remote flags currently require PULUMI_EXPERIMENTAL.
e.Env = append(e.Env, "PULUMI_EXPERIMENTAL=true")

randomSuffix := func() string {
b := make([]byte, 4)
_, err := rand.Read(b)
assert.NoError(t, err)
return hex.EncodeToString(b)
}

owner := os.Getenv("PULUMI_TEST_OWNER")
proj := "go_remote_proj"
stack := strings.ToLower("p-t-remotelifecycle-" + randomSuffix())
fullyQualifiedStack := fmt.Sprintf("%s/%s/%s", owner, proj, stack)

e.RunCommand("pulumi", "stack", "init", "--no-select", "--stack", fullyQualifiedStack)

args := func(command string) []string {
return []string{command, remoteTestRepo, "--stack", fullyQualifiedStack,
"--remote", "--remote-git-branch", "refs/heads/master", "--remote-git-repo-dir", "goproj"}
}

e.RunCommand("pulumi", args("preview")...)
e.RunCommand("pulumi", args("up")...)
e.RunCommand("pulumi", args("refresh")...)
e.RunCommand("pulumi", args("destroy")...)

e.RunCommand("pulumi", "stack", "rm", "--stack", fullyQualifiedStack, "--yes")
}
33 changes: 33 additions & 0 deletions tests/stack_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,39 @@ func TestStackCommands(t *testing.T) {
e.RunCommand("pulumi", "stack", "rm", "--yes")
})

t.Run("StackInitNoSelect", func(t *testing.T) {
t.Parallel()

e := ptesting.NewEnvironment(t)
defer deleteIfNotFailed(e)

integration.CreateBasicPulumiRepo(e)
e.SetBackend(e.LocalURL())
e.RunCommand("pulumi", "stack", "init", "first")
e.RunCommand("pulumi", "stack", "init", "second")

// Last one created is always selected.
stacks, current := integration.GetStacks(e)
if current == nil {
t.Fatalf("No stack was labeled as current among: %v", stacks)
}
assert.Equal(t, "second", *current)

// Specifying `--no-select` prevents selection.
e.RunCommand("pulumi", "stack", "init", "third", "--no-select")
stacks, current = integration.GetStacks(e)
if current == nil {
t.Fatalf("No stack was labeled as current among: %v", stacks)
}
// "second" should still be selected.
assert.Equal(t, "second", *current)

assert.Equal(t, 3, len(stacks))
assert.Contains(t, stacks, "first")
assert.Contains(t, stacks, "second")
assert.Contains(t, stacks, "third")
})

t.Run("StackUnselect", func(t *testing.T) {
t.Parallel()

Expand Down

0 comments on commit 2f1d3fe

Please sign in to comment.