Skip to content

Commit 2cebe3d

Browse files
Support bundle upload functionality works for apps installed via Helm (#1904)
* Gets licenseid and app slug from cluster secrets * Update upload.go * Update cluster_resources.go
1 parent 6ffc83d commit 2cebe3d

File tree

7 files changed

+130
-12
lines changed

7 files changed

+130
-12
lines changed

cmd/troubleshoot/cli/root.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ If no arguments are provided, specs are automatically loaded from the cluster by
141141
cmd.Flags().Bool("auto-upload", false, "automatically upload resulting bundle to replicated.app")
142142
cmd.Flags().String("license-id", "", "license ID for authentication when uploading (auto-detected from bundle if not provided)")
143143
cmd.Flags().String("app-slug", "", "application slug when uploading (auto-detected from bundle if not provided)")
144+
cmd.Flags().String("upload-domain", "", "custom domain for upload (default: replicated.app)")
144145

145146
// Auto-discovery flags
146147
cmd.Flags().Bool("auto", false, "enable auto-discovery of foundational collectors. When used with YAML specs, adds foundational collectors to YAML collectors. When used alone, collects only foundational data")

cmd/troubleshoot/cli/run.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -246,9 +246,15 @@ func runTroubleshoot(v *viper.Viper, args []string) error {
246246
if v.GetBool("auto-upload") && !response.FileUploaded {
247247
licenseID := v.GetString("license-id")
248248
appSlug := v.GetString("app-slug")
249+
uploadDomain := v.GetString("upload-domain")
249250

250-
fmt.Fprintf(os.Stderr, "Auto-uploading bundle to replicated.app...\n")
251-
if err := supportbundle.UploadBundleAutoDetect(response.ArchivePath, licenseID, appSlug); err != nil {
251+
targetDomain := uploadDomain
252+
if targetDomain == "" {
253+
targetDomain = "replicated.app"
254+
}
255+
256+
fmt.Fprintf(os.Stderr, "Auto-uploading bundle to %s...\n", targetDomain)
257+
if err := supportbundle.UploadBundleAutoDetect(response.ArchivePath, licenseID, appSlug, uploadDomain); err != nil {
252258
fmt.Fprintf(os.Stderr, "Auto-upload failed: %v\n", err)
253259
fmt.Fprintf(os.Stderr, "You can manually upload the bundle using: support-bundle upload %s\n", response.ArchivePath)
254260
} else {

cmd/troubleshoot/cli/upload.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@ Examples:
2626
support-bundle upload bundle.tar.gz --license-id YOUR_LICENSE_ID
2727
2828
# Specify both license and app
29-
support-bundle upload bundle.tar.gz --license-id YOUR_LICENSE_ID --app-slug my-app`,
29+
support-bundle upload bundle.tar.gz --license-id YOUR_LICENSE_ID --app-slug my-app
30+
31+
# Upload to a custom domain (e.g., development environment)
32+
support-bundle upload bundle.tar.gz --upload-domain replicated-app-dev.example.com`,
3033
RunE: func(cmd *cobra.Command, args []string) error {
3134
v := viper.GetViper()
3235
bundlePath := args[0]
@@ -39,9 +42,10 @@ Examples:
3942
// Get upload parameters
4043
licenseID := v.GetString("license-id")
4144
appSlug := v.GetString("app-slug")
45+
uploadDomain := v.GetString("upload-domain")
4246

4347
// Use auto-detection for uploads
44-
if err := supportbundle.UploadBundleAutoDetect(bundlePath, licenseID, appSlug); err != nil {
48+
if err := supportbundle.UploadBundleAutoDetect(bundlePath, licenseID, appSlug, uploadDomain); err != nil {
4549
return errors.Wrap(err, "upload failed")
4650
}
4751

@@ -51,6 +55,7 @@ Examples:
5155

5256
cmd.Flags().String("license-id", "", "license ID for authentication (auto-detected from bundle if not provided)")
5357
cmd.Flags().String("app-slug", "", "application slug (auto-detected from bundle if not provided)")
58+
cmd.Flags().String("upload-domain", "", "custom domain for upload (default: replicated.app)")
5459

5560
return cmd
5661
}

pkg/collect/cluster_resources.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,13 @@ func (c *CollectClusterResources) Collect(progressChan chan<- interface{}) (Coll
400400
}
401401

402402
output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, fmt.Sprintf("%s-errors.json", constants.CLUSTER_RESOURCES_CONFIGMAPS)), marshalErrors(configMapsErrors))
403+
404+
// Replicated License
405+
licenseData, licenseErr := replicatedLicense(ctx, client, namespaceNames)
406+
if licenseErr == nil {
407+
output.SaveResult(c.BundlePath, path.Join(constants.CLUSTER_RESOURCES_DIR, constants.CLUSTER_RESOURCES_REPLICATED_LICENSE), bytes.NewBuffer(licenseData))
408+
}
409+
403410
return output, nil
404411
}
405412

@@ -2176,3 +2183,68 @@ func storeCustomResource(name string, objects any, m map[string][]byte) error {
21762183
m[fmt.Sprintf("%s.yaml", name)] = y
21772184
return nil
21782185
}
2186+
2187+
// replicatedLicense searches for the replicated secret across namespaces,
2188+
// extracts the config.yaml field, and extracts the licenseID and appSlug.
2189+
// Note: secret.Data already contains decoded bytes; no base64 decoding is required.
2190+
func replicatedLicense(ctx context.Context, client *kubernetes.Clientset, namespaces []string) ([]byte, error) {
2191+
// Structure to parse the config.yaml content
2192+
type ConfigYAML struct {
2193+
License string `yaml:"license"` // This is a YAML string containing the License object
2194+
}
2195+
2196+
type LicenseSpec struct {
2197+
LicenseID string `yaml:"licenseID"`
2198+
AppSlug string `yaml:"appSlug"`
2199+
}
2200+
2201+
type License struct {
2202+
Spec LicenseSpec `yaml:"spec"`
2203+
}
2204+
2205+
// Search through all namespaces for the replicated secret
2206+
for _, namespace := range namespaces {
2207+
secret, err := client.CoreV1().Secrets(namespace).Get(ctx, "replicated", metav1.GetOptions{})
2208+
if err != nil {
2209+
// Secret not found in this namespace, continue to next
2210+
continue
2211+
}
2212+
2213+
// Extract the config.yaml field from the secret data
2214+
configYAMLBase64, exists := secret.Data["config.yaml"]
2215+
if !exists {
2216+
continue
2217+
}
2218+
2219+
configYAMLBytes := configYAMLBase64
2220+
2221+
// Parse the YAML to extract the license field
2222+
var config ConfigYAML
2223+
if err := yaml.Unmarshal(configYAMLBytes, &config); err != nil {
2224+
// Malformed config in this namespace; try the next namespace
2225+
continue
2226+
}
2227+
2228+
// Parse the license field (which is a YAML string) to extract licenseID and appSlug
2229+
var license License
2230+
if err := yaml.Unmarshal([]byte(config.License), &license); err != nil {
2231+
// Malformed license in this namespace; try the next namespace
2232+
continue
2233+
}
2234+
2235+
// Return both licenseID and appSlug as JSON
2236+
licenseData := map[string]string{
2237+
"licenseID": license.Spec.LicenseID,
2238+
"appSlug": license.Spec.AppSlug,
2239+
}
2240+
licenseJSON, err := json.Marshal(licenseData)
2241+
if err != nil {
2242+
return nil, fmt.Errorf("failed to marshal license data: %w", err)
2243+
}
2244+
2245+
return licenseJSON, nil
2246+
}
2247+
2248+
// No replicated secret with a parsable license found in any namespace
2249+
return nil, fmt.Errorf("replicated secret with parsable license not found in any namespace")
2250+
}

pkg/constants/constants.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ const (
6161
CLUSTER_RESOURCES_LEASES = "leases"
6262
CLUSTER_RESOURCES_VOLUME_ATTACHMENTS = "volumeattachments"
6363
CLUSTER_RESOURCES_CONFIGMAPS = "configmaps"
64+
CLUSTER_RESOURCES_REPLICATED_LICENSE = "license.json"
6465

6566
// SelfSubjectRulesReview evaluation responses
6667
SELFSUBJECTRULESREVIEW_ERROR_AUTHORIZATION_WEBHOOK_UNSUPPORTED = "webhook authorizer does not support user rule resolution"

pkg/supportbundle/extract_license.go

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,9 @@ import (
1616
)
1717

1818
// ExtractLicenseFromBundle extracts the license ID from a support bundle
19-
// It looks in cluster-resources/configmaps/* for a license field
20-
// Returns both the license ID and the app slug (from the filename where license was found)
19+
// It first looks for cluster-resources/license.json, then falls back to searching
20+
// cluster-resources/configmaps/* for a license field
21+
// Returns both the license ID and the app slug
2122
func ExtractLicenseFromBundle(bundlePath string) (string, string, error) {
2223
file, err := os.Open(bundlePath)
2324
if err != nil {
@@ -42,7 +43,27 @@ func ExtractLicenseFromBundle(bundlePath string) (string, string, error) {
4243
return "", "", errors.Wrap(err, "failed to read tar header")
4344
}
4445

45-
// Only process files in cluster-resources/configmaps/ (may be nested under bundle directory)
46+
// First priority: check for the new license.json file
47+
if strings.Contains(header.Name, "cluster-resources/license.json") && header.Typeflag == tar.TypeReg {
48+
content := make([]byte, header.Size)
49+
if _, err := io.ReadFull(tarReader, content); err != nil {
50+
continue
51+
}
52+
53+
// Parse the license.json file
54+
var licenseData struct {
55+
LicenseID string `json:"licenseID"`
56+
AppSlug string `json:"appSlug"`
57+
}
58+
if err := json.Unmarshal(content, &licenseData); err == nil {
59+
if licenseData.LicenseID != "" && licenseData.AppSlug != "" {
60+
return licenseData.LicenseID, licenseData.AppSlug, nil
61+
}
62+
}
63+
continue
64+
}
65+
66+
// Fallback: process files in cluster-resources/configmaps/
4667
if !strings.Contains(header.Name, "cluster-resources/configmaps/") {
4768
continue
4869
}

pkg/supportbundle/upload.go

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import (
1010

1111
// UploadToReplicatedApp uploads a support bundle directly to replicated.app
1212
// using the app slug as the upload path
13-
func UploadToReplicatedApp(bundlePath, licenseID, appSlug string) error {
13+
func UploadToReplicatedApp(bundlePath, licenseID, appSlug, uploadDomain string) error {
1414
// Open the bundle file
1515
file, err := os.Open(bundlePath)
1616
if err != nil {
@@ -23,8 +23,14 @@ func UploadToReplicatedApp(bundlePath, licenseID, appSlug string) error {
2323
return errors.Wrap(err, "failed to stat file")
2424
}
2525

26+
// Use custom domain if provided, otherwise default to replicated.app
27+
domain := uploadDomain
28+
if domain == "" {
29+
domain = "replicated.app"
30+
}
31+
2632
// Build the upload URL using the app slug
27-
uploadURL := fmt.Sprintf("https://replicated.app/supportbundle/upload/%s", appSlug)
33+
uploadURL := fmt.Sprintf("https://%s/supportbundle/upload/%s", domain, appSlug)
2834

2935
// Create the request
3036
req, err := http.NewRequest("POST", uploadURL, file)
@@ -53,7 +59,7 @@ func UploadToReplicatedApp(bundlePath, licenseID, appSlug string) error {
5359
}
5460

5561
// UploadBundleAutoDetect uploads a support bundle with automatic license and app slug detection
56-
func UploadBundleAutoDetect(bundlePath string, providedLicenseID, providedAppSlug string) error {
62+
func UploadBundleAutoDetect(bundlePath string, providedLicenseID, providedAppSlug, uploadDomain string) error {
5763
licenseID := providedLicenseID
5864

5965
// Always extract from bundle to get app slug (and license if not provided)
@@ -79,9 +85,15 @@ func UploadBundleAutoDetect(bundlePath string, providedLicenseID, providedAppSlu
7985
appSlug = extractedAppSlug
8086
}
8187

88+
// Determine target domain for upload message
89+
targetDomain := uploadDomain
90+
if targetDomain == "" {
91+
targetDomain = "replicated.app"
92+
}
93+
8294
// Upload the bundle
83-
fmt.Printf("Uploading support bundle to replicated.app...\n")
84-
if err := UploadToReplicatedApp(bundlePath, licenseID, appSlug); err != nil {
95+
fmt.Printf("Uploading support bundle to %s...\n", targetDomain)
96+
if err := UploadToReplicatedApp(bundlePath, licenseID, appSlug, uploadDomain); err != nil {
8597
return errors.Wrap(err, "failed to upload bundle")
8698
}
8799

0 commit comments

Comments
 (0)