Skip to content

Commit

Permalink
Merge pull request #633 from adamarnold-msm/master
Browse files Browse the repository at this point in the history
Add -parent flag to support parent state files, add priority override
  • Loading branch information
luisdavim authored Dec 9, 2021
2 parents 9142d9f + a9b0ff8 commit 7936aa9
Show file tree
Hide file tree
Showing 7 changed files with 202 additions and 12 deletions.
3 changes: 3 additions & 0 deletions docs/cmd_reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ This lists available CMD options in Helmsman:
`--no-ssm-subst`
turn off SSM parameter substitution globally.

`-spec string`
specification file name, contains locations of desired state files to be merged

`--subst-ssm-values`
turn on SSM parameter substitution in values files.

Expand Down
5 changes: 5 additions & 0 deletions examples/example-spec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
stateFiles:
- path: examples/example.yaml
- path: examples/minimal-example.yaml
- path: examples/minimal-example.toml
69 changes: 62 additions & 7 deletions internal/app/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"flag"
"fmt"
"os"
"sort"
"strings"

"github.com/imdario/mergo"
Expand All @@ -24,6 +25,37 @@ const (
// Allow parsing of multiple string command line options into an array of strings
type stringArray []string

type fileOptionArray []fileOption

type fileOption struct {
name string
priority int
}

func (f *fileOptionArray) String() string {
var a []string
for _, v := range *f {
a = append(a, v.name)
}
return strings.Join(a, " ")
}

func (f *fileOptionArray) Set(value string) error {
var fo fileOption

fo.name = value
*f = append(*f, fo)
return nil
}

func (f fileOptionArray) sort() {
log.Verbose("Sorting files listed in the -spec file based on their priorities... ")

sort.SliceStable(f, func(i, j int) bool {
return (f)[i].priority < (f)[j].priority
})
}

func (i *stringArray) String() string {
return strings.Join(*i, " ")
}
Expand All @@ -35,7 +67,8 @@ func (i *stringArray) Set(value string) error {

type cli struct {
debug bool
files stringArray
files fileOptionArray
spec string
envFiles stringArray
target stringArray
group stringArray
Expand Down Expand Up @@ -88,6 +121,7 @@ func (c *cli) parse() {
flag.Var(&c.group, "group", "limit execution to specific group of apps.")
flag.IntVar(&c.diffContext, "diff-context", -1, "number of lines of context to show around changes in helm diff output")
flag.IntVar(&c.parallel, "p", 1, "max number of concurrent helm releases to run")
flag.StringVar(&c.spec, "spec", "", "specification file name, contains locations of desired state files to be merged")
flag.StringVar(&c.kubeconfig, "kubeconfig", "", "path to the kubeconfig file to use for CLI requests")
flag.StringVar(&c.nsOverride, "ns-override", "", "override defined namespaces with this one")
flag.StringVar(&c.contextOverride, "context-override", "", "override releases context defined in release state with this one")
Expand Down Expand Up @@ -147,6 +181,10 @@ func (c *cli) parse() {
log.Fatal("--target and --group can't be used together.")
}

if len(flags.files) > 0 && len(flags.spec) > 0 {
log.Fatal("-f and -spec can't be used together.")
}

if c.parallel < 1 {
c.parallel = 1
}
Expand All @@ -159,7 +197,7 @@ func (c *cli) parse() {
kubectlVersion := getKubectlVersion()
log.Verbose("kubectl client version: " + kubectlVersion)

if len(c.files) == 0 {
if len(c.files) == 0 && len(c.spec) == 0 {
log.Info("No desired state files provided.")
os.Exit(0)
}
Expand Down Expand Up @@ -218,32 +256,49 @@ func (c *cli) readState(s *state) error {
os.RemoveAll(tempFilesDir)
_ = os.MkdirAll(tempFilesDir, 0o755)

if len(c.spec) > 0 {

sp := new(StateFiles)
sp.specFromYAML(c.spec)

for _, val := range sp.StateFiles {
fo := fileOption{}
fo.name = val.Path
if err := isValidFile(fo.name, validManifestFiles); err != nil {
return fmt.Errorf("invalid -spec file: %w", err)
}
c.files = append(c.files, fo)
}
c.files.sort()
}

// read the TOML/YAML desired state file
for _, f := range c.files {
var fileState state

if err := fileState.fromFile(f); err != nil {
if err := fileState.fromFile(f.name); err != nil {
return err
}
log.Infof("Parsed [[ %s ]] successfully and found [ %d ] apps", f, len(fileState.Apps))

log.Infof("Parsed [[ %s ]] successfully and found [ %d ] apps", f.name, len(fileState.Apps))
// Merge Apps that already existed in the state
for appName, app := range fileState.Apps {
if _, ok := s.Apps[appName]; ok {
if err := mergo.Merge(s.Apps[appName], app, mergo.WithAppendSlice, mergo.WithOverride); err != nil {
return fmt.Errorf("failed to merge %s from desired state file %s: %w", appName, f, err)
return fmt.Errorf("failed to merge %s from desired state file %s: %w", appName, f.name, err)
}
}
}

// Merge the remaining Apps
if err := mergo.Merge(&s.Apps, &fileState.Apps); err != nil {
return fmt.Errorf("failed to merge desired state file %s: %w", f, err)
return fmt.Errorf("failed to merge desired state file %s: %w", f.name, err)
}
// All the apps are already merged, make fileState.Apps empty to avoid conflicts in the final merge
fileState.Apps = make(map[string]*release)

if err := mergo.Merge(s, &fileState, mergo.WithAppendSlice, mergo.WithOverride); err != nil {
return fmt.Errorf("failed to merge desired state file %s: %w", f, err)
return fmt.Errorf("failed to merge desired state file %s: %w", f.name, err)
}
}

Expand Down
10 changes: 5 additions & 5 deletions internal/app/cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ func Test_readState(t *testing.T) {
{
name: "yaml minimal example; no validation",
flags: cli{
files: stringArray([]string{"../../examples/minimal-example.yaml"}),
files: fileOptionArray([]fileOption{fileOption{"../../examples/minimal-example.yaml", 0}}),
skipValidation: true,
},
want: result{
Expand All @@ -35,7 +35,7 @@ func Test_readState(t *testing.T) {
{
name: "toml minimal example; no validation",
flags: cli{
files: stringArray([]string{"../../examples/minimal-example.toml"}),
files: fileOptionArray([]fileOption{fileOption{"../../examples/minimal-example.toml", 0}}),
skipValidation: true,
},
want: result{
Expand All @@ -49,7 +49,7 @@ func Test_readState(t *testing.T) {
name: "yaml minimal example; no validation with bad target",
flags: cli{
target: stringArray([]string{"foo"}),
files: stringArray([]string{"../../examples/minimal-example.yaml"}),
files: fileOptionArray([]fileOption{{"../../examples/minimal-example.yaml", 0}}),
skipValidation: true,
},
want: result{
Expand All @@ -63,7 +63,7 @@ func Test_readState(t *testing.T) {
name: "yaml minimal example; no validation; target jenkins",
flags: cli{
target: stringArray([]string{"jenkins"}),
files: stringArray([]string{"../../examples/minimal-example.yaml"}),
files: fileOptionArray([]fileOption{{"../../examples/minimal-example.yaml", 0}}),
skipValidation: true,
},
want: result{
Expand All @@ -76,7 +76,7 @@ func Test_readState(t *testing.T) {
{
name: "yaml and toml minimal examples merged; no validation",
flags: cli{
files: stringArray([]string{"../../examples/minimal-example.yaml", "../../examples/minimal-example.toml"}),
files: fileOptionArray([]fileOption{{"../../examples/minimal-example.yaml", 0}, {"../../examples/minimal-example.toml", 0}}),
skipValidation: true,
},
want: result{
Expand Down
33 changes: 33 additions & 0 deletions internal/app/spec_state.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package app

import (
"io/ioutil"

"gopkg.in/yaml.v2"
)

type StatePath struct {
Path string `yaml:"path"`
}

type StateFiles struct {
StateFiles []StatePath `yaml:"stateFiles"`
}

// fromYAML reads a yaml file and decodes it to a state type.
// parser which throws an error if the YAML file is not valid.
func (pc *StateFiles) specFromYAML(file string) error {
rawYamlFile, err := ioutil.ReadFile(file)
if err != nil {
log.Errorf("specFromYaml %v %v", file, err)
return err
}

yamlFile := string(rawYamlFile)

if err = yaml.UnmarshalStrict([]byte(yamlFile), pc); err != nil {
return err
}

return nil
}
90 changes: 90 additions & 0 deletions internal/app/spec_state_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package app

import (
"testing"
)

func Test_specFromYAML(t *testing.T) {
type args struct {
file string
s *StateFiles
}
tests := []struct {
name string
args args
want bool
}{
{
name: "test case 1 -- Valid YAML",
args: args{
file: "../../examples/example-spec.yaml",
s: new(StateFiles),
},
want: true,
}, {
name: "test case 2 -- Invalid Yaml",
args: args{
file: "../../tests/Invalid_example_spec.yaml",
s: new(StateFiles),
},
want: false,
},
}

teardownTestCase := setupStateFileTestCase(t)
defer teardownTestCase(t)
for _, tt := range tests {
// os.Args = append(os.Args, "-f ../../examples/example.yaml")
t.Run(tt.name, func(t *testing.T) {
err := tt.args.s.specFromYAML(tt.args.file)
if err != nil {
t.Log(err)
}

got := err == nil
if got != tt.want {
t.Errorf("specFromYaml() = %v, want %v", got, tt.want)
}
})
}
}

func Test_specFileSort(t *testing.T) {
type args struct {
files fileOptionArray
}
tests := []struct {
name string
args args
want [3]int
}{
{
name: "test case 1 -- Files sorted by priority",
args: args{
files: fileOptionArray(
[]fileOption{
fileOption{"third.yaml", 0},
fileOption{"first.yaml", -20},
fileOption{"second.yaml", -10},
}),
},
want: [3]int{-20, -10, 0},
},
}

teardownTestCase := setupStateFileTestCase(t)
defer teardownTestCase(t)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.args.files.sort()

got := [3]int{}
for i, f := range tt.args.files {
got[i] = f.priority
}
if got != tt.want {
t.Errorf("files from spec file are not sorted by priority = %v, want %v", got, tt.want)
}
})
}
}
4 changes: 4 additions & 0 deletions tests/Invalid_example_spec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
stateFiles:
name1: invalid/example.yaml
name2: invalid/minimal-example.yaml

0 comments on commit 7936aa9

Please sign in to comment.