Skip to content

Commit

Permalink
[go] Secrets functionality and ShellExecutor backend (#539)
Browse files Browse the repository at this point in the history
  • Loading branch information
lucasreed committed Mar 15, 2022
1 parent 785c764 commit 628c212
Show file tree
Hide file tree
Showing 6 changed files with 149 additions and 6 deletions.
8 changes: 5 additions & 3 deletions end_to_end_testing/course_files/13_test_lint_good_secret.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ charts:
my-chart:
chart: basic-demo
secrets:
- backend: testbackend
- backend: ShellExecutor
name: testname
allowed: test_other_allowed
script:
- echo
- "test_other_allowed"
repositories:
stable:
url: https://charts.example.com/stable
url: https://charts.example.com/stable
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ minimum_versions: #set minimum version requirements here
secrets:
- name: TEST_SECRET
backend: ShellExecutor
script: |-
echo "THISVALUEISSECRET"
script:
- echo
- "THISVALUEISSECRET"
charts:
shell-executor-chart:
repository: fairwinds-incubator
Expand Down
59 changes: 59 additions & 0 deletions pkg/course/course.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"strconv"
"strings"

"github.com/fairwindsops/reckoner/pkg/secrets"
"github.com/fatih/color"
"github.com/thoas/go-funk"
"github.com/xeipuuv/gojsonschema"
Expand Down Expand Up @@ -64,6 +65,7 @@ type FileV2 struct {
// Default is the default namespace config for this course
Default *NamespaceConfig `yaml:"default" json:"default"`
} `yaml:"namespace_management,omitempty" json:"namespace_management,omitempty"`
Secrets SecretsList `yaml:"secrets,omitempty" json:"secrets,omitempty"`
// Releases is the list of releases that should be maintained by this course file.
Releases []*Release `yaml:"releases,omitempty" json:"releases,omitempty"`
}
Expand Down Expand Up @@ -185,6 +187,7 @@ type FileV1 struct {
// Default is the default namespace config for this course
Default *NamespaceConfig `yaml:"default" json:"default"`
} `yaml:"namespace_management" json:"namespace_management"`
Secrets SecretsList `yaml:"secrets,omitempty" json:"secrets,omitempty"`
// Charts is the list of releases. In the actual file this will be a map, but we must convert to a list to preserve order.
// This conversion is done in the ChartsListV1 UnmarshalYAML function.
Charts ChartsListV1 `yaml:"charts" json:"charts"`
Expand All @@ -207,6 +210,21 @@ type RepositoryV1 struct {
// RepositoryV1List is a set of repositories
type RepositoryV1List map[string]Repository

// Secret is a single instance of a secret including what backend should be hit to retrieve the secret
type Secret struct {
Name string `yaml:"name" json:"name"`
Backend string `yaml:"backend" json:"backend"`
// Script is only used for Backend ShellExecutor
Script []string `yaml:"script" json:"script"`
// ParameterName is only used for Backend type AWSParameterStore
ParameterName string `yaml:"parameter_name" json:"parameter_name"`
// Region is only used for Backend type AWSParameterStore
Region string `yaml:"region" json:"region"`
}

// SecretsList is, you guessed it, a list of Secret structs
type SecretsList []Secret

// convertV1toV2 converts the old python course file to the newer golang v2 schema
func convertV1toV2(fileName string) (*FileV2, error) {
newFile := &FileV2{
Expand Down Expand Up @@ -271,6 +289,7 @@ func convertV1toV2(fileName string) (*FileV2, error) {
return newFile, nil
}

// OpenCourseFile will attempt to open a V2 Course and if the SchemaVersion is not v2, attempt to open the course file as V1
func OpenCourseFile(fileName string, schema []byte) (*FileV2, error) {
courseFile, err := OpenCourseV2(fileName)
if err != nil {
Expand Down Expand Up @@ -315,6 +334,10 @@ func OpenCourseV2(fileName string) (*FileV2, error) {
if err != nil {
return nil, err
}
err = parseSecrets(data)
if err != nil {
return nil, err
}
err = yaml.Unmarshal(data, courseFile)
if err != nil {
klog.V(3).Infof("failed to unmarshal file: %s", err.Error())
Expand All @@ -330,6 +353,10 @@ func OpenCourseV1(fileName string) (*FileV1, error) {
if err != nil {
return nil, err
}
err = parseSecrets(data)
if err != nil {
return nil, err
}
err = yaml.Unmarshal(data, courseFile)
if err != nil {
klog.V(3).Infof("failed to unmarshal file: %s", err.Error())
Expand Down Expand Up @@ -560,6 +587,38 @@ func (f *FileV2) validateJsonSchema(schemaData []byte) error {
return nil
}

func parseSecrets(courseData []byte) error {
var course struct {
Secrets SecretsList
}
if err := yaml.Unmarshal(courseData, &course); err != nil {
return fmt.Errorf("unable to parse secrets: %w", err)
}
for _, secret := range course.Secrets {
var backend *secrets.Backend
switch secret.Backend {
case "ShellExecutor":
if secret.Script == nil || len(secret.Script) == 0 {
return fmt.Errorf("ShellExecutor secret %s has no script, or is not in an array format in the course file", secret.Name)
}
executor, err := newShellExecutor(secret.Script)
if err != nil {
return fmt.Errorf("error creating secret ShellExecutor: %w", err)
}
backend = secrets.NewSecretBackend(executor)

case "AWSParameterStore":
return fmt.Errorf("AWSParameterStore secret backend not yet implemented")
default:
return fmt.Errorf("invalid secret backend: %s", secret.Backend)
}
if err := backend.SetEnv(secret.Name); err != nil {
return fmt.Errorf("error setting secret env: %w", err)
}
}
return nil
}

func parseEnv(data string) (string, error) {
dataWithEnv := os.Expand(data, envMapper)
if strings.Contains(dataWithEnv, "_ENV_NOT_SET_") {
Expand Down
51 changes: 51 additions & 0 deletions pkg/course/shellSecrets.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package course

import (
"bytes"
"fmt"
"os/exec"

"k8s.io/klog/v2"
)

// Executor represents a shell script to run
type executor struct {
Executable string
Args []string
}

// newShellExecutor returns an executor with the given script
func newShellExecutor(script []string) (*executor, error) {
var args []string
if len(script) == 1 {
args = []string{}
} else {
args = script[1:]
}
path, err := exec.LookPath(script[0])
if err != nil {
return nil, fmt.Errorf("failed to find executable for ShellExecutor secret: %s - %w", script[0], err)
}
return &executor{
Executable: path,
Args: args,
}, nil
}

// Get returns the value of the secret and also satisfies the secrets.Getter interface
func (s executor) Get(key string) (string, error) {
cmd := exec.Command(s.Executable, s.Args...)
var stdoutBuf, stderrBuf bytes.Buffer

cmd.Stdout = &stdoutBuf
cmd.Stderr = &stderrBuf

err := cmd.Run()
outStr, errStr := stdoutBuf.String(), stderrBuf.String()
if err != nil {
klog.V(8).Infof("stdout: %s", outStr)
klog.V(7).Infof("stderr: %s", errStr)
return "", fmt.Errorf("exit code %d running command %s - %w", cmd.ProcessState.ExitCode(), cmd.String(), err)
}
return outStr, nil
}
2 changes: 1 addition & 1 deletion pkg/reckoner/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ func NewClient(fileName, version string, plotAll bool, releases []string, kubeCl
// Get the course file
courseFile, err := course.OpenCourseFile(fileName, schema)
if err != nil {
return nil, err
return nil, fmt.Errorf("%w - error opening course file %s: %s", course.SchemaValidationError, fileName, err)
}

// Get a helm client
Expand Down
30 changes: 30 additions & 0 deletions pkg/secrets/secrets.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package secrets

import "os"

type Getter interface {
Get(key string) (string, error)
}

type Backend struct {
getter Getter
}

// NewSecretBackend creates a new SecretBackend based on a concrete secrets.Getter implementation.
func NewSecretBackend(getter Getter) *Backend {
return &Backend{getter: getter}
}

// SetEnv populates the current ENV with the given secret key by fetching it from the SecretBackend and calling os.Setenv.
func (b Backend) SetEnv(key string) error {
value, err := b.get(key)
if err != nil {
return err
}
return os.Setenv(key, value)
}

// get fetches a secret from the implemented SecretBackend.
func (b Backend) get(key string) (string, error) {
return b.getter.Get(key)
}

0 comments on commit 628c212

Please sign in to comment.