Skip to content

Commit 9053485

Browse files
authored
wip: refactor the git-initializer (#1952)
Signed-off-by: Kent Rancourt <kent.rancourt@microsoft.com>
1 parent b7ba275 commit 9053485

File tree

13 files changed

+546
-382
lines changed

13 files changed

+546
-382
lines changed

v2/git-initializer-windows/Dockerfile

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM golang:1.17.2-windowsservercore-1809 as builder
1+
FROM golang:1.18.1-windowsservercore-1809 as builder
22

33
ARG VERSION
44
ARG COMMIT
@@ -19,6 +19,12 @@ RUN go build \
1919

2020
FROM mcr.microsoft.com/windows/nanoserver:1809
2121

22+
COPY --chown=ContainerUser:ContainerUser v2/git-initializer/ssh_config /Users/ContainerUser/.ssh/config
23+
COPY --from=builder /git /git/
2224
COPY --from=builder /src/bin/ /brigade/bin/
2325

26+
USER ContainerAdministrator
27+
RUN setx /M PATH "%PATH%;C:\git\cmd"
28+
USER ContainerUser
29+
2430
ENTRYPOINT ["/brigade/bin/git-initializer.exe"]

v2/git-initializer/Dockerfile

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,16 @@ RUN GOOS=$TARGETOS GOARCH=$TARGETARCH go build \
2020
-ldflags "-w -X github.com/brigadecore/brigade-foundations/version.version=$VERSION -X github.com/brigadecore/brigade-foundations/version.commit=$COMMIT" \
2121
./git-initializer
2222

23-
FROM gcr.io/distroless/static:nonroot as final
23+
FROM alpine:3.15.4 as final
2424

25+
RUN apk update \
26+
&& apk add git openssh-client \
27+
&& addgroup -S -g 65532 nonroot \
28+
&& adduser -S -D -u 65532 -g nonroot -G nonroot nonroot
29+
30+
COPY --chown=nonroot:nonroot v2/git-initializer/ssh_config /home/nonroot/.ssh/config
2531
COPY --from=builder /src/bin/ /brigade/bin/
2632

33+
USER nonroot
34+
2735
ENTRYPOINT ["/brigade/bin/git-initializer"]

v2/git-initializer/auth.go

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
package main
2+
3+
import (
4+
"io/ioutil"
5+
"net/url"
6+
"path"
7+
8+
"github.com/go-git/go-git/v5/plumbing/transport"
9+
"github.com/go-git/go-git/v5/plumbing/transport/http"
10+
gitssh "github.com/go-git/go-git/v5/plumbing/transport/ssh"
11+
"github.com/mitchellh/go-homedir"
12+
"github.com/pkg/errors"
13+
"golang.org/x/crypto/ssh"
14+
)
15+
16+
// setupAuth, if necessary, configures the git CLI for authentication using
17+
// either SSH or the "store" (username/password-based) credential helper since
18+
// the git-initializer component does fall back on the git CLI for certain
19+
// operations. It additionally returns an appropriate implementation of
20+
// transport.AuthMethod for operations that interact with remote repositories
21+
// programmatically.
22+
func setupAuth(evt event) (transport.AuthMethod, error) {
23+
homeDir, err := homedir.Dir()
24+
if err != nil {
25+
return nil, errors.Wrap(err, "error finding user's home directory")
26+
}
27+
28+
// If an SSH key was provided, use that.
29+
if key, ok := evt.Project.Secrets["gitSSHKey"]; ok {
30+
// If a passphrase was supplied for the key, decrypt the key now.
31+
keyPass, ok := evt.Project.Secrets["gitSSHKeyPassword"]
32+
if ok {
33+
var err error
34+
if key, err = decryptKey(key, keyPass); err != nil {
35+
return nil, errors.Wrap(err, "error decrypting SSH key")
36+
}
37+
}
38+
rsaKeyPath := path.Join(homeDir, ".ssh", "id_rsa")
39+
if err := ioutil.WriteFile(rsaKeyPath, []byte(key), 0600); err != nil {
40+
return nil, errors.Wrapf(err, "error writing SSH key to %q", rsaKeyPath)
41+
}
42+
43+
// This is the implementation of the transport.AuthMethod interface that can
44+
// be used for operations that interact with the remote repository
45+
// interactively.
46+
publicKeys, err := gitssh.NewPublicKeys("git", []byte(key), keyPass)
47+
if err != nil {
48+
return nil,
49+
errors.Wrap(err, "error getting transport.AuthMethod using SSH key")
50+
}
51+
52+
// This prevents the CLI from interactively requesting the user to allow
53+
// connection to a new/unrecognized host.
54+
publicKeys.HostKeyCallback = ssh.InsecureIgnoreHostKey() // nolint: gosec
55+
return publicKeys, nil // We're done
56+
}
57+
58+
// If a password was provided, use that.
59+
if password, ok := evt.Project.Secrets["gitPassword"]; ok {
60+
credentialURL, err := url.Parse(evt.Worker.Git.CloneURL)
61+
if err != nil {
62+
return nil,
63+
errors.Wrapf(err, "error parsing URL %q", evt.Worker.Git.CloneURL)
64+
}
65+
// If a username was provided, use it. One may not have been because some
66+
// git providers, like GitHub, for instance, will allow any non-empty
67+
// username to be used in conjunction with a personal access token.
68+
username, ok := evt.Project.Secrets["gitUsername"]
69+
// If a username wasn't provided, we can ALSO try to pick it out of the URL.
70+
if !ok && credentialURL.User != nil {
71+
username = credentialURL.User.Username()
72+
}
73+
// If the username is still the empty string, we assume we're working with a
74+
// git provider like GitHub that only requires the username to be non-empty.
75+
// We arbitrarily set it to "git".
76+
if username == "" {
77+
username = "git"
78+
}
79+
// Remove path and query string components from the URL
80+
credentialURL.Path = ""
81+
credentialURL.RawQuery = ""
82+
// Augment the URL with user/pass information.
83+
credentialURL.User = url.UserPassword(username, password)
84+
// Write the URL to the location used by the "stored" credential helper.
85+
credentialsPath := path.Join(homeDir, ".git-credentials")
86+
if err := ioutil.WriteFile(
87+
credentialsPath,
88+
[]byte(credentialURL.String()),
89+
0600,
90+
); err != nil {
91+
return nil,
92+
errors.Wrapf(err, "error writing credentials to %q", credentialsPath)
93+
}
94+
95+
// This is the implementation of the transport.AuthMethod interface that can
96+
// be used for operations that interact with the remote repository
97+
// interactively.
98+
return &http.BasicAuth{
99+
Username: username,
100+
Password: password,
101+
}, nil // We're done
102+
}
103+
104+
// No auth setup required if we get to here.
105+
return nil, nil
106+
}

v2/git-initializer/cmd.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package main
2+
3+
import (
4+
"bufio"
5+
"log"
6+
"os/exec"
7+
"sync"
8+
9+
"github.com/pkg/errors"
10+
)
11+
12+
// execCommand executes a commands and pipes its stdout and stderr out to the
13+
// git-initializer's own logs.
14+
func execCommand(cmd *exec.Cmd) error {
15+
stdoutReader, err := cmd.StdoutPipe()
16+
if err != nil {
17+
return errors.Wrap(err, "error obtaining stdout pipe")
18+
}
19+
wg := sync.WaitGroup{}
20+
wg.Add(1)
21+
go func() {
22+
defer wg.Done()
23+
scanner := bufio.NewScanner(stdoutReader)
24+
for scanner.Scan() {
25+
log.Println(scanner.Text())
26+
}
27+
}()
28+
stderrReader, err := cmd.StderrPipe()
29+
if err != nil {
30+
return errors.Wrap(err, "error obtaining stderr pipe")
31+
}
32+
wg.Add(1)
33+
go func() {
34+
defer wg.Done()
35+
scanner := bufio.NewScanner(stderrReader)
36+
for scanner.Scan() {
37+
log.Println(scanner.Text())
38+
}
39+
}()
40+
if err = cmd.Start(); err != nil {
41+
return errors.Wrapf(err, "error starting command %q", cmd.String())
42+
}
43+
if err = cmd.Wait(); err != nil {
44+
return errors.Wrapf(err, "error waiting for command %q", cmd.String())
45+
}
46+
wg.Wait() // Make sure we got all the output before returning
47+
return nil
48+
}

v2/git-initializer/config.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package main
2+
3+
import (
4+
"os/exec"
5+
6+
"github.com/pkg/errors"
7+
)
8+
9+
// applyGlobalConfig applies global git CLI configuration options. Specifically,
10+
// it adds /var/vcs as a "safe" directory. This is required due to the fact that
11+
// the nonroot user the git-initializer process runs as doesn't OWN /var/vcs.
12+
// (Kubernetes security context is applied to make /var/vcs group writable, but
13+
// git can be pickier about file permissions than one would normally expect.)
14+
func applyGlobalConfig() error {
15+
return errors.Wrapf(
16+
execCommand(
17+
exec.Command(
18+
"git",
19+
"config",
20+
"--global",
21+
"--add",
22+
"safe.directory",
23+
workspace,
24+
),
25+
),
26+
"error setting %q as a safe directory",
27+
workspace,
28+
)
29+
}

v2/git-initializer/crypto.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"crypto/x509"
6+
"encoding/pem"
7+
8+
"github.com/pkg/errors"
9+
)
10+
11+
// decryptKey decrypts a PEM-encoded RSA private key and returns a PEM-encoded
12+
// RSA private key sans encryption. This is useful for cases where the key is
13+
// being used in conjunction with the git CLI and we wish to avoid interactively
14+
// requesting the key's passphrase.
15+
func decryptKey(key, pass string) (string, error) {
16+
block, _ := pem.Decode([]byte(key))
17+
if block == nil {
18+
return "", errors.Errorf("key is not PEM-encoded")
19+
}
20+
const supportedType = "RSA PRIVATE KEY"
21+
if block.Type != supportedType {
22+
return "", errors.Errorf(
23+
"unsupported key type %q; only %q is supported",
24+
block.Type,
25+
supportedType,
26+
)
27+
}
28+
var err error
29+
if block.Bytes, err = x509.DecryptPEMBlock(block, []byte(pass)); err != nil {
30+
return "", errors.Wrap(err, "error decrypting private key")
31+
}
32+
block.Headers = nil
33+
buf := &bytes.Buffer{}
34+
if err = pem.Encode(buf, block); err != nil {
35+
return "", errors.Wrap(err, "error PEM-encoding decrypted private key")
36+
}
37+
return buf.String(), nil
38+
}

v2/git-initializer/doc.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package main
2+
3+
// This package implements a "hybrid" approach to interacting with remote git
4+
// repositories to acquire source code required by a Brigade Worker or Job. The
5+
// "hybrid" approach does whatever it can using the (pure Go) go-git library,
6+
// but falls back on exec'ing git CLI commands when necessary. This is necessary
7+
// because go-git cannot fetch from remotes hosted by certain git providers --
8+
// namely Azure DevOps, but possibly others.
9+
//
10+
// In an ideal world, we'd have a library that addressed all out needs. libgit2
11+
// (via Go bindings provided by git2go) comes close to meeting our needs, but
12+
// cannot use refspecs that use a sha. I (krancour) expect this to be fixed
13+
// within a few months of this writing (May 2022), and we can possibly revisit
14+
// this component at that time.

v2/git-initializer/events.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package main
2+
3+
import (
4+
"encoding/json"
5+
"io/ioutil"
6+
7+
"github.com/brigadecore/brigade/sdk/v3"
8+
"github.com/pkg/errors"
9+
)
10+
11+
// event is a git-initializer-specific representation of a Brigade Event.
12+
type event struct {
13+
Project struct {
14+
Secrets map[string]string `json:"secrets"`
15+
} `json:"project"`
16+
Worker struct {
17+
Git *sdk.GitConfig `json:"git"`
18+
} `json:"worker"`
19+
}
20+
21+
// getEvent loads an Event from a JSON file on the file system.
22+
func getEvent() (event, error) {
23+
evt := event{}
24+
eventPath := "/var/event/event.json"
25+
data, err := ioutil.ReadFile(eventPath)
26+
if err != nil {
27+
return evt, errors.Wrapf(err, "unable read the event file %q", eventPath)
28+
}
29+
err = json.Unmarshal(data, &evt)
30+
return evt, errors.Wrap(err, "error unmarshaling the event")
31+
}

0 commit comments

Comments
 (0)