Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ inputs:
required: false
default: 'comments_per_run'
mode:
description: 'manual or otherwise'
description: 'manual, drift-detection or otherwise'
required: false
default: ''
command:
Expand All @@ -105,6 +105,10 @@ inputs:
description: 'project name for digger to run in case of manual mode'
required: false
default: ''
drift-detection-slack-notification-url:
description: 'drift-detection slack notification url'
required: false
default: ''

outputs:
output:
Expand Down Expand Up @@ -211,6 +215,7 @@ runs:
INPUT_DIGGER_PROJECT: ${{ inputs.project }}
INPUT_DIGGER_MODE: ${{ inputs.mode }}
INPUT_DIGGER_COMMAND: ${{ inputs.command }}
INPUT_DRIFT_DETECTION_SLACK_NOTIFICATION_URL: ${{ inputs.drift-detection-slack-notification-url }}
run: |
cd ${{ github.action_path }}
go build -o digger ./cmd/digger
Expand All @@ -233,6 +238,7 @@ runs:
INPUT_DIGGER_PROJECT: ${{ inputs.project }}
INPUT_DIGGER_MODE: ${{ inputs.mode }}
INPUT_DIGGER_COMMAND: ${{ inputs.command }}
INPUT_DRIFT_DETECTION_SLACK_NOTIFICATION_URL: ${{ inputs.drift-detection-slack-notification-url }}
id: digger
shell: bash
run: |
Expand Down
26 changes: 26 additions & 0 deletions cmd/digger/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,32 @@ func gitHubCI(lock core_locking.Lock, policyChecker core_policy.Checker, reporti
if err != nil {
reportErrorAndExit(githubActor, fmt.Sprintf("Failed to run commands. %s", err), 8)
}
} else if runningMode == "drift-detection" {

for _, projectConfig := range diggerConfig.Projects {
if !projectConfig.DriftDetection {
continue
}
workflow := diggerConfig.Workflows[projectConfig.Workflow]

stateEnvVars, commandEnvVars := configuration.CollectTerraformEnvConfig(workflow.EnvVars)

projectCommand := models.ProjectCommand{
ProjectName: projectConfig.Name,
ProjectDir: projectConfig.Dir,
ProjectWorkspace: projectConfig.Workspace,
Terragrunt: projectConfig.Terragrunt,
Commands: []string{"digger drift-detect"},
ApplyStage: workflow.Apply,
PlanStage: workflow.Plan,
CommandEnvVars: commandEnvVars,
StateEnvVars: stateEnvVars,
}
err := digger.RunCommandForProject(projectCommand, ghRepository, githubActor, "drift-detect", &githubPrService, policyChecker, nil, currentDir)
if err != nil {
reportErrorAndExit(githubActor, fmt.Sprintf("Failed to run commands. %s", err), 8)
}
}
} else {
parsedGhContext, err := github_models.GetGitHubContext(ghContext)
if err != nil {
Expand Down
1 change: 1 addition & 0 deletions pkg/configuration/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ type Project struct {
IncludePatterns []string
ExcludePatterns []string
DependencyProjects []string
DriftDetection bool
}

type Workflow struct {
Expand Down
5 changes: 5 additions & 0 deletions pkg/configuration/converters.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ const defaultWorkflowName = "default"
func copyProjects(projects []*ProjectYaml) []Project {
result := make([]Project, len(projects))
for i, p := range projects {
driftDetection := true
if p.DriftDetection != nil {
driftDetection = *p.DriftDetection
}
item := Project{p.Name,
p.Dir,
p.Workspace,
Expand All @@ -21,6 +25,7 @@ func copyProjects(projects []*ProjectYaml) []Project {
p.IncludePatterns,
p.ExcludePatterns,
p.DependencyProjects,
driftDetection,
}
result[i] = item
}
Expand Down
1 change: 1 addition & 0 deletions pkg/configuration/yaml.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type ProjectYaml struct {
IncludePatterns []string `yaml:"include_patterns,omitempty"`
ExcludePatterns []string `yaml:"exclude_patterns,omitempty"`
DependencyProjects []string `yaml:"depends_on,omitempty"`
DriftDetection *bool `yaml:"drift_detection,omitempty"`
}

type WorkflowYaml struct {
Expand Down
34 changes: 19 additions & 15 deletions pkg/core/execution/execution.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import (
)

type Executor interface {
Plan() (bool, string, string, error)
Plan() (bool, bool, string, string, error)
Apply() (bool, error)
}

Expand All @@ -26,17 +26,17 @@ type LockingExecutorWrapper struct {
Executor Executor
}

func (l LockingExecutorWrapper) Plan() (bool, string, string, error) {
func (l LockingExecutorWrapper) Plan() (bool, bool, string, string, error) {
plan := ""
locked, err := l.ProjectLock.Lock()
if err != nil {
return false, "", "", fmt.Errorf("error locking project: %v", err)
return false, false, "", "", fmt.Errorf("error locking project: %v", err)
}
log.Printf("Lock result: %t\n", locked)
if locked {
return l.Executor.Plan()
} else {
return false, plan, "", nil
return false, false, plan, "", nil
}
}

Expand Down Expand Up @@ -108,9 +108,10 @@ func (d ProjectPathProvider) StoredPlanFilePath() string {
return path.Join(d.ProjectNamespace, d.PlanFileName())
}

func (d DiggerExecutor) Plan() (bool, string, string, error) {
func (d DiggerExecutor) Plan() (bool, bool, string, string, error) {
plan := ""
terraformPlanOutput := ""
isNonEmptyPlan := false
var planSteps []models.Step

if d.PlanStage != nil {
Expand All @@ -129,32 +130,33 @@ func (d DiggerExecutor) Plan() (bool, string, string, error) {
if step.Action == "init" {
_, _, err := d.TerraformExecutor.Init(step.ExtraArgs, d.StateEnvVars)
if err != nil {
return false, "", "", fmt.Errorf("error running init: %v", err)
return false, false, "", "", fmt.Errorf("error running init: %v", err)
}
}
if step.Action == "plan" {
planArgs := []string{"-out", d.PlanPathProvider.PlanFileName()}
planArgs := []string{"-out", d.PlanPathProvider.PlanFileName(), "-lock-timeout=3m"}
planArgs = append(planArgs, step.ExtraArgs...)
isNonEmptyPlan, stdout, stderr, err := d.TerraformExecutor.Plan(planArgs, d.CommandEnvVars)
nonEmptyPlan, stdout, stderr, err := d.TerraformExecutor.Plan(planArgs, d.CommandEnvVars)
isNonEmptyPlan = nonEmptyPlan
if err != nil {
return false, "", "", fmt.Errorf("error executing plan: %v", err)
return false, false, "", "", fmt.Errorf("error executing plan: %v", err)
}
if d.PlanStorage != nil {
planExists, err := d.PlanStorage.PlanExists(d.PlanPathProvider.StoredPlanFilePath())
if err != nil {
return false, "", "", fmt.Errorf("error checking if plan exists: %v", err)
return false, false, "", "", fmt.Errorf("error checking if plan exists: %v", err)
}

if planExists {
err = d.PlanStorage.DeleteStoredPlan(d.PlanPathProvider.StoredPlanFilePath())
if err != nil {
return false, "", "", fmt.Errorf("error deleting plan: %v", err)
return false, false, "", "", fmt.Errorf("error deleting plan: %v", err)
}
}

err = d.PlanStorage.StorePlan(d.PlanPathProvider.LocalPlanFilePath(), d.PlanPathProvider.StoredPlanFilePath())
if err != nil {
return false, "", "", fmt.Errorf("error storing plan: %v", err)
return false, false, "", "", fmt.Errorf("error storing plan: %v", err)
}
}
plan = cleanupTerraformPlan(isNonEmptyPlan, err, stdout, stderr)
Expand All @@ -176,11 +178,11 @@ func (d DiggerExecutor) Plan() (bool, string, string, error) {
log.Printf("Running %v for **%v**\n", step.Value, d.ProjectNamespace+"#"+d.ProjectName)
_, _, err := d.CommandRunner.Run(d.ProjectPath, step.Shell, commands)
if err != nil {
return false, "", "", fmt.Errorf("error running command: %v", err)
return false, false, "", "", fmt.Errorf("error running command: %v", err)
}
}
}
return true, plan, terraformPlanOutput, nil
return true, isNonEmptyPlan, plan, terraformPlanOutput, nil
}

func (d DiggerExecutor) Apply() (bool, error) {
Expand Down Expand Up @@ -216,7 +218,9 @@ func (d DiggerExecutor) Apply() (bool, error) {
}
}
if step.Action == "apply" {
stdout, stderr, err := d.TerraformExecutor.Apply(step.ExtraArgs, plansFilename, d.CommandEnvVars)
applyArgs := []string{"-lock-timeout=3m"}
applyArgs = append(applyArgs, step.ExtraArgs...)
stdout, stderr, err := d.TerraformExecutor.Apply(applyArgs, plansFilename, d.CommandEnvVars)
applyOutput := cleanupTerraformApply(true, err, stdout, stderr)
formatter := utils.GetTerraformOutputAsCollapsibleComment("Apply for <b>" + d.ProjectNamespace + "#" + d.ProjectName + "</b>")

Expand Down
10 changes: 5 additions & 5 deletions pkg/core/terraform/tf.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,6 @@ func (tf Terraform) Apply(params []string, plan *string, envs map[string]string)
return stdout, stderr, err
}

// runTerraformCommand
func (tf Terraform) runTerraformCommand(command string, envs map[string]string, arg ...string) (string, string, int, error) {
args := []string{command}
args = append(args, arg...)
Expand Down Expand Up @@ -144,7 +143,8 @@ func (tf Terraform) runTerraformCommand(command string, envs map[string]string,

err := cmd.Run()

if err != nil {
// terraform plan can return 2 if there are changes to be applied, so we don't want to fail in that case
if err != nil && cmd.ProcessState.ExitCode() != 2 {
fmt.Println("Error:", err)
}

Expand Down Expand Up @@ -197,12 +197,12 @@ func (tf Terraform) Plan(params []string, envs map[string]string) (bool, string,
return false, "", "", err
}
}
params = append(append(params, "-input=false"), "-no-color")
params = append(append(append(params, "-input=false"), "-no-color"), "-detailed-exitcode")
stdout, stderr, statusCode, err := tf.runTerraformCommand("plan", envs, params...)
if err != nil {
if err != nil && statusCode != 2 {
return false, "", "", err
}
return statusCode != 2, stdout, stderr, nil
return statusCode == 2, stdout, stderr, nil
}

func (tf Terraform) Show(params []string, envs map[string]string) (string, string, error) {
Expand Down
86 changes: 83 additions & 3 deletions pkg/digger/digger.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package digger

import (
"bytes"
"digger/pkg/ci"
"digger/pkg/core/execution"
core_locking "digger/pkg/core/locking"
Expand All @@ -14,9 +15,12 @@ import (
"digger/pkg/locking"
"digger/pkg/reporting"
"digger/pkg/usage"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"net/http"
"os"
"path"
"strings"
Expand Down Expand Up @@ -155,7 +159,7 @@ func RunCommandsPerProject(
if err != nil {
return false, false, fmt.Errorf("failed to set PR status. %v", err)
}
planPerformed, plan, planJsonOutput, err := diggerExecutor.Plan()
planPerformed, isNonEmptyPlan, plan, planJsonOutput, err := diggerExecutor.Plan()

if err != nil {
log.Printf("Failed to run digger plan command. %v", err)
Expand All @@ -165,7 +169,7 @@ func RunCommandsPerProject(
}
return false, false, fmt.Errorf("failed to run digger plan command. %v", err)
} else if planPerformed {
if plan != "" {
if isNonEmptyPlan {
formatter := utils.GetTerraformOutputAsCollapsibleComment("Plan for <b>" + projectLock.LockId() + "</b>")
err = reporter.Report(plan, formatter)
if err != nil {
Expand Down Expand Up @@ -272,6 +276,12 @@ func RunCommandsPerProject(
if err != nil {
return false, false, fmt.Errorf("failed to lock project. %v", err)
}

case "digger drift-detect":
err := runDriftDetection(projectCommands.ProjectName, requestedBy, eventName, diggerExecutor)
if err != nil {
return false, false, fmt.Errorf("failed to run drift detection. %v", err)
}
}
}
}
Expand Down Expand Up @@ -356,7 +366,7 @@ func RunCommandForProject(
if err != nil {
log.Printf("Failed to send usage report. %v", err)
}
_, _, planJsonOutput, err := diggerExecutor.Plan()
_, _, _, planJsonOutput, err := diggerExecutor.Plan()
if err != nil {
log.Printf("Failed to run digger plan command. %v", err)
return fmt.Errorf("failed to run digger plan command. %v", err)
Expand All @@ -382,7 +392,77 @@ func RunCommandForProject(
log.Printf("Failed to run digger apply command. %v", err)
return fmt.Errorf("failed to run digger apply command. %v", err)
}
case "digger drift-detect":
err = runDriftDetection(commands.ProjectName, requestedBy, eventName, diggerExecutor)
if err != nil {
return fmt.Errorf("failed to run digger drift-detect command. %v", err)
}
}

}
return nil
}

func runDriftDetection(projectName string, requestedBy string, eventName string, diggerExecutor execution.Executor) error {
err := usage.SendUsageRecord(requestedBy, eventName, "drift-detect")
if err != nil {
log.Printf("Failed to send usage report. %v", err)
}

planPerformed, nonEmptyPlan, plan, _, err := diggerExecutor.Plan()
if err != nil {
log.Printf("Failed to run digger plan command. %v", err)
return fmt.Errorf("failed to run digger plan command. %v", err)
}

if planPerformed && nonEmptyPlan {
httpClient := &http.Client{}
slackNotificationUrl := os.Getenv("INPUT_DRIFT_DETECTION_SLACK_NOTIFICATION_URL")

if slackNotificationUrl == "" {
log.Printf("No INPUT_DRIFT_DETECTION_SLACK_NOTIFICATION_URL set, not sending notification")
return fmt.Errorf("no INPUT_DRIFT_DETECTION_SLACK_NOTIFICATION_URL set, not sending notification")
}

type SlackMessage struct {
Text string `json:"text"`
}
slackMessage := SlackMessage{
Text: fmt.Sprintf(":bangbang: Drift detected in digger project %v details below: \n```%v```", projectName, plan),
}

jsonData, err := json.Marshal(slackMessage)
if err != nil {
log.Printf("Failed to marshal slack message. %v", err)
return fmt.Errorf("failed to marshal slack message. %v", err)
}

request, err := http.NewRequest("POST", slackNotificationUrl, bytes.NewBuffer(jsonData))
if err != nil {
log.Printf("Failed to create drift detection request. %v", err)
return fmt.Errorf("failed to create drift detection request. %v", err)
}

request.Header.Set("Content-Type", "application/json")
resp, err := httpClient.Do(request)
if err != nil {
log.Printf("Failed to send drift detection request. %v", err)
return fmt.Errorf("failed to send drift detection request. %v", err)
}
if resp.StatusCode != 200 {
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Printf("Failed to read response body. %v", err)
return fmt.Errorf("failed to read response body. %v", err)
}
log.Printf("Failed to send drift detection request. %v. Message: %v", resp.Status, body)
return fmt.Errorf("failed to send drift detection request. %v", resp.Status)
}
defer resp.Body.Close()
} else if planPerformed && !nonEmptyPlan {
log.Printf("No drift detected")
} else {
log.Printf("No plan performed")
}
return nil
}
Expand Down
Loading