diff --git a/internal/core/context.go b/internal/core/context.go index 42d85cac..ae584f08 100644 --- a/internal/core/context.go +++ b/internal/core/context.go @@ -6,8 +6,11 @@ import ( "dagger.io/dagger" "github.com/aweris/gale/internal/config" + "github.com/aweris/gale/internal/dagger/helpers" ) +var _ helpers.WithContainerFuncHook = new(RunnerContext) + type RunnerContext struct { Name string `json:"name"` // Name is the name of the runner. OS string `json:"os"` // OS is the operating system of the runner. @@ -29,18 +32,21 @@ func NewRunnerContext() RunnerContext { } } -// Apply applies the RunnerContext to the given container. -func (c RunnerContext) Apply(container *dagger.Container) *dagger.Container { - return container. - WithEnvVariable("RUNNER_NAME", c.Name). - WithEnvVariable("RUNNER_TEMP", c.Temp). - WithEnvVariable("RUNNER_OS", c.OS). - WithEnvVariable("RUNNER_ARCH", c.Arch). - WithEnvVariable("RUNNER_TOOL_CACHE", c.ToolCache). - WithMountedCache(c.ToolCache, config.Client().CacheVolume("RUNNER_TOOL_CACHE")). - WithEnvVariable("RUNNER_DEBUG", c.Debug) +func (c RunnerContext) WithContainerFunc() dagger.WithContainerFunc { + return func(container *dagger.Container) *dagger.Container { + return container. + WithEnvVariable("RUNNER_NAME", c.Name). + WithEnvVariable("RUNNER_TEMP", c.Temp). + WithEnvVariable("RUNNER_OS", c.OS). + WithEnvVariable("RUNNER_ARCH", c.Arch). + WithEnvVariable("RUNNER_TOOL_CACHE", c.ToolCache). + WithMountedCache(c.ToolCache, config.Client().CacheVolume("RUNNER_TOOL_CACHE")). + WithEnvVariable("RUNNER_DEBUG", c.Debug) + } } +var _ helpers.WithContainerFuncHook = new(GithubRepositoryContext) + // GithubRepositoryContext is a context that contains information about the repository. type GithubRepositoryContext struct { Repository string `json:"repository"` // Repository is the combination of owner and name of the repository. e.g. octocat/hello-world @@ -65,19 +71,22 @@ func NewGithubRepositoryContext(repo *Repository) GithubRepositoryContext { } } -// Apply applies the GithubRepositoryContext to the given container. -func (c GithubRepositoryContext) Apply(container *dagger.Container) *dagger.Container { - return container. - WithEnvVariable("GITHUB_REPOSITORY", c.Repository). - WithEnvVariable("GITHUB_REPOSITORY_ID", c.RepositoryID). - WithEnvVariable("GITHUB_REPOSITORY_OWNER", c.RepositoryOwner). - WithEnvVariable("GITHUB_REPOSITORY_OWNER_ID", c.RepositoryOwnerID). - WithEnvVariable("GITHUB_REPOSITORY_URL", c.RepositoryURL). - WithEnvVariable("GITHUB_WORKSPACE", c.Workspace). - WithMountedDirectory(c.Workspace, c.Dir). - WithWorkdir(c.Workspace) +func (c GithubRepositoryContext) WithContainerFunc() dagger.WithContainerFunc { + return func(container *dagger.Container) *dagger.Container { + return container. + WithEnvVariable("GITHUB_REPOSITORY", c.Repository). + WithEnvVariable("GITHUB_REPOSITORY_ID", c.RepositoryID). + WithEnvVariable("GITHUB_REPOSITORY_OWNER", c.RepositoryOwner). + WithEnvVariable("GITHUB_REPOSITORY_OWNER_ID", c.RepositoryOwnerID). + WithEnvVariable("GITHUB_REPOSITORY_URL", c.RepositoryURL). + WithEnvVariable("GITHUB_WORKSPACE", c.Workspace). + WithMountedDirectory(c.Workspace, c.Dir). + WithWorkdir(c.Workspace) + } } +var _ helpers.WithContainerFuncHook = new(GithubSecretsContext) + // GithubSecretsContext is a context that contains information about the secrets. type GithubSecretsContext struct { Token string `json:"token"` // Token is the GitHub token to use for authentication. @@ -90,11 +99,14 @@ func NewGithubSecretsContext(token string) GithubSecretsContext { } } -// Apply applies the GithubSecretsContext to the given container. -func (c GithubSecretsContext) Apply(container *dagger.Container) *dagger.Container { - return container.WithSecretVariable("GITHUB_TOKEN", config.Client().SetSecret("GITHUB_TOKEN", c.Token)) +func (c GithubSecretsContext) WithContainerFunc() dagger.WithContainerFunc { + return func(container *dagger.Container) *dagger.Container { + return container.WithSecretVariable("GITHUB_TOKEN", config.Client().SetSecret("GITHUB_TOKEN", c.Token)) + } } +var _ helpers.WithContainerFuncHook = new(GithubURLContext) + // GithubURLContext is a context that contains URLs for the Github server and API. type GithubURLContext struct { //nolint:revive,stylecheck // ApiURL is more readable than APIURL @@ -112,12 +124,13 @@ func NewGithubURLContext() GithubURLContext { } } -// Apply applies the GithubURLContext to the given container. -func (c GithubURLContext) Apply(container *dagger.Container) *dagger.Container { - return container. - WithEnvVariable("GITHUB_API_URL", c.ApiURL). - WithEnvVariable("GITHUB_GRAPHQL_URL", c.GraphqlURL). - WithEnvVariable("GITHUB_SERVER_URL", c.ServerURL) +func (c GithubURLContext) WithContainerFunc() dagger.WithContainerFunc { + return func(container *dagger.Container) *dagger.Container { + return container. + WithEnvVariable("GITHUB_API_URL", c.ApiURL). + WithEnvVariable("GITHUB_GRAPHQL_URL", c.GraphqlURL). + WithEnvVariable("GITHUB_SERVER_URL", c.ServerURL) + } } // GithubFilesContext is a context that contains paths for files and directories useful to Github Actions. @@ -129,6 +142,8 @@ type GithubFilesContext struct { Path string `json:"path"` // Path is the path to a temporary file that sets the system PATH variable from workflow commands. } +var _ helpers.WithContainerFuncHook = new(GithubWorkflowContext) + // GithubWorkflowContext is a context that contains information about the workflow. type GithubWorkflowContext struct { Workflow string `json:"workflow"` // Workflow is the name of the workflow. If the workflow file doesn't specify a name, the value of this property is the full path of the workflow file in the repository. @@ -153,18 +168,21 @@ func NewGithubWorkflowContext(repo *Repository, workflow *Workflow, runID string } } -// Apply applies the GithubWorkflowContext to the given container. -func (c GithubWorkflowContext) Apply(container *dagger.Container) *dagger.Container { - return container. - WithEnvVariable("GITHUB_WORKFLOW", c.Workflow). - WithEnvVariable("GITHUB_WORKFLOW_REF", c.WorkflowRef). - WithEnvVariable("GITHUB_WORKFLOW_SHA", c.WorkflowSHA). - WithEnvVariable("GITHUB_RUN_ID", c.RunID). - WithEnvVariable("GITHUB_RUN_NUMBER", c.RunNumber). - WithEnvVariable("GITHUB_RUN_ATTEMPT", c.RunAttempt). - WithEnvVariable("GITHUB_RETENTION_DAYS", c.RetentionDays) +func (c GithubWorkflowContext) WithContainerFunc() dagger.WithContainerFunc { + return func(container *dagger.Container) *dagger.Container { + return container. + WithEnvVariable("GITHUB_WORKFLOW", c.Workflow). + WithEnvVariable("GITHUB_WORKFLOW_REF", c.WorkflowRef). + WithEnvVariable("GITHUB_WORKFLOW_SHA", c.WorkflowSHA). + WithEnvVariable("GITHUB_RUN_ID", c.RunID). + WithEnvVariable("GITHUB_RUN_NUMBER", c.RunNumber). + WithEnvVariable("GITHUB_RUN_ATTEMPT", c.RunAttempt). + WithEnvVariable("GITHUB_RETENTION_DAYS", c.RetentionDays) + } } +var _ helpers.WithContainerFuncHook = new(GithubJobInfoContext) + // GithubJobInfoContext is a context that contains information about the job. type GithubJobInfoContext struct { Job string `json:"job"` // Job is the job_id of the current job. Note: This context property is set by the Actions runner, and is only available within the execution steps of a job. Otherwise, the value of this property will be null. @@ -177,9 +195,10 @@ func NewGithubJobInfoContext(jobID string) GithubJobInfoContext { return GithubJobInfoContext{Job: jobID} } -// Apply applies the GithubJobInfoContext to the given container. -func (c GithubJobInfoContext) Apply(container *dagger.Container) *dagger.Container { - return container.WithEnvVariable("GITHUB_JOB", c.Job) +func (c GithubJobInfoContext) WithContainerFunc() dagger.WithContainerFunc { + return func(container *dagger.Container) *dagger.Container { + return container.WithEnvVariable("GITHUB_JOB", c.Job) + } } // JobContext contains information about the currently running job. diff --git a/internal/core/dagger.go b/internal/core/dagger.go index ded819af..3cc1db07 100644 --- a/internal/core/dagger.go +++ b/internal/core/dagger.go @@ -7,8 +7,11 @@ import ( "dagger.io/dagger" "github.com/aweris/gale/internal/config" + "github.com/aweris/gale/internal/dagger/helpers" ) +var _ helpers.WithContainerFuncHook = new(DaggerContext) + // DaggerContext represents the dagger engine connection information should be passed to the container type DaggerContext struct { RunnerHost string // RunnerHost where the dagger engine is running @@ -32,20 +35,21 @@ func NewDaggerContextFromEnv() *DaggerContext { } } -// Apply applies the dagger context to the container -func (d *DaggerContext) Apply(container *dagger.Container) *dagger.Container { - if d.RunnerHost != "" { - container = container.WithEnvVariable("_EXPERIMENTAL_DAGGER_RUNNER_HOST", d.RunnerHost) - } +func (d *DaggerContext) WithContainerFunc() dagger.WithContainerFunc { + return func(container *dagger.Container) *dagger.Container { + if d.RunnerHost != "" { + container = container.WithEnvVariable("_EXPERIMENTAL_DAGGER_RUNNER_HOST", d.RunnerHost) + } - if d.Session != "" { - container = container.WithEnvVariable("DAGGER_SESSION", d.Session) - } + if d.Session != "" { + container = container.WithEnvVariable("DAGGER_SESSION", d.Session) + } - // as a fallback, we're loading docker socket from the host. - if d.RunnerHost == "" || d.Session == "" { - container = container.WithUnixSocket("/var/run/docker.sock", config.Client().Host().UnixSocket(d.DockerSock)) - } + // as a fallback, we're loading docker socket from the host. + if d.RunnerHost == "" || d.Session == "" { + container = container.WithUnixSocket("/var/run/docker.sock", config.Client().Host().UnixSocket(d.DockerSock)) + } - return container + return container + } } diff --git a/internal/core/job.go b/internal/core/job.go index 0433f4de..8701a9e8 100644 --- a/internal/core/job.go +++ b/internal/core/job.go @@ -3,10 +3,12 @@ package core import ( "context" "encoding/json" + "fmt" "dagger.io/dagger" "github.com/aweris/gale/internal/config" + "github.com/aweris/gale/internal/dagger/helpers" ) // Job represents a single job in a GitHub Actions workflow @@ -20,23 +22,39 @@ type Job struct { // TBD: add more fields when needed } +var _ helpers.WithContainerFuncHook = new(JobRun) + +// JobRun represents a job run configuration that is passed to the container type JobRun struct { RunID string `json:"runID"` // RunID is the ID of the run Job Job `json:"job"` // Job is the job to run } -// MarshalJobRunToDir marshals the job run to a dagger directory -func MarshalJobRunToDir(_ context.Context, jobRun *JobRun) (*dagger.Directory, error) { - data, err := json.Marshal(jobRun) - if err != nil { - return nil, err +// NewJobRun creates a new job run +func NewJobRun(runID string, job Job) JobRun { + return JobRun{ + RunID: runID, + Job: job, } +} - dir := config.Client().Directory() +func (j JobRun) WithContainerFunc() dagger.WithContainerFunc { + return func(container *dagger.Container) *dagger.Container { + data, err := json.Marshal(j) + if err != nil { + return helpers.FailPipeline(container, err) + } - dir = dir.WithNewFile("job_run.json", string(data)) + if len(data) == 0 { + return helpers.FailPipeline(container, fmt.Errorf("job run is empty")) + } - return dir, nil + dir := config.Client().Directory() + + dir = dir.WithNewFile("job_run.json", string(data)) + + return container.WithDirectory(config.GhxRunDir(j.RunID), dir) + } } // UnmarshalJobRunFromDir unmarshal the job run from a dagger directory diff --git a/internal/dagger/helpers/helpers.go b/internal/dagger/helpers/helpers.go new file mode 100644 index 00000000..083fa8bb --- /dev/null +++ b/internal/dagger/helpers/helpers.go @@ -0,0 +1,25 @@ +package helpers + +import ( + "context" + + "dagger.io/dagger" +) + +// FailPipeline returns a container that immediately fails with the given error. This useful for forcing a pipeline to +// fail inside chaining operations. +func FailPipeline(container *dagger.Container, err error) *dagger.Container { + // fail the container with the given error + container = container.WithExec([]string{"sh", "-c", "echo " + err.Error() + " && exit 1"}) + + // forced evaluation of the pipeline to immediately fail + container, _ = container.Sync(context.Background()) + + return container +} + +// WithContainerFuncHook is an interface that ensures that implementers have a WithContainerFunc method. +type WithContainerFuncHook interface { + // WithContainerFunc returns a dagger function that allows the user to modify the container. + WithContainerFunc() dagger.WithContainerFunc +} diff --git a/internal/dagger/services/artifact_service.go b/internal/dagger/services/artifact_service.go index aaed9390..78d1e6f0 100644 --- a/internal/dagger/services/artifact_service.go +++ b/internal/dagger/services/artifact_service.go @@ -7,9 +7,12 @@ import ( "dagger.io/dagger" "github.com/aweris/gale/internal/config" + "github.com/aweris/gale/internal/dagger/helpers" "github.com/aweris/gale/internal/version" ) +var _ helpers.WithContainerFuncHook = new(ArtifactService) + // ArtifactService is the dagger service definitions for the services/artifact directory. type ArtifactService struct { client *dagger.Client @@ -49,15 +52,13 @@ func (a *ArtifactService) Container() *dagger.Container { return a.container } -// ServiceBinding returns a container with the artifact service binding and all necessary configurations. The method -// signature is compatible with the dagger.WithContainerFunc type. It can be used to as -// container.With(service.ServiceBinding) to bind the service to the container. -func (a *ArtifactService) ServiceBinding(container *dagger.Container) *dagger.Container { - container = container.WithServiceBinding(a.alias, a.container) - container = container.WithEnvVariable("ACTIONS_RUNTIME_URL", fmt.Sprintf("http://%s:%s/", a.alias, a.port)) - container = container.WithEnvVariable("ACTIONS_RUNTIME_TOKEN", "token") // dummy token, not used by service - - return container +func (a *ArtifactService) WithContainerFunc() dagger.WithContainerFunc { + return func(container *dagger.Container) *dagger.Container { + return container. + WithServiceBinding(a.alias, a.container). + WithEnvVariable("ACTIONS_RUNTIME_URL", fmt.Sprintf("http://%s:%s/", a.alias, a.port)). + WithEnvVariable("ACTIONS_RUNTIME_TOKEN", "token") // dummy token, not used by service + } } // Artifacts returns a artifact directory for the given run ID. diff --git a/internal/dagger/tools/ghx.go b/internal/dagger/tools/ghx.go index 780d1bcf..bf6e235e 100644 --- a/internal/dagger/tools/ghx.go +++ b/internal/dagger/tools/ghx.go @@ -7,21 +7,37 @@ import ( "dagger.io/dagger" "github.com/aweris/gale/internal/config" + "github.com/aweris/gale/internal/dagger/helpers" "github.com/aweris/gale/internal/version" ) -// Ghx returns a dagger file for the ghx binary. It'll return an error if the binary is not available. -func Ghx(ctx context.Context) (*dagger.File, error) { +var _ helpers.WithContainerFuncHook = new(Ghx) + +type Ghx struct { + tag string + file *dagger.File +} + +// NewGhxBinary returns a dagger file for the ghx binary. It'll return an error if the binary is not available. +func NewGhxBinary() *Ghx { v := version.GetVersion() tag := v.GitVersion file := config.Client().Container().From("ghcr.io/aweris/gale/tools/ghx:" + tag).File("/ghx") - // check, if the file doesn't exist or is empty - if size, err := file.Size(ctx); size == 0 || err != nil { - return nil, fmt.Errorf("ghx@%s binary not available", tag) - } + return &Ghx{tag: tag, file: file} +} + +func (g *Ghx) WithContainerFunc() dagger.WithContainerFunc { + return func(container *dagger.Container) *dagger.Container { + // check, if the file doesn't exist or is empty + if size, err := g.file.Size(context.Background()); size == 0 || err != nil { + return helpers.FailPipeline(container, fmt.Errorf("ghx@%s binary not available", g.tag)) + } - return file, nil + return container.WithFile("/usr/local/bin/ghx", g.file). + WithEnvVariable("GHX_HOME", config.GhxHome()). + WithMountedCache(config.GhxActionsDir(), config.Client().CacheVolume("actions")) + } } diff --git a/pkg/gale/gale.go b/pkg/gale/gale.go index 8cca38f3..eb0d244a 100644 --- a/pkg/gale/gale.go +++ b/pkg/gale/gale.go @@ -2,12 +2,11 @@ package gale import ( "context" - "fmt" "dagger.io/dagger" - "github.com/aweris/gale/internal/config" "github.com/aweris/gale/internal/core" + "github.com/aweris/gale/internal/dagger/helpers" "github.com/aweris/gale/internal/dagger/services" "github.com/aweris/gale/internal/dagger/tools" "github.com/aweris/gale/internal/idgen" @@ -32,22 +31,22 @@ func Run(ctx context.Context, workflow, job string, opts ...RunOpts) dagger.With repo, err := core.GetRepository(opt.Repo, core.GetRepositoryOpts{Branch: opt.Branch, Tag: opt.Tag, Commit: opt.Commit}) if err != nil { - return fail(container, err) + return helpers.FailPipeline(container, err) } workflows, err := repo.LoadWorkflows(ctx, core.RepositoryLoadWorkflowOpts{WorkflowsDir: opt.WorkflowsDir}) if err != nil { - return fail(container, err) + return helpers.FailPipeline(container, err) } wf, ok := workflows[workflow] if !ok { - return fail(container, ErrWorkflowNotFound) + return helpers.FailPipeline(container, ErrWorkflowNotFound) } jm, ok := wf.Jobs[job] if !ok { - return fail(container, ErrJobNotFound) + return helpers.FailPipeline(container, ErrJobNotFound) } // ensure job name is set @@ -57,39 +56,30 @@ func Run(ctx context.Context, workflow, job string, opts ...RunOpts) dagger.With workflowRunID, err := idgen.GenerateWorkflowRunID(repo) if err != nil { - return fail(container, err) + return helpers.FailPipeline(container, err) } jobRunID, err := idgen.GenerateJobRunID(repo) if err != nil { - return fail(container, err) - } - - jr := &core.JobRun{ - RunID: jobRunID, - Job: jm, - } - - dir, err := core.MarshalJobRunToDir(ctx, jr) - if err != nil { - return fail(container, err) + return helpers.FailPipeline(container, err) } token, err := core.GetToken() if err != nil { - return fail(container, err) + return helpers.FailPipeline(container, err) } // context configuration - container = container.With(core.NewRunnerContext().Apply) - container = container.With(core.NewGithubRepositoryContext(repo).Apply) - container = container.With(core.NewGithubSecretsContext(token).Apply) - container = container.With(core.NewGithubURLContext().Apply) - container = container.With(core.NewGithubWorkflowContext(repo, wf, workflowRunID).Apply) - container = container.With(core.NewGithubJobInfoContext(job).Apply) + container = container.With(core.NewRunnerContext().WithContainerFunc()) + container = container.With(core.NewGithubRepositoryContext(repo).WithContainerFunc()) + container = container.With(core.NewGithubSecretsContext(token).WithContainerFunc()) + container = container.With(core.NewGithubURLContext().WithContainerFunc()) + container = container.With(core.NewGithubWorkflowContext(repo, wf, workflowRunID).WithContainerFunc()) + container = container.With(core.NewGithubJobInfoContext(job).WithContainerFunc()) // job run configuration - container = container.WithDirectory(config.GhxRunDir(jobRunID), dir) + container = container.With(core.NewJobRun(jobRunID, jm).WithContainerFunc()) + container = container.WithExec([]string{"/usr/local/bin/ghx", "run", jobRunID}) return container @@ -100,35 +90,14 @@ func Run(ctx context.Context, workflow, job string, opts ...RunOpts) dagger.With func ExecutionEnv(_ context.Context) dagger.WithContainerFunc { return func(container *dagger.Container) *dagger.Container { // pass dagger context to the container - container = container.With(core.NewDaggerContextFromEnv().Apply) + container = container.With(core.NewDaggerContextFromEnv().WithContainerFunc()) // tools - - ghx, err := tools.Ghx(context.Background()) - if err != nil { - fail(container, fmt.Errorf("error getting ghx: %w", err)) - } - - container = container.WithFile("/usr/local/bin/ghx", ghx) - container = container.WithEnvVariable("GHX_HOME", config.GhxHome()) - container = container.WithMountedCache(config.GhxActionsDir(), config.Client().CacheVolume("actions")) + container = container.With(tools.NewGhxBinary().WithContainerFunc()) // services - - container = container.With(services.NewArtifactService().ServiceBinding) + container = container.With(services.NewArtifactService().WithContainerFunc()) // TODO: move service to context or outside to be able to use it later to get artifacts return container } } - -// fail returns a container that immediately fails with the given error. This useful for forcing a pipeline to fail -// inside chaining operations. -func fail(container *dagger.Container, err error) *dagger.Container { - // fail the container with the given error - container = container.WithExec([]string{"sh", "-c", "echo " + err.Error() + " && exit 1"}) - - // forced evaluation of the pipeline to immediately fail - container, _ = container.Sync(context.Background()) - - return container -}