-
Notifications
You must be signed in to change notification settings - Fork 101
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
abstract action creation, refactor proxy, add proxy tests #308
base: main
Are you sure you want to change the base?
abstract action creation, refactor proxy, add proxy tests #308
Conversation
5553478
to
7487e17
Compare
ac3073b
to
e4c6c28
Compare
ea5517a
to
bce1ebc
Compare
bce1ebc
to
de1896a
Compare
if len(args) == 0 { | ||
return errors.New("missing COMMAND") | ||
} | ||
|
||
info, err := checkReadiness(args[0], car != nil && car.PrivateKeyAvailable(), acct != nil, car != 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.
Shouldn't re-use the same symbol for a package and a variable. This would break if there's a conflict in exported names/methods.
On the other hand, I agree that car
isn't ideal here. I'd suggest just using v
instead; this shorthand for a locally-scoped variable is pretty common in Go.
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.
Agreed with the non-awesomeness of shadowing the package with variables, but nothing will "break" though. Once shadowed, you just won't be able to use the underlying package while shadowed. For example:
https://go.dev/play/p/7VhEqRIMXYa
The main downside of shadowing is that you don't know what's what without really paying attention, and that can lead to confusion when reading the code.
I would recommend against v
as a var name; this is common for receivers, but for one of many params in funcs can have have sizable scopes, it'd be better to have slightly more meaningful names.
In this particular case, adding an import alias like veh
or vehicl
or pvehicle
or vehiclep
or vehipkg
or somethingthatsnotvehicle
might work better, because it'll be very obvious that vehicle veh.Vehicle
means veh
is a package, and then the rest of the code reads nicely with vehicle
as a subject to everything.
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.
Actually I think I'd just keep car
in here.
@@ -247,40 +265,40 @@ var commands = map[string]*Command{ | |||
help: "Unlock vehicle", | |||
requiresAuth: true, | |||
requiresFleetAPI: false, | |||
handler: func(ctx context.Context, acct *account.Account, car *vehicle.Vehicle, args map[string]string) error { | |||
return car.Unlock(ctx) | |||
handler: func(ctx context.Context, acct *account.Account, vehicle *vehicle.Vehicle, args map[string]string) 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.
The API should be designed for the consumer, not the developer or the tester, and this API is cumbersome for the consumer.
If we want to abstract the creation of the protobufs, I'd suggest we wrap the code on the other direction:
func (v *Vehicle) Unlock(ctx context.Context) error {
return v.executeAction(ctx, action.Unlock())
}
If the motive here is to improve testability, then this could also be accomplished by implementing a test connector.Connector
that functions similarly to net/http/httptest
in that it records requests/responses and you can verify they are as expected.
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 with focusing on consumer vs. author for the API design.
What we lose with moving to this ExecuteAction
model is that we don't have an easy way to know which actions are available to us, especially because ExecuteAction
takes a loose interface{}
type which prevents tools from inferring this statically.
Couple of thoughts:
- Moving the actions out of
Vehicle
and into anExecuteAction
has the advantage of clearly separating the domains of what we can do with a vehicle (Connect
,Disconnect
,AddKey
, etc) from all the actions. But we'd needvehicle
(the package) to specify anAction
interface to make it easy to statically understand what can be passed in. The implementation details of CarServer vs VSSEC should be kept hidden just like they were before. - If we keep all the actions straight on
Vehicle
(like Seth suggests), we can test either by having a recorder (like Seth suggests), and/or having an action provider with a big interface that would be swappable for tests. The difference here is that it wouldn't clutter the API from a consumer point of view, and would make things testable a bit more directly than with a recorder. (We could also have the privateexecuteAction
be a swappable func for tests).
// The action can be a *carserver.Action_VehicleAction or a *vcsec.UnsignedMessage. | ||
// Actions are created using the action package. | ||
func (v *Vehicle) ExecuteAction(ctx context.Context, action interface{}) error { | ||
switch action := action.(type) { |
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.
Nice
_, err := v.getCarServerResponse(ctx, action) | ||
return err | ||
case *vcsec.UnsignedMessage: | ||
encodedPayload, err := proto.Marshal(action) |
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: Mostly for aesthetic reasons, it'd be nice if getCarServerResponse
and getVCSECResult
had the same signature.
}, nil | ||
// Vehicles must have the public part of skey enrolled on their keychains. | ||
// (This is a command-authentication key, not a TLS key.) | ||
func New(ctx context.Context, skey protocol.ECDHPrivateKey, cacheSize int, accountProvider AccountProvider) *Proxy { |
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.
Consider returning an (always nil) error for the sake of API stability. This would also allow API stability going forward of the implementation changes in a way that re-introduces the possibility of failing.
Expect(action).ToNot(BeNil()) | ||
Expect(action.SubMessage).ToNot(BeNil()) | ||
Expect(action.SubMessage).To(BeAssignableToTypeOf(&vcsec.UnsignedMessage_RKEAction{})) | ||
Expect(action.SubMessage.(*vcsec.UnsignedMessage_RKEAction).RKEAction).To(Equal(vcsec.RKEAction_E_RKE_ACTION_AUTO_SECURE_VEHICLE)) |
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 Golang protobuf code checks for nil receivers and returns default values, so I think you could just do:
action := action.AutoSecureVehicle()
Expect(action.GetRKEAction()).To(Equal(vcsec.RKEAction_E_RKE_ACTION_AUTO_SECURE_VEHICLE))
Note that this wouldn't work for RKEAction_E_RKE_ACTION_UNLOCK
, since the protobuf definition breaks the convention of reserving the 0 (default) value for UNKNOWN.
Copy/Paste this comment a bunch of times below.
@@ -0,0 +1,164 @@ | |||
// Code generated by MockGen. DO NOT EDIT. |
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 know Go makes it hard to avoid including autogenerated code, but... any way to avoid this without breaking go test
out of the box?
@@ -22,5 +22,11 @@ jobs: | |||
make format | |||
git diff --exit-code | |||
|
|||
- name: Mocks up to date | |||
run: | | |||
go install go.uber.org/mock/mockgen@latest |
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 we peg this to the version in our go.mod
instead of latest
?
case "reset_valet_pin": | ||
return func(v *vehicle.Vehicle) error { return v.ResetValetPin(ctx) }, nil | ||
return action.ResetValetPin(), 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.
Tangent: maybe Pin
-> PIN
to be consistent here.
} | ||
|
||
func (p *Proxy) markUnsupportedVIN(vin string) { | ||
func (p *Proxy) markSignedCommandsUnsupportedVIN(vin 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 the VIN
suffix? (The other one doesn't have it)
p.unsupported.Store(vin, true) | ||
} | ||
|
||
func (p *Proxy) isNotSupported(vin string) bool { | ||
func (p *Proxy) signedCommandUnsupported(vin string) bool { |
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.
Previous one says Commands
, this one says Command
, but they're dealing with the same thing.
proxyReq.Header.Set(xff, strings.Join(previous, ", ")) | ||
} | ||
proxyReq.URL.Host = host | ||
acct := req.Context().Value(accountContext).(Account) |
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.
Might be good to check the type assertion here.
return p.accountProvider(token, proxyProtocolVersion) | ||
} | ||
|
||
func writeResponseError(w http.ResponseWriter, code 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.
writeErrorResponse
?
@@ -0,0 +1,2 @@ | |||
// Package action contains functions for creating actions to send to vehicles. | |||
package action |
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.
Note: if kept, we could consider having action
live under vehicle
(so pkg/vehicle/action
) (an idea).
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.
...or internal/action
? What's the value in exporting? Plus, assigning generic names like "action" to exported packages is frowned on.
"fmt" | ||
"time" | ||
|
||
carserver "github.com/teslamotors/vehicle-command/pkg/protocol/protobuf/carserver" |
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.
Note: we should remove all these redundant carserver
aliases.
if len(args) == 0 { | ||
return errors.New("missing COMMAND") | ||
} | ||
|
||
info, err := checkReadiness(args[0], car != nil && car.PrivateKeyAvailable(), acct != nil, car != 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.
Actually I think I'd just keep car
in here.
@@ -247,40 +265,40 @@ var commands = map[string]*Command{ | |||
help: "Unlock vehicle", | |||
requiresAuth: true, | |||
requiresFleetAPI: false, | |||
handler: func(ctx context.Context, acct *account.Account, car *vehicle.Vehicle, args map[string]string) error { | |||
return car.Unlock(ctx) | |||
handler: func(ctx context.Context, acct *account.Account, vehicle *vehicle.Vehicle, args map[string]string) 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 agree with focusing on consumer vs. author for the API design.
What we lose with moving to this ExecuteAction
model is that we don't have an easy way to know which actions are available to us, especially because ExecuteAction
takes a loose interface{}
type which prevents tools from inferring this statically.
Couple of thoughts:
- Moving the actions out of
Vehicle
and into anExecuteAction
has the advantage of clearly separating the domains of what we can do with a vehicle (Connect
,Disconnect
,AddKey
, etc) from all the actions. But we'd needvehicle
(the package) to specify anAction
interface to make it easy to statically understand what can be passed in. The implementation details of CarServer vs VSSEC should be kept hidden just like they were before. - If we keep all the actions straight on
Vehicle
(like Seth suggests), we can test either by having a recorder (like Seth suggests), and/or having an action provider with a big interface that would be swappable for tests. The difference here is that it wouldn't clutter the API from a consumer point of view, and would make things testable a bit more directly than with a recorder. (We could also have the privateexecuteAction
be a swappable func for tests).
// | ||
// The action can be a *carserver.Action_VehicleAction or a *vcsec.UnsignedMessage. | ||
// Actions are created using the action package. | ||
func (v *Vehicle) ExecuteAction(ctx context.Context, action interface{}) 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.
If we keep this model, I'd vote for Execute(context.Context, Action)
rather than ExecuteAction(context.Context, interface{})
.
Description
Lots of refactoring with the overarching goal being to add unit tests for the HTTP proxy.
vehicle
. Now, a vehicle has two methods for actions:ExecuteAction
andExecuteUnsignedAction
.>= 1.22
, use the routing enhancements.Type of change
Please select all options that apply to this change:
Checklist:
Confirm you have completed the following steps: