|
6 | 6 | "compress/gzip" |
7 | 7 | "fmt" |
8 | 8 | "io" |
| 9 | + "log" |
9 | 10 | "os" |
10 | 11 | "path/filepath" |
11 | 12 | "strings" |
@@ -304,91 +305,161 @@ func (r *ReleaseData) parse() error { |
304 | 305 | return fmt.Errorf("failed to copy file out of tar: %w", err) |
305 | 306 | } |
306 | 307 |
|
307 | | - switch { |
308 | | - case bytes.Contains(content.Bytes(), []byte("apiVersion: kots.io/v1beta1")): |
309 | | - if bytes.Contains(content.Bytes(), []byte("kind: Application")) { |
310 | | - parsed, err := parseApplication(content.Bytes()) |
311 | | - if err != nil { |
312 | | - return fmt.Errorf("failed to parse application: %w", err) |
313 | | - } |
314 | | - r.Application = parsed |
315 | | - } else if bytes.Contains(content.Bytes(), []byte("kind: Config")) { |
316 | | - parsed, err := parseAppConfig(content.Bytes()) |
317 | | - if err != nil { |
318 | | - return fmt.Errorf("failed to parse app config: %w", err) |
319 | | - } |
320 | | - r.AppConfig = parsed |
321 | | - } |
| 308 | + // we process special files without splitting YAML documents as either they are not yaml or |
| 309 | + // they are the release data itself which is identified by a comment at the beginning of |
| 310 | + // the file |
| 311 | + if err := r.processDocument(content.Bytes(), header.Name); err != nil { |
| 312 | + return err |
| 313 | + } |
322 | 314 |
|
323 | | - case bytes.Contains(content.Bytes(), []byte("apiVersion: troubleshoot.sh/v1beta2")): |
324 | | - if !bytes.Contains(content.Bytes(), []byte("kind: HostPreflight")) { |
325 | | - break |
326 | | - } |
327 | | - if bytes.Contains(content.Bytes(), []byte("cluster.kurl.sh/v1beta1")) { |
328 | | - break |
329 | | - } |
330 | | - hostPreflights, err := parseHostPreflights(content.Bytes()) |
| 315 | + if !strings.HasPrefix(header.Name, ".") && (strings.HasSuffix(header.Name, ".yaml") || strings.HasSuffix(header.Name, ".yml")) { |
| 316 | + // Split multi-document YAML files |
| 317 | + documents, err := splitYAMLDocuments(content.Bytes()) |
331 | 318 | if err != nil { |
332 | | - return fmt.Errorf("failed to parse host preflights: %w", err) |
333 | | - } |
334 | | - if hostPreflights != nil { |
335 | | - if r.HostPreflights == nil { |
336 | | - r.HostPreflights = &troubleshootv1beta2.HostPreflightSpec{} |
| 319 | + // log only and do not fail here to preserve the previous behavior |
| 320 | + log.Printf("Failed to parse YAML document from release data %s: %v", header.Name, err) |
| 321 | + } else { |
| 322 | + // Process each document |
| 323 | + for _, doc := range documents { |
| 324 | + if err := r.processYAMLDocument(doc, header.Name); err != nil { |
| 325 | + return err |
| 326 | + } |
337 | 327 | } |
338 | | - r.HostPreflights.Collectors = append(r.HostPreflights.Collectors, hostPreflights.Collectors...) |
339 | | - r.HostPreflights.Analyzers = append(r.HostPreflights.Analyzers, hostPreflights.Analyzers...) |
| 328 | + // no need to process the document further |
| 329 | + continue |
340 | 330 | } |
| 331 | + } |
341 | 332 |
|
342 | | - case bytes.Contains(content.Bytes(), []byte("apiVersion: embeddedcluster.replicated.com/v1beta1")): |
343 | | - if !bytes.Contains(content.Bytes(), []byte("kind: Config")) { |
344 | | - break |
345 | | - } |
| 333 | + // for backward compatibility, process files again as YAML documents that failed to parse |
| 334 | + // or do not have the yaml extension |
| 335 | + if err := r.processYAMLDocument(content.Bytes(), header.Name); err != nil { |
| 336 | + return err |
| 337 | + } |
| 338 | + } |
| 339 | +} |
| 340 | + |
| 341 | +// processDocument processes a single non-YAML document and updates the ReleaseData accordingly. |
| 342 | +func (r *ReleaseData) processDocument(content []byte, headerName string) error { |
| 343 | + var err error |
346 | 344 |
|
347 | | - r.EmbeddedClusterConfig, err = parseEmbeddedClusterConfig(content.Bytes()) |
| 345 | + switch { |
| 346 | + case bytes.Contains(content, []byte("# channel release object")): |
| 347 | + r.ChannelRelease, err = parseChannelRelease(content) |
| 348 | + if err != nil { |
| 349 | + return fmt.Errorf("failed to parse channel release: %w", err) |
| 350 | + } |
| 351 | + |
| 352 | + case strings.HasSuffix(headerName, ".tgz"): |
| 353 | + // Skip system files (like macOS ._* files) |
| 354 | + if isSystemFile(headerName) { |
| 355 | + break |
| 356 | + } |
| 357 | + |
| 358 | + // This is a chart archive (.tgz file) |
| 359 | + if r.HelmChartArchives == nil { |
| 360 | + r.HelmChartArchives = [][]byte{} |
| 361 | + } |
| 362 | + r.HelmChartArchives = append(r.HelmChartArchives, content) |
| 363 | + } |
| 364 | + |
| 365 | + return nil |
| 366 | +} |
| 367 | + |
| 368 | +// processYAMLDocument processes a single YAML document and updates the ReleaseData accordingly. |
| 369 | +func (r *ReleaseData) processYAMLDocument(content []byte, headerName string) error { |
| 370 | + var err error |
| 371 | + |
| 372 | + switch { |
| 373 | + case bytes.Contains(content, []byte("apiVersion: kots.io/v1beta1")): |
| 374 | + if bytes.Contains(content, []byte("kind: Application")) { |
| 375 | + parsed, err := parseApplication(content) |
348 | 376 | if err != nil { |
349 | | - return fmt.Errorf("failed to parse embedded cluster config: %w", err) |
| 377 | + return fmt.Errorf("failed to parse application: %w", err) |
350 | 378 | } |
351 | | - |
352 | | - case bytes.Contains(content.Bytes(), []byte("apiVersion: velero.io/v1")): |
353 | | - if bytes.Contains(content.Bytes(), []byte("kind: Backup")) { |
354 | | - r.VeleroBackup, err = parseVeleroBackup(content.Bytes()) |
355 | | - if err != nil { |
356 | | - return fmt.Errorf("failed to parse velero backup: %w", err) |
357 | | - } |
358 | | - } else if bytes.Contains(content.Bytes(), []byte("kind: Restore")) { |
359 | | - r.VeleroRestore, err = parseVeleroRestore(content.Bytes()) |
360 | | - if err != nil { |
361 | | - return fmt.Errorf("failed to parse velero restore: %w", err) |
362 | | - } |
| 379 | + r.Application = parsed |
| 380 | + } else if bytes.Contains(content, []byte("kind: Config")) { |
| 381 | + parsed, err := parseAppConfig(content) |
| 382 | + if err != nil { |
| 383 | + return fmt.Errorf("failed to parse app config: %w", err) |
363 | 384 | } |
| 385 | + r.AppConfig = parsed |
| 386 | + } |
364 | 387 |
|
365 | | - case bytes.Contains(content.Bytes(), []byte("apiVersion: kots.io/v1beta2")): |
366 | | - if bytes.Contains(content.Bytes(), []byte("kind: HelmChart")) { |
367 | | - if r.HelmChartCRs == nil { |
368 | | - r.HelmChartCRs = [][]byte{} |
369 | | - } |
370 | | - r.HelmChartCRs = append(r.HelmChartCRs, content.Bytes()) |
| 388 | + case bytes.Contains(content, []byte("apiVersion: troubleshoot.sh/v1beta2")): |
| 389 | + if !bytes.Contains(content, []byte("kind: HostPreflight")) { |
| 390 | + break |
| 391 | + } |
| 392 | + if bytes.Contains(content, []byte("cluster.kurl.sh/v1beta1")) { |
| 393 | + break |
| 394 | + } |
| 395 | + hostPreflights, err := parseHostPreflights(content) |
| 396 | + if err != nil { |
| 397 | + return fmt.Errorf("failed to parse host preflights: %w", err) |
| 398 | + } |
| 399 | + if hostPreflights != nil { |
| 400 | + if r.HostPreflights == nil { |
| 401 | + r.HostPreflights = &troubleshootv1beta2.HostPreflightSpec{} |
371 | 402 | } |
| 403 | + r.HostPreflights.Collectors = append(r.HostPreflights.Collectors, hostPreflights.Collectors...) |
| 404 | + r.HostPreflights.Analyzers = append(r.HostPreflights.Analyzers, hostPreflights.Analyzers...) |
| 405 | + } |
| 406 | + |
| 407 | + case bytes.Contains(content, []byte("apiVersion: embeddedcluster.replicated.com/v1beta1")): |
| 408 | + if !bytes.Contains(content, []byte("kind: Config")) { |
| 409 | + break |
| 410 | + } |
372 | 411 |
|
373 | | - case bytes.Contains(content.Bytes(), []byte("# channel release object")): |
374 | | - r.ChannelRelease, err = parseChannelRelease(content.Bytes()) |
| 412 | + r.EmbeddedClusterConfig, err = parseEmbeddedClusterConfig(content) |
| 413 | + if err != nil { |
| 414 | + return fmt.Errorf("failed to parse embedded cluster config: %w", err) |
| 415 | + } |
| 416 | + |
| 417 | + case bytes.Contains(content, []byte("apiVersion: velero.io/v1")): |
| 418 | + if bytes.Contains(content, []byte("kind: Backup")) { |
| 419 | + r.VeleroBackup, err = parseVeleroBackup(content) |
375 | 420 | if err != nil { |
376 | | - return fmt.Errorf("failed to parse channel release: %w", err) |
| 421 | + return fmt.Errorf("failed to parse velero backup: %w", err) |
377 | 422 | } |
378 | | - |
379 | | - case strings.HasSuffix(header.Name, ".tgz"): |
380 | | - // Skip system files (like macOS ._* files) |
381 | | - if isSystemFile(header.Name) { |
382 | | - break |
| 423 | + } else if bytes.Contains(content, []byte("kind: Restore")) { |
| 424 | + r.VeleroRestore, err = parseVeleroRestore(content) |
| 425 | + if err != nil { |
| 426 | + return fmt.Errorf("failed to parse velero restore: %w", err) |
383 | 427 | } |
| 428 | + } |
384 | 429 |
|
385 | | - // This is a chart archive (.tgz file) |
386 | | - if r.HelmChartArchives == nil { |
387 | | - r.HelmChartArchives = [][]byte{} |
| 430 | + case bytes.Contains(content, []byte("apiVersion: kots.io/v1beta2")): |
| 431 | + if bytes.Contains(content, []byte("kind: HelmChart")) { |
| 432 | + if r.HelmChartCRs == nil { |
| 433 | + r.HelmChartCRs = [][]byte{} |
388 | 434 | } |
389 | | - r.HelmChartArchives = append(r.HelmChartArchives, content.Bytes()) |
| 435 | + r.HelmChartCRs = append(r.HelmChartCRs, content) |
| 436 | + } |
| 437 | + } |
| 438 | + |
| 439 | + return nil |
| 440 | +} |
| 441 | + |
| 442 | +// splitYAMLDocuments splits a multi-document YAML file into individual documents. |
| 443 | +func splitYAMLDocuments(data []byte) ([][]byte, error) { |
| 444 | + dec := yaml.NewDecoder(bytes.NewReader(data)) |
| 445 | + |
| 446 | + var res [][]byte |
| 447 | + for { |
| 448 | + var value interface{} |
| 449 | + err := dec.Decode(&value) |
| 450 | + if err == io.EOF { |
| 451 | + break |
| 452 | + } |
| 453 | + if err != nil { |
| 454 | + return nil, fmt.Errorf("decode: %w", err) |
| 455 | + } |
| 456 | + valueBytes, err := yaml.Marshal(value) |
| 457 | + if err != nil { |
| 458 | + return nil, fmt.Errorf("marshal: %w", err) |
390 | 459 | } |
| 460 | + res = append(res, valueBytes) |
391 | 461 | } |
| 462 | + return res, nil |
392 | 463 | } |
393 | 464 |
|
394 | 465 | // isSystemFile returns true if the filename represents a system file that should be ignored |
|
0 commit comments