-
Notifications
You must be signed in to change notification settings - Fork 0
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
Cli #1
base: develop
Are you sure you want to change the base?
Cli #1
Conversation
added: CommandParser and associated tests
added: common package for logging
fixed: send PrintUsage() output from flag package to custom logger for cleaner unit test output
added: testing for the ClientService
@rm -rf ./test/common/mocks/* | ||
|
||
deps: | ||
@echo syncing dependencies... |
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 would generally recommend to vendor the dependencies, creates less risk that upstream changes will break things.
We generally use dep (https://github.com/golang/dep), but vgo (https://github.com/golang/go/wiki/vgo) is the current hotness, and there are a few other tools that are all good.
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.
Yes, I agree. I have used Govendor in the past, but I wanted to keep it simple for the prototype. I'll check out vgo.
|
||
.PHONY: test | ||
|
||
all: | clean deps gen_protobuf build gen_mocks test |
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.
Nit: missing Phony on most the targets
} | ||
|
||
type GrpcClientService struct { | ||
ctx context.Context |
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.
Nit: the recommendations for using context are not to embed within a struct. So I would either refactor to be within recommendations, or document this as an intended deviation.
Do not store Contexts inside a struct type; instead, pass a Context explicitly to each function that needs it. The Context should be the first parameter, typically named ctx:
https://golang.org/pkg/context/
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 are correct, I'll refactor.
|
||
var enums []string | ||
|
||
func New(str string) Status { |
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.
Since this is basically an Enum implementation being passed over GRPC as a string, why not just define the enum in the protobuf, and use it without conversions? https://developers.google.com/protocol-buffers/docs/proto#enum
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.
That looks like a good improvement. I just used the boilerplate enum code that I've used before here. The smaller amount of space used in the protobuf message payload would be a nice advantage here.
} | ||
|
||
// Read handles filling the buffer from the LogClient | ||
func (lr *LogReader) Read(p []byte) (n int, err 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.
I'm not sure emulating the Read interface is your best bet here. This feels like it could be significantly simpler, if it just emulates / exposes the interface provided by gRPC, or were to just return a string,error
. I'm not sure what storing in an intermediate buffer really buy's 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 think you are right. Returning a string will make this a simpler interface.
lr.buffer.WriteString(resp.LogMessages) | ||
} | ||
|
||
return lr.buffer.Read(p) |
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 will work as intended by the implementation of buffer, but I think it's a bit risky, because it seems to rely on an implementation detail of Buffer.Read.
Based on https://golang.org/pkg/io/#Reader
When Read encounters an error or end-of-file condition after successfully reading n > 0 bytes, it returns the number of bytes read. It may return the (non-nil) error from the same call or return the error (and n == 0) from a subsequent call. An instance of this general case is that a Reader returning a non-zero number of bytes at the end of the input stream may return either err == EOF or err == nil. The next Read should return 0, EOF.
So if this were to get changed to a different implementation due to code maintenance that returns the Read + io.EOF in the same call, this will potentially break the implementation since it will return an io.EOF when there is more to read from the upstream gRPC server.
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.
Good catch! I will change the code to handle that situation correctly.
} | ||
|
||
message StartResponse { | ||
string error = 1; |
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 don't think you need to define an error message on top of the protobuf messages, gRPC will pass the errors at the RPC protocol layer. You don't need to do it this way, but this doc has some good error handling advice: https://jbrandhorst.com/post/grpc-errors/
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 was wondering about that! I will switch it over to use the recommended method of error propagation.
@@ -0,0 +1,41 @@ | |||
|
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 may want to remove binary files from the PR
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.
Will do.
@rm -rf ./test/cli/mocks/* | ||
@rm -rf ./test/common/mocks/* | ||
|
||
deps: |
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 not use golang's standard dependency manager? e.g dep or vgo
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 have used govendor in the past, and simply did it this way because it was faster for getting the prototype going. I am all for dependency management :-)
@go get -u google.golang.org/grpc | ||
@go get -u github.com/golang/protobuf/protoc-gen-go | ||
|
||
build: build_cli |
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.
.PHONY for build targets?
@${GOPATH}/bin/mockery -all -dir pkg/master -output test/master/mocks -case=underscore | ||
@${GOPATH}/bin/mockery -all -dir gen/protobuf/master -output test/master/mocks -case=underscore | ||
|
||
gen_protobuf: |
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 not reproducible - go get
can render various versions and grpc is not pinned.
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.
Can you elaborate? I am not quite following. Are you referring to this line under deps:
go get -u google.golang.org/grpc
@@ -0,0 +1,33 @@ | |||
package main |
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.
We recommend to follow these guidelines for consistency:
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 haven't seen this before. I will give it a read.
@@ -0,0 +1,5 @@ | |||
package common | |||
|
|||
type Closer 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.
io.Closer
is exactly this interface, what's the purpose of this one?
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 was thinking "don't expose third party things in my lib's public API", but I am now realizing this is in the standard library, so it's fine to use io.Closer instead. Will change.
startCommand := flag.NewFlagSet("start", flag.ContinueOnError) | ||
startCommand.SetOutput(cp.logger) | ||
|
||
startImage := startCommand.String("image", "", "The docker image name (Required)") |
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 are libraries like cobra that make it slightly easier to manage required arguments.
@@ -0,0 +1,184 @@ | |||
package cli |
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.
appreciate the split into cli
and models
, this is helpful
type LogReader struct { | ||
logClient master.Master_LogClient | ||
closer io.Closer | ||
buffer *bytes.Buffer |
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.
bytes.Buffer
is not goroutine safe. You will encouter data corruption in case if one go-routine will write while another attempts to read it.
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.
Besides, it's not clear to me what buffering the whole thing in-memory is really buying 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 am going to change this to just Read() string,err
as per your colleague's suggestion and return incremental results as they come from the gRPC client. You are correct, buffering in memory isn't buying me anything.
client, conn, err := cs.clientFactory.CreateClient(host) | ||
|
||
if err != nil { | ||
return logReader, 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.
logReader
is nil at this point, so explicit intent of return nil, err
will be easier for to read.
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 agree.
I have implemented most of the CLI functionality.
There are two main components:
pkg/command_parser - Used to validate command args and call correct method on ClientService
pkg/client_service - Used to integrate with grpc generated client code
The project can be built by
make
, which will:Let's walk through the code and discuss.