Skip to content

Commit

Permalink
Validate zero modules (#324)
Browse files Browse the repository at this point in the history
* Validate zero modules

Adds two private functions in 'internal/module_config':
- func (cfg ModuleConfig) collectMissing() []string
- func findMissing(reflect.Value, string, string, []string)

These are unexported functions that aid in introspecting a datastructure to see if it has all its expected data.
These functions are implicitly invoked via:
- func LoadModuleConfig(string) (ModuleConfig, error)

If there are errors then the program aborts with appropriate output.

* Set description and author in example zero module

* Remove unused code

* Remove unnecessary newline in error log
  • Loading branch information
GrooveStomp authored Dec 15, 2020
1 parent 151cdf5 commit c45c39e
Show file tree
Hide file tree
Showing 4 changed files with 108 additions and 8 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ require (
github.com/google/uuid v1.1.1
github.com/hashicorp/go-getter v1.4.2-0.20200106182914-9813cbd4eb02
github.com/hashicorp/terraform v0.12.26
github.com/iancoleman/strcase v0.1.2
github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a // indirect
github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88 // indirect
github.com/k0kubun/pp v3.0.1+incompatible
Expand Down
3 changes: 3 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,8 @@ github.com/hashicorp/terraform-config-inspect v0.0.0-20191212124732-c6ae6269b9d7
github.com/hashicorp/terraform-svchost v0.0.0-20191011084731-65d371908596/go.mod h1:kNDNcF7sN4DocDLBkQYz73HGKwN1ANB1blq4lIYLYvg=
github.com/hashicorp/vault v0.10.4/go.mod h1:KfSyffbKxoVyspOdlaGVjIuwLobi07qD1bAbosPMpP0=
github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
github.com/iancoleman/strcase v0.1.2 h1:gnomlvw9tnV3ITTAxzKSgTF+8kFWcU/f+TgttpXGz1U=
github.com/iancoleman/strcase v0.1.2/go.mod h1:SK73tn/9oHe+/Y0h39VT4UCxmurVJkR5NA7kMEAOgSE=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jhump/protoreflect v1.6.0/go.mod h1:eaTn3RZAmMBcV0fifFvlm6VHNz3wSkYyXYWUh7ymB74=
Expand All @@ -227,6 +229,7 @@ github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALr
github.com/keybase/go-crypto v0.0.0-20161004153544-93f5b35093ba/go.mod h1:ghbZscTyKdM07+Fw3KSi0hcJm+AlEUWj8QLlPtijN/M=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
Expand Down
97 changes: 97 additions & 0 deletions internal/config/moduleconfig/module_config.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
package moduleconfig

import (
"fmt"
"io/ioutil"
"log"
"reflect"
"strings"

yaml "gopkg.in/yaml.v2"

"github.com/commitdev/zero/pkg/util/flog"
"github.com/iancoleman/strcase"
)

type ModuleConfig struct {
Expand Down Expand Up @@ -48,16 +55,106 @@ type TemplateConfig struct {
OutputDir string `yaml:"outputDir"`
}

// A "nice" wrapper around findMissing()
func (cfg ModuleConfig) collectMissing() []string {
var missing []string
findMissing(reflect.ValueOf(cfg), "", "", &missing)

return missing
}

func LoadModuleConfig(filePath string) (ModuleConfig, error) {
config := ModuleConfig{}

data, err := ioutil.ReadFile(filePath)
if err != nil {
return config, err
}

err = yaml.Unmarshal(data, &config)
if err != nil {
return config, err
}

missing := config.collectMissing()
if len(missing) > 0 {
flog.Errorf("%v is missing information", filePath)

for _, m := range missing {
flog.Errorf("\t %v", m)
}

log.Fatal("")
}

return config, nil
}

// Recurses through a datastructure to find any missing data.
// This assumes several things:
// 1. The structure matches that defined by ModuleConfig and its child datastructures.
// 2. YAML struct field metadata is sufficient to define whether an attribute is missing or not.
// That is, "yaml:foo,omitempty" tells us this is not a required field because we can omit it.
// 3. Slices and arrays are assumed to be optional.
//
// As this function recurses through the datastructure, it builds up a string
// path representing each node's path within the datastructure.
// If the value of the current node is equal to the zero value for its datatype
// and its struct field does *not* have a "omitempty" value, then we assume it
// is missing and add it to the resultset.
func findMissing(obj reflect.Value, path, metadata string, missing *[]string) {
t := obj.Type()
switch t.Kind() {
case reflect.String:
if obj.String() == "" && !strings.Contains(metadata, "omitempty") {
*missing = append(*missing, path)
}

case reflect.Slice, reflect.Array:
for i := 0; i < obj.Len(); i++ {
prefix := fmt.Sprintf("%v[%v]", path, i)
findMissing(obj.Index(i), prefix, metadata, missing)
}

case reflect.Struct:
for i := 0; i < t.NumField(); i++ {
fieldType := t.Field(i)
fieldTags, _ := fieldType.Tag.Lookup("yaml")
fieldVal := obj.Field(i)

tags := strings.Split(fieldTags, ",")

hasOmitEmpty := false
// We have all metadata yaml tags, now let's remove the "omitempty" tag if
// it is present.
// Then if we have only one tag remaining, this must be the expected yaml
// identifer.
// Otherwise the name of the yaml identifier should match the struct
// attribute name.
for i := len(tags) - 1; i >= 0; i-- {
tag := tags[i]
if tag == "omitempty" {
hasOmitEmpty = true
tags = append(tags[:i], tags[i+1:]...)
}
}

yamlName := strcase.ToLowerCamel(fieldType.Name)
if len(tags) == 1 && tags[0] != "" { // For some reason, empty tag lists are giving a count of 1.
yamlName = tags[0]
}

prefix := yamlName
if path != "" {
prefix = fmt.Sprintf("%v.%v", path, yamlName)
}

zeroVal := reflect.Zero(fieldType.Type)
if fieldVal == zeroVal && !hasOmitEmpty {
*missing = append(*missing, prefix)
}

findMissing(fieldVal, prefix, fieldTags, missing)
}
}
}
15 changes: 7 additions & 8 deletions tests/test_data/modules/ci/zero-module.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: "CI templates"
description: ""
author: ""
description: "CI description"
author: "CI author"
icon: ""
thumbnail: ""

Expand All @@ -10,25 +10,24 @@ requiredCredentials:
- github

# Template variables to populate, these could be overwritten by the file spefic frontmatter variables
template:
# strictMode: true # will only parse files that includes the .tmpl.* extension, otherwise it will copy file
template:
# strictMode: true # will only parse files that includes the .tmpl.* extension, otherwise it will copy file
delimiters:
- "<%"
- "%>"
inputDir: 'templates'
outputDir: ".circleci"

# required context parameters: will throw a warning message at the end if any of the context parameters are not present
# contextRequired:
# contextRequired:
# - cognitoPoolID
# - cognitoClientID

# parameters required from user to populate the template params
parameters:
parameters:
- field: platform
label: CI Platform
# default: github
options:
options:
- github
- circlci

0 comments on commit c45c39e

Please sign in to comment.