Skip to content

Commit

Permalink
feat: Add project depends on functionality (runatlantis#3821)
Browse files Browse the repository at this point in the history
* feat: implemented the code for the depends on functionnality

* chore: Address PR comments

---------

Co-authored-by: Luay-Sol <[email protected]>
  • Loading branch information
2 people authored and ijames-gc committed Feb 13, 2024
1 parent 6860c33 commit 2480525
Show file tree
Hide file tree
Showing 10 changed files with 197 additions and 5 deletions.
21 changes: 21 additions & 0 deletions runatlantis.io/docs/depends-on.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Depends_on Argument
[[toc]]

## Description
The depends_on argument allow you to enforce dependencies between projects. Use the depends_on argument to handle cases
where require one project to be applied prior to the other.

## What Happens if one or more project's dependencies are not applied?
If there's one or more projects in the dependency list is not in an applied status, users will see an error if they try
to run `atlantis apply`.

### Usage
1. In `atlantis.yaml` file specify the `depends_on` key under the project config:
#### atlantis.yaml
```yaml
version: 3
projects:
- dir: .
name: project-2
depends_on: [project-1]
```
8 changes: 8 additions & 0 deletions server/core/config/raw/project.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ type Project struct {
PlanRequirements []string `yaml:"plan_requirements,omitempty"`
ApplyRequirements []string `yaml:"apply_requirements,omitempty"`
ImportRequirements []string `yaml:"import_requirements,omitempty"`
DependsOn []string `yaml:"depends_on,omitempty"`
DeleteSourceBranchOnMerge *bool `yaml:"delete_source_branch_on_merge,omitempty"`
RepoLocking *bool `yaml:"repo_locking,omitempty"`
ExecutionOrderGroup *int `yaml:"execution_order_group,omitempty"`
Expand Down Expand Up @@ -73,12 +74,17 @@ func (p Project) Validate() error {
return errors.Wrapf(err, "parsing: %s", branch)
}

Dependencies := func(value interface{}) error {
return nil
}

return validation.ValidateStruct(&p,
validation.Field(&p.Dir, validation.Required, validation.By(hasDotDot)),
validation.Field(&p.PlanRequirements, validation.By(validPlanReq)),
validation.Field(&p.ApplyRequirements, validation.By(validApplyReq)),
validation.Field(&p.ImportRequirements, validation.By(validImportReq)),
validation.Field(&p.TerraformVersion, validation.By(VersionValidator)),
validation.Field(&p.DependsOn, validation.By(Dependencies)),
validation.Field(&p.Name, validation.By(validName)),
validation.Field(&p.Branch, validation.By(branchValid)),
)
Expand Down Expand Up @@ -122,6 +128,8 @@ func (p Project) ToValid() valid.Project {

v.Name = p.Name

v.DependsOn = p.DependsOn

if p.DeleteSourceBranchOnMerge != nil {
v.DeleteSourceBranchOnMerge = p.DeleteSourceBranchOnMerge
}
Expand Down
5 changes: 2 additions & 3 deletions server/core/config/valid/global_cfg.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,7 @@ const PoliciesPassedCommandReq = "policies_passed"
const PlanRequirementsKey = "plan_requirements"
const ApplyRequirementsKey = "apply_requirements"
const ImportRequirementsKey = "import_requirements"
const PreWorkflowHooksKey = "pre_workflow_hooks"
const WorkflowKey = "workflow"
const PostWorkflowHooksKey = "post_workflow_hooks"
const AllowedWorkflowsKey = "allowed_workflows"
const AllowedOverridesKey = "allowed_overrides"
const AllowCustomWorkflowsKey = "allow_custom_workflows"
const DefaultWorkflowName = "default"
Expand Down Expand Up @@ -90,6 +87,7 @@ type MergedProjectCfg struct {
ImportRequirements []string
Workflow Workflow
AllowedWorkflows []string
DependsOn []string
RepoRelDir string
Workspace string
Name string
Expand Down Expand Up @@ -380,6 +378,7 @@ func (g GlobalCfg) MergeProjectCfg(log logging.SimpleLogging, repoID string, pro
Workflow: workflow,
RepoRelDir: proj.Dir,
Workspace: proj.Workspace,
DependsOn: proj.DependsOn,
Name: proj.GetName(),
AutoplanEnabled: proj.Autoplan.Enabled,
TerraformVersion: proj.TerraformVersion,
Expand Down
1 change: 1 addition & 0 deletions server/core/config/valid/repo_cfg.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ type Project struct {
PlanRequirements []string
ApplyRequirements []string
ImportRequirements []string
DependsOn []string
DeleteSourceBranchOnMerge *bool
RepoLocking *bool
ExecutionOrderGroup int
Expand Down
9 changes: 9 additions & 0 deletions server/events/command/project_context.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,13 @@ type ProjectContext struct {
// If the pull request branch is from the same repository then HeadRepo will
// be the same as BaseRepo.
HeadRepo models.Repo
// DependsOn are a list of project that this project relies on
// their apply status. These projects must be applied first.
//
// Atlantis uses this information to valid the apply
// orders and to warn the user if they're applying a project that
// depends on other projects.
DependsOn []string
// Log is a logger that's been set up for this context.
Log logging.SimpleLogging
// Scope is the scope for reporting stats setup for this context
Expand All @@ -67,6 +74,8 @@ type ProjectContext struct {
ProjectPlanStatus models.ProjectPlanStatus
// ProjectPolicyStatus is the status of policy sets of the current project prior to this command.
ProjectPolicyStatus []models.PolicySetStatus
// PullStatus is the current status of a pull request that is in progress.
PullStatus *models.PullStatus
// Pull is the pull request we're responding to.
Pull models.PullRequest
// ProjectName is the name of the project set in atlantis.yaml. If there was
Expand Down
18 changes: 18 additions & 0 deletions server/events/command_requirement_handler.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
package events

import (
"fmt"

"github.com/runatlantis/atlantis/server/core/config/raw"
"github.com/runatlantis/atlantis/server/core/config/valid"
"github.com/runatlantis/atlantis/server/events/command"
"github.com/runatlantis/atlantis/server/events/models"
)

//go:generate pegomock generate --package mocks -o mocks/mock_command_requirement_handler.go CommandRequirementHandler
type CommandRequirementHandler interface {
ValidateProjectDependencies(ctx command.ProjectContext) (string, error)
ValidatePlanProject(repoDir string, ctx command.ProjectContext) (string, error)
ValidateApplyProject(repoDir string, ctx command.ProjectContext) (string, error)
ValidateImportProject(repoDir string, ctx command.ProjectContext) (string, error)
Expand Down Expand Up @@ -65,6 +69,20 @@ func (a *DefaultCommandRequirementHandler) ValidateApplyProject(repoDir string,
return "", nil
}

func (a *DefaultCommandRequirementHandler) ValidateProjectDependencies(ctx command.ProjectContext) (failure string, err error) {
for _, dependOnProject := range ctx.DependsOn {

for _, project := range ctx.PullStatus.Projects {

if project.ProjectName == dependOnProject && project.Status != models.AppliedPlanStatus {
return fmt.Sprintf("Can't apply your project unless you apply its dependencies: [%s]", project.ProjectName), nil
}
}
}

return "", nil
}

func (a *DefaultCommandRequirementHandler) ValidateImportProject(repoDir string, ctx command.ProjectContext) (failure string, err error) {
for _, req := range ctx.ImportRequirements {
switch req {
Expand Down
107 changes: 107 additions & 0 deletions server/events/command_requirement_handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,113 @@ func TestAggregateApplyRequirements_ValidateApplyProject(t *testing.T) {
}
}

func TestRequirements_ValidateProjectDependencies(t *testing.T) {
tests := []struct {
name string
ctx command.ProjectContext
setup func(workingDir *mocks.MockWorkingDir)
wantFailure string
wantErr assert.ErrorAssertionFunc
}{
{
name: "pass no dependencies",
ctx: command.ProjectContext{},
wantErr: assert.NoError,
},
{
name: "pass all dependencies applied",
ctx: command.ProjectContext{
DependsOn: []string{"project1"},
PullStatus: &models.PullStatus{
Projects: []models.ProjectStatus{
{
ProjectName: "project1",
Status: models.AppliedPlanStatus,
},
},
},
},
wantErr: assert.NoError,
},
{
name: "Fail all dependencies are not applied",
ctx: command.ProjectContext{
DependsOn: []string{"project1", "project2"},
PullStatus: &models.PullStatus{
Projects: []models.ProjectStatus{
{
ProjectName: "project1",
Status: models.PlannedPlanStatus,
},
{
ProjectName: "project2",
Status: models.ErroredApplyStatus,
},
},
},
},
wantFailure: "Can't apply your project unless you apply its dependencies: [project1]",
wantErr: assert.NoError,
},
{
name: "Fail one of dependencies is not applied",
ctx: command.ProjectContext{
DependsOn: []string{"project1", "project2"},
PullStatus: &models.PullStatus{
Projects: []models.ProjectStatus{
{
ProjectName: "project1",
Status: models.AppliedPlanStatus,
},
{
ProjectName: "project2",
Status: models.ErroredApplyStatus,
},
},
},
},
wantFailure: "Can't apply your project unless you apply its dependencies: [project2]",
wantErr: assert.NoError,
},
{
name: "Fail one of dependencies is not applied",
ctx: command.ProjectContext{
DependsOn: []string{"project1", "project2", "project3"},
PullStatus: &models.PullStatus{
Projects: []models.ProjectStatus{
{
ProjectName: "project1",
Status: models.AppliedPlanStatus,
},
{
ProjectName: "project2",
Status: models.ErroredApplyStatus,
},
{
ProjectName: "project3",
Status: models.PlannedPlanStatus,
},
},
},
},
wantFailure: "Can't apply your project unless you apply its dependencies: [project2]",
wantErr: assert.NoError,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
RegisterMockTestingT(t)
workingDir := mocks.NewMockWorkingDir()
a := &events.DefaultCommandRequirementHandler{WorkingDir: workingDir}
gotFailure, err := a.ValidateProjectDependencies(tt.ctx)
if !tt.wantErr(t, err, fmt.Sprintf("ValidateProjectDependencies(%v)", tt.ctx)) {
return
}
assert.Equalf(t, tt.wantFailure, gotFailure, "ValidateProjectDependencies(%v)", tt.ctx)
})
}
}

func TestAggregateApplyRequirements_ValidateImportProject(t *testing.T) {
repoDir := "repoDir"
fullRequirements := []string{
Expand Down
19 changes: 19 additions & 0 deletions server/events/mocks/mock_command_requirement_handler.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 7 additions & 2 deletions server/events/project_command_context_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ func (cb *DefaultProjectCommandContextBuilder) BuildProjectContext(
abortOnExcecutionOrderFail,
ctx.Scope,
ctx.PullRequestStatus,
ctx.PullStatus,
)

projectCmds = append(projectCmds, projectCmdContext)
Expand Down Expand Up @@ -215,6 +216,7 @@ func (cb *PolicyCheckProjectCommandContextBuilder) BuildProjectContext(
abortOnExcecutionOrderFail,
ctx.Scope,
ctx.PullRequestStatus,
ctx.PullStatus,
))
}

Expand All @@ -238,7 +240,8 @@ func newProjectCommandContext(ctx *command.Context,
verbose bool,
abortOnExcecutionOrderFail bool,
scope tally.Scope,
pullStatus models.PullReqStatus,
pullReqStatus models.PullReqStatus,
pullStatus *models.PullStatus,
) command.ProjectContext {

var projectPlanStatus models.ProjectPlanStatus
Expand Down Expand Up @@ -274,6 +277,7 @@ func newProjectCommandContext(ctx *command.Context,
ParallelApplyEnabled: parallelApplyEnabled,
ParallelPlanEnabled: parallelPlanEnabled,
ParallelPolicyCheckEnabled: parallelPlanEnabled,
DependsOn: projCfg.DependsOn,
AutoplanEnabled: projCfg.AutoplanEnabled,
Steps: steps,
HeadRepo: ctx.HeadRepo,
Expand All @@ -296,7 +300,8 @@ func newProjectCommandContext(ctx *command.Context,
PolicySets: policySets,
PolicySetTarget: ctx.PolicySet,
ClearPolicyApproval: ctx.ClearPolicyApproval,
PullReqStatus: pullStatus,
PullReqStatus: pullReqStatus,
PullStatus: pullStatus,
JobID: uuid.New().String(),
ExecutionOrderGroup: projCfg.ExecutionOrderGroup,
AbortOnExcecutionOrderFail: abortOnExcecutionOrderFail,
Expand Down
5 changes: 5 additions & 0 deletions server/events/project_command_runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -598,6 +598,11 @@ func (p *DefaultProjectCommandRunner) doApply(ctx command.ProjectContext) (apply
return "", failure, err
}

failure, err = p.CommandRequirementHandler.ValidateProjectDependencies(ctx)
if failure != "" || err != nil {
return "", failure, err
}

// Acquire internal lock for the directory we're going to operate in.
unlockFn, err := p.WorkingDirLocker.TryLock(ctx.Pull.BaseRepo.FullName, ctx.Pull.Num, ctx.Workspace, ctx.RepoRelDir)
if err != nil {
Expand Down

0 comments on commit 2480525

Please sign in to comment.