Skip to content

Commit 96adc85

Browse files
committed
[FAB-10458] Common CLI infrastructure
This change set adds common CLI infrastructure, to be used by the discovery CLI in v1.2, and hopefully other commands in v1.3 This CLI infrastructure comes with a flexible support for adding commands, persisting configuration to a file, loading configuration from a file, and creating a signer and communication client objects. Change-Id: I18c33f86f1e75e7cd04789c2ede42ddc89be706d Signed-off-by: yacovm <yacovm@il.ibm.com>
1 parent 9fca2ae commit 96adc85

19 files changed

+905
-0
lines changed

cmd/common/cli.go

+157
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
/*
2+
Copyright IBM Corp. All Rights Reserved.
3+
4+
SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
package common
8+
9+
import (
10+
"fmt"
11+
"os"
12+
"path/filepath"
13+
14+
"io"
15+
16+
"github.com/hyperledger/fabric/cmd/common/comm"
17+
"github.com/hyperledger/fabric/cmd/common/signer"
18+
"gopkg.in/alecthomas/kingpin.v2"
19+
)
20+
21+
const (
22+
saveConfigCommand = "saveConfig"
23+
)
24+
25+
var (
26+
// Function used to terminate the CLI
27+
terminate = os.Exit
28+
// Function used to obtain the stdout
29+
outWriter io.Writer = os.Stdout
30+
31+
// CLI arguments
32+
mspID *string
33+
tlsCA, tlsCert, tlsKey, userKey, userCert **os.File
34+
configFile *string
35+
)
36+
37+
// CLICommand defines a command that is added to the CLI
38+
// via an external consumer.
39+
type CLICommand func(Config) error
40+
41+
// CLI defines a command line interpreter
42+
type CLI struct {
43+
app *kingpin.Application
44+
dispatchers map[string]CLICommand
45+
}
46+
47+
// NewCLI creates a new CLI with the given name and help message
48+
func NewCLI(name, help string) *CLI {
49+
return &CLI{
50+
app: kingpin.New(name, help),
51+
dispatchers: make(map[string]CLICommand),
52+
}
53+
}
54+
55+
// Command adds a new top-level command to the CLI
56+
func (cli *CLI) Command(name, help string, onCommand CLICommand) *kingpin.CmdClause {
57+
cmd := cli.app.Command(name, help)
58+
cli.dispatchers[name] = onCommand
59+
return cmd
60+
}
61+
62+
// Run makes the CLI process the arguments and executes the command(s) with the flag(s)
63+
func (cli *CLI) Run(args []string) {
64+
configFile = cli.app.Flag("configFile", "Specifies the config file to load the configuration from").String()
65+
persist := cli.app.Command(saveConfigCommand, fmt.Sprintf("Save the config passed by flags into the file specified by --configFile"))
66+
configureFlags(cli.app)
67+
68+
command := kingpin.MustParse(cli.app.Parse(args))
69+
if command == persist.FullCommand() {
70+
if *configFile == "" {
71+
out("--configFile must be used to specify the configuration file")
72+
return
73+
}
74+
persistConfig(parseFlagsToConfig(), *configFile)
75+
return
76+
}
77+
78+
var conf Config
79+
if *configFile == "" {
80+
conf = parseFlagsToConfig()
81+
} else {
82+
conf = loadConfig(*configFile)
83+
}
84+
85+
f, exists := cli.dispatchers[command]
86+
if !exists {
87+
out("Unknown command:", command)
88+
terminate(1)
89+
return
90+
}
91+
err := f(conf)
92+
if err != nil {
93+
out(err)
94+
}
95+
}
96+
97+
func configureFlags(persistCommand *kingpin.Application) {
98+
// TLS flags
99+
tlsCA = persistCommand.Flag("peerTLSCA", "Sets the TLS CA certificate file path that verifies the TLS peer's certificate").File()
100+
tlsCert = persistCommand.Flag("tlsCert", "(Optional) Sets the client TLS certificate file path that is used when the peer enforces client authentication").File()
101+
tlsKey = persistCommand.Flag("tlsKey", "(Optional) Sets the client TLS key file path that is used when the peer enforces client authentication").File()
102+
// Enrollment flags
103+
userKey = persistCommand.Flag("userKey", "Sets the user's key file path that is used to sign messages sent to the peer").File()
104+
userCert = persistCommand.Flag("userCert", "Sets the user's certificate file path that is used to authenticate the messages sent to the peer").File()
105+
mspID = persistCommand.Flag("MSP", "Sets the MSP ID of the user, which represents the CA(s) that issued its user certificate").String()
106+
}
107+
108+
func persistConfig(conf Config, file string) {
109+
if err := conf.ToFile(file); err != nil {
110+
out("Failed persisting configuration:", err)
111+
terminate(1)
112+
}
113+
}
114+
115+
func loadConfig(file string) Config {
116+
conf, err := ConfigFromFile(file)
117+
if err != nil {
118+
out("Failed loading config", err)
119+
terminate(1)
120+
return Config{}
121+
}
122+
return conf
123+
}
124+
125+
func parseFlagsToConfig() Config {
126+
conf := Config{
127+
SignerConfig: signer.Config{
128+
MSPID: *mspID,
129+
IdentityPath: evaluateFileFlag(userCert),
130+
KeyPath: evaluateFileFlag(userKey),
131+
},
132+
TLSConfig: comm.Config{
133+
KeyPath: evaluateFileFlag(tlsKey),
134+
CertPath: evaluateFileFlag(tlsCert),
135+
PeerCACertPath: evaluateFileFlag(tlsCA),
136+
},
137+
}
138+
return conf
139+
}
140+
141+
func evaluateFileFlag(f **os.File) string {
142+
if f == nil {
143+
return ""
144+
}
145+
if *f == nil {
146+
return ""
147+
}
148+
path, err := filepath.Abs((*f).Name())
149+
if err != nil {
150+
out("Failed listing", (*f).Name(), ":", err)
151+
terminate(1)
152+
}
153+
return path
154+
}
155+
func out(a ...interface{}) {
156+
fmt.Fprintln(outWriter, a)
157+
}

cmd/common/cli_test.go

+108
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/*
2+
Copyright IBM Corp. All Rights Reserved.
3+
4+
SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
package common
8+
9+
import (
10+
"bytes"
11+
"fmt"
12+
"path/filepath"
13+
"testing"
14+
15+
"math/rand"
16+
"os"
17+
18+
"github.com/hyperledger/fabric/cmd/common/signer"
19+
"github.com/stretchr/testify/assert"
20+
)
21+
22+
func TestCLI(t *testing.T) {
23+
var testCmdInvoked bool
24+
var exited bool
25+
// Overwrite exit
26+
terminate = func(_ int) {
27+
exited = true
28+
}
29+
// Overwrite stdout with writing to this buffer
30+
testBuff := &bytes.Buffer{}
31+
outWriter = testBuff
32+
33+
cli := NewCLI("cli", "cli help")
34+
testCommand := func(conf Config) error {
35+
// If we exited, the command wasn't executed
36+
if exited {
37+
return nil
38+
}
39+
// Else, the command was executed - so ensure it was executed
40+
// with the expected config
41+
assert.Equal(t, Config{
42+
SignerConfig: signer.Config{
43+
MSPID: "SampleOrg",
44+
KeyPath: "key.pem",
45+
IdentityPath: "cert.pem",
46+
},
47+
}, conf)
48+
testCmdInvoked = true
49+
return nil
50+
}
51+
cli.Command("test", "test help", testCommand)
52+
53+
t.Run("Loading a non existent config", func(t *testing.T) {
54+
defer testBuff.Reset()
55+
// Overwrite user home directory with testdata
56+
dir := filepath.Join("testdata", "non_existent_config")
57+
cli.Run([]string{"test", "--configFile", filepath.Join(dir, "config.yaml")})
58+
assert.Contains(t, testBuff.String(), fmt.Sprint("Failed loading config open ", dir))
59+
assert.Contains(t, testBuff.String(), "config.yaml: no such file or directory")
60+
assert.True(t, exited)
61+
})
62+
63+
t.Run("Loading a valid config", func(t *testing.T) {
64+
defer testBuff.Reset()
65+
testCmdInvoked = false
66+
exited = false
67+
// Overwrite user home directory with testdata
68+
dir := filepath.Join("testdata", "valid_config")
69+
// Ensure that a valid config results in running our command
70+
cli.Run([]string{"test", "--configFile", filepath.Join(dir, "config.yaml")})
71+
assert.True(t, testCmdInvoked)
72+
})
73+
74+
t.Run("Saving a config", func(t *testing.T) {
75+
defer testBuff.Reset()
76+
testCmdInvoked = false
77+
exited = false
78+
dir := filepath.Join(os.TempDir(), fmt.Sprintf("config%d", rand.Int()))
79+
os.Mkdir(dir, 0700)
80+
defer os.RemoveAll(dir)
81+
82+
userCert := filepath.Join(dir, "cert.pem")
83+
userKey := filepath.Join(dir, "key.pem")
84+
userCertFlag := fmt.Sprintf("--userCert=%s", userCert)
85+
userKeyFlag := fmt.Sprintf("--userKey=%s", userKey)
86+
os.Create(userCert)
87+
os.Create(userKey)
88+
89+
cli.Run([]string{saveConfigCommand, "--MSP=SampleOrg", userCertFlag, userKeyFlag})
90+
assert.Contains(t, testBuff.String(), "--configFile must be used to specify the configuration file")
91+
testBuff.Reset()
92+
// Persist the config
93+
cli.Run([]string{saveConfigCommand, "--MSP=SampleOrg", userCertFlag, userKeyFlag, "--configFile", filepath.Join(dir, "config.yaml")})
94+
95+
// Run a different command and ensure the config was successfully persisted
96+
cli.Command("assert", "", func(conf Config) error {
97+
assert.Equal(t, Config{
98+
SignerConfig: signer.Config{
99+
MSPID: "SampleOrg",
100+
KeyPath: userKey,
101+
IdentityPath: userCert,
102+
},
103+
}, conf)
104+
return nil
105+
})
106+
cli.Run([]string{"assert", "--configFile", filepath.Join(dir, "config.yaml")})
107+
})
108+
}

cmd/common/comm/client.go

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
Copyright IBM Corp. All Rights Reserved.
3+
4+
SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
package comm
8+
9+
import (
10+
"time"
11+
12+
"github.com/hyperledger/fabric/common/crypto/tlsgen"
13+
"github.com/hyperledger/fabric/common/util"
14+
"github.com/hyperledger/fabric/core/comm"
15+
"github.com/pkg/errors"
16+
"google.golang.org/grpc"
17+
)
18+
19+
const defaultTimeout = time.Second * 5
20+
21+
// Client deals with TLS connections
22+
// to the discovery server
23+
type Client struct {
24+
TLSCertHash []byte
25+
*comm.GRPCClient
26+
}
27+
28+
// NewClient creates a new comm client out of the given configuration
29+
func NewClient(conf Config) (*Client, error) {
30+
if conf.Timeout == time.Duration(0) {
31+
conf.Timeout = defaultTimeout
32+
}
33+
sop, err := conf.ToSecureOptions(newSelfSignedTLSCert)
34+
if err != nil {
35+
return nil, errors.WithStack(err)
36+
}
37+
cl, err := comm.NewGRPCClient(comm.ClientConfig{
38+
SecOpts: sop,
39+
Timeout: conf.Timeout,
40+
})
41+
if err != nil {
42+
return nil, err
43+
}
44+
return &Client{GRPCClient: cl, TLSCertHash: util.ComputeSHA256(sop.Certificate)}, nil
45+
}
46+
47+
// NewDialer creates a new dialer from the given endpoint
48+
func (c *Client) NewDialer(endpoint string) func() (*grpc.ClientConn, error) {
49+
return func() (*grpc.ClientConn, error) {
50+
conn, err := c.NewConnection(endpoint, "")
51+
if err != nil {
52+
return nil, errors.WithStack(err)
53+
}
54+
return conn, nil
55+
}
56+
}
57+
58+
func newSelfSignedTLSCert() (*tlsgen.CertKeyPair, error) {
59+
ca, err := tlsgen.NewCA()
60+
if err != nil {
61+
return nil, err
62+
}
63+
return ca.NewClientCertKeyPair()
64+
}

0 commit comments

Comments
 (0)