Skip to content

Commit

Permalink
Rewrite config parsing (#4)
Browse files Browse the repository at this point in the history
* in process of implementing validation

* validate depends

* validated command

* split command parse function into files, one for each command section

* capture os env when run command

* fix staticcheck compalians

* add dockerfile for easier testing locally
  • Loading branch information
kindermax authored Feb 10, 2020
1 parent 3d54512 commit b54686d
Show file tree
Hide file tree
Showing 16 changed files with 276 additions and 65 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,10 +103,10 @@ Yet there is no binaries
- [ ] inherit configs
- [x] LETS_DEBUG env for debugging logs
- [ ] command to only calculate checksum
- [ ] capture env from shell
- [x] capture env from shell
- [ ] env computing
- [ ] global env
- [x] command env
- [ ] dogfood on ci
- [ ] add version flag to lets
- [x] add version flag to lets
- [ ] add verbose flag to lets
4 changes: 0 additions & 4 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,6 @@ func CreateRootCommand(conf *config.Config, out io.Writer, version string) *cobr
return rootCmd
}

func Execute(cmd *cobra.Command) error {
return cmd.Execute()
}

func runHelp(cmd *cobra.Command) error {
return cmd.Help()
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (
func newTestRootCmd(args []string, conf *config.Config) (rootCmd *cobra.Command, out *bytes.Buffer) {
bufOut := new(bytes.Buffer)

rootCommand := CreateRootCommand(conf, bufOut)
rootCommand := CreateRootCommand(conf, bufOut, "test-version")
rootCommand.SetOut(bufOut)
rootCommand.SetErr(bufOut)
rootCommand.SetArgs(args)
Expand Down
33 changes: 33 additions & 0 deletions commands/command/checksum.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,36 @@ func calculateChecksum(patterns []string) (string, error) {
checksum := hasher.Sum(nil)
return fmt.Sprintf("%x", checksum), nil
}

func parseAndValidateChecksum(checksum interface{}, newCmd *Command) error {
patterns, ok := checksum.([]interface{})
if !ok {
return newCommandError(
"must be a list of string (files of glob patterns)",
newCmd.Name,
CHECKSUM,
"",
)
}

var files []string
for _, value := range patterns {
if value, ok := value.(string); ok {
files = append(files, value)
} else {
return newCommandError(
"value of checksum list must be a string",
newCmd.Name,
CHECKSUM,
"",
)
}
}
calcChecksum, err := calculateChecksum(files)
if err == nil {
newCmd.Checksum = calcChecksum
} else {
return fmt.Errorf("failed to calculate checksum: %s", err)
}
return nil
}
28 changes: 28 additions & 0 deletions commands/command/cmd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package command

import (
"os"
"strings"
)

func parseAndValidateCmd(cmd interface{}, newCmd *Command) error {
switch cmd := cmd.(type) {
case string:
newCmd.Cmd = cmd
case []interface{}:
cmdList := make([]string, len(cmd))
for _, v := range cmd {
cmdList = append(cmdList, v.(string))
}
cmdList = append(cmdList, os.Args[1:]...)
newCmd.Cmd = strings.Join(cmdList, " ")
default:
return newCommandError(
"must be either string or list of string",
newCmd.Name,
CMD,
"",
)
}
return nil
}
101 changes: 55 additions & 46 deletions commands/command/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package command

import (
"fmt"
"os"
"strings"
)

Expand All @@ -27,78 +26,88 @@ type Command struct {
Checksum string
}

// TODO interface{} must be replaced
func NewCommand(name string, rawCommand map[interface{}]interface{}) Command {
type CommandError struct {
Path struct {
Full string
Field string
}
Err error
}

func (e *CommandError) Error() string {
return fmt.Sprintf("failed to parse command: %s", e.Err)
}

// env is not proper arg
func newCommandError(msg string, name string, field string, env string) error {
fields := []string{name, field}
if env != "" {
fields = append(fields, env)
}
fullPath := strings.Join(fields, ".")
return &CommandError{
Path: struct {
Full string
Field string
}{
Full: fullPath,
Field: field,
},
Err: fmt.Errorf("field %s: %s", fullPath, msg),
}
}

// NewCommand creates new command struct
func NewCommand(name string) Command {
newCmd := Command{
Name: name,
Env: make(map[string]string),
}
return newCmd
}

// ParseAndValidateCommand parses and validates unmarshaled yaml
func ParseAndValidateCommand(newCmd *Command, rawCommand map[interface{}]interface{}) error {
if cmd, ok := rawCommand[CMD]; ok {
// TODO not safe, need validation
// decide, validate here or top-level validate and return all errors at once
switch cmd := cmd.(type) {
case string:
newCmd.Cmd = cmd
case []interface{}:
cmdList := make([]string, len(cmd))
for _, v := range cmd {
cmdList = append(cmdList, v.(string))
}
cmdList = append(cmdList, os.Args[1:]...)
newCmd.Cmd = strings.Join(cmdList, " ")
default:
fmt.Println("default, must raise an error")
if err := parseAndValidateCmd(cmd, newCmd); err != nil {
return err
}
// TODO here we need to validate if cmd is an array
}

if desc, ok := rawCommand[DESCRIPTION]; ok {
newCmd.Description = desc.(string)
if err := parseAndValidateDescription(desc, newCmd); err != nil {
return err
}
}

if env, ok := rawCommand[ENV]; ok {
// TODO dirty hacks
for name, value := range env.(map[interface{}]interface{}) {
newCmd.Env[name.(string)] = value.(string)
if err := parseAndValidateEnv(env, newCmd); err != nil {
return err
}
}

if evalEnv, ok := rawCommand[EVAL_ENV]; ok {
for name, value := range evalEnv.(map[interface{}]interface{}) {
if computedVal, err := evalEnvVariable(value.(string)); err != nil {
// TODO we have to fail here and log error for user
} else {
newCmd.Env[name.(string)] = computedVal
}
if err := parseAndValidateEvalEnv(evalEnv, newCmd); err != nil {
return err
}
}

if options, ok := rawCommand[OPTIONS]; ok {
newCmd.RawOptions = options.(string)
if err := parseAndValidateOptions(options, newCmd); err != nil {
return err
}
}

if depends, ok := rawCommand[DEPENDS]; ok {
for _, value := range depends.([]interface{}) {
// TODO validate if command is realy exists - in validate
newCmd.Depends = append(newCmd.Depends, value.(string))
if err := parseAndValidateDepends(depends, newCmd); err != nil {
return err
}
}

if checksum, ok := rawCommand[CHECKSUM]; ok {
if patterns, ok := checksum.([]interface{}); ok {
var files []string
for _, value := range patterns {
// TODO validate if command is realy exists - in validate
files = append(files, value.(string))
}
checksum, err := calculateChecksum(files)
if err == nil {
newCmd.Checksum = checksum
} else {
// TODO return error or caclulate checksum upper in the code
fmt.Printf("error while checksum %s\n", err)
}
if err := parseAndValidateChecksum(checksum, newCmd); err != nil {
return err
}
}
return newCmd
return nil
}
27 changes: 27 additions & 0 deletions commands/command/depends.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package command

func parseAndValidateDepends(depends interface{}, newCmd *Command) error {
if depends, ok := depends.([]interface{}); ok {
for _, value := range depends {
if value, ok := value.(string); ok {
// TODO validate if command is really exists - in validate
newCmd.Depends = append(newCmd.Depends, value)
} else {
return newCommandError(
"value of depends list must be a string",
newCmd.Name,
DEPENDS,
"",
)
}
}
} else {
return newCommandError(
"must be a list of string (commands)",
newCmd.Name,
DEPENDS,
"",
)
}
return nil
}
15 changes: 15 additions & 0 deletions commands/command/description.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package command

func parseAndValidateDescription(desc interface{}, newCmd *Command) error {
if value, ok := desc.(string); ok {
newCmd.Description = value
} else {
return newCommandError(
"must be a string",
newCmd.Name,
DESCRIPTION,
"",
)
}
return nil
}
22 changes: 14 additions & 8 deletions commands/command/env.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
package command

import "os/exec"

func evalEnvVariable(rawCmd string) (string, error) {
cmd := exec.Command("sh", "-c", rawCmd)
out, err := cmd.Output()
if err != nil {
return "", err
func parseAndValidateEnv(env interface{}, newCmd *Command) error {
for name, value := range env.(map[interface{}]interface{}) {
nameKey := name.(string)
if value, ok := value.(string); ok {
newCmd.Env[nameKey] = value
} else {
return newCommandError(
"must be a string",
newCmd.Name,
ENV,
nameKey,
)
}
}
return string(out), nil
return nil
}
38 changes: 38 additions & 0 deletions commands/command/eval_env.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package command

import "os/exec"

func evalEnvVariable(rawCmd string) (string, error) {
cmd := exec.Command("sh", "-c", rawCmd)
out, err := cmd.Output()
if err != nil {
return "", err
}
return string(out), nil
}

func parseAndValidateEvalEnv(evalEnv interface{}, newCmd *Command) error {
for name, value := range evalEnv.(map[interface{}]interface{}) {
nameKey := name.(string)
if value, ok := value.(string); ok {
if computedVal, err := evalEnvVariable(value); err != nil {
return err
} else {
newCmd.Env[nameKey] = computedVal
}
} else {
return newCommandError(
"must be a string",
newCmd.Name,
EVAL_ENV,
nameKey,
)
}
if computedVal, err := evalEnvVariable(value.(string)); err != nil {
// TODO we have to fail here and log error for user
} else {
newCmd.Env[name.(string)] = computedVal
}
}
return nil
}
15 changes: 15 additions & 0 deletions commands/command/options.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package command

func parseAndValidateOptions(options interface{}, newCmd *Command) error {
if value, ok := options.(string); ok {
newCmd.RawOptions = value
} else {
return newCommandError(
"must be a string",
newCmd.Name,
OPTIONS,
"",
)
}
return nil
}
3 changes: 2 additions & 1 deletion commands/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"github.com/kindritskyiMax/lets/config"
"github.com/kindritskyiMax/lets/logging"
"io"
"os"
"os/exec"
)

Expand Down Expand Up @@ -64,7 +65,7 @@ func runCmd(cmdToRun command.Command, cfg *config.Config, out io.Writer, isChild
env := convertEnvForCmd(cmdToRun.Env)
optsEnv := convertOptsToEnvForCmd(cmdToRun.Options)
checksumEnv := convertChecksumToEnvForCmd(cmdToRun.Checksum)
cmd.Env = composeEnvs(env, optsEnv, checksumEnv)
cmd.Env = composeEnvs(os.Environ(), env, optsEnv, checksumEnv)
if !isChild {
logging.Log.Debugf("Executing command %s with env:\n%s", cmdToRun.Name, cmd.Env)
} else {
Expand Down
Loading

0 comments on commit b54686d

Please sign in to comment.