-
Notifications
You must be signed in to change notification settings - Fork 8.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[FAB-18196] Add Channel Participation CLI #1907
Conversation
42c0dd7
to
b97f04d
Compare
@stephyee there is no unit tests in this PR, could you add some to make sure the new code proceeds as expected? |
cmd/participation/remove.go
Outdated
|
||
if resp.StatusCode != http.StatusNoContent { | ||
panic(fmt.Sprintf("Removal of channel %s failed with status %d", channelID, resp.StatusCode)) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In case of a failed request that returns an error status code, the response contains an informative error message. It is better to print it as well. This relates to all commands. See
https://docs.google.com/document/d/1kYoicS_Z59gNvmuuB7VFZ6WsOL31C34dWMeOFQiIygA/edit?usp=sharing
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Refactored the code. We now print the status and error generated by the server for all commands.
cmd/participation/join.go
Outdated
bodyBytes, err := ioutil.ReadAll(resp.Body) | ||
if err != nil { | ||
panic(err) | ||
} | ||
resp.Body.Close() | ||
|
||
err = printResponseAsJSON(bodyBytes, &channelInfo{}, os.Stdout) | ||
if err != nil { | ||
panic(err) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about checking the status to detect a failed request, and printing the body if it is a failure?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Refactored. Done.
cmd/participation/list.go
Outdated
bodyBytes, err := getBodyBytes(client, url) | ||
if err != nil { | ||
panic(err) | ||
} | ||
|
||
err = printResponseAsJSON(bodyBytes, &channelList{}, os.Stdout) | ||
if err != nil { | ||
panic(err) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about checking the status to detect a failed request, and printing the body if it is a failure?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Refactored. Done.
cmd/participation/list.go
Outdated
if bodyBytes == nil { | ||
fmt.Printf("channel '%s' not found\n", channelID) | ||
return | ||
} | ||
|
||
err = printResponseAsJSON(bodyBytes, &channelInfo{}, os.Stdout) | ||
if err != nil { | ||
panic(err) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about checking the status to detect a failed request, and printing the body if it is a failure?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Refactored. Done.
cmd/participation/main.go
Outdated
if resp.StatusCode != http.StatusOK { | ||
return nil, nil | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In case of a failed request that returns an error status code, the response contains an informative error message. It is better to print it as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Refactored. Done.
cmd/participation/list.go
Outdated
if bodyBytes == nil { | ||
fmt.Printf("channel '%s' not found\n", channelID) | ||
return | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is a specific error code that captures this. I think it is better to follow these.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Refactored. Done.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like the function is here, but I think it could use some refactoring, and definitely use some tests. I've added some comments of my own in addition to the good set from Yoav.
cmd/participation/main.go
Outdated
} | ||
|
||
func main() { | ||
var osn, channelID, tlsDir, configBlockPath string |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a fairly small CLI, so perhaps it's not worth the effort, but I think the testability could be increased considerably by:
- Defining some sort of argument structure, containing the things that the flags may set.
- Parsing arguments as a []string so that you can test that different command lines stay valid.
- Using a more sophisticated CLI parsing library -- for instance, we use https://godoc.org/gopkg.in/alecthomas/kingpin.v2 in some places, and by specifying the right flag types, you get things like checking that a directory exists, or that a file exists and giving you an open stream to it. Common things that you won't need to write, or test, yourself.
cmd/participation/join.go
Outdated
func join(osn, tlsDir, channelID, configBlockPath string) { | ||
blockBytes, err := ioutil.ReadFile(configBlockPath) | ||
if err != nil { | ||
panic(err) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A general comment, why so many panics here? Why not simply return the error.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Refactored. Done.
cmd/participation/join.go
Outdated
} | ||
resp.Body.Close() | ||
|
||
err = printResponseAsJSON(bodyBytes, &channelInfo{}, os.Stdout) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In general, it's a little odd to have the console printing pushed out into the leaf of the calls. I'd generally expect for a response and error to be propagated back up to the top, where, uniform decisions can be made about how to report success and failure, the output format, etc.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Refactored. Done.
cmd/participation/main.go
Outdated
|
||
joinCmd := flag.NewFlagSet("join", flag.ExitOnError) | ||
joinCmd.StringVar(&osn, "orderer", "", "Ordering service endpoint") | ||
joinCmd.StringVar(&tlsDir, "tlsDir", "", "Path to directory containing TLS materials") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The contents of this directory should be validated before we proceed with the invocations. Going back to a previous comment, we should do all of our parsing and setup early and in a consolidated place.
cmd/participation/main.go
Outdated
return bodyBytes, nil | ||
} | ||
|
||
func printResponseAsJSON(bodyBytes []byte, response interface{}, out io.Writer) error { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not the end of the world, but we have a JSON string, and we're turning it back into a map structure, then turning it back into JSON. You could instead use vanilla indent https://golang.org/pkg/encoding/json/#Indent
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Refactored. Done.
I did a pretty extensive refactoring of the code here. Still need to address the comments around flags and directory validation as well as add unit tests. More to come shortly. |
cmd/participation/main.go
Outdated
func printResponse(resp *http.Response, out io.Writer) { | ||
bodyBytes, err := readBodyBytes(resp.Body) | ||
if err != nil { | ||
log.Fatalf("failed to read http response body: %s", err) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure this is the "correct" thing to do here but for now I figured if we can't read the response body something has gone very wrong.
64e70e0
to
c6cd398
Compare
/azp run |
Azure Pipelines successfully started running 1 pipeline(s). |
8b0bd3e
to
42fa171
Compare
I've updated this CLI to use kingpin and added some unit tests around flag parsing. I think more unit tests are probably need but I'm a little confused on how to actually implement them. I don't see an obvious example to base my tests on. Should I create an http endpoint with a fake channel participation API that sends back faked responses or is that beyond the scope of UT here? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd really like to understand why we are doing this? I apologize if I missed this as part of the RFC, but we really do not need another standalone CLI.
The beauty of REST APIs is that there are tons of code generators for virtually any language which can generate a client from Swagger.
I don't think we should merge this.
Discussed with a few other maintainers
b2bab1b
to
7da6842
Compare
Added a fairly comprehensive set of unit tests now, which also helped me notice that we weren't supporting the various TLS configurations. This CLI now supports TLS, mutual TLS, and TLS disabled. I also moved away from the TLS directory flag with expected file names (an artifact of the test code we used as a basis for the http client code) to a "--caFile" flag (when the server is using TLS) and "--clientCert" + "--clientKey" flags (when the server is using mutual TLS). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Generally looking pretty good -- test coverage looks nice.
I do think with a (small) rework of the main, you could get rid of all the forking, and session reading from your test. And, since the Config struct is not a parameter anywhere outside of the main, it doesn't really need to be exported anymore (or exist).
The biggest thing though that I caught on this pass, is that as written, this CLI is incompatible with intermediate CAs. I see this as a must-fix, although it should be pretty easy to do. Assuming we go the route of allowing the user to concatenate PEMs into a single file, the golang x509 CertPool will happily parse it with no additional work. Then it's just a matter of wiring the cert pool through.
cmd/osnadmin/main.go
Outdated
) | ||
|
||
// command line flags | ||
var ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All but one of these variables is only referenced inside of configFromFlags()
. Only the app
is accessed from main()
.
So, why not simply do the following:
func executeForArgs(args []string) (text string, exit int, err error) {
app := kingping.New(...)
...
cmd, err := app.Parse(args)
// handle err
// handle cmd using the above vars directly.
// make sure you only return an error for argument processing errors
// otherwise simply return the desired output text and code
// if the text could be large, we could consider a reader instead of a string
// but we're already using ioutil.ReadAll generally, so it's equivalent.
}
func main() {
text, exit, err := executeForArgs(os.Args[1:])
if err != nil {
kingpin.Fatalf("Error parsing arguments, %s, try --help", err)
}
fmt.Print(text)
os.Exit(exit)
}
Now, you've ditched all of your package level state, and you can test that command parsing works as expected by passing in a slice of args, rather than having to try to invoke main().
I know I pushed you down the "Config struct" route, but, since you've decided not to make the Config struct a parameter to your functions, its value as an exported entity doesn't really make sense. All it's really serving to do is to add some unnecessary indirection.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Excellent idea. Done.
cmd/osnadmin/main.go
Outdated
var ( | ||
app = kingpin.New("osnadmin", "Orderer Service Node (OSN) administration") | ||
orderer = app.Flag("orderer", "Endpoint of the OSN").String() | ||
caFile = app.Flag("caFile", "Path to the PEM-encoded orderer root CA certificate").String() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why make this a string? Why not an ExistingFile()
Same goes for the other files here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Still need to look into this...
cmd/osnadmin/main.go
Outdated
var ( | ||
app = kingpin.New("osnadmin", "Orderer Service Node (OSN) administration") | ||
orderer = app.Flag("orderer", "Endpoint of the OSN").String() | ||
caFile = app.Flag("caFile", "Path to the PEM-encoded orderer root CA certificate").String() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Help text should probably not say "the root CA", we either need to allow parsing a single pem with lots of certs (we do this some places, not all), or provide a directory of certs. Also, we need to support intermediate certs as well (which is mostly for free if you parse as a cert pool).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Still need to look into this...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
cmd/osnadmin/main_test.go
Outdated
"--orderer", urlString, | ||
"--caFile", ordererCACert, | ||
} | ||
sess := runCommand(args, 0) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you convert the main to allow execution from a slice, then you could eliminate this 'runCommand', and instead pass in the slice, and test the results directly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
internal/osnadmin/httpclient.go
Outdated
) | ||
|
||
func httpClient(tlsClientCert *tls.Certificate, tlsCACert *x509.Certificate) *http.Client { | ||
clientCertPool := x509.NewCertPool() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I really think we need to be passing the cert pool in as the parameter, not the certificate, all the way from the very top. Then we can parse a multi-block pem file and allow users to concatenate CAs into it as needed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Still need to look into this...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
ordererCACert = "not-the-ca-cert-youre-looking-for" | ||
}) | ||
|
||
It("returns with exit code 1 and prints the error", func() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You could probably eliminate most of these test cases by using ExistingFile()
from kingpin.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Still need to look into this...
cmd/osnadmin/main_test.go
Outdated
return sess | ||
} | ||
|
||
func checkOutput(sess *gexec.Session, expectedStatus int, expectedOutput interface{}) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This gets to turn into a vanilla Equals() too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
Expect(err).NotTo(HaveOccurred()) | ||
} | ||
|
||
func blockWithGroups(groups map[string]*cb.ConfigGroup, channelID string) *cb.Block { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's unfortunate how much knowledge of the participation internal workings bleeds into this test, but I understand that this may be the best option.
2845604
to
0bbc9b4
Compare
Adds CLI commands for the list, join, and remove using the new channel participation API. FAB-18196 #done Signed-off-by: Will Lahti <wtlahti@us.ibm.com> Signed-off-by: Tiffany Harris <tiffany.harris@ibm.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good to me!
Type of change
Description
Adds CLI commands for the list, join, and remove channel participation APIs.
Related issues
FAB-18196