Skip to content

Commit c7f01c0

Browse files
authored
fix: parse release files with multiple kots yaml documents (#3097)
1 parent 02d8db0 commit c7f01c0

File tree

7 files changed

+415
-100
lines changed

7 files changed

+415
-100
lines changed

pkg/netutils/network_interface_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ func TestRealNetworkInterface_DefaultNetworkInterfaceProvider(t *testing.T) {
7878
// If there's none skip the remainder of the test
7979
if loopbackInterface == nil {
8080
t.Skip("no loopback interface found on system")
81+
return
8182
}
8283

8384
// Get the loopback interface through the provider interface

pkg/release/release.go

Lines changed: 137 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"compress/gzip"
77
"fmt"
88
"io"
9+
"log"
910
"os"
1011
"path/filepath"
1112
"strings"
@@ -304,91 +305,161 @@ func (r *ReleaseData) parse() error {
304305
return fmt.Errorf("failed to copy file out of tar: %w", err)
305306
}
306307

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+
}
322314

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())
331318
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+
}
337327
}
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
340330
}
331+
}
341332

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
346344

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)
348376
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)
350378
}
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)
363384
}
385+
r.AppConfig = parsed
386+
}
364387

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{}
371402
}
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+
}
372411

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)
375420
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)
377422
}
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)
383427
}
428+
}
384429

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{}
388434
}
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)
390459
}
460+
res = append(res, valueBytes)
391461
}
462+
return res, nil
392463
}
393464

394465
// isSystemFile returns true if the filename represents a system file that should be ignored

0 commit comments

Comments
 (0)