From 710ecdaca8c32b6d5d0800d0932d8ba47a728555 Mon Sep 17 00:00:00 2001 From: Joshua Gilman Date: Fri, 17 Jan 2025 14:20:21 -0800 Subject: [PATCH] feat(cli): performs dry-runs on deployments (#120) --- .github/workflows/ci.yml | 2 +- cli/cmd/cmds/deploy/{deploy.go => push.go} | 7 ++-- cli/pkg/deployment/gitops.go | 37 ++++++++++++++-------- cli/pkg/deployment/gitops_test.go | 26 ++++++++++++++- cli/pkg/deployment/kcl.go | 2 +- cli/pkg/deployment/kcl_test.go | 4 +-- foundry/api/blueprint.cue | 10 +++--- 7 files changed, 61 insertions(+), 27 deletions(-) rename cli/cmd/cmds/deploy/{deploy.go => push.go} (89%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a8407571..5851710a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -162,7 +162,7 @@ jobs: earthly_token: ${{ secrets.earthly_token }} final: - needs: [check, build, package, test, release] + needs: [check, build, package, test, release, deploy] if: ${{ always() && (contains(needs.*.result, 'failure') || !failure() && !cancelled()) }} runs-on: ubuntu-latest steps: diff --git a/cli/cmd/cmds/deploy/deploy.go b/cli/cmd/cmds/deploy/push.go similarity index 89% rename from cli/cmd/cmds/deploy/deploy.go rename to cli/cmd/cmds/deploy/push.go index ea87587d..43b1a19a 100644 --- a/cli/cmd/cmds/deploy/deploy.go +++ b/cli/cmd/cmds/deploy/push.go @@ -19,13 +19,14 @@ func (c *PushCmd) Run(ctx run.RunContext) error { return fmt.Errorf("could not load project: %w", err) } + var dryrun bool eh := events.NewDefaultEventHandler(ctx.Logger) if !eh.Firing(&project, project.GetDeploymentEvents()) && !c.Force { - ctx.Logger.Info("No deployment event is firing, skipping deployment") - return nil + ctx.Logger.Info("No deployment event is firing, performing dry-run") + dryrun = true } - deployer := deployment.NewGitopsDeployer(&project, &ctx.SecretStore, ctx.Logger) + deployer := deployment.NewGitopsDeployer(&project, &ctx.SecretStore, ctx.Logger, dryrun) if err := deployer.Load(); err != nil { return fmt.Errorf("could not load deployer: %w", err) } diff --git a/cli/pkg/deployment/gitops.go b/cli/pkg/deployment/gitops.go index 16198540..aa1b4dcc 100644 --- a/cli/pkg/deployment/gitops.go +++ b/cli/pkg/deployment/gitops.go @@ -45,6 +45,7 @@ func (g gitRemote) Push(repo *git.Repository, o *git.PushOptions) error { // GitopsDeployer is a deployer that deploys projects to a GitOps repository. type GitopsDeployer struct { + dryrun bool fs billy.Filesystem repo *git.Repository kcl KCLRunner @@ -108,21 +109,29 @@ func (g *GitopsDeployer) Deploy() error { } } - changes, err := g.hasChanges() - if err != nil { - return fmt.Errorf("could not check if worktree has changes: %w", err) - } else if !changes { - return ErrNoChanges - } + if !g.dryrun { + changes, err := g.hasChanges() + if err != nil { + return fmt.Errorf("could not check if worktree has changes: %w", err) + } else if !changes { + return ErrNoChanges + } - g.logger.Info("Committing changes", "path", bundlePath) - if err := g.commit(); err != nil { - return fmt.Errorf("could not commit changes: %w", err) - } + g.logger.Info("Committing changes", "path", bundlePath) + if err := g.commit(); err != nil { + return fmt.Errorf("could not commit changes: %w", err) + } - g.logger.Info("Pushing changes") - if err := g.push(); err != nil { - return fmt.Errorf("could not push changes: %w", err) + g.logger.Info("Pushing changes") + if err := g.push(); err != nil { + return fmt.Errorf("could not push changes: %w", err) + } + } else { + g.logger.Info("Dry-run: not committing or pushing changes") + g.logger.Info("Dumping manifests") + for _, r := range result { + fmt.Print(r.Manifests) + } } return nil @@ -245,12 +254,14 @@ func NewGitopsDeployer( project *project.Project, store *secrets.SecretStore, logger *slog.Logger, + dryrun bool, ) GitopsDeployer { if logger == nil { logger = slog.New(slog.NewTextHandler(io.Discard, nil)) } return GitopsDeployer{ + dryrun: dryrun, fs: memfs.New(), kcl: NewKCLRunner(logger), logger: logger, diff --git a/cli/pkg/deployment/gitops_test.go b/cli/pkg/deployment/gitops_test.go index 08b14304..5b95b66f 100644 --- a/cli/pkg/deployment/gitops_test.go +++ b/cli/pkg/deployment/gitops_test.go @@ -76,6 +76,7 @@ func TestDeploy(t *testing.T) { project projectParams yaml string execFail bool + dryrun bool setup func(*testing.T, *GitopsDeployer, *testutils.InMemRepo) validate func(*testing.T, *GitopsDeployer, mockGitRemote, *testutils.InMemRepo) expectErr bool @@ -87,6 +88,7 @@ func TestDeploy(t *testing.T) { project: defaultParams, yaml: "yaml", execFail: false, + dryrun: false, setup: func(t *testing.T, deployer *GitopsDeployer, repo *testutils.InMemRepo) { deployer.token = "test" repo.MkdirAll(t, "deploy/dev/apps") @@ -106,6 +108,27 @@ func TestDeploy(t *testing.T) { expectErr: false, expectedErr: "", }, + { + name: "dry-run", + mock: mockGitRemote{}, + project: defaultParams, + yaml: "yaml", + execFail: false, + dryrun: true, + setup: func(t *testing.T, deployer *GitopsDeployer, repo *testutils.InMemRepo) { + deployer.token = "test" + repo.MkdirAll(t, "deploy/dev/apps") + }, + validate: func(t *testing.T, deployer *GitopsDeployer, mock mockGitRemote, repo *testutils.InMemRepo) { + assert.True(t, repo.Exists(t, "deploy/dev/apps/test/main.yaml"), "main.yaml does not exist") + assert.Equal(t, repo.ReadFile(t, "deploy/dev/apps/test/main.yaml"), []byte("yaml"), "main.yaml content is incorrect") + + _, err := repo.Repo.Head() + require.Error(t, err) // No commit should be made + }, + expectErr: false, + expectedErr: "", + }, { name: "no changes", mock: mockGitRemote{}, @@ -138,7 +161,8 @@ func TestDeploy(t *testing.T) { repo := testutils.NewInMemRepo(t) var calls []string deployer := GitopsDeployer{ - fs: repo.Fs, + dryrun: tt.dryrun, + fs: repo.Fs, kcl: KCLRunner{ logger: testutils.NewNoopLogger(), kcl: newWrappedExecuterMock(tt.yaml, &calls, tt.execFail), diff --git a/cli/pkg/deployment/kcl.go b/cli/pkg/deployment/kcl.go index 487a20fa..77023327 100644 --- a/cli/pkg/deployment/kcl.go +++ b/cli/pkg/deployment/kcl.go @@ -147,7 +147,7 @@ func encodeValues(ctx *cue.Context, module schema.Module) ([]byte, error) { // run runs a KCL module with the given module container and arguments. func (k *KCLRunner) run(container string, moduleArgs KCLModuleArgs) ([]byte, error) { - args := []string{"run", "-q"} + args := []string{"run", "-q", "--no_style"} args = append(args, moduleArgs.Serialize()...) args = append(args, fmt.Sprintf("oci://%s", container)) diff --git a/cli/pkg/deployment/kcl_test.go b/cli/pkg/deployment/kcl_test.go index 801aac33..e2c1c663 100644 --- a/cli/pkg/deployment/kcl_test.go +++ b/cli/pkg/deployment/kcl_test.go @@ -140,8 +140,8 @@ func TestKCLRunnerRunDeployment(t *testing.T) { assert.Equal(t, "key: value\n", r.result["main"].Values) assert.Equal(t, "output", r.result["support"].Manifests) assert.Equal(t, "key1: value1\n", r.result["support"].Values) - assert.Contains(t, r.calls, "run -q -D name= -D namespace=default -D values={\"key\":\"value\"} -D 1.0.0 oci://test.com/module") - assert.Contains(t, r.calls, "run -q -D name= -D namespace=default -D values={\"key1\":\"value1\"} -D 1.0.0 oci://test.com/module1") + assert.Contains(t, r.calls, "run -q --no_style -D name= -D namespace=default -D values={\"key\":\"value\"} -D 1.0.0 oci://test.com/module") + assert.Contains(t, r.calls, "run -q --no_style -D name= -D namespace=default -D values={\"key1\":\"value1\"} -D 1.0.0 oci://test.com/module1") }, }, { diff --git a/foundry/api/blueprint.cue b/foundry/api/blueprint.cue index 3937a1f7..da8f400b 100644 --- a/foundry/api/blueprint.cue +++ b/foundry/api/blueprint.cue @@ -15,9 +15,8 @@ project: { } deployment: { on: { - //merge: {} - //tag: {} - always: {} + merge: {} + tag: {} } environment: "dev" modules: { @@ -47,9 +46,8 @@ project: { release: { docker: { on: { - //merge: {} - //tag: {} - always: {} + merge: {} + tag: {} } config: { tag: _ @forge(name="GIT_HASH_OR_TAG")