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 9, 2024
1 parent d0fef18 commit e3ceeb6
Show file tree
Hide file tree
Showing 13 changed files with 1,306 additions and 461 deletions.
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
176 changes: 163 additions & 13 deletions internal/directives/git_clone_directive.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,15 @@ package directives
import (
"context"
"fmt"
"os"

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 All @@ -26,9 +33,9 @@ type gitCloneDirective struct {

// newGitCloneDirective creates a new git-clone directive.
func newGitCloneDirective() Directive {
return &gitCloneDirective{
schemaLoader: getConfigSchemaLoader("git-clone"),
}
d := &gitCloneDirective{}
d.schemaLoader = getConfigSchemaLoader(d.Name())
return d
}

// Name implements the Directive interface.
Expand All @@ -38,22 +45,165 @@ 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}
// Validate the configuration against the JSON Schema
if err := validate(
g.schemaLoader,
gojsonschema.NewGoLoader(stepCtx.Config),
"git-clone",
); err != nil {
if err := g.validate(stepCtx.Config); 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)
fmt.Errorf("could not convert config into %s config: %w", g.Name(), err)
}
if err = g.run(ctx, stepCtx, cfg); err != nil {
return failure, err
}
// TODO: Add implementation here
return Result{Status: StatusSuccess}, nil
}

// validate validates the git-clone directive configuration against the JSON
// schema.
func (g *gitCloneDirective) validate(cfg Config) error {
return validate(g.schemaLoader, gojsonschema.NewGoLoader(cfg), g.Name())
}

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
if err = ensureRemoteBranch(repo, ref); err != nil {
return fmt.Errorf("error ensuring existence of remote branch %s: %w", ref, err)
}
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,
&git.AddWorkTreeOptions{Ref: 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 on the FS for subsequent directives to use. The directive execution
// engine will handle all work dir cleanup.
return nil
}

// ensureRemoteBranch ensures the existence of a remote branch. If the branch
// does not exist, an empty orphaned branch is created and pushed to the remote.
func ensureRemoteBranch(repo git.BareRepo, branch string) error {
exists, err := repo.RemoteBranchExists(branch)
if err != nil {
return fmt.Errorf(
"error checking if remote branch %q of repo %s exists: %w",
branch, repo.URL(), err,
)
}
if exists {
return nil
}
tmpDir, err := os.MkdirTemp("", "repo-")
if err != nil {
return fmt.Errorf("error creating temporary directory: %w", err)
}
workTree, err := repo.AddWorkTree(tmpDir, &git.AddWorkTreeOptions{Orphan: true})
if err != nil {
return fmt.Errorf(
"error adding temporary working tree for branch %q of repo %s: %w",
branch, repo.URL(), err,
)
}
defer workTree.Close()
// `git worktree add --orphan some/path` (i.e. the preceding
// repo.AddWorkTree() call) creates a new orphaned branch named "path". We
// have no control over the branch name. It will always be equal to the last
// component of the path. So, we will immediately create _another_ orphaned
// branch with the name we really wanted before making an initial commit and
// pushing it to the remote.
if err = workTree.CreateOrphanedBranch(branch); err != nil {
return err
}
if err = workTree.Commit(
"Initial commit",
&git.CommitOptions{AllowEmpty: true},
); err != nil {
return fmt.Errorf(
"error making initial commit to new branch %q of repo %s: %w",
branch, repo.URL(), err,
)
}
if err = workTree.Push(&git.PushOptions{TargetBranch: branch}); err != nil {
return fmt.Errorf(
"error pushing initial commit to new branch %q to repo %s: %w",
branch, repo.URL(), err,
)
}
return nil
}
Loading

0 comments on commit e3ceeb6

Please sign in to comment.