Skip to content

Commit bb2df37

Browse files
authored
feat(fxconfig): add --format flag to info command (hyperledger#93)
- Improvement (improvement to code, performance, etc) #### Description This PR resolves an existing TODO in the `info` command by adding a `--format` flag, allowing users to view the effective configuration in either `yaml` (default) or `env` format. --------- Signed-off-by: Shubham Singh <shubhsoch@gmail.com>
1 parent dfaf029 commit bb2df37

3 files changed

Lines changed: 204 additions & 8 deletions

File tree

tools/fxconfig/docs/README.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,14 @@ fxconfig version
7575

7676
# Display effective configuration
7777
fxconfig info
78+
79+
# Display effective configuration as environment variables
80+
fxconfig info --format env
7881
```
7982

83+
**`info` Flags:**
84+
- `--format=<yaml|env>` - Output format: `yaml` (default) or `env` (flat `FXCONFIG_*=value` pairs)
85+
8086
## Configuration
8187

8288
Configuration is loaded from multiple sources with the following precedence (highest to lowest):
@@ -365,9 +371,12 @@ fxconfig tx submit --help # Submit command help
365371
### Check Configuration
366372

367373
```bash
368-
# Display effective configuration
374+
# Display effective configuration (yaml)
369375
fxconfig info
370376
377+
# Display effective configuration as environment variables
378+
fxconfig info --format env
379+
371380
# Test connectivity
372381
fxconfig namespace list
373382
```

tools/fxconfig/internal/cli/v1/info.go

Lines changed: 65 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,19 @@ SPDX-License-Identifier: Apache-2.0
77
package v1
88

99
import (
10+
"fmt"
11+
"slices"
12+
"strings"
13+
1014
"github.com/spf13/cobra"
1115
"gopkg.in/yaml.v3"
1216
)
1317

1418
// NewInfoCommand returns a command that displays the effective configuration.
15-
// The configuration is shown as YAML after applying all overrides from
16-
// flags, environment variables, and config files.
19+
// The configuration is shown in the requested format (yaml or env) after applying
20+
// all overrides from flags, environment variables, and config files.
1721
func NewInfoCommand(ctx *CLIContext) *cobra.Command {
22+
var format string
1823
cmd := &cobra.Command{
1924
Use: "info",
2025
Short: "Display effective configuration",
@@ -39,15 +44,68 @@ Examples:
3944
# Show configuration as environment variables
4045
fxconfig info --format env`,
4146
RunE: func(_ *cobra.Command, _ []string) error {
42-
// TODO: add flag to show yaml/env config
43-
out, err := yaml.Marshal(ctx.Config)
44-
if err != nil {
45-
return err
47+
switch format {
48+
case "env":
49+
env, err := toEnv("FXCONFIG", ctx.Config)
50+
if err != nil {
51+
return err
52+
}
53+
slices.Sort(env)
54+
ctx.Printer.Print(strings.Join(env, "\n") + "\n")
55+
case "yaml":
56+
out, err := yaml.Marshal(ctx.Config)
57+
if err != nil {
58+
return err
59+
}
60+
ctx.Printer.Print(string(out))
61+
default:
62+
return fmt.Errorf("invalid --format: %s (want yaml|env)", format)
4663
}
47-
ctx.Printer.Print(string(out))
4864
return nil
4965
},
5066
}
5167

68+
cmd.Flags().StringVar(&format, "format", "yaml", "Output format (yaml|env)")
69+
5270
return cmd
5371
}
72+
73+
func toEnv(prefix string, cfg any) ([]string, error) {
74+
// we use yaml marshaling as a shortcut to get a map representation of the config
75+
// that respects all yaml tags and omitempty.
76+
out, err := yaml.Marshal(cfg)
77+
if err != nil {
78+
return nil, err
79+
}
80+
81+
var m map[string]any
82+
if err := yaml.Unmarshal(out, &m); err != nil {
83+
return nil, err
84+
}
85+
86+
return flatten(prefix, m), nil
87+
}
88+
89+
func flatten(prefix string, m map[string]any) []string {
90+
var result []string
91+
for k, v := range m {
92+
key := strings.ToUpper(strings.ReplaceAll(k, "-", "_"))
93+
if prefix != "" {
94+
key = prefix + "_" + key
95+
}
96+
97+
switch val := v.(type) {
98+
case map[string]any:
99+
result = append(result, flatten(key, val)...)
100+
case []any:
101+
strVals := make([]string, 0, len(val))
102+
for _, item := range val {
103+
strVals = append(strVals, fmt.Sprintf("%v", item))
104+
}
105+
result = append(result, fmt.Sprintf("%s=%s", key, strings.Join(strVals, ",")))
106+
default:
107+
result = append(result, fmt.Sprintf("%s=%v", key, val))
108+
}
109+
}
110+
return result
111+
}

tools/fxconfig/internal/cli/v1/info_test.go

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ package v1
99
import (
1010
"bytes"
1111
"testing"
12+
"time"
1213

1314
"github.com/stretchr/testify/require"
1415

@@ -56,3 +57,131 @@ func TestInfoCommand_NilConfigPrintsNull(t *testing.T) {
5657
require.NoError(t, err)
5758
require.Contains(t, outBuf.String(), "null")
5859
}
60+
61+
func TestInfoCommand_PrintsEnvConfig(t *testing.T) {
62+
t.Parallel()
63+
64+
boolPtr := func(b bool) *bool { return &b }
65+
66+
var outBuf bytes.Buffer
67+
ctx := &CLIContext{
68+
Config: &config.Config{
69+
Logging: config.LoggingConfig{
70+
Level: "ERROR",
71+
Format: "%{color}%{level}%{color:reset} %{message}",
72+
},
73+
MSP: config.MSPConfig{
74+
LocalMspID: "Org1MSP",
75+
ConfigPath: "/path/to/msp",
76+
},
77+
TLS: config.TLSConfig{
78+
Enabled: boolPtr(true),
79+
ClientKeyPath: "/path/to/client.key",
80+
ClientCertPath: "/path/to/client.crt",
81+
RootCertPaths: []string{"/path/to/ca.crt"},
82+
},
83+
Orderer: config.OrdererConfig{
84+
EndpointServiceConfig: config.EndpointServiceConfig{
85+
Address: "localhost:7050",
86+
ConnectionTimeout: 30 * time.Second,
87+
TLS: &config.TLSConfig{
88+
Enabled: boolPtr(true),
89+
RootCertPaths: []string{"/path/to/orderer-ca.crt"},
90+
ClientCertPath: "/path/to/orderer-client.crt",
91+
ClientKeyPath: "/path/to/orderer-client.key",
92+
},
93+
},
94+
Channel: "mychannel",
95+
},
96+
Queries: config.QueriesConfig{
97+
EndpointServiceConfig: config.EndpointServiceConfig{
98+
Address: "localhost:7001",
99+
ConnectionTimeout: 30 * time.Second,
100+
TLS: &config.TLSConfig{
101+
Enabled: boolPtr(true),
102+
RootCertPaths: []string{"/path/to/peer-ca.crt"},
103+
},
104+
},
105+
},
106+
Notifications: config.NotificationsConfig{
107+
EndpointServiceConfig: config.EndpointServiceConfig{
108+
Address: "localhost:7001",
109+
ConnectionTimeout: 30 * time.Second,
110+
TLS: &config.TLSConfig{
111+
Enabled: boolPtr(false),
112+
},
113+
},
114+
WaitingTimeout: 30 * time.Second,
115+
},
116+
},
117+
Printer: cliio.NewCLIPrinter(&outBuf, &outBuf, cliio.FormatTable),
118+
}
119+
120+
cmd := NewInfoCommand(ctx)
121+
err := cmd.Flags().Set("format", "env")
122+
require.NoError(t, err)
123+
124+
err = cmd.RunE(cmd, nil)
125+
require.NoError(t, err)
126+
127+
output := outBuf.String()
128+
require.Contains(t, output, "FXCONFIG_LOGGING_LEVEL=ERROR")
129+
require.Contains(t, output, "FXCONFIG_MSP_LOCALMSPID=Org1MSP")
130+
require.Contains(t, output, "FXCONFIG_MSP_CONFIGPATH=/path/to/msp")
131+
require.Contains(t, output, "FXCONFIG_TLS_ENABLED=true")
132+
require.Contains(t, output, "FXCONFIG_TLS_CLIENTKEY=/path/to/client.key")
133+
require.Contains(t, output, "FXCONFIG_TLS_CLIENTCERT=/path/to/client.crt")
134+
require.Contains(t, output, "FXCONFIG_TLS_ROOTCERTS=/path/to/ca.crt")
135+
require.Contains(t, output, "FXCONFIG_ORDERER_ADDRESS=localhost:7050")
136+
require.Contains(t, output, "FXCONFIG_ORDERER_CHANNEL=mychannel")
137+
require.Contains(t, output, "FXCONFIG_ORDERER_CONNECTIONTIMEOUT=30s")
138+
require.Contains(t, output, "FXCONFIG_QUERIES_ADDRESS=localhost:7001")
139+
require.Contains(t, output, "FXCONFIG_NOTIFICATIONS_ADDRESS=localhost:7001")
140+
}
141+
142+
func TestInfoCommand_PrintsEnvConfig_MultipleCerts(t *testing.T) {
143+
t.Parallel()
144+
145+
boolPtr := func(b bool) *bool { return &b }
146+
147+
var outBuf bytes.Buffer
148+
ctx := &CLIContext{
149+
Config: &config.Config{
150+
TLS: config.TLSConfig{
151+
Enabled: boolPtr(true),
152+
ClientKeyPath: "/path/to/client.key",
153+
ClientCertPath: "/path/to/client.crt",
154+
RootCertPaths: []string{"/path/to/ca1.crt", "/path/to/ca2.crt"},
155+
},
156+
},
157+
Printer: cliio.NewCLIPrinter(&outBuf, &outBuf, cliio.FormatTable),
158+
}
159+
160+
cmd := NewInfoCommand(ctx)
161+
err := cmd.Flags().Set("format", "env")
162+
require.NoError(t, err)
163+
164+
err = cmd.RunE(cmd, nil)
165+
require.NoError(t, err)
166+
167+
output := outBuf.String()
168+
require.Contains(t, output, "FXCONFIG_TLS_ROOTCERTS=/path/to/ca1.crt,/path/to/ca2.crt")
169+
}
170+
171+
func TestInfoCommand_InvalidFormat(t *testing.T) {
172+
t.Parallel()
173+
174+
var outBuf bytes.Buffer
175+
ctx := &CLIContext{
176+
Config: &config.Config{},
177+
Printer: cliio.NewCLIPrinter(&outBuf, &outBuf, cliio.FormatTable),
178+
}
179+
180+
cmd := NewInfoCommand(ctx)
181+
err := cmd.Flags().Set("format", "json")
182+
require.NoError(t, err)
183+
184+
err = cmd.RunE(cmd, nil)
185+
require.Error(t, err)
186+
require.Contains(t, err.Error(), "invalid --format: json (want yaml|env)")
187+
}

0 commit comments

Comments
 (0)