diff --git a/cmd/timoni/bundle_lint.go b/cmd/timoni/bundle_vet.go similarity index 59% rename from cmd/timoni/bundle_lint.go rename to cmd/timoni/bundle_vet.go index 75828af4..231f308d 100644 --- a/cmd/timoni/bundle_lint.go +++ b/cmd/timoni/bundle_vet.go @@ -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" @@ -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 { @@ -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 } @@ -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) diff --git a/cmd/timoni/bundle_lint_test.go b/cmd/timoni/bundle_vet_test.go similarity index 62% rename from cmd/timoni/bundle_lint_test.go rename to cmd/timoni/bundle_vet_test.go index a994d8ab..b9120482 100644 --- a/cmd/timoni/bundle_lint_test.go +++ b/cmd/timoni/bundle_vet_test.go @@ -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 @@ -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, )) @@ -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://:password@test.host: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)) +} diff --git a/cmd/timoni/main_test.go b/cmd/timoni/main_test.go index 09d7722f..1a16f24a 100644 --- a/cmd/timoni/main_test.go +++ b/cmd/timoni/main_test.go @@ -129,7 +129,7 @@ func resetCmdArgs() { pullModArgs = pullModFlags{} pushModArgs = pushModFlags{} bundleApplyArgs = bundleApplyFlags{} - bundleLintArgs = bundleLintFlags{} + bundleVetArgs = bundleVetFlags{} bundleDelArgs = bundleDelFlags{} bundleBuildArgs = bundleBuildFlags{} vendorCrdArgs = vendorCrdFlags{} diff --git a/docs/bundle.md b/docs/bundle.md index bf3ac17c..0287993b 100644 --- a/docs/bundle.md +++ b/docs/bundle.md @@ -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. diff --git a/docs/concepts.md b/docs/concepts.md index 50db10e0..264fd32d 100644 --- a/docs/concepts.md +++ b/docs/concepts.md @@ -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). diff --git a/mkdocs.yml b/mkdocs.yml index 277209f8..789e58f9 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -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