mirror of
https://github.com/replicatedhq/troubleshoot.git
synced 2026-04-15 07:16:34 +00:00
* Gets licenseid and app slug from cluster secrets * Update upload.go * Update cluster_resources.go
364 lines
10 KiB
Go
364 lines
10 KiB
Go
package supportbundle
|
|
|
|
import (
|
|
"archive/tar"
|
|
"compress/gzip"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/pkg/errors"
|
|
"gopkg.in/yaml.v2"
|
|
)
|
|
|
|
// ExtractLicenseFromBundle extracts the license ID from a support bundle
|
|
// It first looks for cluster-resources/license.json, then falls back to searching
|
|
// cluster-resources/configmaps/* for a license field
|
|
// Returns both the license ID and the app slug
|
|
func ExtractLicenseFromBundle(bundlePath string) (string, string, error) {
|
|
file, err := os.Open(bundlePath)
|
|
if err != nil {
|
|
return "", "", errors.Wrap(err, "failed to open bundle file")
|
|
}
|
|
defer file.Close()
|
|
|
|
gzReader, err := gzip.NewReader(file)
|
|
if err != nil {
|
|
return "", "", errors.Wrap(err, "failed to create gzip reader")
|
|
}
|
|
defer gzReader.Close()
|
|
|
|
tarReader := tar.NewReader(gzReader)
|
|
|
|
for {
|
|
header, err := tarReader.Next()
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
if err != nil {
|
|
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 {
|
|
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
|
|
}
|
|
}
|
|
continue
|
|
}
|
|
|
|
// Fallback: process files in cluster-resources/configmaps/
|
|
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
|
|
}
|
|
|
|
// Try to extract license from this configmap
|
|
var license string
|
|
if strings.HasSuffix(header.Name, ".json") {
|
|
license = extractLicenseFromJSON(content)
|
|
} else {
|
|
license = extractLicenseFromConfigMap(content)
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
return "", "", nil // No license found
|
|
}
|
|
|
|
// extractLicenseFromConfigMap attempts to extract a license ID from a ConfigMap YAML
|
|
func extractLicenseFromConfigMap(content []byte) string {
|
|
// First try to parse as YAML
|
|
var configMap map[string]interface{}
|
|
if err := yaml.Unmarshal(content, &configMap); err != nil {
|
|
// If YAML parsing fails, try regex as fallback
|
|
return extractLicenseWithRegex(string(content))
|
|
}
|
|
|
|
// Look for data field in ConfigMap
|
|
data, ok := configMap["data"].(map[interface{}]interface{})
|
|
if !ok {
|
|
return extractLicenseWithRegex(string(content))
|
|
}
|
|
|
|
// Check for license field in data
|
|
for key, value := range data {
|
|
keyStr, ok := key.(string)
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
// Look for license-related keys
|
|
if strings.ToLower(keyStr) == "license" || strings.Contains(strings.ToLower(keyStr), "license") {
|
|
valueStr, ok := value.(string)
|
|
if ok && isValidLicenseID(valueStr) {
|
|
return valueStr
|
|
}
|
|
// The license might be YAML within YAML
|
|
if licenseID := extractLicenseFromNested(valueStr); licenseID != "" {
|
|
return licenseID
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fallback to regex search
|
|
return extractLicenseWithRegex(string(content))
|
|
}
|
|
|
|
// extractLicenseFromNested tries to extract license from nested YAML content
|
|
func extractLicenseFromNested(content string) string {
|
|
// Try to parse as YAML
|
|
var nested map[string]interface{}
|
|
if err := yaml.Unmarshal([]byte(content), &nested); err != nil {
|
|
return extractLicenseWithRegex(content)
|
|
}
|
|
|
|
// Look for licenseID or license field
|
|
if licenseID, ok := nested["licenseID"].(string); ok && isValidLicenseID(licenseID) {
|
|
return licenseID
|
|
}
|
|
if licenseID, ok := nested["license_id"].(string); ok && isValidLicenseID(licenseID) {
|
|
return licenseID
|
|
}
|
|
if licenseID, ok := nested["license"].(string); ok && isValidLicenseID(licenseID) {
|
|
return licenseID
|
|
}
|
|
|
|
return extractLicenseWithRegex(content)
|
|
}
|
|
|
|
// extractLicenseWithRegex uses regex to find license patterns in text
|
|
func extractLicenseWithRegex(content string) string {
|
|
// Common patterns for license IDs in various formats
|
|
// Including patterns that might appear in embedded YAML within JSON
|
|
patterns := []string{
|
|
`licenseID:\s*["']?([a-zA-Z0-9]{20,30})["']?`,
|
|
`license_id:\s*["']?([a-zA-Z0-9]{20,30})["']?`,
|
|
`license:\s*["']?([a-zA-Z0-9]{20,30})["']?`,
|
|
`"licenseID":\s*"([a-zA-Z0-9]{20,30})"`,
|
|
`"license_id":\s*"([a-zA-Z0-9]{20,30})"`,
|
|
`"license":\s*"([a-zA-Z0-9]{20,30})"`,
|
|
`licenseID: ([a-zA-Z0-9]{20,30})`, // YAML format without quotes
|
|
`\\nlicenseID: ([a-zA-Z0-9]{20,30})`, // With escaped newline
|
|
}
|
|
|
|
for _, pattern := range patterns {
|
|
re := regexp.MustCompile(pattern)
|
|
if matches := re.FindStringSubmatch(content); len(matches) > 1 {
|
|
if isValidLicenseID(matches[1]) {
|
|
return matches[1]
|
|
}
|
|
}
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
// extractLicenseFromJSON extracts license ID from a JSON file
|
|
func extractLicenseFromJSON(content []byte) string {
|
|
// First try to find license ID directly in the raw content
|
|
// This handles cases where the license is in embedded YAML/strings
|
|
if license := extractLicenseWithRegex(string(content)); license != "" {
|
|
return license
|
|
}
|
|
|
|
// Then try parsing as JSON for structured search
|
|
var data map[string]interface{}
|
|
if err := json.Unmarshal(content, &data); err != nil {
|
|
return ""
|
|
}
|
|
|
|
// Look for license-related fields at any level
|
|
return findLicenseInMap(data)
|
|
}
|
|
|
|
// findLicenseInMap recursively searches for license ID in a map
|
|
func findLicenseInMap(data interface{}) string {
|
|
switch v := data.(type) {
|
|
case map[string]interface{}:
|
|
// Check for license fields at this level
|
|
for key, value := range v {
|
|
keyLower := strings.ToLower(key)
|
|
if keyLower == "licenseid" || keyLower == "license_id" || keyLower == "license" {
|
|
if str, ok := value.(string); ok && isValidLicenseID(str) {
|
|
return str
|
|
}
|
|
}
|
|
}
|
|
// Recurse into nested objects
|
|
for _, value := range v {
|
|
if result := findLicenseInMap(value); result != "" {
|
|
return result
|
|
}
|
|
}
|
|
case []interface{}:
|
|
// Recurse into arrays
|
|
for _, item := range v {
|
|
if result := findLicenseInMap(item); result != "" {
|
|
return result
|
|
}
|
|
}
|
|
case string:
|
|
// Check if this string itself is a license
|
|
if isValidLicenseID(v) {
|
|
return v
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// isValidLicenseID checks if a string looks like a valid license ID
|
|
func isValidLicenseID(s string) bool {
|
|
// License IDs are typically 20-30 character alphanumeric strings
|
|
if len(s) < 20 || len(s) > 30 {
|
|
return false
|
|
}
|
|
|
|
// Must be alphanumeric
|
|
for _, c := range s {
|
|
if !((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// ExtractAppSlugFromBundle attempts to extract the app slug from a support bundle
|
|
// by looking in configmaps for appSlug field
|
|
func ExtractAppSlugFromBundle(bundlePath string) (string, error) {
|
|
file, err := os.Open(bundlePath)
|
|
if err != nil {
|
|
return "", errors.Wrap(err, "failed to open bundle file")
|
|
}
|
|
defer file.Close()
|
|
|
|
gzReader, err := gzip.NewReader(file)
|
|
if err != nil {
|
|
return "", errors.Wrap(err, "failed to create gzip reader")
|
|
}
|
|
defer gzReader.Close()
|
|
|
|
tarReader := tar.NewReader(gzReader)
|
|
|
|
for {
|
|
header, err := tarReader.Next()
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
if err != nil {
|
|
return "", errors.Wrap(err, "failed to read tar header")
|
|
}
|
|
|
|
// Only process files in cluster-resources/configmaps/ (may be nested under bundle directory)
|
|
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
|
|
}
|
|
|
|
// Try to extract app slug from this content
|
|
if appSlug := extractAppSlugFromContent(string(content)); appSlug != "" {
|
|
return appSlug, nil
|
|
}
|
|
|
|
// Also try to extract from the filename as fallback
|
|
filename := filepath.Base(header.Name)
|
|
filename = strings.TrimSuffix(filename, ".yaml")
|
|
filename = strings.TrimSuffix(filename, ".yml")
|
|
filename = strings.TrimSuffix(filename, ".json")
|
|
|
|
// Skip common Kubernetes configmaps
|
|
if filename == "kube-root-ca.crt" || strings.HasPrefix(filename, "kube-") ||
|
|
strings.HasPrefix(filename, "kotsadm-") {
|
|
continue
|
|
}
|
|
|
|
// Use the filename as a potential app slug
|
|
if filename != "" && !strings.Contains(filename, "..") {
|
|
return filename, nil
|
|
}
|
|
}
|
|
|
|
return "", fmt.Errorf("could not determine app slug from bundle")
|
|
}
|
|
|
|
// extractAppSlugFromContent tries to find app slug in file content
|
|
func extractAppSlugFromContent(content string) string {
|
|
// Patterns to find app slug in various formats
|
|
patterns := []string{
|
|
`appSlug:\s*["']?([a-zA-Z0-9\-]+)["']?`,
|
|
`app_slug:\s*["']?([a-zA-Z0-9\-]+)["']?`,
|
|
`"appSlug":\s*"([a-zA-Z0-9\-]+)"`,
|
|
`"app_slug":\s*"([a-zA-Z0-9\-]+)"`,
|
|
`appSlug: ([a-zA-Z0-9\-]+)`, // YAML format without quotes
|
|
`\\nappSlug: ([a-zA-Z0-9\-]+)`, // With escaped newline
|
|
}
|
|
|
|
for _, pattern := range patterns {
|
|
re := regexp.MustCompile(pattern)
|
|
if matches := re.FindStringSubmatch(content); len(matches) > 1 {
|
|
return matches[1]
|
|
}
|
|
}
|
|
|
|
return ""
|
|
}
|