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

Introduce timoni bundle vet command #234

Merged
merged 2 commits into from
Nov 1, 2023
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
66 changes: 44 additions & 22 deletions cmd/timoni/bundle_lint.go → cmd/timoni/bundle_vet.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"maps"
"os"

"cuelang.org/go/cue"
"cuelang.org/go/cue/cuecontext"
"github.com/go-logr/logr"
"github.com/spf13/cobra"
Expand All @@ -32,45 +33,57 @@ import (
"github.com/stefanprodan/timoni/internal/runtime"
)

var bundleLintCmd = &cobra.Command{
Use: "lint",
Short: "Validate bundle definitions",
Long: `The bundle lint command validates that a bundle definition conforms with Timoni's schema.'.
var bundleVetCmd = &cobra.Command{
Use: "vet",
Aliases: []string{"lint"},
Short: "Validate a bundle definition",
Long: `The bundle vet command validates that a bundle definition conforms
with Timoni's schema and optionally prints the computed value.
`,
Example: ` # Validate a bundle
timoni bundle lint -f bundle.cue
Example: ` # Validate a bundle and list its instances
timoni bundle vet -f bundle.cue

# Validate a bundle defined in multiple files
timoni bundle lint \
# Validate a bundle defined in multiple files and print the computed value
timoni bundle vet \
-f ./bundle.cue \
-f ./bundle_secrets.cue
-f ./bundle_secrets.cue \
--print-value

# Validate a bundle with runtime attributes and print the computed value
timoni bundle vet \
-f bundle.cue \
-r runtime.cue \
--print-value
`,
RunE: runBundleLintCmd,
RunE: runBundleVetCmd,
}

type bundleLintFlags struct {
type bundleVetFlags struct {
pkg flags.Package
files []string
runtimeFromEnv bool
runtimeFiles []string
printValue bool
}

var bundleLintArgs bundleLintFlags
var bundleVetArgs bundleVetFlags

func init() {
bundleLintCmd.Flags().VarP(&bundleLintArgs.pkg, bundleLintArgs.pkg.Type(), bundleLintArgs.pkg.Shorthand(), bundleLintArgs.pkg.Description())
bundleLintCmd.Flags().StringSliceVarP(&bundleLintArgs.files, "file", "f", nil,
bundleVetCmd.Flags().VarP(&bundleVetArgs.pkg, bundleVetArgs.pkg.Type(), bundleVetArgs.pkg.Shorthand(), bundleVetArgs.pkg.Description())
bundleVetCmd.Flags().StringSliceVarP(&bundleVetArgs.files, "file", "f", nil,
"The local path to bundle.cue files.")
bundleLintCmd.Flags().BoolVar(&bundleLintArgs.runtimeFromEnv, "runtime-from-env", false,
bundleVetCmd.Flags().BoolVar(&bundleVetArgs.runtimeFromEnv, "runtime-from-env", false,
"Inject runtime values from the environment.")
bundleLintCmd.Flags().StringSliceVarP(&bundleLintArgs.runtimeFiles, "runtime", "r", nil,
bundleVetCmd.Flags().StringSliceVarP(&bundleVetArgs.runtimeFiles, "runtime", "r", nil,
"The local path to runtime.cue files.")
bundleCmd.AddCommand(bundleLintCmd)
bundleVetCmd.Flags().BoolVar(&bundleVetArgs.printValue, "print-value", false,
"Print the computed value of the bundle.")
bundleCmd.AddCommand(bundleVetCmd)
}

func runBundleLintCmd(cmd *cobra.Command, args []string) error {
func runBundleVetCmd(cmd *cobra.Command, args []string) error {
log := LoggerFrom(cmd.Context())
files := bundleLintArgs.files
files := bundleVetArgs.files

tmpDir, err := os.MkdirTemp("", apiv1.FieldManager)
if err != nil {
Expand All @@ -83,15 +96,15 @@ func runBundleLintCmd(cmd *cobra.Command, args []string) error {

runtimeValues := make(map[string]string)

if bundleLintArgs.runtimeFromEnv {
if bundleVetArgs.runtimeFromEnv {
maps.Copy(runtimeValues, engine.GetEnv())
}

if len(bundleLintArgs.runtimeFiles) > 0 {
if len(bundleVetArgs.runtimeFiles) > 0 {
kctx, cancel := context.WithTimeout(cmd.Context(), rootArgs.timeout)
defer cancel()

rt, err := buildRuntime(bundleLintArgs.runtimeFiles)
rt, err := buildRuntime(bundleVetArgs.runtimeFiles)
if err != nil {
return err
}
Expand Down Expand Up @@ -129,6 +142,15 @@ func runBundleLintCmd(cmd *cobra.Command, args []string) error {
return fmt.Errorf("no instances found in bundle")
}

if bundleVetArgs.printValue {
val := v.LookupPath(cue.ParsePath("bundle"))
if val.Err() != nil {
return err
}
_, err := rootCmd.OutOrStdout().Write([]byte(fmt.Sprintf("bundle: %v\n", val)))
return err
}

for _, i := range bundle.Instances {
if i.Namespace == "" {
return fmt.Errorf("instance %s does not have a namespace", i.Name)
Expand Down
98 changes: 96 additions & 2 deletions cmd/timoni/bundle_lint_test.go → cmd/timoni/bundle_vet_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import (
. "github.com/onsi/gomega"
)

func Test_BundleLint(t *testing.T) {
func Test_BundleVet(t *testing.T) {

tests := []struct {
name string
Expand Down Expand Up @@ -189,7 +189,7 @@ bundle: {
g.Expect(err).ToNot(HaveOccurred())

_, err = executeCommand(fmt.Sprintf(
"bundle lint -f %s --runtime-from-env",
"bundle vet -f %s --runtime-from-env",
bundlePath,
))

Expand All @@ -198,3 +198,97 @@ bundle: {
})
}
}

func Test_BundleVet_PrintValue(t *testing.T) {
g := NewWithT(t)

bundleCue := `
bundle: {
apiVersion: "v1alpha1"
name: "podinfo"
_secrets: {
host: string @timoni(runtime:string:TEST_BVET_HOST)
password: string @timoni(runtime:string:TEST_BVET_PASS)
}
instances: {
podinfo: {
module: url: "oci://ghcr.io/stefanprodan/modules/podinfo"
module: version: "latest"
namespace: "podinfo"
values: caching: {
enabled: true
redisURL: "tcp://:\(_secrets.password)@\(_secrets.host):6379"
}
}
}
}
`
bundleYaml := `
bundle:
instances:
podinfo:
values:
monitoring:
enabled: true
`
bundleJson := `
{
"bundle": {
"instances": {
"podinfo": {
"values": {
"autoscaling": {
"enabled": true
}
}
}
}
}
}
`
bundleComputed := `bundle: {
apiVersion: "v1alpha1"
name: "podinfo"
instances: {
podinfo: {
module: {
url: "oci://ghcr.io/stefanprodan/modules/podinfo"
version: "latest"
}
namespace: "podinfo"
values: {
caching: {
enabled: true
redisURL: "tcp://:[email protected]:6379"
}
monitoring: {
enabled: true
}
autoscaling: {
enabled: true
}
}
}
}
}
`
wd := t.TempDir()
cuePath := filepath.Join(wd, "bundle.cue")
g.Expect(os.WriteFile(cuePath, []byte(bundleCue), 0644)).ToNot(HaveOccurred())

yamlPath := filepath.Join(wd, "bundle.yaml")
g.Expect(os.WriteFile(yamlPath, []byte(bundleYaml), 0644)).ToNot(HaveOccurred())

jsonPath := filepath.Join(wd, "bundle.json")
g.Expect(os.WriteFile(jsonPath, []byte(bundleJson), 0644)).ToNot(HaveOccurred())

t.Setenv("TEST_BVET_HOST", "test.host")
t.Setenv("TEST_BVET_PASS", "password")

output, err := executeCommand(fmt.Sprintf(
"bundle vet -f %s -f %s -f %s -p main --runtime-from-env --print-value",
cuePath, yamlPath, jsonPath,
))
g.Expect(err).ToNot(HaveOccurred())
g.Expect(output).To(BeEquivalentTo(bundleComputed))
}
2 changes: 1 addition & 1 deletion cmd/timoni/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ func resetCmdArgs() {
pullModArgs = pullModFlags{}
pushModArgs = pushModFlags{}
bundleApplyArgs = bundleApplyFlags{}
bundleLintArgs = bundleLintFlags{}
bundleVetArgs = bundleVetFlags{}
bundleDelArgs = bundleDelFlags{}
bundleBuildArgs = bundleBuildFlags{}
vendorCrdArgs = vendorCrdFlags{}
Expand Down
18 changes: 15 additions & 3 deletions docs/bundle.md
Original file line number Diff line number Diff line change
Expand Up @@ -497,17 +497,29 @@ the apply command will exit with an error.

The readiness check is enabled by default, to opt-out set `--wait=false`.

### Lint
### Vetting

To verify that one or more CUE files contain a valid Bundle definition,
you can use the `timoni bundle lint` command.
you can use the `timoni bundle vet` command.

Example:

```shell
timoni bundle lint -f bundle.cue -f extras.cue
timoni bundle vet -f bundle.cue -f extras.cue
```

If the validation passes, Timoni will list all the instances found in the computed bundle.

When `--print-value` is specified, Timoni will write the Bundle computed value to stdout.

Example:

```shell
timoni bundle vet -f bundle.cue --runtime-from-env --print-value
```

Printing the computed value is particular useful when debugging runtime attributes.

### Format

To format Bundle files, you can use the `cue fmt` command.
Expand Down
4 changes: 2 additions & 2 deletions docs/concepts.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,10 +145,10 @@ string interpolation and everything else that CUE std lib supports.

Commands for working with bundles:

- `timoni bundle lint -f bundle.cue`
- `timoni bundle build -f bundle.cue -f bundle_extras.cue`
- `timoni bundle apply -f bundle.cue --runtime runtime.cue --diff`
- `timoni bundle build -f bundle.cue -f bundle_extras.cue`
- `timoni bundle delete -f bundle.cue`
- `timoni bundle vet -f bundle.cue`

To learn more about bundles, please see the [Bundle API documentation](bundle.md)
and the [Bundle Runtime API documentation](bundle-runtime.md).
Expand Down
2 changes: 1 addition & 1 deletion mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -130,8 +130,8 @@ nav:
- cmd/timoni_bundle_apply.md
- cmd/timoni_bundle_build.md
- cmd/timoni_bundle_delete.md
- cmd/timoni_bundle_lint.md
- cmd/timoni_bundle_status.md
- cmd/timoni_bundle_vet.md
- Runtime:
- cmd/timoni_runtime.md
- cmd/timoni_runtime_build.md
Expand Down