Skip to content
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

Open
wants to merge 6 commits into
base: develop
Choose a base branch
from
Open

Cli #1

wants to merge 6 commits into from

Conversation

kmacmcfarlane
Copy link
Owner

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:

  • Download dependencies
  • Generate grpc code
  • Build the cli executable
  • Generate mocks
  • Run automated tests

Let's walk through the code and discuss.

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
@kmacmcfarlane kmacmcfarlane self-assigned this Jan 24, 2019
@rm -rf ./test/common/mocks/*

deps:
@echo syncing dependencies...
Copy link

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.

Copy link
Owner Author

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
Copy link

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
Copy link

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/

Copy link
Owner Author

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 {
Copy link

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

Copy link
Owner Author

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){
Copy link

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?

Copy link
Owner Author

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)
Copy link

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.

Copy link
Owner Author

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;
Copy link

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/

Copy link
Owner Author

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 @@

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

Copy link
Owner Author

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:

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

Copy link
Owner Author

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

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:

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.

Copy link
Owner Author

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

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:

https://github.com/golang/go/wiki/CodeReviewComments

Copy link
Owner Author

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 {

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?

Copy link
Owner Author

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)")

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

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

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.

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.

Copy link
Owner Author

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

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.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants