Skip to content

Commit

Permalink
feat(template): template command with initial docs (#644)
Browse files Browse the repository at this point in the history
* feat(template): template command with initial docs

* feat(template): support for merging config file data with environment variables

* chore(docs): generate cli docs after updates to main

* feat(template): template unit and e2e tests

* fix(wip): working on a CLI testing example

* fix(template): basic cli tests for template command

* fix(template): fix demo test template

* fix(template): revert default lula-config

* fix(template): resolving comments

* fix(template):  override with env var test addition

* fix(template): update test files after modification

* fix(template): revert current template for downstream impact

* fix(template): revert env override

* fix(template): revert golden files
  • Loading branch information
brandtkeller authored Sep 13, 2024
1 parent fbe7b8f commit 89be460
Show file tree
Hide file tree
Showing 12 changed files with 505 additions and 0 deletions.
1 change: 1 addition & 0 deletions docs/cli-commands/lula_tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Collection of additional commands to make OSCAL easier
* [lula](./lula.md) - Risk Management as Code
* [lula tools compose](./lula_tools_compose.md) - compose an OSCAL component definition
* [lula tools lint](./lula_tools_lint.md) - Validate OSCAL against schema
* [lula tools template](./lula_tools_template.md) - Template an artifact
* [lula tools upgrade](./lula_tools_upgrade.md) - Upgrade OSCAL document to a new version if possible.
* [lula tools uuidgen](./lula_tools_uuidgen.md) - Generate a UUID

49 changes: 49 additions & 0 deletions docs/cli-commands/lula_tools_template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
---
title: lula tools template
description: Lula CLI command reference for <code>lula tools template</code>.
type: docs
---
## lula tools template

Template an artifact

### Synopsis

Resolving templated artifacts with configuration data

```
lula tools template [flags]
```

### Examples

```
To template an OSCAL Model:
lula tools template -f ./oscal-component.yaml
To indicate a specific output file:
lula tools template -f ./oscal-component.yaml -o templated-oscal-component.yaml
Data for the templating should be stored under the 'variables' configuration item in a lula-config.yaml file
```

### Options

```
-h, --help help for template
-f, --input-file string the path to the target artifact
-o, --output-file string the path to the output file. If not specified, the output file will be directed to stdout
```

### Options inherited from parent commands

```
-l, --log-level string Log level when running Lula. Valid options are: warn, info, debug, trace (default "info")
```

### SEE ALSO

* [lula tools](./lula_tools.md) - Collection of additional commands to make OSCAL easier

3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ require (
github.com/charmbracelet/lipgloss v0.13.0
github.com/charmbracelet/x/exp/teatest v0.0.0-20240906161213-162f3037fef5
github.com/defenseunicorns/go-oscal v0.6.0
github.com/defenseunicorns/pkg/helpers v1.1.3
github.com/hashicorp/go-version v1.7.0
github.com/kyverno/kyverno-json v0.0.3
github.com/mattn/go-runewidth v0.0.16
Expand Down Expand Up @@ -113,6 +114,7 @@ require (
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/otiai10/copy v1.14.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
github.com/pkg/errors v0.9.1 // indirect
Expand Down Expand Up @@ -169,6 +171,7 @@ require (
k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect
k8s.io/metrics v0.31.1 // indirect
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 // indirect
oras.land/oras-go/v2 v2.5.0 // indirect
sigs.k8s.io/controller-runtime v0.18.2 // indirect
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
sigs.k8s.io/kustomize/api v0.17.2 // indirect
Expand Down
8 changes: 8 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ github.com/daviddengcn/go-colortext v1.0.0 h1:ANqDyC0ys6qCSvuEK7l3g5RaehL/Xck9EX
github.com/daviddengcn/go-colortext v1.0.0/go.mod h1:zDqEI5NVUop5QPpVJUxE9UO10hRnmkD5G4Pmri9+m4c=
github.com/defenseunicorns/go-oscal v0.6.0 h1:eflEKfk7edu4L4kWf6aNQpS94ljfGP8lgWpsPYNtE1Q=
github.com/defenseunicorns/go-oscal v0.6.0/go.mod h1:UHp2yK9ty2mYJDun7oNhbstCq6SAAwP4YGbw9n7uG6o=
github.com/defenseunicorns/pkg/helpers v1.1.3 h1:EVVuniq02qfAouR//AT0eoCngLWfFORj8H6+pI8M7uo=
github.com/defenseunicorns/pkg/helpers v1.1.3/go.mod h1:F4S5VZLDrlNWQKklzv4v9tFWjjZNhxJ1gT79j4XiLwk=
github.com/dgraph-io/badger/v3 v3.2103.5 h1:ylPa6qzbjYRQMU6jokoj4wzcaweHylt//CH0AKt0akg=
github.com/dgraph-io/badger/v3 v3.2103.5/go.mod h1:4MPiseMeDQ3FNCYwRbbcBOGJLf5jsE0PPFzRiKjtcdw=
github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8=
Expand Down Expand Up @@ -286,6 +288,10 @@ github.com/open-policy-agent/opa v0.68.0 h1:Jl3U2vXRjwk7JrHmS19U3HZO5qxQRinQbJ2e
github.com/open-policy-agent/opa v0.68.0/go.mod h1:5E5SvaPwTpwt2WM177I9Z3eT7qUpmOGjk1ZdHs+TZ4w=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/otiai10/copy v1.14.0 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU=
github.com/otiai10/copy v1.14.0/go.mod h1:ECfuL02W+/FkTWZWgQqXPWZgW9oeKCSQ5qVfSc4qc4w=
github.com/otiai10/mint v1.5.1 h1:XaPLeE+9vGbuyEHem1JNk3bYc7KKqyI/na0/mLd/Kks=
github.com/otiai10/mint v1.5.1/go.mod h1:MJm72SBthJjz8qhefc4z1PYEieWmy8Bku7CjcAqyUSM=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI=
Expand Down Expand Up @@ -547,6 +553,8 @@ k8s.io/metrics v0.31.1 h1:h4I4dakgh/zKflWYAOQhwf0EXaqy8LxAIyE/GBvxqRc=
k8s.io/metrics v0.31.1/go.mod h1:JuH1S9tJiH9q1VCY0yzSCawi7kzNLsDzlWDJN4xR+iA=
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A=
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
oras.land/oras-go/v2 v2.5.0 h1:o8Me9kLY74Vp5uw07QXPiitjsw7qNXi8Twd+19Zf02c=
oras.land/oras-go/v2 v2.5.0/go.mod h1:z4eisnLP530vwIOUOJeBIj0aGI0L1C3d53atvCBqZHg=
sigs.k8s.io/controller-runtime v0.18.2 h1:RqVW6Kpeaji67CY5nPEfRz6ZfFMk0lWQlNrLqlNpx+Q=
sigs.k8s.io/controller-runtime v0.18.2/go.mod h1:tuAt1+wbVsXIT8lPtk5RURxqAnq7xkpv2Mhttslg7Hw=
sigs.k8s.io/e2e-framework v0.4.0 h1:4yYmFDNNoTnazqmZJXQ6dlQF1vrnDbutmxlyvBpC5rY=
Expand Down
7 changes: 7 additions & 0 deletions src/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ var rootCmd = &cobra.Command{
Long: `Real Time Risk Transparency through automated validation`,
}

func RootCommand() *cobra.Command {

cmd := rootCmd

return cmd
}

func Execute() {

cobra.CheckErr(rootCmd.Execute())
Expand Down
89 changes: 89 additions & 0 deletions src/cmd/tools/template.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package tools

import (
"os"

"github.com/defenseunicorns/go-oscal/src/pkg/files"
"github.com/defenseunicorns/lula/src/cmd/common"
"github.com/defenseunicorns/lula/src/internal/template"
pkgCommon "github.com/defenseunicorns/lula/src/pkg/common"
"github.com/defenseunicorns/lula/src/pkg/message"
"github.com/spf13/cobra"
)

type templateFlags struct {
InputFile string // -f --input-file
OutputFile string // -o --output-file
}

var templateOpts = &templateFlags{}

var templateHelp = `
To template an OSCAL Model:
lula tools template -f ./oscal-component.yaml
To indicate a specific output file:
lula tools template -f ./oscal-component.yaml -o templated-oscal-component.yaml
Data for the templating should be stored under the 'variables' configuration item in a lula-config.yaml file
`
var templateCmd = &cobra.Command{
Use: "template",
Short: "Template an artifact",
Long: "Resolving templated artifacts with configuration data",
Args: cobra.NoArgs,
Example: templateHelp,
Run: func(cmd *cobra.Command, args []string) {
// Read file
data, err := pkgCommon.ReadFileToBytes(templateOpts.InputFile)
if err != nil {
message.Fatal(err, err.Error())
}

// Get current viper pointer
v := common.GetViper()
// Get all viper settings
// This will only return config file items and resolved environment variables
// that have an associated key in the config file
viperData := v.AllSettings()

// Handles merging viper config file data + environment variables
mergedMap := template.CollectTemplatingData(viperData)

templatedData, err := template.ExecuteTemplate(mergedMap, string(data))
if err != nil {
message.Fatalf(err, "error templating validation: %v", err)
}

if templateOpts.OutputFile == "" {
_, err := os.Stdout.Write(templatedData)
if err != nil {
message.Fatalf(err, "failed to write to stdout: %v", err)
}
} else {
err = files.CreateFileDirs(templateOpts.OutputFile)
if err != nil {
message.Fatalf(err, "failed to create output file path: %s\n", err)
}
err = os.WriteFile(templateOpts.OutputFile, templatedData, 0644)
if err != nil {
message.Fatal(err, err.Error())
}
}

},
}

func TemplateCommand() *cobra.Command {
return templateCmd
}

func init() {
common.InitViper()

toolsCmd.AddCommand(templateCmd)

templateCmd.Flags().StringVarP(&templateOpts.InputFile, "input-file", "f", "", "the path to the target artifact")
templateCmd.MarkFlagRequired("input-file")
templateCmd.Flags().StringVarP(&templateOpts.OutputFile, "output-file", "o", "", "the path to the output file. If not specified, the output file will be directed to stdout")
}
71 changes: 71 additions & 0 deletions src/internal/template/template.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package template

import (
"os"
"strings"
"text/template"

"github.com/defenseunicorns/pkg/helpers"
)

const PREFIX = "LULA_"

// ExecuteTemplate templates the template string with the data map
func ExecuteTemplate(data map[string]interface{}, templateString string) ([]byte, error) {
tmpl, err := template.New("template").Parse(templateString)
if err != nil {
return []byte{}, err
}
tmpl.Option("missingkey=default")

var buffer strings.Builder
err = tmpl.Execute(&buffer, data)
if err != nil {
return []byte{}, err
}

return []byte(buffer.String()), nil
}

// Prepare the map of data for use in templating

func CollectTemplatingData(data map[string]interface{}) map[string]interface{} {

// Get all environment variables with a specific prefix
envMap := GetEnvVars(PREFIX)

// Merge the data into a single map for use with templating
mergedMap := helpers.MergeMapRecursive(envMap, data)

return mergedMap

}

// get all environment variables with the established prefix
func GetEnvVars(prefix string) map[string]interface{} {
envMap := make(map[string]interface{})

// Get all environment variables
envVars := os.Environ()

// Iterate over environment variables
for _, envVar := range envVars {
// Split the environment variable into key and value
pair := strings.SplitN(envVar, "=", 2)
if len(pair) != 2 {
continue
}

key := pair[0]
value := pair[1]

// Check if the key starts with the specified prefix
if strings.HasPrefix(key, prefix) {
// Remove the prefix from the key and convert to lowercase
strippedKey := strings.TrimPrefix(key, prefix)
envMap[strings.ToLower(strippedKey)] = value
}
}

return envMap
}
66 changes: 66 additions & 0 deletions src/internal/template/template_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package template_test

import (
"os"
"strings"
"testing"

"github.com/defenseunicorns/lula/src/internal/template"
)

func TestExecuteTemplate(t *testing.T) {

test := func(t *testing.T, data map[string]interface{}, preTemplate string, expected string) {
t.Helper()
// templateData returned
got, err := template.ExecuteTemplate(data, preTemplate)
if err != nil {
t.Fatalf("error templating data: %s\n", err.Error())
}

if string(got) != expected {
t.Fatalf("Expected %s - Got %s\n", expected, string(got))
}
}

t.Run("Test {{ .testVar }} with data", func(t *testing.T) {
data := map[string]interface{}{
"testVar": "testing",
}

test(t, data, "{{ .testVar }}", "testing")
})

t.Run("Test {{ .testVar }} but empty data", func(t *testing.T) {
data := map[string]interface{}{}

test(t, data, "{{ .testVar }}", "<no value>")
})

}

func TestGetEnvVars(t *testing.T) {

test := func(t *testing.T, prefix string, key string, value string) {
t.Helper()

os.Setenv(key, value)
envMap := template.GetEnvVars(prefix)

// convert key to expected format
strippedKey := strings.TrimPrefix(key, prefix)

if envMap[strings.ToLower(strippedKey)] != value {
t.Fatalf("Expected %s - Got %s\n", value, envMap[strings.ToLower(strippedKey)])
}
os.Unsetenv(key)
}

t.Run("Test LULA_RESOURCE - Pass", func(t *testing.T) {
test(t, "LULA_", "LULA_RESOURCE", "pods")
})

t.Run("Test OTHER_RESOURCE - Pass", func(t *testing.T) {
test(t, "OTHER_", "OTHER_RESOURCE", "deployments")
})
}
8 changes: 8 additions & 0 deletions src/test/e2e/standard/lula-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
resources:
jsoncm: configmaps
yamlcm: configmaps
secret: secrets
pod: pods

type: software
title: lula
Loading

0 comments on commit 89be460

Please sign in to comment.