diff --git a/cmd/timoni/bundle_vet.go b/cmd/timoni/bundle_vet.go index 7c91f514..ac017107 100644 --- a/cmd/timoni/bundle_vet.go +++ b/cmd/timoni/bundle_vet.go @@ -21,6 +21,7 @@ import ( "fmt" "maps" "os" + "path" "cuelang.org/go/cue" "cuelang.org/go/cue/cuecontext" @@ -112,65 +113,94 @@ func runBundleVetCmd(cmd *cobra.Command, args []string) error { maps.Copy(runtimeValues, engine.GetEnv()) } - if len(bundleArgs.runtimeFiles) > 0 { - kctx, cancel := context.WithTimeout(cmd.Context(), rootArgs.timeout) - defer cancel() + rt, err := buildRuntime(bundleArgs.runtimeFiles) + if err != nil { + return err + } - rt, err := buildRuntime(bundleArgs.runtimeFiles) - if err != nil { - return err - } + clusters := rt.SelectClusters(bundleArgs.runtimeCluster, bundleArgs.runtimeClusterGroup) + if len(clusters) == 0 { + return fmt.Errorf("no cluster found") + } + + kctx, cancel := context.WithTimeout(cmd.Context(), rootArgs.timeout) + defer cancel() + + for _, cluster := range clusters { + kubeconfigArgs.Context = &cluster.KubeContext + + clusterValues := make(map[string]string) + // add values from env + maps.Copy(clusterValues, runtimeValues) + + // add values from cluster rm, err := runtime.NewResourceManager(kubeconfigArgs) if err != nil { return err } - reader := runtime.NewResourceReader(rm) rv, err := reader.Read(kctx, rt.Refs) if err != nil { return err } + maps.Copy(clusterValues, rv) - maps.Copy(runtimeValues, rv) - } - - if err := bm.InitWorkspace(tmpDir, runtimeValues); err != nil { - return describeErr(tmpDir, "failed to parse bundle", err) - } + // add cluster info + maps.Copy(clusterValues, cluster.NameGroupValues()) - v, err := bm.Build() - if err != nil { - return describeErr(tmpDir, "failed to build bundle", err) - } + // create cluster workspace + workspace := path.Join(tmpDir, cluster.Name) + if err := os.MkdirAll(workspace, os.ModePerm); err != nil { + return err + } - bundle, err := bm.GetBundle(v) - if err != nil { - return err - } - log = LoggerBundle(logr.NewContext(cmd.Context(), log), bundle.Name, apiv1.RuntimeDefaultName) + if err := bm.InitWorkspace(workspace, clusterValues); err != nil { + return describeErr(workspace, "failed to parse bundle", err) + } - if len(bundle.Instances) == 0 { - return fmt.Errorf("no instances found in bundle") - } + v, err := bm.Build() + if err != nil { + return describeErr(workspace, "failed to build bundle", err) + } - if bundleVetArgs.printValue { - val := v.LookupPath(cue.ParsePath("bundle")) - if val.Err() != nil { + bundle, err := bm.GetBundle(v) + if 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) + log = LoggerBundle(logr.NewContext(cmd.Context(), log), bundle.Name, apiv1.RuntimeDefaultName) + + if len(bundle.Instances) == 0 { + return fmt.Errorf("no instances found in bundle") + } + + if bundleVetArgs.printValue { + val := v.LookupPath(cue.ParsePath("bundle")) + if val.Err() != nil { + return err + } + bundleCue := fmt.Sprintf("bundle: %v\n", val) + if cluster.Name != apiv1.RuntimeDefaultName { + bundleCue = fmt.Sprintf("\"%s\": bundle: %v\n", cluster.Name, val) + } + _, err := rootCmd.OutOrStdout().Write([]byte(bundleCue)) + if err != nil { + return err + } + } else { + for _, i := range bundle.Instances { + if i.Namespace == "" { + return fmt.Errorf("instance %s does not have a namespace", i.Name) + } + log := LoggerBundleInstance(logr.NewContext(cmd.Context(), log), bundle.Name, cluster.Name, i.Name) + log.Info("instance is valid") + } } - log := LoggerBundleInstance(logr.NewContext(cmd.Context(), log), bundle.Name, apiv1.RuntimeDefaultName, i.Name) - log.Info("instance is valid") } - log.Info("bundle is valid") + if !bundleVetArgs.printValue { + log.Info("bundle is valid") + } return nil } diff --git a/cmd/timoni/bundle_vet_test.go b/cmd/timoni/bundle_vet_test.go index b9120482..9b14bc42 100644 --- a/cmd/timoni/bundle_vet_test.go +++ b/cmd/timoni/bundle_vet_test.go @@ -292,3 +292,117 @@ bundle: g.Expect(err).ToNot(HaveOccurred()) g.Expect(output).To(BeEquivalentTo(bundleComputed)) } + +func Test_BundleVet_Clusters(t *testing.T) { + g := NewWithT(t) + + bundleCue := ` +bundle: { + _cluster: "dev" @timoni(runtime:string:TIMONI_CLUSTER_NAME) + _env: "dev" @timoni(runtime:string:TIMONI_CLUSTER_GROUP) + + apiVersion: "v1alpha1" + name: "fleet-test" + instances: { + "frontend": { + module: { + url: "oci://ghcr.io/stefanprodan/timoni/minimal" + version: "latest" + } + namespace: "fleet-test" + values: { + message: "Hello from cluster \(_cluster)" + test: enabled: true + + if _env == "staging" { + replicas: 2 + } + + if _env == "production" { + replicas: 3 + } + } + } + } +} +` + runtimeCue := ` +runtime: { + apiVersion: "v1alpha1" + name: "fleet-test" + clusters: { + "staging": { + group: "staging" + kubeContext: "envtest" + } + "production": { + group: "production" + kubeContext: "envtest" + } + } + values: [ + { + query: "k8s:v1:Namespace:kube-system" + for: { + "CLUSTER_UID": "obj.metadata.uid" + } + }, + ] +} +` + + bundleComputed := `"staging": bundle: { + apiVersion: "v1alpha1" + name: "fleet-test" + instances: { + frontend: { + module: { + url: "oci://ghcr.io/stefanprodan/timoni/minimal" + version: "latest" + } + namespace: "fleet-test" + values: { + message: "Hello from cluster staging" + replicas: 2 + test: { + enabled: true + } + } + } + } +} +"production": bundle: { + apiVersion: "v1alpha1" + name: "fleet-test" + instances: { + frontend: { + module: { + url: "oci://ghcr.io/stefanprodan/timoni/minimal" + version: "latest" + } + namespace: "fleet-test" + values: { + message: "Hello from cluster production" + replicas: 3 + test: { + enabled: true + } + } + } + } +} +` + wd := t.TempDir() + bundlePath := filepath.Join(wd, "bundle.cue") + g.Expect(os.WriteFile(bundlePath, []byte(bundleCue), 0644)).ToNot(HaveOccurred()) + + runtimePath := filepath.Join(wd, "runtime.cue") + g.Expect(os.WriteFile(runtimePath, []byte(runtimeCue), 0644)).ToNot(HaveOccurred()) + + output, err := executeCommand(fmt.Sprintf( + "bundle vet -f %s -r %s -p main --print-value", + bundlePath, runtimePath, + )) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(output).To(BeEquivalentTo(bundleComputed)) +}