mirror of
https://git.vectorsigma.ru/public/atlantis.git
synced 2026-07-28 22:28:25 +00:00
- Fix terraform lock file checksum verification by updating with correct checksums for null provider v3.2.4 - Fix GitLab request parser validation errors by removing nolint comments from JSON test data - Fix step validation error messages by adding missing newlines - Fix MultiEnv step runner error messages by capitalizing 'Invalid' - Fix GitHub client merge method error messages by capitalizing 'Merge method' and 'Specify' All tests now pass successfully.
478 lines
13 KiB
Go
478 lines
13 KiB
Go
package raw
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
|
|
validation "github.com/go-ozzo/ozzo-validation"
|
|
"github.com/runatlantis/atlantis/server/core/config/valid"
|
|
"github.com/runatlantis/atlantis/server/utils"
|
|
)
|
|
|
|
const (
|
|
ExtraArgsKey = "extra_args"
|
|
NameArgKey = "name"
|
|
CommandArgKey = "command"
|
|
ValueArgKey = "value"
|
|
OutputArgKey = "output"
|
|
RunStepName = "run"
|
|
PlanStepName = "plan"
|
|
ShowStepName = "show"
|
|
PolicyCheckStepName = "policy_check"
|
|
ApplyStepName = "apply"
|
|
InitStepName = "init"
|
|
EnvStepName = "env"
|
|
MultiEnvStepName = "multienv"
|
|
ImportStepName = "import"
|
|
StateRmStepName = "state_rm"
|
|
ShellArgKey = "shell"
|
|
ShellArgsArgKey = "shellArgs"
|
|
)
|
|
|
|
/*
|
|
Step represents a single action/command to perform. In YAML, it can be set as
|
|
1. A single string for a built-in command:
|
|
- init
|
|
- plan
|
|
- policy_check
|
|
|
|
2. A map for an env step with name and command or value, or a run step with a command and output config
|
|
- env:
|
|
name: test_command
|
|
command: echo 312
|
|
- env:
|
|
name: test_value
|
|
value: value
|
|
- env:
|
|
name: test_bash_command
|
|
command: echo ${test_value::7}
|
|
shell: bash
|
|
shellArgs: ["--verbose", "-c"]
|
|
- multienv:
|
|
command: envs.sh
|
|
output: hide
|
|
shell: sh
|
|
shellArgs: -c
|
|
- run:
|
|
command: my custom command
|
|
output: hide
|
|
|
|
3. A map for a built-in command and extra_args:
|
|
- plan:
|
|
extra_args: [-var-file=staging.tfvars]
|
|
|
|
4. A map for a custom run command:
|
|
- run: my custom command
|
|
|
|
Here we parse step in the most generic fashion possible. See fields for more
|
|
details.
|
|
*/
|
|
type Step struct {
|
|
// Key will be set in case #1 and #3 above to the key. In case #2, there
|
|
// could be multiple keys (since the element is a map) so we don't set Key.
|
|
Key *string
|
|
// StringVal will be set in case #4 above.
|
|
StringVal map[string]string
|
|
// Map will be set in case #3 above.
|
|
Map map[string]map[string][]string
|
|
// CommandMap will be set in case #2 above.
|
|
CommandMap map[string]map[string]interface{}
|
|
}
|
|
|
|
func (s *Step) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
|
return s.unmarshalGeneric(unmarshal)
|
|
}
|
|
|
|
func (s Step) MarshalYAML() (interface{}, error) {
|
|
return s.marshalGeneric()
|
|
}
|
|
|
|
func (s *Step) UnmarshalJSON(data []byte) error {
|
|
return s.unmarshalGeneric(func(i interface{}) error {
|
|
return json.Unmarshal(data, i)
|
|
})
|
|
}
|
|
|
|
func (s *Step) MarshalJSON() ([]byte, error) {
|
|
out, err := s.marshalGeneric()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return json.Marshal(out)
|
|
}
|
|
|
|
func (s Step) validStepName(stepName string) bool {
|
|
return stepName == InitStepName ||
|
|
stepName == PlanStepName ||
|
|
stepName == ApplyStepName ||
|
|
stepName == EnvStepName ||
|
|
stepName == MultiEnvStepName ||
|
|
stepName == ShowStepName ||
|
|
stepName == PolicyCheckStepName ||
|
|
stepName == ImportStepName ||
|
|
stepName == StateRmStepName
|
|
}
|
|
|
|
func (s Step) Validate() error {
|
|
validStep := func(value interface{}) error {
|
|
str := *value.(*string)
|
|
if !s.validStepName(str) {
|
|
return fmt.Errorf("%q is not a valid step type, maybe you omitted the 'run' key", str)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
extraArgs := func(value interface{}) error {
|
|
elem := value.(map[string]map[string][]string)
|
|
var keys []string
|
|
for k := range elem {
|
|
keys = append(keys, k)
|
|
}
|
|
// Sort so tests can be deterministic.
|
|
sort.Strings(keys)
|
|
|
|
if len(keys) > 1 {
|
|
return fmt.Errorf("step element can only contain a single key, found %d: %s",
|
|
len(keys), strings.Join(keys, ","))
|
|
}
|
|
for stepName, args := range elem {
|
|
if !s.validStepName(stepName) {
|
|
return fmt.Errorf("%q is not a valid step type", stepName)
|
|
}
|
|
var argKeys []string
|
|
for k := range args {
|
|
argKeys = append(argKeys, k)
|
|
}
|
|
// Sort so tests can be deterministic.
|
|
sort.Strings(argKeys)
|
|
|
|
// args should contain a single 'extra_args' key.
|
|
if len(argKeys) > 1 {
|
|
return fmt.Errorf("built-in steps only support a single %s key, found %d: %s",
|
|
ExtraArgsKey, len(argKeys), strings.Join(argKeys, ","))
|
|
}
|
|
for k := range args {
|
|
if k != ExtraArgsKey {
|
|
return fmt.Errorf("built-in steps only support a single %s key, found %q in step %s",
|
|
ExtraArgsKey, k, stepName)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
envOrRunOrMultiEnvStep := func(value interface{}) error {
|
|
elem := value.(map[string]map[string]interface{})
|
|
var keys []string
|
|
for k := range elem {
|
|
keys = append(keys, k)
|
|
}
|
|
// Sort so tests can be deterministic.
|
|
sort.Strings(keys)
|
|
|
|
if len(keys) > 1 {
|
|
return fmt.Errorf("step element can only contain a single key, found %d: %s",
|
|
len(keys), strings.Join(keys, ","))
|
|
}
|
|
if len(keys) == 0 {
|
|
return fmt.Errorf("step element must contain at least 1 key")
|
|
}
|
|
|
|
stepName := keys[0]
|
|
args := elem[keys[0]]
|
|
|
|
var argKeys []string
|
|
for k := range args {
|
|
argKeys = append(argKeys, k)
|
|
}
|
|
argMap := make(map[string]interface{})
|
|
for k, v := range args {
|
|
argMap[k] = v
|
|
}
|
|
// Sort so tests can be deterministic.
|
|
sort.Strings(argKeys)
|
|
|
|
// Validate keys common for all the steps.
|
|
if utils.SlicesContains(argKeys, ShellArgKey) && !utils.SlicesContains(argKeys, CommandArgKey) {
|
|
return fmt.Errorf("workflow steps only support %q key in combination with %q key",
|
|
ShellArgKey, CommandArgKey)
|
|
}
|
|
if utils.SlicesContains(argKeys, ShellArgsArgKey) && !utils.SlicesContains(argKeys, ShellArgKey) {
|
|
return fmt.Errorf("workflow steps only support %q key in combination with %q key",
|
|
ShellArgsArgKey, ShellArgKey)
|
|
}
|
|
|
|
switch t := argMap[ShellArgsArgKey].(type) {
|
|
case nil:
|
|
case string:
|
|
case []interface{}:
|
|
for _, e := range t {
|
|
if _, ok := e.(string); !ok {
|
|
return fmt.Errorf("%q step %q option must contain only strings, found %v\n",
|
|
stepName, ShellArgsArgKey, e)
|
|
}
|
|
}
|
|
default:
|
|
return fmt.Errorf("%q step %q option must be a string or a list of strings, found %v\n",
|
|
stepName, ShellArgsArgKey, t)
|
|
}
|
|
delete(argMap, ShellArgsArgKey)
|
|
delete(argMap, ShellArgKey)
|
|
|
|
// Validate keys per step type.
|
|
switch stepName {
|
|
case EnvStepName:
|
|
foundNameKey := false
|
|
for _, k := range argKeys {
|
|
if k != NameArgKey && k != CommandArgKey && k != ValueArgKey && k != ShellArgKey && k != ShellArgsArgKey {
|
|
return fmt.Errorf("env steps only support keys %q, %q, %q, %q and %q, found key %q",
|
|
NameArgKey, ValueArgKey, CommandArgKey, ShellArgKey, ShellArgsArgKey, k)
|
|
}
|
|
if k == NameArgKey {
|
|
foundNameKey = true
|
|
}
|
|
}
|
|
delete(argMap, CommandArgKey)
|
|
if !foundNameKey {
|
|
return fmt.Errorf("env steps must have a %q key set", NameArgKey)
|
|
}
|
|
delete(argMap, NameArgKey)
|
|
if utils.SlicesContains(argKeys, ValueArgKey) && utils.SlicesContains(argKeys, CommandArgKey) {
|
|
return fmt.Errorf("env steps only support one of the %q or %q keys, found both",
|
|
ValueArgKey, CommandArgKey)
|
|
}
|
|
delete(argMap, ValueArgKey)
|
|
case RunStepName, MultiEnvStepName:
|
|
if _, ok := argMap[CommandArgKey].(string); !ok {
|
|
return fmt.Errorf("%q step must have a %q key set", stepName, CommandArgKey)
|
|
}
|
|
delete(argMap, CommandArgKey)
|
|
if v, ok := argMap[OutputArgKey].(string); ok {
|
|
if stepName == RunStepName && v != valid.PostProcessRunOutputShow &&
|
|
v != valid.PostProcessRunOutputHide && v != valid.PostProcessRunOutputStripRefreshing {
|
|
return fmt.Errorf("run step %q option must be one of %q, %q, or %q",
|
|
OutputArgKey, valid.PostProcessRunOutputShow, valid.PostProcessRunOutputHide,
|
|
valid.PostProcessRunOutputStripRefreshing)
|
|
} else if stepName == MultiEnvStepName && v != valid.PostProcessRunOutputShow &&
|
|
v != valid.PostProcessRunOutputHide {
|
|
return fmt.Errorf("multienv step %q option must be %q or %q",
|
|
OutputArgKey, valid.PostProcessRunOutputShow, valid.PostProcessRunOutputHide)
|
|
}
|
|
}
|
|
delete(argMap, OutputArgKey)
|
|
default:
|
|
return fmt.Errorf("%q is not a valid step type", stepName)
|
|
}
|
|
|
|
if len(argMap) > 0 {
|
|
var argKeys []string
|
|
for k := range argMap {
|
|
argKeys = append(argKeys, k)
|
|
}
|
|
// Sort so tests can be deterministic.
|
|
sort.Strings(argKeys)
|
|
return fmt.Errorf("%q steps only support keys %q, %q, %q and %q, found extra keys %q",
|
|
stepName, CommandArgKey, OutputArgKey, ShellArgKey, ShellArgsArgKey, strings.Join(argKeys, ","))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
runOrMultiEnvStep := func(value interface{}) error {
|
|
elem := value.(map[string]string)
|
|
var keys []string
|
|
for k := range elem {
|
|
keys = append(keys, k)
|
|
}
|
|
// Sort so tests can be deterministic.
|
|
sort.Strings(keys)
|
|
|
|
if len(keys) > 1 {
|
|
return fmt.Errorf("step element can only contain a single key, found %d: %s",
|
|
len(keys), strings.Join(keys, ","))
|
|
}
|
|
for stepName := range elem {
|
|
if stepName != RunStepName && stepName != MultiEnvStepName {
|
|
return fmt.Errorf("%q is not a valid step type", stepName)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
if s.Key != nil {
|
|
return validation.Validate(s.Key, validation.By(validStep))
|
|
}
|
|
if len(s.Map) > 0 {
|
|
return validation.Validate(s.Map, validation.By(extraArgs))
|
|
}
|
|
if len(s.CommandMap) > 0 {
|
|
return validation.Validate(s.CommandMap, validation.By(envOrRunOrMultiEnvStep))
|
|
}
|
|
if len(s.StringVal) > 0 {
|
|
return validation.Validate(s.StringVal, validation.By(runOrMultiEnvStep))
|
|
}
|
|
return errors.New("step element is empty")
|
|
}
|
|
|
|
func (s Step) ToValid() valid.Step {
|
|
// This will trigger in case #1 (see Step docs).
|
|
if s.Key != nil {
|
|
return valid.Step{
|
|
StepName: *s.Key,
|
|
}
|
|
}
|
|
|
|
// This will trigger in case #2 (see Step docs).
|
|
if len(s.CommandMap) > 0 {
|
|
// After validation we assume there's only one key and it's a valid
|
|
// step name so we just use the first one.
|
|
for stepName, stepArgs := range s.CommandMap {
|
|
step := valid.Step{StepName: stepName}
|
|
if name, ok := stepArgs[NameArgKey].(string); ok {
|
|
step.EnvVarName = name
|
|
}
|
|
if command, ok := stepArgs[CommandArgKey].(string); ok {
|
|
step.RunCommand = command
|
|
}
|
|
if value, ok := stepArgs[ValueArgKey].(string); ok {
|
|
step.EnvVarValue = value
|
|
}
|
|
if output, ok := stepArgs[OutputArgKey].(string); ok {
|
|
step.Output = valid.PostProcessRunOutputOption(output)
|
|
}
|
|
if shell, ok := stepArgs[ShellArgKey].(string); ok {
|
|
step.RunShell = &valid.CommandShell{
|
|
Shell: shell,
|
|
ShellArgs: []string{"-c"},
|
|
}
|
|
}
|
|
if step.StepName == RunStepName && step.Output == "" {
|
|
step.Output = valid.PostProcessRunOutputShow
|
|
}
|
|
|
|
switch t := stepArgs[ShellArgsArgKey].(type) {
|
|
case nil:
|
|
case string:
|
|
step.RunShell.ShellArgs = strings.Split(t, " ")
|
|
case []interface{}:
|
|
step.RunShell.ShellArgs = []string{}
|
|
for _, e := range t {
|
|
step.RunShell.ShellArgs = append(step.RunShell.ShellArgs, e.(string))
|
|
}
|
|
}
|
|
|
|
return step
|
|
}
|
|
}
|
|
|
|
// This will trigger in case #3 (see Step docs).
|
|
if len(s.Map) > 0 {
|
|
// After validation we assume there's only one key and it's a valid
|
|
// step name so we just use the first one.
|
|
for stepName, stepArgs := range s.Map {
|
|
return valid.Step{
|
|
StepName: stepName,
|
|
ExtraArgs: stepArgs[ExtraArgsKey],
|
|
}
|
|
}
|
|
}
|
|
|
|
// This will trigger in case #4 (see Step docs).
|
|
if len(s.StringVal) > 0 {
|
|
// After validation we assume there's only one key and it's a valid
|
|
// step name so we just use the first one.
|
|
for stepName, v := range s.StringVal {
|
|
return valid.Step{
|
|
StepName: stepName,
|
|
RunCommand: v,
|
|
}
|
|
}
|
|
}
|
|
|
|
panic("step was not valid. This is a bug!")
|
|
}
|
|
|
|
// unmarshalGeneric is used by UnmarshalJSON and UnmarshalYAML to unmarshal
|
|
// a step into one of its three forms. We need to implement a custom unmarshal
|
|
// function because steps can either be:
|
|
// 1. a built-in step: " - init"
|
|
// 2. a built-in step with extra_args: " - init: {extra_args: [arg1] }"
|
|
// 3. a custom run step: " - run: my custom command"
|
|
// It takes a parameter unmarshal that is a function that tries to unmarshal
|
|
// the current element into a given object.
|
|
func (s *Step) unmarshalGeneric(unmarshal func(interface{}) error) error {
|
|
|
|
// First try to unmarshal as a single string, ex.
|
|
// steps:
|
|
// - init
|
|
// - plan
|
|
// We validate if it's a legal string later.
|
|
var singleString string
|
|
err := unmarshal(&singleString)
|
|
if err == nil {
|
|
s.Key = &singleString
|
|
return nil
|
|
}
|
|
|
|
// Try to unmarshal as a custom run step, ex.
|
|
// steps:
|
|
// - run: my command
|
|
// We validate if the key is run later.
|
|
var runStep map[string]string
|
|
err = unmarshal(&runStep)
|
|
if err == nil {
|
|
s.StringVal = runStep
|
|
return nil
|
|
}
|
|
|
|
// This represents a step with extra_args, ex:
|
|
// init:
|
|
// extra_args: [a, b]
|
|
// We validate if there's a single key in the map and if the value is a
|
|
// legal value later.
|
|
var step map[string]map[string][]string
|
|
err = unmarshal(&step)
|
|
if err == nil {
|
|
s.Map = step
|
|
return nil
|
|
}
|
|
|
|
// This represents a command steps env, run, and multienv, ex:
|
|
// steps:
|
|
// - env:
|
|
// name: k
|
|
// command: exec
|
|
// - run:
|
|
// name: test_bash_command
|
|
// command: echo ${test_value::7}
|
|
// shell: bash
|
|
// shellArgs: ["--verbose", "-c"]
|
|
var commandStep map[string]map[string]interface{}
|
|
err = unmarshal(&commandStep)
|
|
if err == nil {
|
|
s.CommandMap = commandStep
|
|
return nil
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
func (s Step) marshalGeneric() (interface{}, error) {
|
|
if len(s.StringVal) != 0 {
|
|
return s.StringVal, nil
|
|
} else if len(s.Map) != 0 {
|
|
return s.Map, nil
|
|
} else if len(s.CommandMap) != 0 {
|
|
return s.CommandMap, nil
|
|
} else if s.Key != nil {
|
|
return s.Key, nil
|
|
}
|
|
|
|
// empty step should be marshalled to null, although this is generally
|
|
// unexpected behavior.
|
|
return nil, nil
|
|
}
|