Skip to content

Commit

Permalink
refactor(controller): overhaul git repo interfaces (#2483)
Browse files Browse the repository at this point in the history
Signed-off-by: Kent Rancourt <[email protected]>
  • Loading branch information
krancour authored Sep 3, 2024
1 parent b94b61a commit 575b00c
Show file tree
Hide file tree
Showing 19 changed files with 1,728 additions and 781 deletions.
5 changes: 5 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ ARG VERSION
ARG GIT_COMMIT
ARG GIT_TREE_STATE

RUN GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build \
-o bin/credential-helper \
./cmd/credential-helper

RUN GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build \
-ldflags "-w -X ${VERSION_PACKAGE}.version=${VERSION} -X ${VERSION_PACKAGE}.buildDate=$(date -u +'%Y-%m-%dT%H:%M:%SZ') -X ${VERSION_PACKAGE}.gitCommit=${GIT_COMMIT} -X ${VERSION_PACKAGE}.gitTreeState=${GIT_TREE_STATE}" \
-o bin/kargo \
Expand Down Expand Up @@ -92,6 +96,7 @@ FROM base AS back-end-dev

USER root

COPY bin/credential-helper /usr/local/bin/credential-helper
COPY bin/controlplane/kargo /usr/local/bin/kargo

RUN adduser -D -H -u 1000 kargo
Expand Down
16 changes: 13 additions & 3 deletions Tiltfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ local_resource(
'CGO_ENABLED=0 GOOS=linux GOARCH=$(go env GOARCH) go build -o bin/controlplane/kargo ./cmd/controlplane',
deps=[
'api/',
'cmd/',
'cmd/controlplane/',
'internal/',
'pkg/',
'go.mod',
Expand All @@ -17,10 +17,20 @@ local_resource(
labels = ['native-processes'],
trigger_mode = TRIGGER_MODE_AUTO
)
local_resource(
'credential-helper-compile',
'CGO_ENABLED=0 GOOS=linux GOARCH=$(go env GOARCH) go build -o bin/credential-helper ./cmd/credential-helper',
deps=['cmd/credential-helper/'],
labels = ['native-processes'],
trigger_mode = TRIGGER_MODE_AUTO
)
docker_build(
'ghcr.io/akuity/kargo',
'.',
only = ['bin/controlplane/kargo'],
only = [
'bin/controlplane/kargo',
'bin/credential-helper'
],
target = 'back-end-dev', # Just the back end, built natively, copied to the image
)

Expand Down Expand Up @@ -99,7 +109,7 @@ k8s_resource(
'kargo-controller-rollouts:clusterrole',
'kargo-controller-rollouts:clusterrolebinding'
],
resource_deps=['back-end-compile']
resource_deps=['back-end-compile', 'credential-helper-compile', ]
)

k8s_resource(
Expand Down
15 changes: 15 additions & 0 deletions cmd/credential-helper/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package main

import (
"fmt"
"os"
)

func main() {
password := os.Getenv("GIT_PASSWORD")
if password == "" {
fmt.Fprintln(os.Stderr, "GIT_PASSWORD must be set")
os.Exit(1)
}
fmt.Println(password)
}
4 changes: 3 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ require (
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/rs/cors v1.11.1
github.com/sirupsen/logrus v1.9.3
github.com/sosedoff/gitkit v0.4.0
github.com/spf13/cobra v1.8.1
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.9.0
Expand Down Expand Up @@ -71,7 +72,9 @@ require (
github.com/blang/semver/v4 v4.0.0 // indirect
github.com/containerd/errdefs v0.1.0 // indirect
github.com/containerd/platforms v0.2.1 // indirect
github.com/evanphx/json-patch v5.9.0+incompatible // indirect
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/gofrs/uuid v4.0.0+incompatible // indirect
github.com/google/go-github/v62 v62.0.0 // indirect
github.com/google/s2a-go v0.1.8 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
Expand Down Expand Up @@ -102,7 +105,6 @@ require (
github.com/docker/go-metrics v0.0.1 // indirect
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 // indirect
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
github.com/evanphx/json-patch v5.6.0+incompatible // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/go-errors/errors v1.4.2 // indirect
Expand Down
9 changes: 7 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -137,8 +137,8 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U=
github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
github.com/evanphx/json-patch v5.9.0+incompatible h1:fBXyNpNMuTTDdquAq/uisOr2lShz4oaXpDTX2bLe7ls=
github.com/evanphx/json-patch v5.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg=
github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ=
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
Expand Down Expand Up @@ -177,6 +177,8 @@ github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1v
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw=
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
Expand Down Expand Up @@ -390,6 +392,8 @@ github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPx
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/sosedoff/gitkit v0.4.0 h1:opyQJ/h9xMRLsz2ca/2CRXtstePcpldiZN8DpLLF8Os=
github.com/sosedoff/gitkit v0.4.0/go.mod h1:V3EpGZ0nvCBhXerPsbDeqtyReNb48cwP9KtkUYTKT5I=
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
Expand Down Expand Up @@ -473,6 +477,7 @@ golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnf
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
Expand Down
244 changes: 244 additions & 0 deletions internal/controller/git/bare_repo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
package git

import (
"bufio"
"bytes"
"fmt"
"os"
"path/filepath"
"slices"
"strings"

libExec "github.com/akuity/kargo/internal/exec"
)

// BareRepo is an interface for interacting with a bare Git repository.
type BareRepo interface {
// AddWorkTree adds a working tree to the repository. The working tree will be
// created at the specified path and will be checked out to the specified ref.
AddWorkTree(path, ref string) (WorkTree, error)
// Close cleans up file system resources used by this repository. This should
// always be called before a repository goes out of scope.
Close() error
// Dir returns an absolute path to the repository.
Dir() string
// HomeDir returns an absolute path to the home directory of the system user
// who has cloned this repo.
HomeDir() string
// RemoveWorkTree removes a working tree from the repository. The working tree
// will be removed from the file system.
RemoveWorkTree(path string) error
// URL returns the remote URL of the repository.
URL() string
// WorkTrees returns a list of working trees associated with the repository.
WorkTrees() ([]WorkTree, error)
}

// bareRepo is an implementation of the BareRepo interface for interacting with
// a bare Git repository.
type bareRepo struct {
*baseRepo
}

// BareCloneOptions represents options for cloning a Git repository without a
// working tree.
type BareCloneOptions struct {
// BaseDir is an existing directory within which all other directories created
// and managed by the BareRepo implementation will be created. If not
// specified, the operating system's temporary directory will be used.
// Overriding that default is useful under certain circumstances.
BaseDir string
// InsecureSkipTLSVerify specifies whether certificate verification errors
// should be ignored when cloning the repository. The setting will be
// remembered for subsequent interactions with the remote repository.
InsecureSkipTLSVerify bool
}

// CloneBare produces a local, bare clone of the remote Git repository at the
// specified URL and returns an implementation of the BareRepo interface that is
// stateful and NOT suitable for use across multiple goroutines. This function
// will also perform any setup that is required for successfully authenticating
// to the remote repository.
func CloneBare(
repoURL string,
clientOpts *ClientOptions,
cloneOpts *BareCloneOptions,
) (BareRepo, error) {
if clientOpts == nil {
clientOpts = &ClientOptions{}
}
if cloneOpts == nil {
cloneOpts = &BareCloneOptions{}
}
homeDir, err := os.MkdirTemp(cloneOpts.BaseDir, "repo-")
if err != nil {
return nil,
fmt.Errorf("error creating home directory for repo %q: %w", repoURL, err)
}
if homeDir, err = filepath.EvalSymlinks(homeDir); err != nil {
return nil,
fmt.Errorf("error resolving symlinks in path %s: %w", homeDir, err)
}
b := &bareRepo{
baseRepo: &baseRepo{
creds: clientOpts.Credentials,
dir: filepath.Join(homeDir, "repo"),
homeDir: homeDir,
insecureSkipTLSVerify: cloneOpts.InsecureSkipTLSVerify,
url: repoURL,
},
}
if err = b.setupClient(clientOpts); err != nil {
return nil, err
}
if err = b.clone(); err != nil {
return nil, err
}
if err = b.saveDirs(); err != nil {
return nil, err
}
return b, nil
}

func (b *bareRepo) clone() error {
cmd := b.buildGitCommand("clone", "--bare", b.url, b.dir)
cmd.Dir = b.homeDir // Override the cmd.Dir that's set by r.buildGitCommand()
if _, err := libExec.Exec(cmd); err != nil {
return fmt.Errorf("error cloning repo %q into %q: %w", b.url, b.dir, err)
}
return nil
}

type LoadBareRepoOptions struct {
Credentials *RepoCredentials
InsecureSkipTLSVerify bool
}

func LoadBareRepo(path string, opts *LoadBareRepoOptions) (BareRepo, error) {
if opts == nil {
opts = &LoadBareRepoOptions{}
}
b := &bareRepo{
baseRepo: &baseRepo{
creds: opts.Credentials,
dir: path,
insecureSkipTLSVerify: opts.InsecureSkipTLSVerify,
},
}
if err := b.loadHomeDir(); err != nil {
return nil, fmt.Errorf("error reading repo home dir from config: %w", err)
}
if err := b.loadURL(); err != nil {
return nil,
fmt.Errorf(`error reading URL of remote "origin" from config: %w`, err)
}
if err := b.setupAuth(); err != nil {
return nil, fmt.Errorf("error configuring the credentials: %w", err)
}
return b, nil
}

func (b *bareRepo) AddWorkTree(path, ref string) (WorkTree, error) {
path, err := filepath.Abs(path)
if err != nil {
return nil, fmt.Errorf("error resolving absolute path for %s: %w", path, err)
}
workTreePaths, err := b.workTrees()
if err != nil {
return nil, err
}
if slices.Contains(workTreePaths, path) {
return nil, fmt.Errorf("working tree already exists at %q", path)
}
if _, err = libExec.Exec(
b.buildGitCommand("worktree", "add", path, ref),
); err != nil {
return nil, fmt.Errorf("error adding working tree at %q: %w", path, err)
}
if path, err = filepath.EvalSymlinks(path); err != nil {
return nil, fmt.Errorf("error resolving symlinks in path %s: %w", path, err)
}
return &workTree{
baseRepo: &baseRepo{
creds: b.creds,
dir: path,
homeDir: b.homeDir,
insecureSkipTLSVerify: b.insecureSkipTLSVerify,
url: b.url,
},
bareRepo: b,
}, nil
}

func (b *bareRepo) Close() error {
workTreePaths, err := b.workTrees()
if err != nil {
return err
}
for _, workTreePath := range workTreePaths {
if err := b.RemoveWorkTree(workTreePath); err != nil {
return err
}
}
return os.RemoveAll(b.homeDir)
}

func (b *bareRepo) RemoveWorkTree(path string) error {
workTreePaths, err := b.workTrees()
if err != nil {
return err
}
if !slices.Contains(workTreePaths, path) {
return fmt.Errorf("no working tree exists at %q", path)
}
if _, err := libExec.Exec(
b.buildGitCommand("worktree", "remove", path),
); err != nil {
return fmt.Errorf("error removing working tree at %q: %w", path, err)
}
if err := os.RemoveAll(path); err != nil {
return fmt.Errorf("error removing working tree at %q: %w", path, err)
}
return nil
}

func (b *bareRepo) WorkTrees() ([]WorkTree, error) {
workTreePaths, err := b.workTrees()
if err != nil {
return nil, err
}
workTrees := make([]WorkTree, len(workTreePaths))
for i, workTreePath := range workTreePaths {
workTrees[i] = &workTree{
baseRepo: &baseRepo{
creds: b.creds,
dir: workTreePath,
homeDir: b.homeDir,
insecureSkipTLSVerify: b.insecureSkipTLSVerify,
url: b.url,
},
bareRepo: b,
}
}
return workTrees, err
}

func (b *bareRepo) workTrees() ([]string, error) {
res, err := libExec.Exec(b.buildGitCommand("worktree", "list"))
if err != nil {
return nil, fmt.Errorf("error listing working trees: %w", err)
}
workTrees := []string{}
scanner := bufio.NewScanner(bytes.NewReader(res))
for scanner.Scan() {
line := scanner.Text()
if !strings.HasSuffix(line, "(bare)") {
fields := strings.Fields(line)
if len(fields) != 3 {
return nil, fmt.Errorf("unexpected number of fields: %q", line)
}
workTrees = append(workTrees, fields[0])
}
}
return workTrees, err
}
Loading

0 comments on commit 575b00c

Please sign in to comment.