Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: ssh access to git repository #92

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 37 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ The easiest way to get started is to run the `envbuilder` Docker container that

> `/tmp/envbuilder` is used to persist data between commands for the purpose of this demo. You can change it to any directory you want.

for public repository:
```bash
docker run -it --rm \
-v /tmp/envbuilder:/workspaces \
Expand Down Expand Up @@ -121,7 +122,7 @@ DOCKER_CONFIG_BASE64=ewoJImF1dGhzIjogewoJCSJodHRwczovL2luZGV4LmRvY2tlci5pby92MS8
```

## Git Authentication

### Token-based authentication
`GIT_USERNAME` and `GIT_PASSWORD` are environment variables to provide Git authentication for private repositories.

For access token-based authentication, follow the following schema (if empty, there's no need to provide the field):
Expand All @@ -147,6 +148,41 @@ resource "docker_container" "dev" {
}
```

### PrivateKey-based authentication
* Prebuild private key in your envbuilder image
Copy link

@ggjulio ggjulio Jun 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this dangerous if we use a cache registry ?
The layer containing the private key may end up in the registry, or am I missing something ?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My original assumption was that this image may be stored in a private registry, so I didn't take into consideration the potential issue of having a private key in the image layers.

Script will generate private key under `scripts/.ssh` or you can place your key in same path to skip key generation.
```bash
# use `--ssh` flag to install ssh key in envbuilder images
scripts/build.sh \
--arch=amd64 \
--base=envbuilder \
--tag=latest \
--ssh
```

* Use envbuilder to clone repository
>Remenber to register the public key on git (`scripts/.ssh/*.pub`)
```bash
# for Github, Gitlab
docker run -it --rm \
-v /tmp/envbuilder:/workspaces \
-e GIT_URL=git@github.com:<username>/<project_name>.git \
-e INIT_SCRIPT=bash \
-e GIT_SSH=true \
-e GIT_SSH_KEY=/root/.ssh \
envbuilder:latest

# for Gerrit (needs to specify ssh key user)
docker run -it --rm \
-v /tmp/envbuilder:/workspaces \
-e GIT_URL="ssh://<gerrit_user>@<gerrit_url>:29418/<repository_name>" \
-e INIT_SCRIPT=bash \
-e GIT_SSH=true \
-e GIT_USERNAME=<gerrit_user> \
-e GIT_SSH_KEY=/root/.ssh \
envbuilder:latest
```

## Layer Caching

Cache layers in a container registry to speed up builds. To enable caching, [authenticate with your registry](#container-registry-authentication) and set the `CACHE_REPO` environment variable.
Expand Down
99 changes: 84 additions & 15 deletions envbuilder.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,17 @@ import (
"errors"
"fmt"
"io"
"log"
"net"
"net/http"
"net/url"
"os"
"os/exec"
"os/user"
"path"
"path/filepath"
"reflect"
"regexp"
"strconv"
"strings"
"syscall"
Expand All @@ -41,7 +44,10 @@ import (
"github.com/fatih/color"
"github.com/go-git/go-billy/v5"
"github.com/go-git/go-billy/v5/osfs"

"github.com/go-git/go-git/v5/plumbing/transport"
githttp "github.com/go-git/go-git/v5/plumbing/transport/http"
"github.com/go-git/go-git/v5/plumbing/transport/ssh"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/remote"
"github.com/sirupsen/logrus"
Expand Down Expand Up @@ -194,6 +200,15 @@ type Options struct {
// This is optional!
GitPassword string `env:"GIT_PASSWORD"`

// GitSSH will use private key to access Git.
GitSSH bool `env:"GIT_SSH"`

// GitSSHKey is the private key to use for Git authentication.
// This is optional! (default to $HOME/.ssh/id_rsa)
GitSSHKey string `env:"GIT_SSH_KEY"`

RepoAuth transport.AuthMethod

// WorkspaceFolder is the path to the workspace folder
// that will be built. This is optional!
WorkspaceFolder string `env:"WORKSPACE_FOLDER"`
Expand Down Expand Up @@ -338,25 +353,45 @@ func Run(ctx context.Context, options Options) error {
}
}()

if options.GitUsername != "" || options.GitPassword != "" {
if options.GitSSH {
if options.GitSSHKey != "" {
privateKey, err := LoadPrivateKey(options.GitUsername, options.GitSSHKey)
if err != nil {
logf(codersdk.LogLevelError, "Failed to load private ssh key for Git: %s", err)
} else {
options.RepoAuth = privateKey
}
} else {
logf(codersdk.LogLevelInfo, "GIT_SSH set to True, load default ssh key from path: %s", "/root/.ssh/id_ed25519")
ast9501 marked this conversation as resolved.
Show resolved Hide resolved
privateKey, err := LoadPrivateKey(options.GitUsername, "/root/.ssh/id_ed25519")
if err != nil {
logf(codersdk.LogLevelError, "Failed to load private ssh key for Git: %s", err)
} else {
options.RepoAuth = privateKey
}
}
} else if options.GitUsername != "" || options.GitPassword != "" {
gitURL, err := url.Parse(options.GitURL)
if err != nil {
return fmt.Errorf("parse git url: %w", err)
}
gitURL.User = url.UserPassword(options.GitUsername, options.GitPassword)
options.GitURL = gitURL.String()
options.RepoAuth = &githttp.BasicAuth{
Username: options.GitUsername,
Password: options.GitPassword,
}
} else {
logf(codersdk.LogLevelInfo, "Treat as public repository")
}

cloned, fallbackErr = CloneRepo(ctx, CloneRepoOptions{
Path: options.WorkspaceFolder,
Storage: options.Filesystem,
RepoURL: options.GitURL,
Insecure: options.Insecure,
Progress: writer,
RepoAuth: &githttp.BasicAuth{
Username: options.GitUsername,
Password: options.GitPassword,
},
Path: options.WorkspaceFolder,
Storage: options.Filesystem,
RepoURL: options.GitURL,
Insecure: options.Insecure,
Progress: writer,
RepoAuth: options.RepoAuth,
SingleBranch: options.GitCloneSingleBranch,
Depth: options.GitCloneDepth,
CABundle: caBundle,
Expand Down Expand Up @@ -968,11 +1003,11 @@ func DefaultWorkspaceFolder(repoURL string) (string, error) {
if repoURL == "" {
return "/workspaces/empty", nil
}
parsed, err := url.Parse(repoURL)
if err != nil {
return "", err
}
name := strings.Split(parsed.Path, "/")

reg := regexp.MustCompile(`.*/`)
parsed := reg.ReplaceAllString(repoURL, "")
ast9501 marked this conversation as resolved.
Show resolved Hide resolved

name := strings.Split(parsed, "/")
return fmt.Sprintf("/workspaces/%s", name[len(name)-1]), nil
}

Expand Down Expand Up @@ -1164,3 +1199,37 @@ type osfsWithChmod struct {
func (fs *osfsWithChmod) Chmod(name string, mode os.FileMode) error {
return os.Chmod(name, mode)
}

// load valid private key under given path
func LoadPrivateKey(username, keyPath string) (*ssh.PublicKeys, error) {
e, err := os.ReadDir(keyPath)
if err != nil {
log.Fatalf("Failed to read sshkey path: %s", err)
return nil, err
}

for _, entry := range e {
if !entry.IsDir() {
content, err := os.ReadFile(path.Join(keyPath, entry.Name()))
if err != nil {
log.Fatalf("Failed to read sshkey file: %s", err)
}

var publicKey *ssh.PublicKeys
if username != "" {
publicKey, err = ssh.NewPublicKeys(username, []byte(content), "")
} else {
// Username must be "git" for SSH auth to work, not your real username.
// See https://github.com/src-d/go-git/issues/637
publicKey, err = ssh.NewPublicKeys("git", []byte(content), "")
}

// return valid key
if err == nil {
return publicKey, err
}
}
}

return nil, errors.New("no valid key found")
}
14 changes: 4 additions & 10 deletions git.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"context"
"errors"
"fmt"
"net/url"

"github.com/go-git/go-billy/v5"
"github.com/go-git/go-git/v5"
Expand Down Expand Up @@ -34,20 +33,15 @@ type CloneRepoOptions struct {
//
// The bool returned states whether the repository was cloned or not.
func CloneRepo(ctx context.Context, opts CloneRepoOptions) (bool, error) {
parsed, err := url.Parse(opts.RepoURL)
if err != nil {
return false, fmt.Errorf("parse url %q: %w", opts.RepoURL, err)
}
err = opts.Storage.MkdirAll(opts.Path, 0755)
var reference string

err := opts.Storage.MkdirAll(opts.Path, 0755)
if err != nil {
return false, fmt.Errorf("mkdir %q: %w", opts.Path, err)
}
reference := parsed.Fragment
ast9501 marked this conversation as resolved.
Show resolved Hide resolved
if reference == "" && opts.SingleBranch {
reference = "refs/heads/main"
}
parsed.RawFragment = ""
parsed.Fragment = ""
fs, err := opts.Storage.Chroot(opts.Path)
if err != nil {
return false, fmt.Errorf("chroot %q: %w", opts.Path, err)
Expand All @@ -70,7 +64,7 @@ func CloneRepo(ctx context.Context, opts CloneRepoOptions) (bool, error) {
}

_, err = git.CloneContext(ctx, gitStorage, fs, &git.CloneOptions{
URL: parsed.String(),
URL: opts.RepoURL,
Auth: opts.RepoAuth,
Progress: opts.Progress,
ReferenceName: plumbing.ReferenceName(reference),
Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ require (
github.com/skeema/knownhosts v1.2.1 // indirect
github.com/spf13/afero v1.9.5 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/src-d/gcfg v1.4.0 // indirect
github.com/tabbed/pqtype v0.1.1 // indirect
github.com/tailscale/certstore v0.1.1-0.20220316223106-78d6e1c49d8d // indirect
github.com/tailscale/golang-x-crypto v0.0.0-20221102133106-bc99ab8c2d17 // indirect
Expand Down Expand Up @@ -256,6 +257,7 @@ require (
google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc // indirect
google.golang.org/grpc v1.56.3 // indirect
google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/src-d/go-git.v4 v4.13.1 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
Expand Down
Loading