mirror of
https://github.com/replicatedhq/troubleshoot.git
synced 2026-08-27 00:37:20 +00:00
add a '--metadata' flag to support-bundle (#1993)
* add a '--metadata' flag to support-bundle * test the metadata flag e2e
This commit is contained in:
@@ -137,6 +137,7 @@ If no arguments are provided, specs are automatically loaded from the cluster by
|
||||
cmd.Flags().Bool("debug", false, "enable debug logging. This is equivalent to --v=0")
|
||||
cmd.Flags().Bool("dry-run", false, "print support bundle spec without collecting anything")
|
||||
cmd.Flags().Bool("auto-update", true, "enable automatic binary self-update check and install")
|
||||
cmd.Flags().StringSlice("metadata", []string{}, "user-provided metadata key=value pairs to include in the bundle (can be specified multiple times)")
|
||||
|
||||
// Upload flags
|
||||
cmd.Flags().Bool("auto-upload", false, "automatically upload resulting bundle to replicated.app")
|
||||
|
||||
@@ -200,6 +200,11 @@ func runTroubleshoot(v *viper.Viper, args []string) error {
|
||||
}()
|
||||
}
|
||||
|
||||
userMetadata, err := parseMetadataFlag(v.GetStringSlice("metadata"))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "invalid metadata flag")
|
||||
}
|
||||
|
||||
createOpts := supportbundle.SupportBundleCreateOpts{
|
||||
CollectorProgressCallback: collectorCB,
|
||||
CollectWithoutPermissions: v.GetBool("collect-without-permissions"),
|
||||
@@ -221,6 +226,7 @@ func runTroubleshoot(v *viper.Viper, args []string) error {
|
||||
VerifyTokenization: v.GetBool("verify-tokenization"),
|
||||
BundleID: v.GetString("bundle-id"),
|
||||
TokenizationStats: v.GetBool("tokenization-stats"),
|
||||
UserMetadata: userMetadata,
|
||||
}
|
||||
|
||||
nonInteractiveOutput := analysisOutput{}
|
||||
@@ -626,3 +632,18 @@ func VerifyTokenizationSetup(v *viper.Viper) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseMetadataFlag(values []string) (map[string]string, error) {
|
||||
if len(values) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
metadata := make(map[string]string, len(values))
|
||||
for _, v := range values {
|
||||
k, val, ok := strings.Cut(v, "=")
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid metadata format %q, expected key=value", v)
|
||||
}
|
||||
metadata[k] = val
|
||||
}
|
||||
return metadata, nil
|
||||
}
|
||||
|
||||
@@ -464,3 +464,54 @@ func TestCollectTimeoutFlag(t *testing.T) {
|
||||
assert.Equal(t, 90, actualTimeout, "remote-host-collect-timeout should be 90 when --remote-host-collect-timeout=90 is passed")
|
||||
})
|
||||
}
|
||||
|
||||
func TestParseMetadataFlag(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
values []string
|
||||
want map[string]string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "nil input",
|
||||
values: nil,
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "empty input",
|
||||
values: []string{},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "single pair",
|
||||
values: []string{"env=staging"},
|
||||
want: map[string]string{"env": "staging"},
|
||||
},
|
||||
{
|
||||
name: "multiple pairs",
|
||||
values: []string{"env=staging", "version=1.2.3"},
|
||||
want: map[string]string{"env": "staging", "version": "1.2.3"},
|
||||
},
|
||||
{
|
||||
name: "value contains equals",
|
||||
values: []string{"config=key=value"},
|
||||
want: map[string]string{"config": "key=value"},
|
||||
},
|
||||
{
|
||||
name: "missing equals",
|
||||
values: []string{"noequals"},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := parseMetadataFlag(tt.values)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package supportbundle
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
@@ -51,6 +52,8 @@ type SupportBundleCreateOpts struct {
|
||||
VerifyTokenization bool // Validation mode only
|
||||
BundleID string // Custom bundle identifier
|
||||
TokenizationStats bool // Include detailed tokenization statistics
|
||||
|
||||
UserMetadata map[string]string // User-provided key=value metadata pairs
|
||||
}
|
||||
|
||||
type SupportBundleResponse struct {
|
||||
@@ -182,6 +185,17 @@ func CollectSupportBundleFromSpec(
|
||||
return nil, errors.Wrap(err, "failed to write version")
|
||||
}
|
||||
|
||||
if len(opts.UserMetadata) > 0 {
|
||||
metadataJSON, err := json.MarshalIndent(opts.UserMetadata, "", " ")
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to marshal user metadata")
|
||||
}
|
||||
err = result.SaveResult(bundlePath, "metadata/user.json", bytes.NewBuffer(metadataJSON))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to write user metadata")
|
||||
}
|
||||
}
|
||||
|
||||
// Run Analyzers
|
||||
analyzeResults, err := AnalyzeSupportBundle(ctx, spec, bundlePath)
|
||||
if err != nil {
|
||||
|
||||
@@ -64,7 +64,7 @@ func TestSupportBundleMetadata(t *testing.T) {
|
||||
}
|
||||
return ctx
|
||||
}).
|
||||
Assess("check metadata/cluster.json contents", func(ctx context.Context, t *testing.T, c *envconf.Config) context.Context {
|
||||
Assess("check metadata/cluster.json and metadata/user.json contents", func(ctx context.Context, t *testing.T, c *envconf.Config) context.Context {
|
||||
var out bytes.Buffer
|
||||
|
||||
namespace := c.Namespace()
|
||||
@@ -75,8 +75,18 @@ func TestSupportBundleMetadata(t *testing.T) {
|
||||
err := os.WriteFile(specPath, []byte(spec), 0644)
|
||||
require.NoError(t, err)
|
||||
|
||||
expectedUserMetadata := map[string]string{
|
||||
"contactEmail": "support@example.com",
|
||||
"ticketID": "ISSUE-42",
|
||||
}
|
||||
|
||||
tarPath := fmt.Sprintf("%s.tar.gz", supportBundleName)
|
||||
cmd := exec.CommandContext(ctx, sbBinary(), specPath, "--interactive=false", fmt.Sprintf("-o=%s", supportBundleName))
|
||||
cmd := exec.CommandContext(ctx, sbBinary(), specPath,
|
||||
"--interactive=false",
|
||||
fmt.Sprintf("-o=%s", supportBundleName),
|
||||
"--metadata=contactEmail=support@example.com",
|
||||
"--metadata=ticketID=ISSUE-42",
|
||||
)
|
||||
cmd.Stdout = &out
|
||||
err = cmd.Run()
|
||||
if err != nil {
|
||||
@@ -90,14 +100,25 @@ func TestSupportBundleMetadata(t *testing.T) {
|
||||
}
|
||||
}()
|
||||
|
||||
metadataJSON, err := readFileFromTar(tarPath, fmt.Sprintf("%s/metadata/cluster.json", supportBundleName))
|
||||
// Validate metadata/cluster.json from the secret
|
||||
clusterJSON, err := readFileFromTar(tarPath, fmt.Sprintf("%s/metadata/cluster.json", supportBundleName))
|
||||
require.NoError(t, err)
|
||||
|
||||
var result map[string]string
|
||||
err = json.Unmarshal(metadataJSON, &result)
|
||||
var clusterResult map[string]string
|
||||
err = json.Unmarshal(clusterJSON, &clusterResult)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, expectedData, result)
|
||||
assert.Equal(t, expectedData, clusterResult)
|
||||
|
||||
// Validate metadata/user.json from the --metadata flag
|
||||
userJSON, err := readFileFromTar(tarPath, fmt.Sprintf("%s/metadata/user.json", supportBundleName))
|
||||
require.NoError(t, err)
|
||||
|
||||
var userResult map[string]string
|
||||
err = json.Unmarshal(userJSON, &userResult)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, expectedUserMetadata, userResult)
|
||||
|
||||
return ctx
|
||||
}).Feature()
|
||||
|
||||
Reference in New Issue
Block a user