Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(template): template command with initial docs #644

Merged
merged 17 commits into from
Sep 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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) {
brandtkeller marked this conversation as resolved.
Show resolved Hide resolved

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