Skip to content

Commit

Permalink
git-based directives wip
Browse files Browse the repository at this point in the history
Signed-off-by: Kent Rancourt <[email protected]>
  • Loading branch information
krancour committed Sep 3, 2024
1 parent 249873d commit f8ad9b4
Show file tree
Hide file tree
Showing 6 changed files with 240 additions and 12 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ require (
github.com/bacongobbler/browser v1.1.0
github.com/bombsimon/logrusr/v4 v4.1.0
github.com/coreos/go-oidc/v3 v3.11.0
github.com/cyphar/filepath-securejoin v0.3.1
github.com/evanphx/json-patch/v5 v5.9.0
github.com/fatih/structtag v1.2.0
github.com/gobwas/glob v0.2.3
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/cyphar/filepath-securejoin v0.3.1 h1:1V7cHiaW+C+39wEfpH6XlLBQo3j/PciWFrgfCLS8XrE=
github.com/cyphar/filepath-securejoin v0.3.1/go.mod h1:F7i41x/9cBF7lzCrVsYs9fuzwRZm4NQsGTBdpp6mETc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
Expand Down
2 changes: 2 additions & 0 deletions internal/directives/directive.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ type StepContext struct {
Config Config
// Project is the Project that the Promotion is associated with.
Project string
// Stage is the Stage that the Promotion is targeting.
Stage string
// FreightRequests is the list of Freight from various origins that is
// requested by the Stage targeted by the Promotion. This information is
// sometimes useful to Steps that reference a particular artifact and, in the
Expand Down
100 changes: 96 additions & 4 deletions internal/directives/git_clone_directive.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@ import (
"context"
"fmt"

securejoin "github.com/cyphar/filepath-securejoin"
"github.com/xeipuuv/gojsonschema"

kargoapi "github.com/akuity/kargo/api/v1alpha1"
"github.com/akuity/kargo/internal/controller/freight"
"github.com/akuity/kargo/internal/controller/git"
"github.com/akuity/kargo/internal/credentials"
)

func init() {
Expand Down Expand Up @@ -38,7 +44,7 @@ func (g *gitCloneDirective) Name() string {

// Run implements the Directive interface.
func (g *gitCloneDirective) Run(
_ context.Context,
ctx context.Context,
stepCtx *StepContext,
) (Result, error) {
failure := Result{Status: StatusFailure}
Expand All @@ -50,10 +56,96 @@ func (g *gitCloneDirective) Run(
); err != nil {
return failure, err
}
if _, err := configToStruct[GitCloneConfig](stepCtx.Config); err != nil {
cfg, err := configToStruct[GitCloneConfig](stepCtx.Config)
if err != nil {
return failure,
fmt.Errorf("could not convert config into git-clone config: %w", err)
}
// TODO: Add implementation here
return Result{Status: StatusSuccess}, nil
if err = g.run(ctx, stepCtx, cfg); err != nil {
return failure, err
}
return Result{
Status: StatusSuccess,
}, nil
}

func (g *gitCloneDirective) run(
ctx context.Context,
stepCtx *StepContext,
cfg GitCloneConfig,
) error {
var repoCreds *git.RepoCredentials
if creds, found, err := stepCtx.CredentialsDB.Get(
ctx,
stepCtx.Project,
credentials.TypeGit,
cfg.RepoURL,
); err != nil {
return fmt.Errorf("error getting credentials for %s: %w", cfg.RepoURL, err)
} else if found {
repoCreds = &git.RepoCredentials{
Username: creds.Username,
Password: creds.Password,
SSHPrivateKey: creds.SSHPrivateKey,
}
}
repo, err := git.CloneBare(
cfg.RepoURL,
&git.ClientOptions{
Credentials: repoCreds,
},
&git.BareCloneOptions{
BaseDir: stepCtx.WorkDir,
InsecureSkipTLSVerify: cfg.InsecureSkipTLSVerify,
},
)
if err != nil {
return fmt.Errorf("error cloning %s: %w", cfg.RepoURL, err)
}
for _, checkout := range cfg.Checkout {
var ref string
switch {
case checkout.Branch != "":
ref = checkout.Branch
case checkout.FromFreight:
var desiredOrigin *kargoapi.FreightOrigin
if checkout.FromOrigin == nil {
desiredOrigin = &kargoapi.FreightOrigin{
Kind: kargoapi.FreightOriginKind(checkout.FromOrigin.Kind),
}
}
var commit *kargoapi.GitCommit
if commit, err = freight.FindCommit(
ctx,
stepCtx.KargoClient,
stepCtx.Project,
stepCtx.FreightRequests,
desiredOrigin,
stepCtx.Freight.References(),
cfg.RepoURL,
); err != nil {
return fmt.Errorf("error finding commit from repo %s: %w", cfg.RepoURL, err)
}
ref = commit.ID
case checkout.Tag != "":
ref = checkout.Tag
}
path, err := securejoin.SecureJoin(stepCtx.WorkDir, checkout.Path)
if err != nil {
return fmt.Errorf(
"error joining path %s with work dir %s: %w",
checkout.Path, stepCtx.WorkDir, err,
)
}
if _, err = repo.AddWorkTree(path, ref); err != nil {
return fmt.Errorf(
"error adding work tree %s to repo %s: %w",
checkout.Path, cfg.RepoURL, err,
)
}
}
// Note: We do NOT defer repo.Close() because we want to keep the repository
// around onf the FS for subsequent directives to use. The directive execution
// engine will handle all work dir cleanup.
return nil
}
69 changes: 65 additions & 4 deletions internal/directives/git_commit_directive.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import (
"fmt"

"github.com/xeipuuv/gojsonschema"

"github.com/akuity/kargo/internal/controller/git"
"github.com/akuity/kargo/internal/credentials"
)

func init() {
Expand Down Expand Up @@ -32,7 +35,7 @@ func (g *gitCommitDirective) Name() string {

// Run implements the Directive interface.
func (g *gitCommitDirective) Run(
_ context.Context,
ctx context.Context,
stepCtx *StepContext,
) (Result, error) {
failure := Result{Status: StatusFailure}
Expand All @@ -44,10 +47,68 @@ func (g *gitCommitDirective) Run(
); err != nil {
return failure, err
}
if _, err := configToStruct[GitCommitConfig](stepCtx.Config); err != nil {
cfg, err := configToStruct[GitCommitConfig](stepCtx.Config)
if err != nil {
return failure,
fmt.Errorf("could not convert config into git-commit config: %w", err)
}
// TODO: Add implementation here
return Result{Status: StatusSuccess}, nil
if err = g.run(ctx, stepCtx, cfg); err != nil {
return failure, err
}
return Result{
Status: StatusSuccess,
}, nil
}

func (g *gitCommitDirective) run(
ctx context.Context,
stepCtx *StepContext,
cfg GitCommitConfig,
) error {
// This is kind of hacky, but we needed to load the working tree to get the
// URL of the repository. With that in hand, we can look for applicable
// credentials and, if found, reload the work tree with the credentials.
loadOpts := &git.LoadWorkTreeOptions{}
workTree, err := git.LoadWorkTree(cfg.Path, loadOpts)
if err != nil {
return fmt.Errorf("error loading working tree from %s: %w", cfg.Path, err)
}
var creds credentials.Credentials
var found bool
if creds, found, err = stepCtx.CredentialsDB.Get(
ctx,
stepCtx.Project,
credentials.TypeGit,
workTree.URL(),
); err != nil {
return fmt.Errorf(
"error getting credentials for %s: %w", workTree.URL(), err,
)
} else if found {
loadOpts.Credentials = &git.RepoCredentials{
Username: creds.Username,
Password: creds.Password,
SSHPrivateKey: creds.SSHPrivateKey,
}
}
if workTree, err = git.LoadWorkTree(cfg.Path, nil); err != nil {
return fmt.Errorf("error loading working tree from %s: %w", cfg.Path, err)
}
if err = workTree.AddAll(); err != nil {
return fmt.Errorf("error adding all changes to working tree: %w", err)
}
commitOpts := &git.CommitOptions{}
if cfg.Author != nil {
commitOpts.Author = &git.User{}
if cfg.Author.Name != "" {
commitOpts.Author.Name = cfg.Author.Name
}
if cfg.Author.Email != "" {
commitOpts.Author.Email = cfg.Author.Email
}
}
if err = workTree.Commit(cfg.Message, commitOpts); err != nil {
return fmt.Errorf("error committing to working tree: %w", err)
}
return nil
}
78 changes: 74 additions & 4 deletions internal/directives/git_push_directive.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import (
"fmt"

"github.com/xeipuuv/gojsonschema"

"github.com/akuity/kargo/internal/controller/git"
"github.com/akuity/kargo/internal/credentials"
)

func init() {
Expand Down Expand Up @@ -32,7 +35,7 @@ func (g *gitPushDirective) Name() string {

// Run implements the Directive interface.
func (g *gitPushDirective) Run(
_ context.Context,
ctx context.Context,
stepCtx *StepContext,
) (Result, error) {
failure := Result{Status: StatusFailure}
Expand All @@ -44,10 +47,77 @@ func (g *gitPushDirective) Run(
); err != nil {
return failure, err
}
if _, err := configToStruct[GitPushConfig](stepCtx.Config); err != nil {
cfg, err := configToStruct[GitPushConfig](stepCtx.Config)
if err != nil {
return failure,
fmt.Errorf("could not convert config into git-push config: %w", err)
}
// TODO: Add implementation here
return Result{Status: StatusSuccess}, nil
targetBranch, err := g.run(ctx, stepCtx, cfg)
if err != nil {
return failure, err
}
return Result{
Status: StatusSuccess,
Output: State{
"branch": targetBranch,
},
}, nil
}

func (g *gitPushDirective) run(
ctx context.Context,
stepCtx *StepContext,
cfg GitPushConfig,
) (string, error) {
// This is kind of hacky, but we needed to load the working tree to get the
// URL of the repository. With that in hand, we can look for applicable
// credentials and, if found, reload the work tree with the credentials.
loadOpts := &git.LoadWorkTreeOptions{}
workTree, err := git.LoadWorkTree(cfg.Path, loadOpts)
if err != nil {
return "", fmt.Errorf("error loading working tree from %s: %w", cfg.Path, err)
}
var creds credentials.Credentials
var found bool
if creds, found, err = stepCtx.CredentialsDB.Get(
ctx,
stepCtx.Project,
credentials.TypeGit,
workTree.URL(),
); err != nil {
return "", fmt.Errorf(
"error getting credentials for %s: %w", workTree.URL(), err,
)
} else if found {
loadOpts.Credentials = &git.RepoCredentials{
Username: creds.Username,
Password: creds.Password,
SSHPrivateKey: creds.SSHPrivateKey,
}
}
if workTree, err = git.LoadWorkTree(cfg.Path, loadOpts); err != nil {
return "", fmt.Errorf("error loading working tree from %s: %w", cfg.Path, err)
}
pushOpts := &git.PushOptions{
// Start with whatever was specified in the config, which may be empty
TargetBranch: cfg.TargetBranch,
}
// If we're supposed to generate a target branch name, do so
if cfg.GenerateTargetBranch {
pushOpts.TargetBranch = fmt.Sprintf("kargo/%s/%s/promotion", stepCtx.Project, stepCtx.Stage)
pushOpts.Force = true
}
retBranch := pushOpts.TargetBranch
if retBranch == "" {
// If retBranch is still empty, we want to set it to the current branch
// because we will want to return the branch that was pushed to, but we
// don't want to mess with the options any further.
if retBranch, err = workTree.CurrentBranch(); err != nil {
return "", fmt.Errorf("error getting current branch: %w", err)
}
}
if err := workTree.Push(pushOpts); err != nil {
return "", fmt.Errorf("error pushing commits to remote: %w", err)
}
return retBranch, nil
}

0 comments on commit f8ad9b4

Please sign in to comment.