// 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) }) } }