mirror of https://github.com/pulumi/pulumi.git
392 lines
12 KiB
Go
392 lines
12 KiB
Go
package auto
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"crypto/rsa"
|
|
"crypto/x509"
|
|
"encoding/pem"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
git "github.com/go-git/go-git/v5"
|
|
"github.com/go-git/go-git/v5/plumbing"
|
|
"github.com/go-git/go-git/v5/plumbing/object"
|
|
"github.com/go-git/go-git/v5/plumbing/transport"
|
|
"github.com/go-git/go-git/v5/plumbing/transport/http"
|
|
"github.com/go-git/go-git/v5/plumbing/transport/ssh"
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
|
|
"github.com/pulumi/pulumi/sdk/v3/go/common/util/gitutil"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"golang.org/x/net/nettest"
|
|
)
|
|
|
|
// This takes the unusual step of testing an unexported func. The rationale is to be able to test
|
|
// git code in isolation; testing the user of the unexported func (NewLocalWorkspace) drags in lots
|
|
// of other factors.
|
|
|
|
func TestGitClone(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// This makes a git repo to clone from, so to avoid relying on something at GitHub that could
|
|
// change or be inaccessible.
|
|
tmpDir := t.TempDir()
|
|
originDir := filepath.Join(tmpDir, "origin")
|
|
|
|
origin, err := git.PlainInit(originDir, false)
|
|
assert.NoError(t, err)
|
|
w, err := origin.Worktree()
|
|
assert.NoError(t, err)
|
|
nondefaultHead, err := w.Commit("nondefault branch", &git.CommitOptions{
|
|
Author: &object.Signature{
|
|
Name: "testo",
|
|
Email: "testo@example.com",
|
|
},
|
|
AllowEmptyCommits: true,
|
|
})
|
|
assert.NoError(t, err)
|
|
|
|
// The following sets up some tags and branches: with `default` becoming the "default" branch
|
|
// when cloning, since it's left as the HEAD of the repo.
|
|
|
|
assert.NoError(t, w.Checkout(&git.CheckoutOptions{
|
|
Branch: plumbing.NewBranchReferenceName("nondefault"),
|
|
Create: true,
|
|
}))
|
|
|
|
// tag the nondefault head so we can test getting a tag too
|
|
_, err = origin.CreateTag("v0.0.1", nondefaultHead, nil)
|
|
assert.NoError(t, err)
|
|
|
|
// make a branch with slashes in it, so that can be tested too
|
|
assert.NoError(t, w.Checkout(&git.CheckoutOptions{
|
|
Branch: plumbing.NewBranchReferenceName("branch/with/slashes"),
|
|
Create: true,
|
|
}))
|
|
|
|
assert.NoError(t, w.Checkout(&git.CheckoutOptions{
|
|
Branch: plumbing.NewBranchReferenceName("default"),
|
|
Create: true,
|
|
}))
|
|
defaultHead, err := w.Commit("default branch", &git.CommitOptions{
|
|
Author: &object.Signature{
|
|
Name: "testo",
|
|
Email: "testo@example.com",
|
|
},
|
|
AllowEmptyCommits: true,
|
|
})
|
|
assert.NoError(t, err)
|
|
|
|
type testcase struct {
|
|
branchName string
|
|
commitHash string
|
|
testName string // use when supplying a hash, for a stable name
|
|
expectedHead plumbing.Hash
|
|
expectedError string
|
|
}
|
|
|
|
for _, tc := range []testcase{
|
|
{branchName: "default", expectedHead: defaultHead},
|
|
{branchName: "nondefault", expectedHead: nondefaultHead},
|
|
{branchName: "branch/with/slashes", expectedHead: nondefaultHead},
|
|
// https://github.com/pulumi/pulumi-kubernetes-operator/issues/103#issuecomment-1107891475
|
|
// advises using `refs/heads/<default>` for the default, and `refs/remotes/origin/<branch>`
|
|
// for a non-default branch -- so we can expect all these varieties to be in use.
|
|
{branchName: "refs/heads/default", expectedHead: defaultHead},
|
|
{branchName: "refs/heads/nondefault", expectedHead: nondefaultHead},
|
|
{branchName: "refs/heads/branch/with/slashes", expectedHead: nondefaultHead},
|
|
|
|
{branchName: "refs/remotes/origin/default", expectedHead: defaultHead},
|
|
{branchName: "refs/remotes/origin/nondefault", expectedHead: nondefaultHead},
|
|
{branchName: "refs/remotes/origin/branch/with/slashes", expectedHead: nondefaultHead},
|
|
// try the special tag case
|
|
{branchName: "refs/tags/v0.0.1", expectedHead: nondefaultHead},
|
|
// ask specifically for the commit hash
|
|
{testName: "head of default as hash", commitHash: defaultHead.String(), expectedHead: defaultHead},
|
|
{testName: "head of nondefault as hash", commitHash: nondefaultHead.String(), expectedHead: nondefaultHead},
|
|
} {
|
|
tc := tc
|
|
if tc.testName == "" {
|
|
tc.testName = tc.branchName
|
|
}
|
|
t.Run(tc.testName, func(t *testing.T) {
|
|
t.Parallel()
|
|
repo := &GitRepo{
|
|
URL: originDir,
|
|
Branch: tc.branchName,
|
|
CommitHash: tc.commitHash,
|
|
}
|
|
|
|
tmp, err := os.MkdirTemp(tmpDir, "testcase") // i.e., under the tmp dir from earlier
|
|
assert.NoError(t, err)
|
|
|
|
_, err = setupGitRepo(context.Background(), tmp, repo)
|
|
assert.NoError(t, err)
|
|
|
|
r, err := git.PlainOpen(tmp)
|
|
assert.NoError(t, err)
|
|
head, err := r.Head()
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, tc.expectedHead, head.Hash())
|
|
})
|
|
}
|
|
|
|
// test that these result in errors
|
|
for _, tc := range []testcase{
|
|
{
|
|
testName: "simple branch doesn't exist",
|
|
branchName: "doesnotexist",
|
|
expectedError: "unable to clone repo: reference not found",
|
|
},
|
|
{
|
|
testName: "full branch doesn't exist",
|
|
branchName: "refs/heads/doesnotexist",
|
|
expectedError: "unable to clone repo: reference not found",
|
|
},
|
|
{
|
|
testName: "malformed branch name",
|
|
branchName: "refs/notathing/default",
|
|
expectedError: "unable to clone repo: reference not found",
|
|
},
|
|
{
|
|
testName: "simple tag name won't work",
|
|
branchName: "v1.0.0",
|
|
expectedError: "unable to clone repo: reference not found",
|
|
},
|
|
{
|
|
testName: "wrong remote",
|
|
branchName: "refs/remotes/upstream/default",
|
|
expectedError: "a remote ref must begin with 'refs/remote/origin/', " +
|
|
"but got \"refs/remotes/upstream/default\"",
|
|
},
|
|
} {
|
|
tc := tc
|
|
if tc.testName == "" {
|
|
tc.testName = tc.branchName
|
|
}
|
|
t.Run(tc.testName, func(t *testing.T) {
|
|
t.Parallel()
|
|
repo := &GitRepo{
|
|
URL: originDir,
|
|
Branch: tc.branchName,
|
|
CommitHash: tc.commitHash,
|
|
}
|
|
|
|
tmp, err := os.MkdirTemp(tmpDir, "testcase") // i.e., under the tmp dir from earlier
|
|
assert.NoError(t, err)
|
|
|
|
_, err = setupGitRepo(context.Background(), tmp, repo)
|
|
assert.EqualError(t, err, tc.expectedError)
|
|
})
|
|
}
|
|
}
|
|
|
|
//nolint:paralleltest // global environment variables
|
|
func TestGitAuthParse(t *testing.T) {
|
|
// Set up a fake SSH_AUTH_SOCK for testing
|
|
l, err := nettest.NewLocalListener("unix")
|
|
defer contract.IgnoreClose(l)
|
|
require.NoError(t, err)
|
|
|
|
type testcase struct {
|
|
name string
|
|
url string
|
|
auth *GitAuth
|
|
setSSHAgentSocket bool
|
|
|
|
expectedURL string
|
|
expectedError string
|
|
expectedPublicKeysTransport bool
|
|
|
|
// Expect a basic auth instance with the provided username and password
|
|
expectedBasicAuth *http.BasicAuth
|
|
// The same as the above, but expects a TransportAuth wrapper
|
|
expectedWrappedBasicAuth *http.BasicAuth
|
|
}
|
|
|
|
for _, tc := range []testcase{
|
|
{
|
|
name: "private key path",
|
|
url: "git@example.com:repo.git",
|
|
auth: &GitAuth{
|
|
SSHPrivateKeyPath: generateSSHKeyFile(t),
|
|
},
|
|
expectedURL: "git@example.com:repo.git",
|
|
expectedPublicKeysTransport: true,
|
|
},
|
|
{
|
|
name: "invalid private key path",
|
|
url: "git@example.com:repo.git",
|
|
auth: &GitAuth{
|
|
SSHPrivateKeyPath: "/invalid/path",
|
|
},
|
|
expectedError: "unable to use SSH Private Key Path: open /invalid/path",
|
|
},
|
|
{
|
|
name: "private key value",
|
|
url: "git@example.com:repo.git",
|
|
auth: &GitAuth{
|
|
SSHPrivateKey: generateSSHKeyString(t),
|
|
},
|
|
expectedURL: "git@example.com:repo.git",
|
|
expectedPublicKeysTransport: true,
|
|
},
|
|
{
|
|
name: "invalid private key value",
|
|
url: "git@example.com:repo.git",
|
|
auth: &GitAuth{
|
|
SSHPrivateKey: "invalid-string",
|
|
},
|
|
expectedError: "unable to use SSH Private Key: ssh: no key found",
|
|
},
|
|
{
|
|
name: "personal access token",
|
|
url: "git@example.com:repo.git",
|
|
auth: &GitAuth{
|
|
PersonalAccessToken: "dummy-token",
|
|
},
|
|
expectedURL: "git@example.com:repo.git",
|
|
expectedBasicAuth: &http.BasicAuth{Username: "git", Password: "dummy-token"},
|
|
},
|
|
{
|
|
name: "username and password",
|
|
url: "git@example.com:repo.git",
|
|
auth: &GitAuth{
|
|
Username: "user",
|
|
Password: "pass",
|
|
},
|
|
expectedURL: "git@example.com:repo.git",
|
|
expectedError: "",
|
|
expectedBasicAuth: &http.BasicAuth{Username: "user", Password: "pass"},
|
|
},
|
|
{
|
|
name: "incompatible auth options",
|
|
url: "git@example.com:repo.git",
|
|
auth: &GitAuth{
|
|
SSHPrivateKeyPath: "/path/to/private/key",
|
|
Username: "user",
|
|
},
|
|
expectedError: "please specify one authentication option",
|
|
},
|
|
{
|
|
name: "incompatible auth options",
|
|
url: "git@example.com:repo.git",
|
|
auth: &GitAuth{
|
|
PersonalAccessToken: "dummy-token",
|
|
Username: "user",
|
|
},
|
|
expectedError: "please specify one authentication option",
|
|
},
|
|
{
|
|
url: "git@example.com:repo.git",
|
|
name: "default auth with SSH agent",
|
|
setSSHAgentSocket: true,
|
|
|
|
expectedURL: "git@example.com:repo.git",
|
|
},
|
|
{
|
|
name: "url with basic auth",
|
|
url: "https://user:password@example.com/repo.git",
|
|
|
|
expectedURL: "https://example.com/repo.git",
|
|
expectedWrappedBasicAuth: &http.BasicAuth{
|
|
Username: "user",
|
|
Password: "password",
|
|
},
|
|
},
|
|
} {
|
|
tc := tc
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
if tc.setSSHAgentSocket {
|
|
t.Setenv("SSH_AUTH_SOCK", l.Addr().String())
|
|
} else {
|
|
os.Unsetenv("SSH_AUTH_SOCK")
|
|
}
|
|
|
|
url, authMethod, err := func() (string, transport.AuthMethod, error) {
|
|
var _ context.Context = context.Background()
|
|
return setupGitRepoAuth(tc.url, tc.auth)
|
|
}()
|
|
|
|
if tc.expectedError != "" {
|
|
if err == nil {
|
|
t.Fatalf("expected an error but got nil")
|
|
}
|
|
if !strings.Contains(err.Error(), tc.expectedError) {
|
|
t.Fatalf("expected error %q to contain %q", err.Error(), tc.expectedError)
|
|
}
|
|
} else if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
if url != tc.expectedURL {
|
|
t.Errorf("expected url %q, got %q", tc.expectedURL, url)
|
|
}
|
|
|
|
if _, ok := authMethod.(*ssh.PublicKeys); ok != tc.expectedPublicKeysTransport {
|
|
condition := "to be"
|
|
if !tc.expectedPublicKeysTransport {
|
|
condition = "not to be"
|
|
}
|
|
t.Errorf("expected transport %s of type *ssh.PublicKeys, got %T", condition, authMethod)
|
|
}
|
|
|
|
if tc.expectedBasicAuth != nil {
|
|
basicAuth, ok := authMethod.(*http.BasicAuth)
|
|
require.Truef(t, ok, "expected Auth to be of type *http.BasicAuth, got %T", authMethod)
|
|
if basicAuth.Username != tc.expectedBasicAuth.Username ||
|
|
basicAuth.Password != tc.expectedBasicAuth.Password {
|
|
t.Errorf("expected BasicAuth to have username %q and password %q, got username %q and password %q",
|
|
tc.expectedBasicAuth.Username, tc.expectedBasicAuth.Password,
|
|
basicAuth.Username, basicAuth.Password)
|
|
}
|
|
}
|
|
|
|
if tc.expectedWrappedBasicAuth != nil {
|
|
wrapper, ok := authMethod.(*gitutil.TransportAuth)
|
|
require.Truef(t, ok, "expected transport to be of type *gitutil.TransportAuth, got %T", authMethod)
|
|
basicAuth, ok := wrapper.AuthMethod.(*http.BasicAuth)
|
|
require.Truef(t, ok, "expected Auth to be of type *http.BasicAuth, got %T", wrapper.AuthMethod)
|
|
if basicAuth.Username != tc.expectedWrappedBasicAuth.Username ||
|
|
basicAuth.Password != tc.expectedWrappedBasicAuth.Password {
|
|
t.Errorf("expected BasicAuth to have username %q and password %q, got username %q and password %q",
|
|
tc.expectedWrappedBasicAuth.Username, tc.expectedWrappedBasicAuth.Password,
|
|
basicAuth.Username, basicAuth.Password)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func generateSSHKeyBlock(t *testing.T) *pem.Block {
|
|
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
|
require.NoError(t, err)
|
|
|
|
return &pem.Block{
|
|
Type: "RSA PRIVATE KEY",
|
|
Bytes: x509.MarshalPKCS1PrivateKey(key),
|
|
}
|
|
}
|
|
|
|
func generateSSHKeyFile(t *testing.T) string {
|
|
block := generateSSHKeyBlock(t)
|
|
|
|
path := filepath.Join(t.TempDir(), "test-key")
|
|
err := os.WriteFile(path, pem.EncodeToMemory(block), 0o600)
|
|
t.Cleanup(func() {
|
|
os.Remove(path)
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
return path
|
|
}
|
|
|
|
func generateSSHKeyString(t *testing.T) string {
|
|
block := generateSSHKeyBlock(t)
|
|
|
|
return string(pem.EncodeToMemory(block))
|
|
}
|