@@ -20,11 +20,14 @@ import (
2020 "context"
2121 "encoding/json"
2222 "fmt"
23+ "io"
24+ "net/http"
2325 "os"
2426 "os/exec"
2527 "path/filepath"
2628 "strings"
2729
30+ "github.com/xeipuuv/gojsonschema"
2831 corev1 "k8s.io/api/core/v1"
2932 "k8s.io/apimachinery/pkg/api/errors"
3033 "k8s.io/apimachinery/pkg/api/meta"
@@ -54,6 +57,7 @@ const (
5457 VirtualClusterConditionAvailable = "Available"
5558 VirtualClusterConditionDeploying = "Deploying"
5659 VirtualClusterConditionError = "Error"
60+ VirtualClusterConditionValidated = "SchemaValidated"
5761)
5862
5963// VirtualClusterReconciler reconciles a VirtualCluster object
@@ -298,7 +302,7 @@ func (r *VirtualClusterReconciler) createValuesFile(ctx context.Context, vcluste
298302 logger := log .FromContext (ctx )
299303
300304 // Convert the values to YAML
301- valuesYAML , err := yaml .Marshal (vcluster .Spec .Values . Config )
305+ valuesYAML , err := yaml .Marshal (vcluster .Spec .Values )
302306 if err != nil {
303307 logger .Error (err , "Failed to marshal values to YAML" )
304308 return "" , err
@@ -333,6 +337,58 @@ func (r *VirtualClusterReconciler) installOrUpgradeVCluster(ctx context.Context,
333337 releaseName := vcluster .Name
334338 namespace := vcluster .Namespace
335339
340+ // Get the chart version from spec if provided, otherwise use default
341+ chartVersion := vclusterVersion
342+ if vcluster .Spec .Chart .Version != "" {
343+ chartVersion = vcluster .Spec .Chart .Version
344+ logger .Info ("Using chart version from spec" , "version" , chartVersion )
345+ } else {
346+ logger .Info ("Using default chart version" , "version" , chartVersion )
347+ }
348+
349+ // Ensure schema ConfigMap exists
350+ schemaData , err := r .ensureSchemaConfigMap (ctx , vcluster , chartVersion )
351+ if err != nil {
352+ logger .Error (err , "Failed to ensure schema ConfigMap exists" )
353+ return err
354+ }
355+
356+ // Validate values against schema if we have the schema
357+ if schemaData != "" {
358+ if err := r .validateValuesAgainstSchema (ctx , vcluster , schemaData ); err != nil {
359+ logger .Error (err , "Schema validation failed" )
360+ // Don't return error, we continue but update the status
361+ meta .SetStatusCondition (& vcluster .Status .Conditions , metav1.Condition {
362+ Type : VirtualClusterConditionValidated ,
363+ Status : metav1 .ConditionFalse ,
364+ Reason : "ValidationFailed" ,
365+ Message : fmt .Sprintf ("Values schema validation failed: %v" , err ),
366+ })
367+
368+ if vcluster .Status .Message == "" || ! strings .Contains (vcluster .Status .Message , "Schema validation" ) {
369+ oldMessage := vcluster .Status .Message
370+ vcluster .Status .Message = fmt .Sprintf ("Schema validation failed, helm install might fail: %v. %s" , err , oldMessage )
371+ }
372+
373+ err := r .Status ().Update (ctx , vcluster )
374+ if err != nil {
375+ logger .Error (err , "Failed to update status with validation error" )
376+ }
377+ } else {
378+ // Update the status to indicate successful validation
379+ meta .SetStatusCondition (& vcluster .Status .Conditions , metav1.Condition {
380+ Type : VirtualClusterConditionValidated ,
381+ Status : metav1 .ConditionTrue ,
382+ Reason : "ValidationSucceeded" ,
383+ Message : "Values successfully validated against schema" ,
384+ })
385+ err := r .Status ().Update (ctx , vcluster )
386+ if err != nil {
387+ logger .Error (err , "Failed to update status with validation success" )
388+ }
389+ }
390+ }
391+
336392 // Add the vCluster repo if not exists
337393 addRepoCmd := exec .Command ("helm" , "repo" , "add" , "loft" , vclusterRepo )
338394 if output , err := addRepoCmd .CombinedOutput (); err != nil {
@@ -354,7 +410,7 @@ func (r *VirtualClusterReconciler) installOrUpgradeVCluster(ctx context.Context,
354410 "helm" , "upgrade" ,
355411 releaseName ,
356412 fmt .Sprintf ("loft/%s" , vclusterChart ),
357- "--version" , vclusterVersion ,
413+ "--version" , chartVersion ,
358414 "--namespace" , namespace ,
359415 "--values" , valuesFile ,
360416 )
@@ -365,7 +421,7 @@ func (r *VirtualClusterReconciler) installOrUpgradeVCluster(ctx context.Context,
365421 "helm" , "install" ,
366422 releaseName ,
367423 fmt .Sprintf ("loft/%s" , vclusterChart ),
368- "--version" , vclusterVersion ,
424+ "--version" , chartVersion ,
369425 "--namespace" , namespace ,
370426 "--create-namespace" ,
371427 "--values" , valuesFile ,
@@ -383,6 +439,128 @@ func (r *VirtualClusterReconciler) installOrUpgradeVCluster(ctx context.Context,
383439 return nil
384440}
385441
442+ // ensureSchemaConfigMap ensures that a ConfigMap with the schema for the specified version exists
443+ // Returns the schema data if available, empty string otherwise
444+ func (r * VirtualClusterReconciler ) ensureSchemaConfigMap (ctx context.Context , vcluster * corev1alpha1.VirtualCluster , version string ) (string , error ) {
445+ logger := log .FromContext (ctx )
446+
447+ // Define ConfigMap name based on version
448+ configMapName := fmt .Sprintf ("vcluster-schema-%s" , strings .ReplaceAll (version , "." , "-" ))
449+ configMapNamespace := vcluster .Namespace
450+
451+ // Check if ConfigMap already exists
452+ configMap := & corev1.ConfigMap {}
453+ err := r .Get (ctx , client.ObjectKey {Namespace : configMapNamespace , Name : configMapName }, configMap )
454+ if err == nil {
455+ logger .Info ("Schema ConfigMap already exists" , "name" , configMapName , "namespace" , configMapNamespace )
456+ return configMap .Data ["values.schema.json" ], nil
457+ }
458+
459+ if ! errors .IsNotFound (err ) {
460+ return "" , err
461+ }
462+
463+ // ConfigMap doesn't exist, fetch schema and create it
464+ logger .Info ("Fetching schema for vCluster version" , "version" , version )
465+
466+ // Construct URL to fetch schema
467+ schemaURL := fmt .Sprintf ("https://github.com/loft-sh/vcluster/releases/download/%s/values.schema.json" , version )
468+
469+ // Fetch schema
470+ resp , err := http .Get (schemaURL )
471+ if err != nil {
472+ logger .Error (err , "Failed to fetch schema" , "url" , schemaURL )
473+ // Don't return error, we'll continue without the schema
474+ logger .Info ("Continuing without schema ConfigMap" )
475+ return "" , nil
476+ }
477+ defer resp .Body .Close ()
478+
479+ if resp .StatusCode != http .StatusOK {
480+ logger .Error (fmt .Errorf ("HTTP status code: %d" , resp .StatusCode ), "Failed to fetch schema" , "url" , schemaURL )
481+ // Don't return error, we'll continue without the schema
482+ logger .Info ("Continuing without schema ConfigMap" )
483+ return "" , nil
484+ }
485+
486+ // Read schema content
487+ schemaContent , err := io .ReadAll (resp .Body )
488+ if err != nil {
489+ logger .Error (err , "Failed to read schema content" )
490+ // Don't return error, we'll continue without the schema
491+ logger .Info ("Continuing without schema ConfigMap" )
492+ return "" , nil
493+ }
494+
495+ // Create ConfigMap
496+ newConfigMap := & corev1.ConfigMap {
497+ ObjectMeta : metav1.ObjectMeta {
498+ Name : configMapName ,
499+ Namespace : configMapNamespace ,
500+ Labels : map [string ]string {
501+ "app.kubernetes.io/managed-by" : "openvc-controller" ,
502+ "app.kubernetes.io/name" : "vcluster-schema" ,
503+ "app.kubernetes.io/version" : version ,
504+ },
505+ },
506+ Data : map [string ]string {
507+ "values.schema.json" : string (schemaContent ),
508+ },
509+ }
510+
511+ // Set owner reference
512+ if err := ctrl .SetControllerReference (vcluster , newConfigMap , r .Scheme ); err != nil {
513+ logger .Error (err , "Failed to set controller reference on ConfigMap" )
514+ // Don't return error, we'll continue without the schema
515+ logger .Info ("Continuing without schema ConfigMap" )
516+ return "" , nil
517+ }
518+
519+ // Create ConfigMap
520+ if err := r .Create (ctx , newConfigMap ); err != nil {
521+ logger .Error (err , "Failed to create schema ConfigMap" )
522+ // Don't return error, we'll continue without the schema
523+ logger .Info ("Continuing without schema ConfigMap" )
524+ return "" , nil
525+ }
526+
527+ logger .Info ("Created schema ConfigMap" , "name" , configMapName , "namespace" , configMapNamespace )
528+ return string (schemaContent ), nil
529+ }
530+
531+ // validateValuesAgainstSchema validates the values against the schema
532+ func (r * VirtualClusterReconciler ) validateValuesAgainstSchema (ctx context.Context , vcluster * corev1alpha1.VirtualCluster , schemaData string ) error {
533+ logger := log .FromContext (ctx )
534+ logger .Info ("Validating values against schema" )
535+
536+ // Convert the values to JSON for validation
537+ valuesJSON , err := json .Marshal (vcluster .Spec .Values )
538+ if err != nil {
539+ return fmt .Errorf ("failed to marshal values to JSON: %w" , err )
540+ }
541+
542+ // Create schema and document loaders
543+ schemaLoader := gojsonschema .NewStringLoader (schemaData )
544+ documentLoader := gojsonschema .NewBytesLoader (valuesJSON )
545+
546+ // Validate
547+ result , err := gojsonschema .Validate (schemaLoader , documentLoader )
548+ if err != nil {
549+ return fmt .Errorf ("validation failed: %w" , err )
550+ }
551+
552+ if ! result .Valid () {
553+ var errors []string
554+ for _ , desc := range result .Errors () {
555+ errors = append (errors , desc .String ())
556+ }
557+ return fmt .Errorf ("schema validation errors: %s" , strings .Join (errors , "; " ))
558+ }
559+
560+ logger .Info ("Values successfully validated against schema" )
561+ return nil
562+ }
563+
386564// helmReleaseExists checks if a Helm release exists
387565func (r * VirtualClusterReconciler ) helmReleaseExists (ctx context.Context , vcluster * corev1alpha1.VirtualCluster ) (bool , error ) {
388566 logger := log .FromContext (ctx )
0 commit comments