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:
Andrew Lavery
2026-03-12 13:30:28 -04:00
committed by GitHub
parent 9293164e4a
commit e8bf6435e4
5 changed files with 114 additions and 6 deletions
+1
View File
@@ -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")
+21
View File
@@ -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
}
+51
View File
@@ -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)
}
})
}
}
+14
View File
@@ -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()