Architecture
January 26, 2024 ยท View on GitHub
This repository contains the source code for the ccr CLI. This tool allow application developers to run CodeCatalyst workflows locally.
Project Layout
At a high level, this repository consists of Go workspace with three modules. Each module follows the Standard Go Project Layout.
-
codecatalyst-runner/ - the code to define actions with CodeCatalyst ADK as well as the code for the
ccrexecutable to run CodeCatalyst workflows locally- main.go - the main entrypoint for the CLI. The arguments are processed by code in
cmd/package. - cmd/ - code that defines the arguments and subcommands for the CLI using the Cobra CLI framework. Each subcommand (e.g.
ccr execute) is defined in a separate file. The command incmd/should be lightweight and delegate actual work to thepkg/packages. - pkg/ - code that runs actions. This code is used by the CLI.
- actions/ - code to load actions from local paths or remote URLs. Also contains implementations of the Plan interface that is defined from the action. Definesfeatures to run actions such as input/output handlers.
- workflows/ - code to load CodeCatalyst workflows from local paths. Also contains implementations of the Plan interface that is defined from the workflow. Defines feature to run workflows such as report processors, variable handlers, and file caching.
- main.go - the main entrypoint for the CLI. The arguments are processed by code in
-
command-runner/ - the code to plan and execute commands in Docker, Finch or the local shell.
- internal/ - code that supports the
pkg/packages but is not available to be called outside thecommand-runnermodule. This package includes utilities to work with tar files, zip files, JSONL files, and git repositories. - pkg/ - code that defines plans and runs them as commands in Docker, Finch, or the local shell. This code is used by the
codecatalyst-runner.- common/ - code that implements the executor pattern.
- features/ - generic features that aren't tied to CodeCatalyst specific capabilities. Examples include loggers, working directory importers, and plan dependencies.
- runner/ - code that defines the Plan and Feature interfaces. Also contains code to implement Finch, Docker, and Shell plan runners.
- internal/ - code that supports the
Patterns
The following patterns are found throughout this repository.
Plan
The Plan interface describes a set of commands to run along with the environment to be used for running those commands. This abstraction allows plans to be defined in various forms (e.g. CodeCatalyst workflows) and run consistently. Additionally, the abstraction allows different runner to be implemented (e.g. Finch, Docker, and Shell).
Feature
The Feature type describes a function that wraps the running of a plan. This construct allows new features to be developed and applied during the execution of Plan. These features can be developed, tested, and applied indepenendently. Features have the ability to update the Plan before being run.
Executor
The executor pattern is defined in command-runner/pkg/common/executor.go. An Executor is a functino that receives a Context and optionally returns an error. This pattern enables a functional style of programming where pipelines of Executors are constructed to run in series or in parallel. Errors can be caught through Catch().
type Executor func(ctx context.Context) error
Dependency Injection
Dependencies ought to be passed into functions, rather than instantiated within functions. For example, a session from AWS S3 SDK should not be initialized within a function that uses it. Instead, pass an Interface as a Struct Param to the function. This approach improves composability and testability of the code.
Struct Params
The standard signature should use a Context for the first parameter and a struct for the second. The struct should be specifically defined for the function being called with the same name plus a Params suffix.
type MyFuncParams struct {
Foo string
Bar string
}
func MyFunc(ctx context.Context, params MyFuncParams) error {
....
}
Interfaces for Testing
Functions should define their own interfaces for the parameters they take. This allows unit tests to be created that mock these interfaces. The example below demonstrates a function that depends on the AWS S3 SDK. Rather than pass the entire S3 client, an interface is defined with just the functions that theMyFunc() uses. This allows us to write unit tests for MyFunc() and we only have to mock the two functions from the S3 SDK that are used.
type MyFuncParams struct {
Foo string
Bar string
S3Client MyObjectAPIClient
}
type MyObjectAPIClient interface {
GetObject(context.Context, *s3.GetObjectInput, ...func(*s3.Options)) (*s3.GetObjectOutput, error)
PutObject(context.Context, *s3.PutObjectInput, ...func(*s3.Options)) (*s3.PutObjectOutput, error)
}
func MyFunc(ctx context.Context, params MyFuncParams) error {
....
}
Table Driven Tests
Tests are kept close the the code under test. Create a file of the same name as the code under test with a _test.go suffix in the same directory as the code under test. Use table driven tests to reduce duplication in tests.
Go Doc Comments
All types and functions that are available outside their package (start with an upper case) must be documented with go doc comments.