@@ -62,7 +62,7 @@ type AssembleOptions struct {
6262}
6363
6464// AssemblePackage takes a package definition and returns a package layout with all the resources collected
65- func AssemblePackage (ctx context.Context , pkg v1alpha1.ZarfPackage , packagePath string , opts AssembleOptions ) (* PackageLayout , error ) {
65+ func AssemblePackage (ctx context.Context , pkg v1alpha1.ZarfPackage , packagePath string , importedSchemas [] string , opts AssembleOptions ) (* PackageLayout , error ) {
6666 l := logger .From (ctx )
6767 l .Info ("assembling package" , "path" , packagePath )
6868
@@ -180,11 +180,8 @@ func AssemblePackage(ctx context.Context, pkg v1alpha1.ZarfPackage, packagePath
180180 return nil , err
181181 }
182182
183- // Copy schema file if specified
184- if pkg .Values .Schema != "" {
185- if err = copyValuesSchema (ctx , pkg .Values .Schema , packagePath , buildPath ); err != nil {
186- return nil , err
187- }
183+ if err = mergeAndWriteValuesSchema (ctx , pkg .Values .Schema , importedSchemas , packagePath , buildPath ); err != nil {
184+ return nil , err
188185 }
189186
190187 if err = createDocumentationTar (pkg , packagePath , buildPath ); err != nil {
@@ -244,11 +241,11 @@ type AssembleSkeletonOptions struct {
244241}
245242
246243// AssembleSkeleton creates a skeleton package and returns the path to the created package.
247- func AssembleSkeleton (ctx context.Context , pkg v1alpha1.ZarfPackage , packagePath string , opts AssembleSkeletonOptions ) (* PackageLayout , error ) {
244+ func AssembleSkeleton (ctx context.Context , pkg v1alpha1.ZarfPackage , packagePath string , importedSchemas [] string , opts AssembleSkeletonOptions ) (* PackageLayout , error ) {
248245 pkg .Metadata .Architecture = v1alpha1 .SkeletonArch
249246
250- // Creating skeletons packages with the values feature is not yet supported
251- if len (pkg .Values .Files ) > 0 || pkg .Values .Schema != "" {
247+ // Creating skeleton packages with the values feature is not yet supported
248+ if len (pkg .Values .Files ) > 0 || pkg .Values .Schema != "" || len ( importedSchemas ) > 0 {
252249 return nil , errors .New ("creating skeleton packages with the values feature is not yet supported" )
253250 }
254251
@@ -1031,36 +1028,131 @@ func mergeAndWriteValuesFile(ctx context.Context, files []string, packagePath, b
10311028 return nil
10321029}
10331030
1034- // copyValuesSchema validates and copies a values schema file to the build directory.
1035- // It validates the schema is valid JSON Schema, checks for path traversal, and copies
1036- // the file to the package root.
1037- func copyValuesSchema (ctx context.Context , schema , packagePath , buildPath string ) error {
1031+ // mergeAndWriteValuesSchema merges imported child schemas with the parent schema (parent wins)
1032+ // and writes the result to buildPath/values.schema.json. If only a parent schema exists with
1033+ // no imports, it is validated and copied as-is. If only child schemas exist, they are merged
1034+ // and written. If neither exists, the function is a no-op.
1035+ //
1036+ // Schemas containing "$ref" pointers are rejected in all cases because references may point
1037+ // to files unavailable after assembly.
1038+ func mergeAndWriteValuesSchema (ctx context.Context , parentSchema string , importedSchemas []string , packagePath , buildPath string ) error {
10381039 l := logger .From (ctx )
1039- l .Debug ("copying values schema file to package" , "schema" , schema )
10401040
1041- // Resolve the schema source path from package root
1042- schemaSrc := schema
1043- if ! filepath .IsAbs (schemaSrc ) {
1044- schemaSrc = filepath .Join (packagePath , schema )
1041+ if parentSchema == "" && len (importedSchemas ) == 0 {
1042+ return nil
1043+ }
1044+
1045+ // loadSchema reads a schema file, rejects any external "$ref" pointers before handing
1046+ // the document to gojsonschema, then validates the schema structure. CheckNoExternalRefs
1047+ // runs first so that gojsonschema never attempts to resolve external URIs.
1048+ loadSchema := func (relPath , label string ) (map [string ]any , error ) {
1049+ src := relPath
1050+ if ! filepath .IsAbs (src ) {
1051+ src = filepath .Join (packagePath , relPath )
1052+ }
1053+ b , err := os .ReadFile (src )
1054+ if err != nil {
1055+ return nil , fmt .Errorf ("reading %s schema: %w" , label , err )
1056+ }
1057+ var s map [string ]any
1058+ if err := json .Unmarshal (b , & s ); err != nil {
1059+ return nil , fmt .Errorf ("parsing %s schema: %w" , label , err )
1060+ }
1061+ if err := value .CheckNoExternalRefs (s ); err != nil {
1062+ return nil , fmt .Errorf ("%s schema %s: %w" , label , relPath , err )
1063+ }
1064+ if err := value .ValidateSchemaDocument (s ); err != nil {
1065+ return nil , fmt .Errorf ("%s schema validation failed: %w" , label , err )
1066+ }
1067+ return s , nil
1068+ }
1069+
1070+ // No child schemas — check for $ref, validate, then copy the parent schema file verbatim.
1071+ if len (importedSchemas ) == 0 {
1072+ src := parentSchema
1073+ if ! filepath .IsAbs (src ) {
1074+ src = filepath .Join (packagePath , parentSchema )
1075+ }
1076+ if _ , err := loadSchema (parentSchema , "parent" ); err != nil {
1077+ return err
1078+ }
1079+ dst := filepath .Join (buildPath , ValuesSchema )
1080+ l .Debug ("copying values schema file" , "src" , src , "dst" , dst )
1081+ if err := helpers .CreatePathAndCopy (src , dst ); err != nil {
1082+ return fmt .Errorf ("failed to copy values schema file %s: %w" , parentSchema , err )
1083+ }
1084+ return os .Chmod (dst , helpers .ReadWriteUser )
1085+ }
1086+
1087+ l .Debug ("merging values schemas" , "parent" , parentSchema , "imported" , len (importedSchemas ))
1088+
1089+ // Merge child schemas left-to-right; among children the earlier one wins.
1090+ var merged map [string ]any
1091+ for _ , schemaRelPath := range importedSchemas {
1092+ child , err := loadSchema (schemaRelPath , "imported" )
1093+ if err != nil {
1094+ return err
1095+ }
1096+ if schemaVersion (child ) == "" {
1097+ return fmt .Errorf ("imported schema %s: missing \" $schema\" version declaration; all schemas being merged must specify a version" , schemaRelPath )
1098+ }
1099+ if merged == nil {
1100+ merged = child
1101+ } else {
1102+ if err := checkCompatibleVersion (merged , child , schemaRelPath ); err != nil {
1103+ return err
1104+ }
1105+ merged = value .MergeSchemas (merged , child )
1106+ }
10451107 }
10461108
1047- // Validate the schema is valid JSON Schema
1048- if err := value .ValidateSchemaFile (schemaSrc ); err != nil {
1049- return fmt .Errorf ("values schema validation failed: %w" , err )
1109+ // Load the parent schema and merge it on top — parent wins over all children.
1110+ if parentSchema != "" {
1111+ parent , err := loadSchema (parentSchema , "parent" )
1112+ if err != nil {
1113+ return err
1114+ }
1115+ if schemaVersion (parent ) == "" {
1116+ return fmt .Errorf ("parent schema %s: missing \" $schema\" version declaration; all schemas being merged must specify a version" , parentSchema )
1117+ }
1118+ if err := checkCompatibleVersion (parent , merged , "imported schemas" ); err != nil {
1119+ return err
1120+ }
1121+ merged = value .MergeSchemas (parent , merged )
10501122 }
10511123
1052- // Copy schema file to package root
1053- schemaDst := filepath .Join (buildPath , ValuesSchema )
1054- l .Debug ("copying values schema file" , "src" , schemaSrc , "dst" , schemaDst )
1055- if err := helpers .CreatePathAndCopy (schemaSrc , schemaDst ); err != nil {
1056- return fmt .Errorf ("failed to copy values schema file %s: %w" , schemaSrc , err )
1124+ if err := value .ValidateSchemaDocument (merged ); err != nil {
1125+ return fmt .Errorf ("merged values schema is invalid: %w" , err )
10571126 }
10581127
1059- // Set appropriate file permissions
1060- if err := os .Chmod (schemaDst , helpers .ReadWriteUser ); err != nil {
1061- return fmt .Errorf ("failed to set permissions on values schema file %s: %w" , schemaDst , err )
1128+ dst := filepath .Join (buildPath , ValuesSchema )
1129+ l .Debug ("writing merged values schema" , "dst" , dst )
1130+ b , err := json .MarshalIndent (merged , "" , " " )
1131+ if err != nil {
1132+ return fmt .Errorf ("failed to marshal merged values schema: %w" , err )
1133+ }
1134+ if err := os .WriteFile (dst , b , helpers .ReadWriteUser ); err != nil {
1135+ return fmt .Errorf ("failed to write merged values schema: %w" , err )
10621136 }
1137+ return nil
1138+ }
10631139
1140+ // schemaVersion extracts the "$schema" version URI from a schema map, returning "" if absent or not a string.
1141+ func schemaVersion (s map [string ]any ) string {
1142+ if v , ok := s ["$schema" ].(string ); ok {
1143+ return v
1144+ }
1145+ return ""
1146+ }
1147+
1148+ // checkCompatibleVersion errors when the accumulated merged schema and an incoming schema declare
1149+ // different "$schema" versions, preventing silent cross-version merge bugs.
1150+ func checkCompatibleVersion (accumulated , incoming map [string ]any , incomingLabel string ) error {
1151+ a := schemaVersion (accumulated )
1152+ b := schemaVersion (incoming )
1153+ if a != b {
1154+ return fmt .Errorf ("cannot merge schemas with different versions: accumulated schema uses %q but %s declares %q" , a , incomingLabel , b )
1155+ }
10641156 return nil
10651157}
10661158
0 commit comments