|
5 | 5 | "os" |
6 | 6 | "path/filepath" |
7 | 7 | "reflect" |
| 8 | + "strings" |
8 | 9 |
|
9 | 10 | jsApi "github.com/jumpstarter-dev/jumpstarter-controller/api/v1alpha1" |
10 | 11 | api "github.com/jumpstarter-dev/jumpstarter-lab-config/api/v1alpha1" |
@@ -95,18 +96,89 @@ func init() { |
95 | 96 | codecFactory = serializer.NewCodecFactory(scheme, serializer.EnableStrict) |
96 | 97 | } |
97 | 98 |
|
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) { |
100 | 140 | yamlFile, err := os.ReadFile(filePath) |
101 | 141 | if err != nil { |
102 | 142 | return nil, fmt.Errorf("error reading YAML file %s: %w", filePath, err) |
103 | 143 | } |
| 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)) |
104 | 152 | 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) |
108 | 175 | } |
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 |
110 | 182 | } |
111 | 183 |
|
112 | 184 | // processResourceGlobs finds files matching a list of glob patterns, decodes them, |
@@ -139,39 +211,42 @@ func processResourceGlobs(globPatterns []string, targetMap interface{}, resource |
139 | 211 | expectedMapValueType := mapVal.Type().Elem() // e.g., *api.PhysicalLocation |
140 | 212 |
|
141 | 213 | for _, filePath := range allFilePaths { |
142 | | - obj, err := readAndDecodeYAMLFile(filePath) |
| 214 | + objects, err := readAndDecodeYAMLFile(filePath) |
143 | 215 | if err != nil { |
144 | 216 | // Stop at first error encountered |
145 | 217 | return fmt.Errorf("processResourceGlob: error processing file %s for %s: %w", filePath, resourceTypeName, err) |
146 | 218 | } |
147 | 219 |
|
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) |
155 | 249 | } |
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) |
175 | 250 | } |
176 | 251 | return nil |
177 | 252 | } |
|
0 commit comments