Skip to content

Commit 2d547af

Browse files
authored
Merge pull request #6 from michalskrivanek/multiresource
handle multi-resource yaml files
2 parents df1de16 + 34bc27b commit 2d547af

File tree

1 file changed

+108
-33
lines changed

1 file changed

+108
-33
lines changed

internal/config/loader.go

Lines changed: 108 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"os"
66
"path/filepath"
77
"reflect"
8+
"strings"
89

910
jsApi "github.com/jumpstarter-dev/jumpstarter-controller/api/v1alpha1"
1011
api "github.com/jumpstarter-dev/jumpstarter-lab-config/api/v1alpha1"
@@ -95,18 +96,89 @@ func init() {
9596
codecFactory = serializer.NewCodecFactory(scheme, serializer.EnableStrict)
9697
}
9798

98-
// readAndDecodeYAMLFile reads a YAML file and decodes it into a runtime.Object.
99-
func readAndDecodeYAMLFile(filePath string) (runtime.Object, error) {
99+
// splitYAMLDocuments splits YAML content by proper document separators (--- at start of line)
100+
func splitYAMLDocuments(content string) []string {
101+
lines := strings.Split(content, "\n")
102+
var documents []string
103+
var currentDoc strings.Builder
104+
105+
for _, line := range lines {
106+
trimmed := strings.TrimSpace(line)
107+
// Check if this line is a document separator (starts with ---)
108+
if strings.HasPrefix(trimmed, "---") {
109+
// Save current document if it has content
110+
if currentDoc.Len() > 0 {
111+
documents = append(documents, currentDoc.String())
112+
currentDoc.Reset()
113+
}
114+
continue // Skip the separator line itself
115+
}
116+
117+
// Add line to current document
118+
if currentDoc.Len() > 0 {
119+
currentDoc.WriteString("\n")
120+
}
121+
currentDoc.WriteString(line)
122+
}
123+
124+
// Add the last document if it has content
125+
if currentDoc.Len() > 0 {
126+
documents = append(documents, currentDoc.String())
127+
}
128+
129+
// If no documents were found (no --- separators), return the entire content as one document
130+
if len(documents) == 0 {
131+
return []string{content}
132+
}
133+
134+
return documents
135+
}
136+
137+
// readAndDecodeYAMLFile reads a YAML file and decodes it into runtime.Objects.
138+
// It handles both single-document and multi-document YAML files (separated by ---).
139+
func readAndDecodeYAMLFile(filePath string) ([]runtime.Object, error) {
100140
yamlFile, err := os.ReadFile(filePath)
101141
if err != nil {
102142
return nil, fmt.Errorf("error reading YAML file %s: %w", filePath, err)
103143
}
144+
145+
// Split the file content by --- to handle multi-document YAML
146+
// Only split on --- that appear at the beginning of a line (proper YAML document separators)
147+
content := string(yamlFile)
148+
documents := splitYAMLDocuments(content)
149+
150+
// Pre-allocate slice with estimated capacity
151+
objects := make([]runtime.Object, 0, len(documents))
104152
decode := codecFactory.UniversalDeserializer().Decode
105-
obj, gvk, err := decode(yamlFile, nil, nil)
106-
if err != nil {
107-
return nil, fmt.Errorf("error decoding YAML from file %s (GVK: %v): %w", filePath, gvk, err)
153+
154+
for i, doc := range documents {
155+
// For single-document files, preserve original content exactly (no trimming)
156+
// For multi-document files, trim each document
157+
var docContent string
158+
if len(documents) == 1 {
159+
// Single document - use original content to preserve exact formatting
160+
docContent = doc
161+
} else {
162+
// Multi-document - trim whitespace from each document
163+
trimmed := strings.TrimSpace(doc)
164+
if trimmed == "" {
165+
continue
166+
}
167+
docContent = trimmed
168+
}
169+
170+
obj, gvk, err := decode([]byte(docContent), nil, nil)
171+
if err != nil {
172+
return nil, fmt.Errorf("error decoding YAML document %d from file %s (GVK: %v): %w", i, filePath, gvk, err)
173+
}
174+
objects = append(objects, obj)
108175
}
109-
return obj, nil
176+
177+
if len(objects) == 0 {
178+
return nil, fmt.Errorf("no valid YAML documents found in file %s", filePath)
179+
}
180+
181+
return objects, nil
110182
}
111183

112184
// processResourceGlobs finds files matching a list of glob patterns, decodes them,
@@ -139,39 +211,42 @@ func processResourceGlobs(globPatterns []string, targetMap interface{}, resource
139211
expectedMapValueType := mapVal.Type().Elem() // e.g., *api.PhysicalLocation
140212

141213
for _, filePath := range allFilePaths {
142-
obj, err := readAndDecodeYAMLFile(filePath)
214+
objects, err := readAndDecodeYAMLFile(filePath)
143215
if err != nil {
144216
// Stop at first error encountered
145217
return fmt.Errorf("processResourceGlob: error processing file %s for %s: %w", filePath, resourceTypeName, err)
146218
}
147219

148-
metaObj, ok := obj.(metav1.Object)
149-
if !ok {
150-
return fmt.Errorf("processResourceGlob: object from file %s (%T) does not implement metav1.Object, expected for %s", filePath, obj, resourceTypeName)
151-
}
152-
name := metaObj.GetName()
153-
if name == "" {
154-
return fmt.Errorf("processResourceGlob: object from file %s for %s is missing metadata.name", filePath, resourceTypeName)
220+
// Process each object in the file (handles both single and multi-document YAML)
221+
for docIndex, obj := range objects {
222+
metaObj, ok := obj.(metav1.Object)
223+
if !ok {
224+
return fmt.Errorf("processResourceGlob: object %d from file %s (%T) does not implement metav1.Object, expected for %s", docIndex, filePath, obj, resourceTypeName)
225+
}
226+
name := metaObj.GetName()
227+
if name == "" {
228+
return fmt.Errorf("processResourceGlob: object %d from file %s for %s is missing metadata.name", docIndex, filePath, resourceTypeName)
229+
}
230+
231+
objValue := reflect.ValueOf(obj)
232+
if !objValue.Type().AssignableTo(expectedMapValueType) {
233+
return fmt.Errorf("processResourceGlobs: file %s document %d (name: %s) decoded to type %T, but expected assignable to %s for %s map", filePath, docIndex, name, obj, expectedMapValueType, resourceTypeName)
234+
}
235+
236+
if mapVal.MapIndex(reflect.ValueOf(name)).IsValid() {
237+
// Find the original file that contained this duplicate name
238+
originalFile := sourceFiles[resourceTypeName][name]
239+
return fmt.Errorf("processResourceGlobs: duplicate %s name: '%s' found in file %s document %d (originally defined in %s)", resourceTypeName, name, filePath, docIndex, originalFile)
240+
}
241+
242+
// Track the source file for this resource
243+
if sourceFiles[resourceTypeName] == nil {
244+
sourceFiles[resourceTypeName] = make(map[string]string)
245+
}
246+
sourceFiles[resourceTypeName][name] = filePath
247+
248+
mapVal.SetMapIndex(reflect.ValueOf(name), objValue)
155249
}
156-
157-
objValue := reflect.ValueOf(obj)
158-
if !objValue.Type().AssignableTo(expectedMapValueType) {
159-
return fmt.Errorf("processResourceGlobs: file %s (name: %s) decoded to type %T, but expected assignable to %s for %s map", filePath, name, obj, expectedMapValueType, resourceTypeName)
160-
}
161-
162-
if mapVal.MapIndex(reflect.ValueOf(name)).IsValid() {
163-
// Find the original file that contained this duplicate name
164-
originalFile := sourceFiles[resourceTypeName][name]
165-
return fmt.Errorf("processResourceGlobs: duplicate %s name: '%s' found in file %s (originally defined in %s)", resourceTypeName, name, filePath, originalFile)
166-
}
167-
168-
// Track the source file for this resource
169-
if sourceFiles[resourceTypeName] == nil {
170-
sourceFiles[resourceTypeName] = make(map[string]string)
171-
}
172-
sourceFiles[resourceTypeName][name] = filePath
173-
174-
mapVal.SetMapIndex(reflect.ValueOf(name), objValue)
175250
}
176251
return nil
177252
}

0 commit comments

Comments
 (0)