mirror of
https://git.vectorsigma.ru/public/atlantis.git
synced 2026-07-29 00:48:21 +00:00
WIP Add operators and get compiled.
This commit is contained in:
@@ -1,92 +0,0 @@
|
||||
// Copyright 2017 HootSuite Media Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the License);
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an AS IS BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
// Modified hereafter by contributors to runatlantis/atlantis.
|
||||
//
|
||||
package events
|
||||
|
||||
import (
|
||||
"github.com/runatlantis/atlantis/server/events/models"
|
||||
"github.com/runatlantis/atlantis/server/events/run"
|
||||
"github.com/runatlantis/atlantis/server/events/terraform"
|
||||
"github.com/runatlantis/atlantis/server/events/vcs"
|
||||
"github.com/runatlantis/atlantis/server/events/webhooks"
|
||||
)
|
||||
|
||||
// ApplyExecutor handles executing terraform apply.
|
||||
type ApplyExecutor struct {
|
||||
VCSClient vcs.ClientProxy
|
||||
Terraform *terraform.DefaultClient
|
||||
RequireApproval bool
|
||||
Run *run.Run
|
||||
AtlantisWorkspace AtlantisWorkspace
|
||||
ProjectLocker *DefaultProjectLocker
|
||||
Webhooks webhooks.Sender
|
||||
ExecutionPlanner *ExecutionPlanner
|
||||
}
|
||||
|
||||
// Execute executes apply for the ctx.
|
||||
func (a *ApplyExecutor) Execute(ctx *CommandContext) CommandResponse {
|
||||
//if a.RequireApproval {
|
||||
// approved, err := a.VCSClient.PullIsApproved(ctx.BaseRepo, ctx.Pull)
|
||||
// if err != nil {
|
||||
// return CommandResponse{Error: errors.Wrap(err, "checking if pull request was approved")}
|
||||
// }
|
||||
// if !approved {
|
||||
// return CommandResponse{Failure: "Pull request must be approved before running apply."}
|
||||
// }
|
||||
// ctx.Log.Info("confirmed pull request was approved")
|
||||
//}
|
||||
|
||||
repoDir, err := a.AtlantisWorkspace.GetWorkspace(ctx.BaseRepo, ctx.Pull, ctx.Command.Workspace)
|
||||
if err != nil {
|
||||
return CommandResponse{Failure: "No workspace found. Did you run plan?"}
|
||||
}
|
||||
ctx.Log.Info("found workspace in %q", repoDir)
|
||||
|
||||
stage, err := a.ExecutionPlanner.BuildApplyStage(ctx.Log, repoDir, ctx.Command.Workspace, ctx.Command.Dir, ctx.Command.Flags, ctx.User.Username)
|
||||
if err != nil {
|
||||
return CommandResponse{Error: err}
|
||||
}
|
||||
|
||||
// check if we have the lock
|
||||
tryLockResponse, err := a.ProjectLocker.TryLock(ctx, models.NewProject(ctx.BaseRepo.FullName, ctx.Command.Dir))
|
||||
if err != nil {
|
||||
return CommandResponse{ProjectResults: []ProjectResult{{Error: err}}}
|
||||
}
|
||||
if !tryLockResponse.LockAcquired {
|
||||
return CommandResponse{ProjectResults: []ProjectResult{{Failure: tryLockResponse.LockFailureReason}}}
|
||||
}
|
||||
|
||||
// Check apply requirements.
|
||||
for _, req := range stage.ApplyRequirements {
|
||||
isMet, reason := req.IsMet()
|
||||
if !isMet {
|
||||
return CommandResponse{Failure: reason}
|
||||
}
|
||||
}
|
||||
|
||||
out, err := stage.Run()
|
||||
|
||||
// Send webhooks even if there's an error.
|
||||
a.Webhooks.Send(ctx.Log, webhooks.ApplyResult{ // nolint: errcheck
|
||||
Workspace: ctx.Command.Workspace,
|
||||
User: ctx.User,
|
||||
Repo: ctx.BaseRepo,
|
||||
Pull: ctx.Pull,
|
||||
Success: err == nil,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return CommandResponse{Error: err}
|
||||
}
|
||||
return CommandResponse{ProjectResults: []ProjectResult{{ApplySuccess: out}}}
|
||||
}
|
||||
@@ -53,8 +53,6 @@ type GitlabMergeRequestGetter interface {
|
||||
|
||||
// CommandHandler is the first step when processing a comment command.
|
||||
type CommandHandler struct {
|
||||
PlanExecutor Executor
|
||||
ApplyExecutor Executor
|
||||
VCSClient vcs.ClientProxy
|
||||
GithubPullGetter GithubPullGetter
|
||||
GitlabMergeRequestGetter GitlabMergeRequestGetter
|
||||
@@ -68,7 +66,8 @@ type CommandHandler struct {
|
||||
// AllowForkPRsFlag is the name of the flag that controls fork PR's. We use
|
||||
// this in our error message back to the user on a forked PR so they know
|
||||
// how to enable this functionality.
|
||||
AllowForkPRsFlag string
|
||||
AllowForkPRsFlag string
|
||||
PullRequestOperator PullRequestOperator
|
||||
}
|
||||
|
||||
// ExecuteCommand executes the command.
|
||||
@@ -169,9 +168,13 @@ func (c *CommandHandler) run(ctx *CommandContext) {
|
||||
var cr CommandResponse
|
||||
switch ctx.Command.Name {
|
||||
case Plan:
|
||||
cr = c.PlanExecutor.Execute(ctx)
|
||||
if ctx.Command.Autoplan {
|
||||
cr = c.PullRequestOperator.Autoplan(ctx)
|
||||
} else {
|
||||
cr = c.PullRequestOperator.PlanViaComment(ctx)
|
||||
}
|
||||
case Apply:
|
||||
cr = c.ApplyExecutor.Execute(ctx)
|
||||
cr = c.PullRequestOperator.ApplyViaComment(ctx)
|
||||
default:
|
||||
ctx.Log.Err("failed to determine desired command, neither plan nor apply")
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ type Command struct {
|
||||
// Dir is the path relative to the repo root to run the command in.
|
||||
// Will never be an empty string and will never end in "/".
|
||||
Dir string
|
||||
// Flags are the extra arguments appended to comment,
|
||||
// CommentArgs are the extra arguments appended to comment,
|
||||
// ex. atlantis plan -- -target=resource
|
||||
Flags []string
|
||||
Name CommandName
|
||||
|
||||
@@ -1,215 +0,0 @@
|
||||
package events
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/hashicorp/go-version"
|
||||
"github.com/runatlantis/atlantis/server/events/models"
|
||||
"github.com/runatlantis/atlantis/server/events/runtime"
|
||||
"github.com/runatlantis/atlantis/server/events/yaml"
|
||||
"github.com/runatlantis/atlantis/server/events/yaml/valid"
|
||||
"github.com/runatlantis/atlantis/server/logging"
|
||||
)
|
||||
|
||||
const PlanStageName = "plan"
|
||||
const ApplyStageName = "apply"
|
||||
const AtlantisYAMLFilename = "atlantis.yaml"
|
||||
|
||||
type ExecutionPlanner struct {
|
||||
TerraformExecutor TerraformExec
|
||||
DefaultTFVersion *version.Version
|
||||
ParserValidator *yaml.ParserValidator
|
||||
ProjectFinder ProjectFinder
|
||||
}
|
||||
|
||||
type TerraformExec interface {
|
||||
RunCommandWithVersion(log *logging.SimpleLogger, path string, args []string, v *version.Version, workspace string) (string, error)
|
||||
}
|
||||
|
||||
func (s *ExecutionPlanner) BuildPlanStage(log *logging.SimpleLogger, repoDir string, workspace string, relProjectPath string, extraCommentArgs []string, username string) (runtime.PlanStage, error) {
|
||||
defaults := s.defaultPlanSteps(log, repoDir, workspace, relProjectPath, extraCommentArgs, username)
|
||||
steps, err := s.buildStage(PlanStageName, log, repoDir, workspace, relProjectPath, extraCommentArgs, username, defaults)
|
||||
if err != nil {
|
||||
return runtime.PlanStage{}, err
|
||||
}
|
||||
return runtime.PlanStage{
|
||||
Steps: steps,
|
||||
Workspace: workspace,
|
||||
ProjectPath: relProjectPath,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *ExecutionPlanner) BuildAutoplanStages(log *logging.SimpleLogger, repoFullName string, repoDir string, username string, modifiedFiles []string) ([]runtime.PlanStage, error) {
|
||||
// If there is an atlantis.yaml
|
||||
// -> Get modified files from pull request.
|
||||
// -> For each project, if autoplan == true && files match
|
||||
// ->-> Build plan stage for that project.
|
||||
// Else
|
||||
// -> Get modified files
|
||||
// -> For each modified project use default plan stage.
|
||||
config, err := s.ParserValidator.ReadConfig(repoDir)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// If there is no config file, then we try to plan for each project that
|
||||
// was modified in the pull request.
|
||||
if os.IsNotExist(err) {
|
||||
projects := s.ProjectFinder.DetermineProjects(log, modifiedFiles, repoFullName, repoDir)
|
||||
var stages []runtime.PlanStage
|
||||
for _, p := range projects {
|
||||
// NOTE: we use the default workspace because we don't know about
|
||||
// other workspaces. If users want to plan for other workspaces they
|
||||
// need to use a config file.
|
||||
steps := s.defaultPlanSteps(log, repoDir, models.DefaultWorkspace, p.Path, nil, username)
|
||||
stages = append(stages, runtime.PlanStage{
|
||||
Steps: steps,
|
||||
Workspace: models.DefaultWorkspace,
|
||||
ProjectPath: p.Path,
|
||||
})
|
||||
}
|
||||
return stages, nil
|
||||
}
|
||||
|
||||
// Else we run plan according to the config file.
|
||||
var stages []runtime.PlanStage
|
||||
for _, p := range config.Projects {
|
||||
if s.shouldAutoplan(p.Autoplan, modifiedFiles) {
|
||||
// todo
|
||||
stages = append(stages)
|
||||
}
|
||||
}
|
||||
return stages, nil
|
||||
}
|
||||
|
||||
func (s *ExecutionPlanner) shouldAutoplan(autoplan valid.Autoplan, modifiedFiles []string) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *ExecutionPlanner) getSteps() {
|
||||
|
||||
}
|
||||
|
||||
func (s *ExecutionPlanner) BuildApplyStage(log *logging.SimpleLogger, repoDir string, workspace string, relProjectPath string, extraCommentArgs []string, username string) (*runtime.ApplyStage, error) {
|
||||
defaults := s.defaultApplySteps(log, repoDir, workspace, relProjectPath, extraCommentArgs, username)
|
||||
steps, err := s.buildStage(ApplyStageName, log, repoDir, workspace, relProjectPath, extraCommentArgs, username, defaults)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &runtime.ApplyStage{
|
||||
Steps: steps,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *ExecutionPlanner) buildStage(stageName string, log *logging.SimpleLogger, repoDir string, workspace string, relProjectPath string, extraCommentArgs []string, username string, defaults []runtime.Step) ([]runtime.Step, error) {
|
||||
config, err := s.ParserValidator.ReadConfig(repoDir)
|
||||
|
||||
// If there's no config file, use defaults.
|
||||
if os.IsNotExist(err) {
|
||||
log.Info("no %s file found––continuing with defaults", AtlantisYAMLFilename)
|
||||
return defaults, nil
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get this project's configuration.
|
||||
for _, p := range config.Projects {
|
||||
if p.Dir == relProjectPath && p.Workspace == workspace {
|
||||
workflowNamePtr := p.Workflow
|
||||
|
||||
// If they didn't specify a workflow, use the default.
|
||||
if workflowNamePtr == nil {
|
||||
log.Info("no %s workflow set––continuing with defaults", AtlantisYAMLFilename)
|
||||
return defaults, nil
|
||||
}
|
||||
|
||||
// If they did specify a workflow, find it.
|
||||
workflowName := *workflowNamePtr
|
||||
workflow, exists := config.Workflows[workflowName]
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("no workflow with key %q defined", workflowName)
|
||||
}
|
||||
|
||||
// We have a workflow defined, so now we need to build it.
|
||||
meta := s.buildMeta(log, repoDir, workspace, relProjectPath, extraCommentArgs, username)
|
||||
var steps []runtime.Step
|
||||
var stepsConfig []valid.Step
|
||||
if stageName == PlanStageName {
|
||||
stepsConfig = workflow.Plan.Steps
|
||||
} else {
|
||||
stepsConfig = workflow.Apply.Steps
|
||||
}
|
||||
for _, stepConfig := range stepsConfig {
|
||||
var step runtime.Step
|
||||
switch stepConfig.StepName {
|
||||
case "init":
|
||||
step = &runtime.InitStep{
|
||||
Meta: meta,
|
||||
ExtraArgs: stepConfig.ExtraArgs,
|
||||
}
|
||||
case "plan":
|
||||
step = &runtime.PlanStep{
|
||||
Meta: meta,
|
||||
ExtraArgs: stepConfig.ExtraArgs,
|
||||
}
|
||||
case "apply":
|
||||
step = &runtime.ApplyStep{
|
||||
Meta: meta,
|
||||
ExtraArgs: stepConfig.ExtraArgs,
|
||||
}
|
||||
case "run":
|
||||
step = &runtime.RunStep{
|
||||
Meta: meta,
|
||||
Commands: stepConfig.RunCommand,
|
||||
}
|
||||
}
|
||||
steps = append(steps, step)
|
||||
}
|
||||
return steps, nil
|
||||
}
|
||||
}
|
||||
// They haven't defined this project, use the default workflow.
|
||||
log.Info("no project with dir %q and workspace %q defined; continuing with defaults", relProjectPath, workspace)
|
||||
return defaults, nil
|
||||
}
|
||||
|
||||
func (s *ExecutionPlanner) buildMeta(log *logging.SimpleLogger, repoDir string, workspace string, relProjectPath string, extraCommentArgs []string, username string) runtime.StepMeta {
|
||||
return runtime.StepMeta{
|
||||
Log: log,
|
||||
Workspace: workspace,
|
||||
AbsolutePath: filepath.Join(repoDir, relProjectPath),
|
||||
DirRelativeToRepoRoot: relProjectPath,
|
||||
// If there's no config then we should use the default tf version.
|
||||
TerraformVersion: s.DefaultTFVersion,
|
||||
TerraformExecutor: s.TerraformExecutor,
|
||||
ExtraCommentArgs: extraCommentArgs,
|
||||
Username: username,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ExecutionPlanner) defaultPlanSteps(log *logging.SimpleLogger, repoDir string, workspace string, relProjectPath string, extraCommentArgs []string, username string) []runtime.Step {
|
||||
meta := s.buildMeta(log, repoDir, workspace, relProjectPath, extraCommentArgs, username)
|
||||
return []runtime.Step{
|
||||
&runtime.InitStep{
|
||||
ExtraArgs: nil,
|
||||
Meta: meta,
|
||||
},
|
||||
&runtime.PlanStep{
|
||||
ExtraArgs: nil,
|
||||
Meta: meta,
|
||||
},
|
||||
}
|
||||
}
|
||||
func (s *ExecutionPlanner) defaultApplySteps(log *logging.SimpleLogger, repoDir string, workspace string, relProjectPath string, extraCommentArgs []string, username string) []runtime.Step {
|
||||
meta := s.buildMeta(log, repoDir, workspace, relProjectPath, extraCommentArgs, username)
|
||||
return []runtime.Step{
|
||||
&runtime.ApplyStep{
|
||||
ExtraArgs: nil,
|
||||
Meta: meta,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,8 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/runatlantis/atlantis/server/events/yaml/valid"
|
||||
"github.com/runatlantis/atlantis/server/logging"
|
||||
)
|
||||
|
||||
// DefaultWorkspace is the default Terraform workspace for both Atlantis and
|
||||
@@ -210,3 +212,25 @@ func (h VCSHostType) String() string {
|
||||
}
|
||||
return "<missing String() implementation>"
|
||||
}
|
||||
|
||||
type ProjectCommandContext struct {
|
||||
// BaseRepo is the repository that the pull request will be merged into.
|
||||
BaseRepo Repo
|
||||
// HeadRepo is the repository that is getting merged into the BaseRepo.
|
||||
// If the pull request branch is from the same repository then HeadRepo will
|
||||
// be the same as BaseRepo.
|
||||
// See https://help.github.com/articles/about-pull-request-merges/.
|
||||
HeadRepo Repo
|
||||
Pull PullRequest
|
||||
// User is the user that triggered this command.
|
||||
User User
|
||||
Log *logging.SimpleLogger
|
||||
RepoRelPath string
|
||||
ProjectConfig *valid.Project
|
||||
GlobalConfig *valid.Spec
|
||||
|
||||
// CommentArgs are the extra arguments appended to comment,
|
||||
// ex. atlantis plan -- -target=resource
|
||||
CommentArgs []string
|
||||
Workspace string
|
||||
}
|
||||
|
||||
@@ -1,109 +0,0 @@
|
||||
// Copyright 2017 HootSuite Media Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the License);
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an AS IS BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
// Modified hereafter by contributors to runatlantis/atlantis.
|
||||
//
|
||||
package events
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/runatlantis/atlantis/server/events/locking"
|
||||
"github.com/runatlantis/atlantis/server/events/run"
|
||||
"github.com/runatlantis/atlantis/server/events/runtime"
|
||||
"github.com/runatlantis/atlantis/server/events/terraform"
|
||||
"github.com/runatlantis/atlantis/server/events/vcs"
|
||||
)
|
||||
|
||||
//go:generate pegomock generate -m --use-experimental-model-gen --package mocks -o mocks/mock_lock_url_generator.go LockURLGenerator
|
||||
|
||||
type LockURLGenerator interface {
|
||||
GenerateLockURL(lockID string) string
|
||||
}
|
||||
|
||||
// PlanExecutor handles everything related to running terraform plan.
|
||||
type PlanExecutor struct {
|
||||
VCSClient vcs.ClientProxy
|
||||
Terraform terraform.Client
|
||||
Locker locking.Locker
|
||||
Run run.Runner
|
||||
Workspace AtlantisWorkspace
|
||||
ProjectFinder ProjectFinder
|
||||
ProjectLocker ProjectLocker
|
||||
ExecutionPlanner *ExecutionPlanner
|
||||
LockURLGenerator LockURLGenerator
|
||||
}
|
||||
|
||||
// PlanSuccess is the result of a successful plan.
|
||||
type PlanSuccess struct {
|
||||
TerraformOutput string
|
||||
LockURL string
|
||||
}
|
||||
|
||||
// Execute executes terraform plan for the ctx.
|
||||
func (p *PlanExecutor) Execute(ctx *CommandContext) CommandResponse {
|
||||
cloneDir, err := p.Workspace.Clone(ctx.Log, ctx.BaseRepo, ctx.HeadRepo, ctx.Pull, ctx.Command.Workspace)
|
||||
if err != nil {
|
||||
return CommandResponse{Error: err}
|
||||
}
|
||||
|
||||
var stages []runtime.PlanStage
|
||||
if ctx.Command.Autoplan {
|
||||
modifiedFiles, err := p.VCSClient.GetModifiedFiles(ctx.BaseRepo, ctx.Pull)
|
||||
if err != nil {
|
||||
return CommandResponse{Error: errors.Wrap(err, "getting modified files")}
|
||||
}
|
||||
stages, err = p.ExecutionPlanner.BuildAutoplanStages(ctx.Log, ctx.BaseRepo.FullName, cloneDir, ctx.User.Username, modifiedFiles)
|
||||
if err != nil {
|
||||
return CommandResponse{Error: err}
|
||||
}
|
||||
} else {
|
||||
stage, err := p.ExecutionPlanner.BuildPlanStage(ctx.Log, cloneDir, ctx.Command.Workspace, ctx.Command.Dir, ctx.Command.Flags, ctx.User.Username)
|
||||
if err != nil {
|
||||
return CommandResponse{Error: err}
|
||||
}
|
||||
stages = append(stages, stage)
|
||||
}
|
||||
|
||||
var projectResults []ProjectResult
|
||||
for _, stage := range stages {
|
||||
projectResult := ProjectResult{
|
||||
Path: stage.ProjectPath,
|
||||
Workspace: stage.Workspace,
|
||||
}
|
||||
|
||||
// todo: this should be moved into the plan stage
|
||||
//tryLockResponse, err := p.ProjectLocker.TryLock(ctx, models.NewProject(ctx.BaseRepo.FullName, ctx.Command.Dir))
|
||||
//if err != nil {
|
||||
// return CommandResponse{ProjectResults: []ProjectResult{{Error: err}}}
|
||||
//}
|
||||
//if !tryLockResponse.LockAcquired {
|
||||
// return CommandResponse{ProjectResults: []ProjectResult{{Failure: tryLockResponse.LockFailureReason}}}
|
||||
//}
|
||||
// todo: endtodo
|
||||
|
||||
out, err := stage.Run()
|
||||
if err != nil {
|
||||
//if unlockErr := tryLockResponse.UnlockFn(); unlockErr != nil {
|
||||
// ctx.Log.Err("error unlocking state after plan error: %s", unlockErr)
|
||||
//}
|
||||
projectResult.Error = fmt.Errorf("%s\n%s", err.Error(), out)
|
||||
} else {
|
||||
projectResult.PlanSuccess = &PlanSuccess{
|
||||
TerraformOutput: out,
|
||||
//LockURL: p.LockURLGenerator.GenerateLockURL(tryLockResponse.LockKey),
|
||||
}
|
||||
}
|
||||
projectResults = append(projectResults, projectResult)
|
||||
}
|
||||
return CommandResponse{ProjectResults: projectResults}
|
||||
}
|
||||
@@ -18,8 +18,7 @@ import (
|
||||
|
||||
"github.com/runatlantis/atlantis/server/events/locking"
|
||||
"github.com/runatlantis/atlantis/server/events/models"
|
||||
"github.com/runatlantis/atlantis/server/events/run"
|
||||
"github.com/runatlantis/atlantis/server/events/terraform"
|
||||
"github.com/runatlantis/atlantis/server/logging"
|
||||
)
|
||||
|
||||
//go:generate pegomock generate -m --use-experimental-model-gen --package mocks -o mocks/mock_project_lock.go ProjectLocker
|
||||
@@ -33,15 +32,12 @@ type ProjectLocker interface {
|
||||
// The third return value is a function that can be called to unlock the
|
||||
// lock. It will only be set if the lock was acquired. Any errors will set
|
||||
// error.
|
||||
TryLock(ctx *CommandContext, project models.Project) (*TryLockResponse, error)
|
||||
TryLock(log *logging.SimpleLogger, pull models.PullRequest, user models.User, workspace string, project models.Project) (*TryLockResponse, error)
|
||||
}
|
||||
|
||||
// DefaultProjectLocker implements ProjectLocker.
|
||||
type DefaultProjectLocker struct {
|
||||
Locker locking.Locker
|
||||
ConfigReader ProjectConfigReader
|
||||
Terraform terraform.Client
|
||||
Run run.Runner
|
||||
Locker locking.Locker
|
||||
}
|
||||
|
||||
// TryLockResponse is the result of trying to lock a project.
|
||||
@@ -60,13 +56,12 @@ type TryLockResponse struct {
|
||||
}
|
||||
|
||||
// TryLock implements ProjectLocker.TryLock.
|
||||
func (p *DefaultProjectLocker) TryLock(ctx *CommandContext, project models.Project) (*TryLockResponse, error) {
|
||||
workspace := ctx.Command.Workspace
|
||||
lockAttempt, err := p.Locker.TryLock(project, workspace, ctx.Pull, ctx.User)
|
||||
func (p *DefaultProjectLocker) TryLock(log *logging.SimpleLogger, pull models.PullRequest, user models.User, workspace string, project models.Project) (*TryLockResponse, error) {
|
||||
lockAttempt, err := p.Locker.TryLock(project, workspace, pull, user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !lockAttempt.LockAcquired && lockAttempt.CurrLock.Pull.Num != ctx.Pull.Num {
|
||||
if !lockAttempt.LockAcquired && lockAttempt.CurrLock.Pull.Num != pull.Num {
|
||||
failureMsg := fmt.Sprintf(
|
||||
"This project is currently locked by #%d. The locking plan must be applied or discarded before future plans can execute.",
|
||||
lockAttempt.CurrLock.Pull.Num)
|
||||
@@ -75,7 +70,7 @@ func (p *DefaultProjectLocker) TryLock(ctx *CommandContext, project models.Proje
|
||||
LockFailureReason: failureMsg,
|
||||
}, nil
|
||||
}
|
||||
ctx.Log.Info("acquired lock with id %q", lockAttempt.LockKey)
|
||||
log.Info("acquired lock with id %q", lockAttempt.LockKey)
|
||||
return &TryLockResponse{
|
||||
LockAcquired: true,
|
||||
UnlockFn: func() error {
|
||||
|
||||
190
server/events/project_operator.go
Normal file
190
server/events/project_operator.go
Normal file
@@ -0,0 +1,190 @@
|
||||
// Copyright 2017 HootSuite Media Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the License);
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an AS IS BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
// Modified hereafter by contributors to runatlantis/atlantis.
|
||||
//
|
||||
package events
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/runatlantis/atlantis/server/events/models"
|
||||
"github.com/runatlantis/atlantis/server/events/runtime"
|
||||
"github.com/runatlantis/atlantis/server/events/webhooks"
|
||||
"github.com/runatlantis/atlantis/server/events/yaml/valid"
|
||||
)
|
||||
|
||||
//go:generate pegomock generate -m --use-experimental-model-gen --package mocks -o mocks/mock_lock_url_generator.go LockURLGenerator
|
||||
|
||||
type LockURLGenerator interface {
|
||||
GenerateLockURL(lockID string) string
|
||||
}
|
||||
|
||||
// PlanSuccess is the result of a successful plan.
|
||||
type PlanSuccess struct {
|
||||
TerraformOutput string
|
||||
LockURL string
|
||||
}
|
||||
|
||||
type ProjectOperator struct {
|
||||
Locker ProjectLocker
|
||||
LockURLGenerator LockURLGenerator
|
||||
InitStepOperator runtime.InitStepOperator
|
||||
PlanStepOperator runtime.PlanStepOperator
|
||||
ApplyStepOperator runtime.ApplyStepOperator
|
||||
RunStepOperator runtime.RunStepOperator
|
||||
ApprovalOperator runtime.ApprovalOperator
|
||||
Workspace AtlantisWorkspace
|
||||
Webhooks *webhooks.MultiWebhookSender
|
||||
}
|
||||
|
||||
func (p *ProjectOperator) Plan(ctx models.ProjectCommandContext, projAbsPathPtr *string) ProjectResult {
|
||||
// Acquire Atlantis lock for this repo/dir/workspace.
|
||||
lockAttempt, err := p.Locker.TryLock(ctx.Log, ctx.Pull, ctx.User, ctx.Workspace, models.NewProject(ctx.BaseRepo.FullName, ctx.RepoRelPath))
|
||||
if err != nil {
|
||||
return ProjectResult{Error: errors.Wrap(err, "acquiring lock")}
|
||||
}
|
||||
if !lockAttempt.LockAcquired {
|
||||
return ProjectResult{Failure: lockAttempt.LockFailureReason}
|
||||
}
|
||||
|
||||
// Ensure project has been cloned.
|
||||
var projAbsPath string
|
||||
if projAbsPathPtr == nil {
|
||||
repoDir, err := p.Workspace.Clone(ctx.Log, ctx.BaseRepo, ctx.HeadRepo, ctx.Pull, ctx.Workspace)
|
||||
if err != nil {
|
||||
if unlockErr := lockAttempt.UnlockFn(); unlockErr != nil {
|
||||
ctx.Log.Err("error unlocking state after plan error: %v", unlockErr)
|
||||
}
|
||||
return ProjectResult{Error: err}
|
||||
}
|
||||
projAbsPath = filepath.Join(repoDir, ctx.RepoRelPath)
|
||||
} else {
|
||||
projAbsPath = *projAbsPathPtr
|
||||
}
|
||||
|
||||
// Use default stage unless another workflow is defined in config
|
||||
stage := p.defaultPlanStage()
|
||||
if ctx.ProjectConfig != nil && ctx.ProjectConfig.Workflow != nil {
|
||||
configuredStage := ctx.GlobalConfig.GetPlanStage(*ctx.ProjectConfig.Workflow)
|
||||
if configuredStage != nil {
|
||||
stage = *configuredStage
|
||||
}
|
||||
}
|
||||
outputs, err := p.runSteps(stage.Steps, ctx, projAbsPath)
|
||||
if err != nil {
|
||||
if unlockErr := lockAttempt.UnlockFn(); unlockErr != nil {
|
||||
ctx.Log.Err("error unlocking state after plan error: %v", unlockErr)
|
||||
}
|
||||
// todo: include output from other steps.
|
||||
return ProjectResult{Error: err}
|
||||
}
|
||||
|
||||
return ProjectResult{
|
||||
PlanSuccess: &PlanSuccess{
|
||||
LockURL: p.LockURLGenerator.GenerateLockURL(lockAttempt.LockKey),
|
||||
TerraformOutput: strings.Join(outputs, "\n"),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ProjectOperator) runSteps(steps []valid.Step, ctx models.ProjectCommandContext, absPath string) ([]string, error) {
|
||||
var outputs []string
|
||||
for _, step := range steps {
|
||||
var out string
|
||||
var err error
|
||||
switch step.StepName {
|
||||
case "init":
|
||||
out, err = p.InitStepOperator.Run(ctx, step.ExtraArgs, absPath)
|
||||
case "plan":
|
||||
out, err = p.PlanStepOperator.Run(ctx, step.ExtraArgs, absPath)
|
||||
case "apply":
|
||||
out, err = p.ApplyStepOperator.Run(ctx, step.ExtraArgs, absPath)
|
||||
case "run":
|
||||
out, err = p.RunStepOperator.Run(ctx, step.RunCommand, absPath)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
// todo: include output from other steps.
|
||||
return nil, err
|
||||
}
|
||||
if out != "" {
|
||||
outputs = append(outputs, out)
|
||||
}
|
||||
}
|
||||
return outputs, nil
|
||||
}
|
||||
|
||||
func (p *ProjectOperator) Apply(ctx models.ProjectCommandContext, absPath string) ProjectResult {
|
||||
if ctx.ProjectConfig != nil {
|
||||
for _, req := range ctx.ProjectConfig.ApplyRequirements {
|
||||
switch req {
|
||||
case "approved":
|
||||
approved, err := p.ApprovalOperator.IsApproved(ctx.BaseRepo, ctx.Pull)
|
||||
if err != nil {
|
||||
return ProjectResult{Error: errors.Wrap(err, "checking if pull request was approved")}
|
||||
}
|
||||
if !approved {
|
||||
return ProjectResult{Failure: "Pull request must be approved before running apply."}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Use default stage unless another workflow is defined in config
|
||||
stage := p.defaultApplyStage()
|
||||
if ctx.ProjectConfig != nil && ctx.ProjectConfig.Workflow != nil {
|
||||
configuredStage := ctx.GlobalConfig.GetApplyStage(*ctx.ProjectConfig.Workflow)
|
||||
if configuredStage != nil {
|
||||
stage = *configuredStage
|
||||
}
|
||||
}
|
||||
outputs, err := p.runSteps(stage.Steps, ctx, absPath)
|
||||
p.Webhooks.Send(ctx.Log, webhooks.ApplyResult{ // nolint: errcheck
|
||||
Workspace: ctx.Workspace,
|
||||
User: ctx.User,
|
||||
Repo: ctx.BaseRepo,
|
||||
Pull: ctx.Pull,
|
||||
Success: err == nil,
|
||||
})
|
||||
if err != nil {
|
||||
// todo: include output from other steps.
|
||||
return ProjectResult{Error: err}
|
||||
}
|
||||
return ProjectResult{
|
||||
ApplySuccess: strings.Join(outputs, "\n"),
|
||||
}
|
||||
}
|
||||
|
||||
func (p ProjectOperator) defaultPlanStage() valid.Stage {
|
||||
return valid.Stage{
|
||||
Steps: []valid.Step{
|
||||
{
|
||||
StepName: "init",
|
||||
},
|
||||
{
|
||||
StepName: "plan",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (p ProjectOperator) defaultApplyStage() valid.Stage {
|
||||
return valid.Stage{
|
||||
Steps: []valid.Step{
|
||||
{
|
||||
StepName: "apply",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -116,7 +116,7 @@ package events_test
|
||||
// p, runner, _ := setupPlanExecutorTest(t)
|
||||
// ctx := deepcopy.Copy(planCtx).(events.CommandContext)
|
||||
// ctx.Log = logging.NewNoopLogger()
|
||||
// ctx.Command.Flags = []string{"\"-target=resource\"", "\"-var\"", "\"a=b\"", "\";\"", "\"echo\"", "\"hi\""}
|
||||
// ctx.Command.CommentArgs = []string{"\"-target=resource\"", "\"-var\"", "\"a=b\"", "\";\"", "\"echo\"", "\"hi\""}
|
||||
//
|
||||
// When(p.VCSClient.GetModifiedFiles(matchers.AnyModelsRepo(), matchers.AnyModelsPullRequest())).ThenReturn([]string{"file.tf"}, nil)
|
||||
// When(p.Workspace.Clone(ctx.Log, ctx.BaseRepo, ctx.HeadRepo, ctx.Pull, "workspace")).
|
||||
193
server/events/pull_request_operator.go
Normal file
193
server/events/pull_request_operator.go
Normal file
@@ -0,0 +1,193 @@
|
||||
package events
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/hashicorp/go-version"
|
||||
"github.com/runatlantis/atlantis/server/events/models"
|
||||
"github.com/runatlantis/atlantis/server/events/vcs"
|
||||
"github.com/runatlantis/atlantis/server/events/yaml"
|
||||
"github.com/runatlantis/atlantis/server/events/yaml/valid"
|
||||
"github.com/runatlantis/atlantis/server/logging"
|
||||
)
|
||||
|
||||
type PullRequestOperator struct {
|
||||
TerraformExecutor TerraformExec
|
||||
DefaultTFVersion *version.Version
|
||||
ParserValidator *yaml.ParserValidator
|
||||
ProjectFinder ProjectFinder
|
||||
VCSClient vcs.ClientProxy
|
||||
Workspace AtlantisWorkspace
|
||||
ProjectOperator ProjectOperator
|
||||
}
|
||||
|
||||
type TerraformExec interface {
|
||||
RunCommandWithVersion(log *logging.SimpleLogger, path string, args []string, v *version.Version, workspace string) (string, error)
|
||||
}
|
||||
|
||||
func (p *PullRequestOperator) Autoplan(ctx *CommandContext) CommandResponse {
|
||||
// check out repo to parse atlantis.yaml
|
||||
// this will check out the repo to a * dir
|
||||
repoDir, err := p.Workspace.Clone(ctx.Log, ctx.BaseRepo, ctx.HeadRepo, ctx.Pull, ctx.Command.Workspace)
|
||||
if err != nil {
|
||||
return CommandResponse{Error: err}
|
||||
}
|
||||
|
||||
// Parse config file if it exists.
|
||||
config, err := p.ParserValidator.ReadConfig(repoDir)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return CommandResponse{Error: err}
|
||||
}
|
||||
noAtlantisYAML := os.IsNotExist(err)
|
||||
|
||||
// We'll need the list of modified files.
|
||||
modifiedFiles, err := p.VCSClient.GetModifiedFiles(ctx.BaseRepo, ctx.Pull)
|
||||
if err != nil {
|
||||
return CommandResponse{Error: err}
|
||||
}
|
||||
|
||||
// Prepare the project contexts so the ProjectOperator can execute.
|
||||
var projCtxs []models.ProjectCommandContext
|
||||
|
||||
// If there is no config file, then we try to plan for each project that
|
||||
// was modified in the pull request.
|
||||
if noAtlantisYAML {
|
||||
modifiedProjects := p.ProjectFinder.DetermineProjects(ctx.Log, modifiedFiles, ctx.BaseRepo.FullName, repoDir)
|
||||
for _, mp := range modifiedProjects {
|
||||
projCtxs = append(projCtxs, models.ProjectCommandContext{
|
||||
BaseRepo: ctx.BaseRepo,
|
||||
HeadRepo: ctx.HeadRepo,
|
||||
Pull: ctx.Pull,
|
||||
User: ctx.User,
|
||||
Log: ctx.Log,
|
||||
RepoRelPath: mp.Path,
|
||||
ProjectConfig: nil,
|
||||
GlobalConfig: nil,
|
||||
CommentArgs: nil,
|
||||
Workspace: DefaultWorkspace,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// Otherwise, we use the projects that match the WhenModified fields
|
||||
// in the config file.
|
||||
matchingProjects := p.matchingProjects(modifiedFiles, config)
|
||||
for _, mp := range matchingProjects {
|
||||
projCtxs = append(projCtxs, models.ProjectCommandContext{
|
||||
BaseRepo: ctx.BaseRepo,
|
||||
HeadRepo: ctx.HeadRepo,
|
||||
Pull: ctx.Pull,
|
||||
User: ctx.User,
|
||||
Log: ctx.Log,
|
||||
CommentArgs: nil,
|
||||
Workspace: mp.Workspace,
|
||||
RepoRelPath: mp.Dir,
|
||||
ProjectConfig: &mp,
|
||||
GlobalConfig: &config,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Execute the operations.
|
||||
var results []ProjectResult
|
||||
for _, pCtx := range projCtxs {
|
||||
res := p.ProjectOperator.Plan(pCtx, nil)
|
||||
res.Path = pCtx.RepoRelPath
|
||||
res.Workspace = pCtx.Workspace
|
||||
results = append(results, res)
|
||||
}
|
||||
return CommandResponse{ProjectResults: results}
|
||||
}
|
||||
|
||||
func (p *PullRequestOperator) PlanViaComment(ctx *CommandContext) CommandResponse {
|
||||
repoDir, err := p.Workspace.Clone(ctx.Log, ctx.BaseRepo, ctx.HeadRepo, ctx.Pull, ctx.Command.Workspace)
|
||||
if err != nil {
|
||||
return CommandResponse{Error: err}
|
||||
}
|
||||
|
||||
var projCfg *valid.Project
|
||||
var globalCfg *valid.Spec
|
||||
|
||||
// Parse config file if it exists.
|
||||
config, err := p.ParserValidator.ReadConfig(repoDir)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return CommandResponse{Error: err}
|
||||
}
|
||||
if !os.IsNotExist(err) {
|
||||
projCfg = config.FindProject(ctx.Command.Dir, ctx.Command.Workspace)
|
||||
globalCfg = &config
|
||||
}
|
||||
|
||||
projCtx := models.ProjectCommandContext{
|
||||
BaseRepo: ctx.BaseRepo,
|
||||
HeadRepo: ctx.HeadRepo,
|
||||
Pull: ctx.Pull,
|
||||
User: ctx.User,
|
||||
Log: ctx.Log,
|
||||
CommentArgs: ctx.Command.Flags,
|
||||
Workspace: ctx.Command.Workspace,
|
||||
RepoRelPath: ctx.Command.Dir,
|
||||
ProjectConfig: projCfg,
|
||||
GlobalConfig: globalCfg,
|
||||
}
|
||||
projAbsPath := filepath.Join(repoDir, ctx.Command.Dir)
|
||||
res := p.ProjectOperator.Plan(projCtx, &projAbsPath)
|
||||
res.Workspace = projCtx.Workspace
|
||||
res.Path = projCtx.RepoRelPath
|
||||
return CommandResponse{
|
||||
ProjectResults: []ProjectResult{
|
||||
res,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PullRequestOperator) ApplyViaComment(ctx *CommandContext) CommandResponse {
|
||||
repoDir, err := p.Workspace.GetWorkspace(ctx.BaseRepo, ctx.Pull, ctx.Command.Workspace)
|
||||
if err != nil {
|
||||
return CommandResponse{Failure: "No workspace found. Did you run plan?"}
|
||||
}
|
||||
|
||||
// todo: can deduplicate this between PlanViaComment
|
||||
var projCfg *valid.Project
|
||||
var globalCfg *valid.Spec
|
||||
|
||||
// Parse config file if it exists.
|
||||
config, err := p.ParserValidator.ReadConfig(repoDir)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return CommandResponse{Error: err}
|
||||
}
|
||||
if !os.IsNotExist(err) {
|
||||
projCfg = config.FindProject(ctx.Command.Dir, ctx.Command.Workspace)
|
||||
globalCfg = &config
|
||||
}
|
||||
|
||||
projCtx := models.ProjectCommandContext{
|
||||
BaseRepo: ctx.BaseRepo,
|
||||
HeadRepo: ctx.HeadRepo,
|
||||
Pull: ctx.Pull,
|
||||
User: ctx.User,
|
||||
Log: ctx.Log,
|
||||
CommentArgs: ctx.Command.Flags,
|
||||
Workspace: ctx.Command.Workspace,
|
||||
RepoRelPath: ctx.Command.Dir,
|
||||
ProjectConfig: projCfg,
|
||||
GlobalConfig: globalCfg,
|
||||
}
|
||||
res := p.ProjectOperator.Apply(projCtx, filepath.Join(repoDir, ctx.Command.Dir))
|
||||
res.Workspace = projCtx.Workspace
|
||||
res.Path = projCtx.RepoRelPath
|
||||
return CommandResponse{
|
||||
ProjectResults: []ProjectResult{
|
||||
res,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// matchingProjects returns the list of projects whose WhenModified fields match
|
||||
// any of the modifiedFiles.
|
||||
func (p *PullRequestOperator) matchingProjects(modifiedFiles []string, config valid.Spec) []valid.Project {
|
||||
//todo
|
||||
// match the modified files against the config
|
||||
// remember the modified_files paths are relative to the project paths
|
||||
return nil
|
||||
}
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
func TestBuildStage_NoConfigFile(t *testing.T) {
|
||||
var defaultTFVersion *version.Version
|
||||
var terraformExecutor runtime.TerraformExec
|
||||
e := events.ExecutionPlanner{
|
||||
e := events.PullRequestOperator{
|
||||
DefaultTFVersion: defaultTFVersion,
|
||||
TerraformExecutor: terraformExecutor,
|
||||
}
|
||||
@@ -71,7 +71,7 @@ func TestBuildStage_NoConfigFile(t *testing.T) {
|
||||
func TestBuildStage(t *testing.T) {
|
||||
var defaultTFVersion *version.Version
|
||||
var terraformExecutor runtime.TerraformExec
|
||||
e := events.ExecutionPlanner{
|
||||
e := events.PullRequestOperator{
|
||||
DefaultTFVersion: defaultTFVersion,
|
||||
TerraformExecutor: terraformExecutor,
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
package runtime
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// ApplyStep runs `terraform apply`.
|
||||
type ApplyStep struct {
|
||||
ExtraArgs []string
|
||||
Meta StepMeta
|
||||
}
|
||||
|
||||
func (a *ApplyStep) Run() (string, error) {
|
||||
planPath := filepath.Join(a.Meta.AbsolutePath, a.Meta.Workspace+".tfplan")
|
||||
stat, err := os.Stat(planPath)
|
||||
if err != nil || stat.IsDir() {
|
||||
return "", fmt.Errorf("no plan found at path %q and workspace %q–did you run plan?", a.Meta.DirRelativeToRepoRoot, a.Meta.Workspace)
|
||||
}
|
||||
|
||||
tfApplyCmd := append(append(append([]string{"apply", "-no-color"}, a.ExtraArgs...), a.Meta.ExtraCommentArgs...), planPath)
|
||||
return a.Meta.TerraformExecutor.RunCommandWithVersion(a.Meta.Log, a.Meta.AbsolutePath, tfApplyCmd, a.Meta.TerraformVersion, a.Meta.Workspace)
|
||||
}
|
||||
30
server/events/runtime/apply_step_operator.go
Normal file
30
server/events/runtime/apply_step_operator.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package runtime
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/hashicorp/go-version"
|
||||
"github.com/runatlantis/atlantis/server/events/models"
|
||||
)
|
||||
|
||||
// ApplyStepOperator runs `terraform apply`.
|
||||
type ApplyStepOperator struct {
|
||||
TerraformExecutor TerraformExec
|
||||
}
|
||||
|
||||
func (a *ApplyStepOperator) Run(ctx models.ProjectCommandContext, extraArgs []string, path string) (string, error) {
|
||||
planPath := filepath.Join(path, ctx.Workspace+".tfplan")
|
||||
stat, err := os.Stat(planPath)
|
||||
if err != nil || stat.IsDir() {
|
||||
return "", fmt.Errorf("no plan found at path %q and workspace %q–did you run plan?", ctx.RepoRelPath, ctx.Workspace)
|
||||
}
|
||||
|
||||
tfApplyCmd := append(append(append([]string{"apply", "-no-color"}, extraArgs...), ctx.CommentArgs...), planPath)
|
||||
var tfVersion *version.Version
|
||||
if ctx.ProjectConfig != nil && ctx.ProjectConfig.TerraformVersion != nil {
|
||||
tfVersion = ctx.ProjectConfig.TerraformVersion
|
||||
}
|
||||
return a.TerraformExecutor.RunCommandWithVersion(ctx.Log, path, tfApplyCmd, tfVersion, ctx.Workspace)
|
||||
}
|
||||
18
server/events/runtime/approval_operator.go
Normal file
18
server/events/runtime/approval_operator.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package runtime
|
||||
|
||||
import (
|
||||
"github.com/runatlantis/atlantis/server/events/models"
|
||||
"github.com/runatlantis/atlantis/server/events/vcs"
|
||||
)
|
||||
|
||||
type ApprovalOperator struct {
|
||||
VCSClient vcs.ClientProxy
|
||||
}
|
||||
|
||||
func (a *ApprovalOperator) IsApproved(baseRepo models.Repo, pull models.PullRequest) (bool, error) {
|
||||
approved, err := a.VCSClient.PullIsApproved(baseRepo, pull)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return approved, nil
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
package runtime
|
||||
|
||||
// InitStep runs `terraform init`.
|
||||
type InitStep struct {
|
||||
ExtraArgs []string
|
||||
Meta StepMeta
|
||||
}
|
||||
|
||||
func (i *InitStep) Run() (string, error) {
|
||||
// If we're running < 0.9 we have to use `terraform get` instead of `init`.
|
||||
if MustConstraint("< 0.9.0").Check(i.Meta.TerraformVersion) {
|
||||
i.Meta.Log.Info("running terraform version %s so will use `get` instead of `init`", i.Meta.TerraformVersion)
|
||||
terraformGetCmd := append([]string{"get", "-no-color"}, i.ExtraArgs...)
|
||||
_, err := i.Meta.TerraformExecutor.RunCommandWithVersion(i.Meta.Log, i.Meta.AbsolutePath, terraformGetCmd, i.Meta.TerraformVersion, i.Meta.Workspace)
|
||||
return "", err
|
||||
} else {
|
||||
_, err := i.Meta.TerraformExecutor.RunCommandWithVersion(i.Meta.Log, i.Meta.AbsolutePath, append([]string{"init", "-no-color"}, i.ExtraArgs...), i.Meta.TerraformVersion, i.Meta.Workspace)
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
29
server/events/runtime/init_step_operator.go
Normal file
29
server/events/runtime/init_step_operator.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package runtime
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/go-version"
|
||||
"github.com/runatlantis/atlantis/server/events/models"
|
||||
)
|
||||
|
||||
// InitStep runs `terraform init`.
|
||||
type InitStepOperator struct {
|
||||
TerraformExecutor TerraformExec
|
||||
DefaultTFVersion *version.Version
|
||||
}
|
||||
|
||||
func (i *InitStepOperator) Run(ctx models.ProjectCommandContext, extraArgs []string, path string) (string, error) {
|
||||
tfVersion := i.DefaultTFVersion
|
||||
if ctx.ProjectConfig != nil && ctx.ProjectConfig.TerraformVersion != nil {
|
||||
tfVersion = ctx.ProjectConfig.TerraformVersion
|
||||
}
|
||||
// If we're running < 0.9 we have to use `terraform get` instead of `init`.
|
||||
if MustConstraint("< 0.9.0").Check(tfVersion) {
|
||||
ctx.Log.Info("running terraform version %s so will use `get` instead of `init`", tfVersion)
|
||||
terraformGetCmd := append([]string{"get", "-no-color"}, extraArgs...)
|
||||
_, err := i.TerraformExecutor.RunCommandWithVersion(ctx.Log, path, terraformGetCmd, tfVersion, ctx.Workspace)
|
||||
return "", err
|
||||
} else {
|
||||
_, err := i.TerraformExecutor.RunCommandWithVersion(ctx.Log, path, append([]string{"init", "-no-color"}, extraArgs...), tfVersion, ctx.Workspace)
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
@@ -43,23 +43,17 @@ func TestRun_UsesGetOrInitForRightVersion(t *testing.T) {
|
||||
|
||||
tfVersion, _ := version.NewVersion(c.version)
|
||||
logger := logging.NewNoopLogger()
|
||||
s := runtime.InitStep{
|
||||
Meta: runtime.StepMeta{
|
||||
Log: logger,
|
||||
Workspace: "workspace",
|
||||
AbsolutePath: "/path",
|
||||
DirRelativeToRepoRoot: ".",
|
||||
TerraformVersion: tfVersion,
|
||||
TerraformExecutor: terraform,
|
||||
ExtraCommentArgs: []string{"comment", "args"},
|
||||
Username: "username",
|
||||
},
|
||||
ExtraArgs: []string{"extra", "args"},
|
||||
iso := runtime.InitStepOperator{
|
||||
TerraformExecutor: terraform,
|
||||
}
|
||||
|
||||
When(terraform.RunCommandWithVersion(matchers.AnyPtrToLoggingSimpleLogger(), AnyString(), AnyStringSlice(), matchers2.AnyPtrToGoVersionVersion(), AnyString())).
|
||||
ThenReturn("output", nil)
|
||||
output, err := s.Run()
|
||||
output, err := iso.Run(runtime.ProjectCommandContext{
|
||||
Log: logger,
|
||||
Workspace: "workspace",
|
||||
AbsPath: "/path",
|
||||
RepoRelPath: ".",
|
||||
}, []string{"extra", "args"})
|
||||
Ok(t, err)
|
||||
// Shouldn't return output since we don't print init output to PR.
|
||||
Equals(t, "", output)
|
||||
@@ -5,6 +5,9 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/go-version"
|
||||
"github.com/runatlantis/atlantis/server/events/models"
|
||||
)
|
||||
|
||||
// atlantisUserTFVar is the name of the variable we execute terraform
|
||||
@@ -12,45 +15,48 @@ import (
|
||||
const atlantisUserTFVar = "atlantis_user"
|
||||
const defaultWorkspace = "default"
|
||||
|
||||
// PlanStep runs `terraform plan`.
|
||||
type PlanStep struct {
|
||||
ExtraArgs []string
|
||||
Meta StepMeta
|
||||
type PlanStepOperator struct {
|
||||
TerraformExecutor TerraformExec
|
||||
DefaultTFVersion *version.Version
|
||||
}
|
||||
|
||||
func (p *PlanStep) Run() (string, error) {
|
||||
func (p *PlanStepOperator) Run(ctx models.ProjectCommandContext, extraArgs []string, path string) (string, error) {
|
||||
tfVersion := p.DefaultTFVersion
|
||||
if ctx.ProjectConfig != nil && ctx.ProjectConfig.TerraformVersion != nil {
|
||||
tfVersion = ctx.ProjectConfig.TerraformVersion
|
||||
}
|
||||
|
||||
// We only need to switch workspaces in version 0.9.*. In older versions,
|
||||
// there is no such thing as a workspace so we don't need to do anything.
|
||||
// In newer versions, the TF_WORKSPACE env var is respected and will handle
|
||||
// using the right workspace and even creating it if it doesn't exist.
|
||||
// This variable is set inside the Terraform executor.
|
||||
if err := p.switchWorkspace(); err != nil {
|
||||
if err := p.switchWorkspace(ctx, path, tfVersion); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
planFile := filepath.Join(p.Meta.AbsolutePath, fmt.Sprintf("%s.tfplan", p.Meta.Workspace))
|
||||
userVar := fmt.Sprintf("%s=%s", atlantisUserTFVar, p.Meta.Username)
|
||||
tfPlanCmd := append(append([]string{"plan", "-refresh", "-no-color", "-out", planFile, "-var", userVar}, p.ExtraArgs...), p.Meta.ExtraCommentArgs...)
|
||||
planFile := filepath.Join(path, fmt.Sprintf("%s.tfplan", ctx.Workspace))
|
||||
userVar := fmt.Sprintf("%s=%s", atlantisUserTFVar, ctx.User.Username)
|
||||
tfPlanCmd := append(append([]string{"plan", "-refresh", "-no-color", "-out", planFile, "-var", userVar}, extraArgs...), ctx.CommentArgs...)
|
||||
|
||||
// Check if env/{workspace}.tfvars exist and include it. This is a use-case
|
||||
// from Hootsuite where Atlantis was first created so we're keeping this as
|
||||
// an homage and a favor so they don't need to refactor all their repos.
|
||||
// It's also a nice way to structure your repos to reduce duplication.
|
||||
optionalEnvFile := filepath.Join(p.Meta.AbsolutePath, "env", p.Meta.Workspace+".tfvars")
|
||||
optionalEnvFile := filepath.Join(path, "env", ctx.Workspace+".tfvars")
|
||||
if _, err := os.Stat(optionalEnvFile); err == nil {
|
||||
tfPlanCmd = append(tfPlanCmd, "-var-file", optionalEnvFile)
|
||||
}
|
||||
|
||||
return p.Meta.TerraformExecutor.RunCommandWithVersion(p.Meta.Log, filepath.Join(p.Meta.AbsolutePath), tfPlanCmd, p.Meta.TerraformVersion, p.Meta.Workspace)
|
||||
return p.TerraformExecutor.RunCommandWithVersion(ctx.Log, filepath.Join(path), tfPlanCmd, tfVersion, ctx.Workspace)
|
||||
}
|
||||
|
||||
// switchWorkspace changes the terraform workspace if necessary and will create
|
||||
// it if it doesn't exist. It handles differences between versions.
|
||||
func (p *PlanStep) switchWorkspace() error {
|
||||
func (p *PlanStepOperator) switchWorkspace(ctx models.ProjectCommandContext, path string, tfVersion *version.Version) error {
|
||||
// In versions less than 0.9 there is no support for workspaces.
|
||||
noWorkspaceSupport := MustConstraint("<0.9").Check(p.Meta.TerraformVersion)
|
||||
if noWorkspaceSupport && p.Meta.Workspace != defaultWorkspace {
|
||||
return fmt.Errorf("terraform version %s does not support workspaces", p.Meta.TerraformVersion)
|
||||
noWorkspaceSupport := MustConstraint("<0.9").Check(tfVersion)
|
||||
// If the user tried to set a specific workspace in the comment but their
|
||||
// version of TF doesn't support workspaces then error out.
|
||||
if noWorkspaceSupport && ctx.Workspace != defaultWorkspace {
|
||||
return fmt.Errorf("terraform version %s does not support workspaces", tfVersion)
|
||||
}
|
||||
if noWorkspaceSupport {
|
||||
return nil
|
||||
@@ -58,7 +64,7 @@ func (p *PlanStep) switchWorkspace() error {
|
||||
|
||||
// In version 0.9.* the workspace command was called env.
|
||||
workspaceCmd := "workspace"
|
||||
runningZeroPointNine := MustConstraint(">=0.9,<0.10").Check(p.Meta.TerraformVersion)
|
||||
runningZeroPointNine := MustConstraint(">=0.9,<0.10").Check(tfVersion)
|
||||
if runningZeroPointNine {
|
||||
workspaceCmd = "env"
|
||||
}
|
||||
@@ -67,12 +73,12 @@ func (p *PlanStep) switchWorkspace() error {
|
||||
// already in the right workspace then no need to switch. This will save us
|
||||
// about ten seconds. This command is only available in > 0.10.
|
||||
if !runningZeroPointNine {
|
||||
workspaceShowOutput, err := p.Meta.TerraformExecutor.RunCommandWithVersion(p.Meta.Log, p.Meta.AbsolutePath, []string{workspaceCmd, "show"}, p.Meta.TerraformVersion, p.Meta.Workspace)
|
||||
workspaceShowOutput, err := p.TerraformExecutor.RunCommandWithVersion(ctx.Log, path, []string{workspaceCmd, "show"}, tfVersion, ctx.Workspace)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// If `show` says we're already on this workspace then we're done.
|
||||
if strings.TrimSpace(workspaceShowOutput) == p.Meta.Workspace {
|
||||
if strings.TrimSpace(workspaceShowOutput) == ctx.Workspace {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -82,11 +88,11 @@ func (p *PlanStep) switchWorkspace() error {
|
||||
// we can create it if it doesn't. To do this we can either select and catch
|
||||
// the error or use list and then look for the workspace. Both commands take
|
||||
// the same amount of time so that's why we're running select here.
|
||||
_, err := p.Meta.TerraformExecutor.RunCommandWithVersion(p.Meta.Log, p.Meta.AbsolutePath, []string{workspaceCmd, "select", "-no-color", p.Meta.Workspace}, p.Meta.TerraformVersion, p.Meta.Workspace)
|
||||
_, err := p.TerraformExecutor.RunCommandWithVersion(ctx.Log, path, []string{workspaceCmd, "select", "-no-color", ctx.Workspace}, tfVersion, ctx.Workspace)
|
||||
if err != nil {
|
||||
// If terraform workspace select fails we run terraform workspace
|
||||
// new to create a new workspace automatically.
|
||||
_, err = p.Meta.TerraformExecutor.RunCommandWithVersion(p.Meta.Log, p.Meta.AbsolutePath, []string{workspaceCmd, "new", "-no-color", p.Meta.Workspace}, p.Meta.TerraformVersion, p.Meta.Workspace)
|
||||
_, err = p.TerraformExecutor.RunCommandWithVersion(ctx.Log, path, []string{workspaceCmd, "new", "-no-color", ctx.Workspace}, tfVersion, ctx.Workspace)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
@@ -1,93 +0,0 @@
|
||||
package runtime
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/go-version"
|
||||
"github.com/runatlantis/atlantis/server/logging"
|
||||
)
|
||||
|
||||
type TerraformExec interface {
|
||||
RunCommandWithVersion(log *logging.SimpleLogger, path string, args []string, v *version.Version, workspace string) (string, error)
|
||||
}
|
||||
|
||||
type ApplyRequirement interface {
|
||||
// IsMet returns true if the requirement is met and false if not.
|
||||
// If it returns false, it also returns a string describing why not.
|
||||
IsMet() (bool, string)
|
||||
}
|
||||
|
||||
type PlanStage struct {
|
||||
Steps []Step
|
||||
Workspace string
|
||||
ProjectPath string
|
||||
}
|
||||
|
||||
type ApplyStage struct {
|
||||
Steps []Step
|
||||
ApplyRequirements []ApplyRequirement
|
||||
}
|
||||
|
||||
func (p PlanStage) Run() (string, error) {
|
||||
var outputs string
|
||||
for _, step := range p.Steps {
|
||||
out, err := step.Run()
|
||||
if err != nil {
|
||||
return outputs, err
|
||||
}
|
||||
if out != "" {
|
||||
// Outputs are separated by newlines.
|
||||
outputs += "\n" + out
|
||||
}
|
||||
}
|
||||
return outputs, nil
|
||||
}
|
||||
|
||||
func (a ApplyStage) Run() (string, error) {
|
||||
var outputs string
|
||||
for _, step := range a.Steps {
|
||||
out, err := step.Run()
|
||||
if err != nil {
|
||||
return outputs, err
|
||||
}
|
||||
if out != "" {
|
||||
// Outputs are separated by newlines.
|
||||
outputs += "\n" + out
|
||||
}
|
||||
}
|
||||
return outputs, nil
|
||||
}
|
||||
|
||||
type Step interface {
|
||||
// Run runs the step. It returns any output that needs to be commented back
|
||||
// onto the pull request and error.
|
||||
Run() (string, error)
|
||||
}
|
||||
|
||||
// StepMeta is the data that is available to all steps.
|
||||
type StepMeta struct {
|
||||
Log *logging.SimpleLogger
|
||||
Workspace string
|
||||
// AbsolutePath is the path to this project on disk. It's not necessarily
|
||||
// the repository root since a project can be in a subdir of the root.
|
||||
AbsolutePath string
|
||||
// DirRelativeToRepoRoot is the directory for this project relative
|
||||
// to the repo root.
|
||||
DirRelativeToRepoRoot string
|
||||
TerraformVersion *version.Version
|
||||
TerraformExecutor TerraformExec
|
||||
// ExtraCommentArgs are the arguments that may have been appended to the comment.
|
||||
// For example 'atlantis plan -- -target=resource'. They are already quoted
|
||||
// further up the call tree to mitigate security issues.
|
||||
ExtraCommentArgs []string
|
||||
// VCS username of who caused this step to be executed. For example the
|
||||
// commenter, or who pushed a new commit.
|
||||
Username string
|
||||
}
|
||||
|
||||
// MustConstraint returns a constraint. It panics on error.
|
||||
func MustConstraint(constraint string) version.Constraints {
|
||||
c, err := version.NewConstraint(constraint)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return c
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
package runtime
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// RunStep runs custom commands.
|
||||
type RunStep struct {
|
||||
Commands []string
|
||||
Meta StepMeta
|
||||
}
|
||||
|
||||
func (r *RunStep) Run() (string, error) {
|
||||
if len(r.Commands) < 1 {
|
||||
return "", errors.New("no commands for run step")
|
||||
}
|
||||
path := r.Meta.AbsolutePath
|
||||
|
||||
cmd := exec.Command("sh", "-c", strings.Join(r.Commands, " ")) // #nosec
|
||||
cmd.Dir = path
|
||||
out, err := cmd.CombinedOutput()
|
||||
|
||||
commandStr := strings.Join(r.Commands, " ")
|
||||
if err != nil {
|
||||
err = fmt.Errorf("%s: running %q in %q: \n%s", err, commandStr, path, out)
|
||||
r.Meta.Log.Debug("error: %s", err)
|
||||
return string(out), err
|
||||
}
|
||||
r.Meta.Log.Info("successfully ran %q in %q", commandStr, path)
|
||||
return string(out), nil
|
||||
}
|
||||
33
server/events/runtime/run_step_operator.go
Normal file
33
server/events/runtime/run_step_operator.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package runtime
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/runatlantis/atlantis/server/events/models"
|
||||
)
|
||||
|
||||
// RunStepOperator runs custom commands.
|
||||
type RunStepOperator struct {
|
||||
}
|
||||
|
||||
func (r *RunStepOperator) Run(ctx models.ProjectCommandContext, command []string, path string) (string, error) {
|
||||
if len(command) < 1 {
|
||||
return "", errors.New("no commands for run step")
|
||||
}
|
||||
|
||||
cmd := exec.Command("sh", "-c", strings.Join(command, " ")) // #nosec
|
||||
cmd.Dir = path
|
||||
out, err := cmd.CombinedOutput()
|
||||
|
||||
commandStr := strings.Join(command, " ")
|
||||
if err != nil {
|
||||
err = fmt.Errorf("%s: running %q in %q: \n%s", err, commandStr, path, out)
|
||||
ctx.Log.Debug("error: %s", err)
|
||||
return string(out), err
|
||||
}
|
||||
ctx.Log.Info("successfully ran %q in %q", commandStr, path)
|
||||
return string(out), nil
|
||||
}
|
||||
@@ -2,3 +2,21 @@
|
||||
// based on configuration and defaults. The handlers can then execute this
|
||||
// graph.
|
||||
package runtime
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/go-version"
|
||||
"github.com/runatlantis/atlantis/server/logging"
|
||||
)
|
||||
|
||||
type TerraformExec interface {
|
||||
RunCommandWithVersion(log *logging.SimpleLogger, path string, args []string, v *version.Version, workspace string) (string, error)
|
||||
}
|
||||
|
||||
// MustConstraint returns a constraint. It panics on error.
|
||||
func MustConstraint(constraint string) version.Constraints {
|
||||
c, err := version.NewConstraint(constraint)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
@@ -87,14 +87,17 @@ func (c *DefaultClient) Version() *version.Version {
|
||||
}
|
||||
|
||||
// RunCommandWithVersion executes the provided version of terraform with
|
||||
// the provided args in path. v is the version of terraform executable to use
|
||||
// and workspace is the workspace specified by the user commenting
|
||||
// "atlantis plan/apply {workspace}" which is set to "default" by default.
|
||||
// the provided args in path. v is the version of terraform executable to use.
|
||||
// If v is nil, will use the default version.
|
||||
// Workspace is the terraform workspace to run in. We won't switch workspaces
|
||||
// but will set the TERRAFORM_WORKSPACE environment variable.
|
||||
func (c *DefaultClient) RunCommandWithVersion(log *logging.SimpleLogger, path string, args []string, v *version.Version, workspace string) (string, error) {
|
||||
tfExecutable := "terraform"
|
||||
tfVersionStr := c.defaultVersion.String()
|
||||
// if version is the same as the default, don't need to prepend the version name to the executable
|
||||
if !v.Equal(c.defaultVersion) {
|
||||
if v != nil && !v.Equal(c.defaultVersion) {
|
||||
tfExecutable = fmt.Sprintf("%s%s", tfExecutable, v.String())
|
||||
tfVersionStr = v.String()
|
||||
}
|
||||
|
||||
// set environment variables
|
||||
@@ -115,7 +118,7 @@ func (c *DefaultClient) RunCommandWithVersion(log *logging.SimpleLogger, path st
|
||||
// because it's probably safer for users to rely on it. Terraform might
|
||||
// change the way TF_WORKSPACE works in the future.
|
||||
fmt.Sprintf("WORKSPACE=%s", workspace),
|
||||
fmt.Sprintf("ATLANTIS_TERRAFORM_VERSION=%s", v.String()),
|
||||
fmt.Sprintf("ATLANTIS_TERRAFORM_VERSION=%s", tfVersionStr),
|
||||
fmt.Sprintf("DIR=%s", path),
|
||||
}
|
||||
envVars = append(envVars, os.Environ()...)
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/go-ozzo/ozzo-validation"
|
||||
"github.com/hashicorp/go-version"
|
||||
"github.com/runatlantis/atlantis/server/events/yaml/valid"
|
||||
)
|
||||
|
||||
@@ -39,9 +40,17 @@ func (p Project) Validate() error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
validTFVersion := func(value interface{}) error {
|
||||
// Safe to dereference because this is only called if the pointer is
|
||||
// not nil.
|
||||
versionStr := *value.(*string)
|
||||
_, err := version.NewVersion(versionStr)
|
||||
return err
|
||||
}
|
||||
return validation.ValidateStruct(&p,
|
||||
validation.Field(&p.Dir, validation.Required, validation.By(hasDotDot)),
|
||||
validation.Field(&p.ApplyRequirements, validation.By(validApplyReq)),
|
||||
validation.Field(&p.TerraformVersion, validation.By(validTFVersion)),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -56,7 +65,9 @@ func (p Project) ToValid() valid.Project {
|
||||
}
|
||||
|
||||
v.Workflow = p.Workflow
|
||||
v.TerraformVersion = p.TerraformVersion
|
||||
if p.TerraformVersion != nil {
|
||||
v.TerraformVersion, _ = version.NewVersion(*p.TerraformVersion)
|
||||
}
|
||||
if p.Autoplan == nil {
|
||||
v.Autoplan = DefaultAutoPlan()
|
||||
} else {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package valid
|
||||
|
||||
import "github.com/hashicorp/go-version"
|
||||
|
||||
// Spec is the atlantis yaml spec after it's been parsed and validated.
|
||||
// The raw.Spec is transformed into the ValidSpec which is then used by the
|
||||
// rest of Atlantis.
|
||||
@@ -11,11 +13,38 @@ type Spec struct {
|
||||
Workflows map[string]Workflow
|
||||
}
|
||||
|
||||
func (s Spec) GetPlanStage(workflowName string) *Stage {
|
||||
for name, flow := range s.Workflows {
|
||||
if name == workflowName {
|
||||
return flow.Plan
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s Spec) GetApplyStage(workflowName string) *Stage {
|
||||
for name, flow := range s.Workflows {
|
||||
if name == workflowName {
|
||||
return flow.Apply
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s Spec) FindProject(dir string, workspace string) *Project {
|
||||
for _, p := range s.Projects {
|
||||
if p.Dir == dir && p.Workspace == workspace {
|
||||
return &p
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type Project struct {
|
||||
Dir string
|
||||
Workspace string
|
||||
Workflow *string
|
||||
TerraformVersion *string
|
||||
TerraformVersion *version.Version
|
||||
Autoplan Autoplan
|
||||
ApplyRequirements []string
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ import (
|
||||
"github.com/runatlantis/atlantis/server/events/locking"
|
||||
"github.com/runatlantis/atlantis/server/events/locking/boltdb"
|
||||
"github.com/runatlantis/atlantis/server/events/models"
|
||||
"github.com/runatlantis/atlantis/server/events/run"
|
||||
"github.com/runatlantis/atlantis/server/events/runtime"
|
||||
"github.com/runatlantis/atlantis/server/events/terraform"
|
||||
"github.com/runatlantis/atlantis/server/events/vcs"
|
||||
"github.com/runatlantis/atlantis/server/events/webhooks"
|
||||
@@ -188,23 +188,12 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
|
||||
return nil, err
|
||||
}
|
||||
lockingClient := locking.NewClient(boltdb)
|
||||
run := &run.Run{}
|
||||
configReader := &events.ProjectConfigManager{}
|
||||
workspaceLocker := events.NewDefaultAtlantisWorkspaceLocker()
|
||||
workspace := &events.FileWorkspace{
|
||||
DataDir: userConfig.DataDir,
|
||||
}
|
||||
projectLocker := &events.DefaultProjectLocker{
|
||||
Locker: lockingClient,
|
||||
Run: run,
|
||||
ConfigReader: configReader,
|
||||
Terraform: terraformClient,
|
||||
}
|
||||
executionPlanner := &events.ExecutionPlanner{
|
||||
ParserValidator: &yaml.ParserValidator{},
|
||||
DefaultTFVersion: terraformClient.Version(),
|
||||
TerraformExecutor: terraformClient,
|
||||
ProjectFinder: &events.DefaultProjectFinder{},
|
||||
Locker: lockingClient,
|
||||
}
|
||||
underlyingRouter := mux.NewRouter()
|
||||
router := &Router{
|
||||
@@ -213,27 +202,6 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
|
||||
LockViewRouteName: LockViewRouteName,
|
||||
Underlying: underlyingRouter,
|
||||
}
|
||||
applyExecutor := &events.ApplyExecutor{
|
||||
VCSClient: vcsClient,
|
||||
Terraform: terraformClient,
|
||||
RequireApproval: userConfig.RequireApproval,
|
||||
Run: run,
|
||||
AtlantisWorkspace: workspace,
|
||||
ProjectLocker: projectLocker,
|
||||
ExecutionPlanner: executionPlanner,
|
||||
Webhooks: webhooksManager,
|
||||
}
|
||||
planExecutor := &events.PlanExecutor{
|
||||
VCSClient: vcsClient,
|
||||
Terraform: terraformClient,
|
||||
Run: run,
|
||||
Workspace: workspace,
|
||||
ProjectLocker: projectLocker,
|
||||
Locker: lockingClient,
|
||||
ProjectFinder: &events.DefaultProjectFinder{},
|
||||
ExecutionPlanner: executionPlanner,
|
||||
LockURLGenerator: router,
|
||||
}
|
||||
pullClosedExecutor := &events.PullClosedExecutor{
|
||||
VCSClient: vcsClient,
|
||||
Locker: lockingClient,
|
||||
@@ -252,9 +220,8 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
|
||||
GitlabUser: userConfig.GitlabUser,
|
||||
GitlabToken: userConfig.GitlabToken,
|
||||
}
|
||||
defaultTfVersion := terraformClient.Version()
|
||||
commandHandler := &events.CommandHandler{
|
||||
ApplyExecutor: applyExecutor,
|
||||
PlanExecutor: planExecutor,
|
||||
EventParser: eventParser,
|
||||
VCSClient: vcsClient,
|
||||
GithubPullGetter: githubClient,
|
||||
@@ -265,6 +232,35 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
|
||||
Logger: logger,
|
||||
AllowForkPRs: userConfig.AllowForkPRs,
|
||||
AllowForkPRsFlag: config.AllowForkPRsFlag,
|
||||
PullRequestOperator: events.PullRequestOperator{
|
||||
TerraformExecutor: terraformClient,
|
||||
DefaultTFVersion: defaultTfVersion,
|
||||
ParserValidator: &yaml.ParserValidator{},
|
||||
ProjectFinder: &events.DefaultProjectFinder{},
|
||||
VCSClient: vcsClient,
|
||||
Workspace: workspace,
|
||||
ProjectOperator: events.ProjectOperator{
|
||||
Locker: projectLocker,
|
||||
LockURLGenerator: router,
|
||||
InitStepOperator: runtime.InitStepOperator{
|
||||
TerraformExecutor: terraformClient,
|
||||
DefaultTFVersion: defaultTfVersion,
|
||||
},
|
||||
PlanStepOperator: runtime.PlanStepOperator{
|
||||
TerraformExecutor: terraformClient,
|
||||
DefaultTFVersion: defaultTfVersion,
|
||||
},
|
||||
ApplyStepOperator: runtime.ApplyStepOperator{
|
||||
TerraformExecutor: terraformClient,
|
||||
},
|
||||
RunStepOperator: runtime.RunStepOperator{},
|
||||
ApprovalOperator: runtime.ApprovalOperator{
|
||||
VCSClient: vcsClient,
|
||||
},
|
||||
Workspace: workspace,
|
||||
Webhooks: webhooksManager,
|
||||
},
|
||||
},
|
||||
}
|
||||
repoWhitelist := &events.RepoWhitelist{
|
||||
Whitelist: userConfig.RepoWhitelist,
|
||||
|
||||
Reference in New Issue
Block a user