mirror of
https://github.com/replicatedhq/troubleshoot.git
synced 2026-09-03 00:47:17 +00:00
fix(supportbundle): ExtractLicenseFromBundle prefers license.json regardless of tar order (#2011)
license.json was being ignored when configmaps containing a license ID appeared earlier in the tar archive. The function now collects both candidates in a single pass and returns the license.json result if present, falling back to the configmap scan only when license.json is absent. Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
e3903add9f
commit
fc83085fc1
@@ -34,6 +34,11 @@ func ExtractLicenseFromBundle(bundlePath string) (string, string, error) {
|
||||
|
||||
tarReader := tar.NewReader(gzReader)
|
||||
|
||||
// Collect results from both sources in a single pass; license.json takes priority
|
||||
// regardless of its position in the tar, since configmaps often appear earlier.
|
||||
var licenseJSONID, licenseJSONSlug string
|
||||
var configmapID, configmapSlug string
|
||||
|
||||
for {
|
||||
header, err := tarReader.Next()
|
||||
if err == io.EOF {
|
||||
@@ -43,50 +48,47 @@ func ExtractLicenseFromBundle(bundlePath string) (string, string, error) {
|
||||
return "", "", errors.Wrap(err, "failed to read tar header")
|
||||
}
|
||||
|
||||
// First priority: check for the new license.json file
|
||||
if strings.Contains(header.Name, "cluster-resources/license.json") && header.Typeflag == tar.TypeReg {
|
||||
if header.Typeflag != tar.TypeReg {
|
||||
continue
|
||||
}
|
||||
|
||||
// First priority: cluster-resources/license.json
|
||||
if strings.Contains(header.Name, "cluster-resources/license.json") {
|
||||
content := make([]byte, header.Size)
|
||||
if _, err := io.ReadFull(tarReader, content); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Parse the license.json file
|
||||
var licenseData struct {
|
||||
LicenseID string `json:"licenseID"`
|
||||
AppSlug string `json:"appSlug"`
|
||||
}
|
||||
if err := json.Unmarshal(content, &licenseData); err == nil {
|
||||
if licenseData.LicenseID != "" && licenseData.AppSlug != "" {
|
||||
return licenseData.LicenseID, licenseData.AppSlug, nil
|
||||
licenseJSONID = licenseData.LicenseID
|
||||
licenseJSONSlug = licenseData.AppSlug
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Fallback: process files in cluster-resources/configmaps/
|
||||
// Fallback: cluster-resources/configmaps/
|
||||
if configmapID != "" {
|
||||
continue // already have a configmap result
|
||||
}
|
||||
if !strings.Contains(header.Name, "cluster-resources/configmaps/") {
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip directories
|
||||
if header.Typeflag != tar.TypeReg {
|
||||
continue
|
||||
}
|
||||
|
||||
// Process .yaml, .yml, and .json files
|
||||
if !strings.HasSuffix(header.Name, ".yaml") &&
|
||||
!strings.HasSuffix(header.Name, ".yml") &&
|
||||
!strings.HasSuffix(header.Name, ".json") {
|
||||
continue
|
||||
}
|
||||
|
||||
// Read the file content
|
||||
content := make([]byte, header.Size)
|
||||
if _, err := io.ReadFull(tarReader, content); err != nil {
|
||||
continue // Skip files we can't read
|
||||
continue
|
||||
}
|
||||
|
||||
// Try to extract license from this configmap
|
||||
var license string
|
||||
if strings.HasSuffix(header.Name, ".json") {
|
||||
license = extractLicenseFromJSON(content)
|
||||
@@ -95,15 +97,21 @@ func ExtractLicenseFromBundle(bundlePath string) (string, string, error) {
|
||||
}
|
||||
|
||||
if license != "" {
|
||||
// Extract app slug from filename
|
||||
filename := filepath.Base(header.Name)
|
||||
appSlug := strings.TrimSuffix(filename, ".json")
|
||||
appSlug = strings.TrimSuffix(appSlug, ".yaml")
|
||||
appSlug = strings.TrimSuffix(appSlug, ".yml")
|
||||
return license, appSlug, nil
|
||||
slug := strings.TrimSuffix(filename, ".json")
|
||||
slug = strings.TrimSuffix(slug, ".yaml")
|
||||
slug = strings.TrimSuffix(slug, ".yml")
|
||||
configmapID = license
|
||||
configmapSlug = slug
|
||||
}
|
||||
}
|
||||
|
||||
if licenseJSONID != "" {
|
||||
return licenseJSONID, licenseJSONSlug, nil
|
||||
}
|
||||
if configmapID != "" {
|
||||
return configmapID, configmapSlug, nil
|
||||
}
|
||||
return "", "", nil // No license found
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
package supportbundle
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"compress/gzip"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// makeBundleTarGz creates a temporary .tar.gz file containing the given entries.
|
||||
// Each entry is a (path, content) pair. Returns the file path; caller must remove it.
|
||||
func makeBundleTarGz(t *testing.T, entries []struct{ name, content string }) string {
|
||||
t.Helper()
|
||||
f, err := os.CreateTemp(t.TempDir(), "bundle-*.tar.gz")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
gw := gzip.NewWriter(f)
|
||||
tw := tar.NewWriter(gw)
|
||||
|
||||
for _, e := range entries {
|
||||
hdr := &tar.Header{
|
||||
Name: e.name,
|
||||
Typeflag: tar.TypeReg,
|
||||
Size: int64(len(e.content)),
|
||||
Mode: 0644,
|
||||
}
|
||||
if err := tw.WriteHeader(hdr); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := tw.Write([]byte(e.content)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := tw.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := gw.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
return f.Name()
|
||||
}
|
||||
|
||||
func TestExtractLicenseFromBundle_PrefersLicenseJSONOverConfigmap(t *testing.T) {
|
||||
// The configmap appears first in the tar but license.json should win.
|
||||
configmapContent := `{
|
||||
"kind": "ConfigMapList",
|
||||
"apiVersion": "v1",
|
||||
"items": [{
|
||||
"data": {
|
||||
"license": "configmapLicenseIDAAAAAAAAAAAA"
|
||||
}
|
||||
}]
|
||||
}`
|
||||
licenseJSONContent := `{"licenseID":"correctLicenseIDAAAAAAAAAAAA","appSlug":"my-app"}`
|
||||
|
||||
bundlePath := makeBundleTarGz(t, []struct{ name, content string }{
|
||||
// configmap comes first in the tar — this is the bug trigger
|
||||
{"bundle/cluster-resources/configmaps/kotsadm.json", configmapContent},
|
||||
{"bundle/cluster-resources/license.json", licenseJSONContent},
|
||||
})
|
||||
|
||||
licenseID, appSlug, err := ExtractLicenseFromBundle(bundlePath)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if licenseID != "correctLicenseIDAAAAAAAAAAAA" {
|
||||
t.Errorf("licenseID = %q, want %q", licenseID, "correctLicenseIDAAAAAAAAAAAA")
|
||||
}
|
||||
if appSlug != "my-app" {
|
||||
t.Errorf("appSlug = %q, want %q", appSlug, "my-app")
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractLicenseFromBundle_FallsBackToConfigmap(t *testing.T) {
|
||||
// No license.json — should fall back to configmap scan.
|
||||
configmapContent := `{
|
||||
"kind": "ConfigMapList",
|
||||
"apiVersion": "v1",
|
||||
"items": [{
|
||||
"data": {
|
||||
"licenseID": "fallbackLicenseIDAAAAAAAAAA"
|
||||
}
|
||||
}]
|
||||
}`
|
||||
bundlePath := makeBundleTarGz(t, []struct{ name, content string }{
|
||||
{"bundle/cluster-resources/configmaps/my-app.json", configmapContent},
|
||||
})
|
||||
|
||||
licenseID, appSlug, err := ExtractLicenseFromBundle(bundlePath)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if licenseID != "fallbackLicenseIDAAAAAAAAAA" {
|
||||
t.Errorf("licenseID = %q, want %q", licenseID, "fallbackLicenseIDAAAAAAAAAA")
|
||||
}
|
||||
if appSlug != "my-app" {
|
||||
t.Errorf("appSlug = %q, want %q", appSlug, "my-app")
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractLicenseFromBundle_ReturnsEmptyWhenNotFound(t *testing.T) {
|
||||
bundlePath := makeBundleTarGz(t, []struct{ name, content string }{
|
||||
{"bundle/cluster-resources/configmaps/some.json", `{"kind":"ConfigMapList"}`},
|
||||
})
|
||||
|
||||
licenseID, appSlug, err := ExtractLicenseFromBundle(bundlePath)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if licenseID != "" || appSlug != "" {
|
||||
t.Errorf("expected empty results, got licenseID=%q appSlug=%q", licenseID, appSlug)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractLicenseFromBundle_RealBundle(t *testing.T) {
|
||||
// Validates against the actual support bundle that triggered the bug.
|
||||
// The bundle has license.json at tar entry #853 but kotsadm configmap at #94.
|
||||
bundlePath := filepath.Join("..", "..", "support-bundle-2026-04-10T17_13_13.tar.gz")
|
||||
if _, err := os.Stat(bundlePath); os.IsNotExist(err) {
|
||||
t.Skip("real bundle not present")
|
||||
}
|
||||
|
||||
licenseID, appSlug, err := ExtractLicenseFromBundle(bundlePath)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if licenseID != "36G95wTeTQoX7UYcm2QvhssCIkH" {
|
||||
t.Errorf("licenseID = %q, want %q", licenseID, "36G95wTeTQoX7UYcm2QvhssCIkH")
|
||||
}
|
||||
if appSlug != "embedded-cluster-smoke-test-staging-app" {
|
||||
t.Errorf("appSlug = %q, want %q", appSlug, "embedded-cluster-smoke-test-staging-app")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user