secrets control-api implementation#1567
Conversation
e373f59 to
a805231
Compare
Current coverage is 54.00% (diff: 52.31%)@@ master #1567 diff @@
==========================================
Files 83 84 +1
Lines 13621 13833 +212
Methods 0 0
Messages 0 0
Branches 0 0
==========================================
+ Hits 7360 7471 +111
- Misses 5265 5358 +93
- Partials 996 1004 +8
|
a805231 to
733f210
Compare
aaronlehmann
left a comment
There was a problem hiding this comment.
Overall LGTM! Just one comment about digest formatting.
manager/controlapi/secret.go
Outdated
| ID: identity.NewID(), | ||
| Spec: *spec, | ||
| SecretSize: uint32(len(spec.Data)), | ||
| Digest: hex.EncodeToString(checksumBytes[:]), |
There was a problem hiding this comment.
I suggest using the github.com/docker/distribution/digest package for this. digest.FromBytes(spec.Data) will give a result like sha256: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855. Including the digest algorithm as a prefix is nice because then we aren't implicitly tied to a particular format going forward. We use this package in a few other places in swarmkit.
There was a problem hiding this comment.
Ah, thanks for the link to that package! I originally had the algorithm in the front, but it made the display of the hash a little longer, so removed it, but agree it's a better idea to include. And thanks for the quick review!
cmd/swarmctl/secrets/inspect.go
Outdated
|
|
||
| var ( | ||
| inspectCmd = &cobra.Command{ | ||
| Use: "inspect <secret ID orname>", |
733f210 to
e53ed9e
Compare
manager/controlapi/secret_test.go
Outdated
| "with:colon", | ||
| "with;semicolon", | ||
| "snowman☃", | ||
| "o_______________________________________________________________o", // exactly 65 characters |
There was a problem hiding this comment.
Aww, I liked the giant frowny face. :) But yes strings.Repeat would make it clearer, thanks.
manager/controlapi/secret.go
Outdated
| // MaxSecretSize is the maximum byte length of the `Secret.Spec.Data` field. | ||
| const MaxSecretSize = 500 * 1024 // 500KB | ||
|
|
||
| var isValidSecretName = regexp.MustCompile(`^[a-zA-Z0-9](?:[a-zA-Z0-9-_.]{0,62}[a-zA-Z0-9])*$`) |
There was a problem hiding this comment.
validSecretNameRegexp.
Describe what this regexp is supposed to do.
Looks like we can structure this in a more simple manner:
^[a-zA-Z0-9]+(?:[-_.]{1}[a-zA-Z0-9]+)*$
Validate total length outside; regexp isn't good at doing length checks. As they change, maintaining the length property may break down.
Also, consider not starting with numbers.
There was a problem hiding this comment.
I don't have any strong feelings about it, but any particular reason to not start with numbers?
There was a problem hiding this comment.
Also, this change would consider multiple underscores/dashes/dots invalid - again any reason for the limitation? (am just curious - again I don't feel strongly; although double symbols ugly for names, not sure if it should be invalid)
There was a problem hiding this comment.
IRL discussion: We just want to make sure the names would work for what we're going to use it for (for example, in case we wanted to be able to use the name for environment variables). Which I think excludes - and . However, if we wanted them to also work for filenames maybe having a . is ok?
Either way, I think we were going with requiring the user give us a target value, and it may make sense to let users be a little more expressive in their names (to make it easier to script naming conventions, etc.) if they need to be able to describe what the secret is.
I don't feel super strongly - I'll leave this as is for now, unless someone has a stronger opinion (and we can also resolve this in a later PR). I have made the variable name change, and updated the length checking to happen in the validation function instead of in the regex.
manager/controlapi/secret.go
Outdated
| return &api.Secret{ | ||
| ID: identity.NewID(), | ||
| Spec: *spec, | ||
| SecretSize: uint32(len(spec.Data)), |
There was a problem hiding this comment.
Please change the type of this field to int64. No need to use unsigned here: we aren't interested in the field-arithmetic properties.
There was a problem hiding this comment.
Just curious - what's wrong with using unsigned in situations like this? I tend to think it makes sense in cases where a negative value would clearly be invalid. In some cases it can avoid bugs that happen when something trying to use the value uses it in an arithmetic way that assumes the value is positive.
There was a problem hiding this comment.
@aaronlehmann @stevvooe explained it as a way to signal invalid values - it's possible to accidentally set a uint to a value that will just overflow without it being an error (https://play.golang.org/p/ItoXPaiHtO), in which case it'd look like you'd have a potentially valid value, whereas if it were an int64 a bounds check would be able to detect that it's negative
| ID: identity.NewID(), | ||
| Spec: *spec, | ||
| SecretSize: uint32(len(spec.Data)), | ||
| Digest: digest.FromBytes(spec.Data).String(), |
There was a problem hiding this comment.
You can use a custom type in the protobuf to avoid the string conversion.
There was a problem hiding this comment.
A digest.Digest object is just a hex-encoded string with the algorithm as the prefix, and Digest.String() just returns itself cast into a string. I can create a custom object to store the algorithm + bytes, if that would make more sense to store as an object?
(It seems clearer to me to just use string in this case, rather than someone having to look up with digest.Digest is, especially on a protocol definition, but I don't feel particularly strongly.)
There was a problem hiding this comment.
No, don't create a new protobuf type. Just tell the compiler to generate this field with type digest.Digest using the custom type extension.
There was a problem hiding this comment.
Out of band discussion:
using: string digest = 4 [(gogoproto.customtype) = "github.com/docker/distribution/digest.Digest"];, make generate produces a compiled object.pb.go file with no errors, but that file refuses to build:
$ make build
🐳 build
github.com/docker/swarmkit
github.com/docker/swarmkit/api/duration
github.com/docker/swarmkit/api/timestamp
github.com/docker/swarmkit/protobuf/plugin
github.com/docker/swarmkit/manager/testcluster
github.com/docker/swarmkit/protobuf/plugin/deepcopy/test
github.com/docker/swarmkit/protobuf/plugin/raftproxy/test
github.com/docker/swarmkit/protobuf/ptypes
github.com/docker/swarmkit/api
github.com/docker/swarmkit/protobuf/plugin/authenticatedwrapper
github.com/docker/swarmkit/protobuf/plugin/deepcopy
github.com/docker/swarmkit/cmd/protoc-gen-gogoswarm
# github.com/docker/swarmkit/api
api/objects.pb.go:1307: invalid argument m.Digest (type *digest.Digest) for len
api/objects.pb.go:1310: invalid argument m.Digest (type *digest.Digest) for len
api/objects.pb.go:1311: second argument to copy should be slice or string; have *digest.Digest
api/objects.pb.go:1597: invalid argument m.Digest (type *digest.Digest) for len
api/objects.pb.go:3844: cannot use digest.Digest(data[iNdEx:postIndex]) (type digest.Digest) as type *digest.Digest in assignment
make: *** [build] Error 2
This may be a bug with gogo, so we're just going to leave it a string for now and maybe work it out later.
aluzzardi
left a comment
There was a problem hiding this comment.
Overall looks good, a few nits in the CLI but the rest of the code (store/control API) looks great
cmd/swarmctl/main.go
Outdated
| "github.com/docker/swarmkit/cmd/swarmctl/cluster" | ||
| "github.com/docker/swarmkit/cmd/swarmctl/network" | ||
| "github.com/docker/swarmkit/cmd/swarmctl/node" | ||
| "github.com/docker/swarmkit/cmd/swarmctl/secrets" |
There was a problem hiding this comment.
nit: This should be named secret to be consistent with the rest (e.g. node, network, ...)
cmd/swarmctl/main.go
Outdated
| version.Cmd, | ||
| network.Cmd, | ||
| cluster.Cmd, | ||
| secrets.Cmd, |
| Short: "Create a secret", | ||
| RunE: func(cmd *cobra.Command, args []string) error { | ||
| if len(args) != 1 { | ||
| return errors.New("create command takes a unique secret name as an argument, and accepts secret data via stdin") |
There was a problem hiding this comment.
I guess the UX doesn't really matter for swarmctl as users will be using the docker CLI, but how to you envision that UX?
As in, do we expect secrets to be passed through stdin only?
I have a counter proposal (assuming foo is the secret name and bar the secret value):
Pass as an argument:
docker secret create foo bar
Pass to stdin:
echo bar | docker secret create foo -
Pass as a file:
echo bar > value.txt && docker secret create foo -f value.txt
Thoughts? /cc @cyli @diogomonica @aaronlehmann @stevvooe
There was a problem hiding this comment.
I think @diogomonica proposed value in the doc, too. I'm +1 on this.
There was a problem hiding this comment.
I actually like passing the secrets on stdin, because doing it on the command line is really bad practice (visible through the process table, and often saved to shell history). Building the UI to support it encourages risky practices and looks unprofessional (unfortunately, we made this mistake with join tokens).
So stdin and reading from files are both legitimate ways to input the secret, but it's really easy to just use shell pipelines and not have two ways of doing the same thing. (But I'm not opposed to a -f option, I guess).
There was a problem hiding this comment.
I don't want to hold up this PR for this since the docker UX can and will be different, but I don't necessarily agree.
echo blah | docker secret createwould show up in the process list / shell history as well- If you have local root access, you can grab the secrets from memory anyway
- stdin can be messy for the users, especially since we don't display the secrets back to the user. Having a trailing
\nwill be a pretty common problem - All the secret stores I've played with so far allow to provide secrets through the command line
- A manager is not "the enemy". We're not trying to protect secrets against the manager machine itself
There was a problem hiding this comment.
Oh wait, sorry, I misread the first comment. I think taking a secret as an argument may not be great, since it's visible on the shell history. I was +1 on passing in a file via -f argument, but I don't feel super strongly between that and @aaronlehmann's stdin. The positive thing about @aaronlehmann's version of stdin being constructed by pipes was being able to pipe it in from some other source (e.g. maybe some other command line utility)
There was a problem hiding this comment.
swarmctl is good practice for the big show...
cmd/swarmctl/secrets/inspect.go
Outdated
| common.FprintfIfNotEmpty(w, "Digest\t: %s\n", secret.Digest) | ||
| common.FprintfIfNotEmpty(w, "Size\t: %d\n", secret.SecretSize) | ||
|
|
||
| created := time.Unix(int64(secret.Meta.CreatedAt.Seconds), int64(secret.Meta.CreatedAt.Nanos)) |
There was a problem hiding this comment.
There's a ptypes package that provides helpers to format protobuf datetimes so you don't need to convert manually.
These 2 lines could become this:
common.FprintfIfNotEmpty(w, "Created\t: %s\n", ptypes.TimestampString(secret.Meta.CreatedAt))
There are examples in various inspect.go such as https://github.com/docker/swarmkit/blob/master/cmd/swarmctl/task/inspect.go
There was a problem hiding this comment.
s.Meta.CreatedAt seems to be the wrong type (I get type *"github.com/docker/swarmkit/api/timestamp".Timestamp) as type *"github.com/golang/protobuf/ptypes/timestamp".Timestamp in argument to ptypes.Timestamp when I tried to do it previously), so none of the ptypes functions work with it. Am I missing something in the protocols, or some kind of translation, etc. that I'm supposed to do such that it's the right type?
I can convert to a ptypes.Timestamp type instead of a time.Time type to use it with that package instead?
There was a problem hiding this comment.
You need to use "github.com/docker/swarmkit/protobuf/ptypes"
There was a problem hiding this comment.
Oops, that's what I get for depending on autocomplete and gofmt. :D Thanks!
cmd/swarmctl/secrets/list.go
Outdated
| func (k secretSorter) Len() int { return len(k) } | ||
| func (k secretSorter) Swap(i, j int) { k[i], k[j] = k[j], k[i] } | ||
| func (k secretSorter) Less(i, j int) bool { | ||
| iTime := time.Unix(k[i].Meta.CreatedAt.Seconds, int64(k[i].Meta.CreatedAt.Nanos)) |
There was a problem hiding this comment.
ptypes can help here as well:
iTime, err := ptypes.Timestamp(k[i].Meta.CreatedAt)
if err != nil {
panic(err)
}
jTime, err := ptypes.Timestamp(k[j].Meta.CreatedAt)
if err != nil {
panic(err)
}
return jTime.Before(iTime)
cmd/swarmctl/secrets/list.go
Outdated
| }() | ||
| common.PrintHeader(w, "ID", "Name", "Created", "Digest", "Size") | ||
| output = func(s *api.Secret) { | ||
| created := time.Unix(int64(s.Meta.CreatedAt.Seconds), int64(s.Meta.CreatedAt.Nanos)) |
There was a problem hiding this comment.
created, err := ptypes.Timestamp(s.Meta.CreatedAt)e53ed9e to
8777042
Compare
api/objects.proto
Outdated
|
|
||
| // Size represents the size (number of bytes) of the secret data, and is | ||
| // SecretSize represents the size (number of bytes) of the secret data, and is | ||
| // calculated from the data contained in `Secret.Spec.data`.. |
manager/controlapi/secret.go
Outdated
| return nil, grpc.Errorf(codes.NotFound, "secret %s not found", request.SecretID) | ||
| } | ||
|
|
||
| secret.Spec.Data = nil |
There was a problem hiding this comment.
Add comment as to why.
manager/controlapi/secret.go
Outdated
| s.store.View(func(tx store.ReadTx) { | ||
| secrets, err = store.FindSecrets(tx, by) | ||
| }) | ||
|
|
| if request.SecretID == "" { | ||
| return nil, grpc.Errorf(codes.InvalidArgument, "secret ID must be provided") | ||
| } | ||
|
|
There was a problem hiding this comment.
For this and the above comment, I just removed a couple newlines in several of the functions.
| case store.ErrNotExist: | ||
| return nil, grpc.Errorf(codes.NotFound, "secret %s not found", request.SecretID) | ||
| case nil: | ||
| return &api.RemoveSecretResponse{}, nil |
There was a problem hiding this comment.
should secret response include the secret that was removed?
There was a problem hiding this comment.
Not sure if that would be useful. The other control APIs return empty responses - do we want to make that the case for all the others as well?
manager/controlapi/secret.go
Outdated
| } | ||
|
|
||
| if len(spec.Data) > MaxSecretSize || len(spec.Data) == 0 { | ||
| return grpc.Errorf(codes.InvalidArgument, "secret data must be between 1 and %d bytes inclusive", MaxSecretSize) |
There was a problem hiding this comment.
Secret data must be larger than 0 and smaller than MaxSecret bytes
There was a problem hiding this comment.
Updated, and also changed the comparison for MaxSecretSize to >=.
8777042 to
2f2b67c
Compare
…n Lehmann <aaron.lehmann@docker.com> Signed-off-by: cyli <ying.li@docker.com>
Signed-off-by: cyli <ying.li@docker.com>
Signed-off-by: cyli <ying.li@docker.com>
|
Anything else or can we go ahead and merge this PR? |
|
@aluzzardi I think I've semi-addressed #1567 (comment) and #1567 (comment) (the latter mainly I just commented). I think if @stevvooe is ok with those, and you are ok with addressing #1567 (comment) in a later PR or just in docker itself, it's ready to go. (e.g. I believe those were the only three comments I haven't fully addressed in code, just in comments) |
|
I'm okay with addressing #1567 (comment) in a follow up or even just in Docker so we can get this one going since it's not super priority and UX questions tend to take a while to get resolved :) |
Signed-off-by: cyli <ying.li@docker.com>
2f2b67c to
aebced0
Compare
|
(have rebased) |
Sorry, this is biggish because it also includes the swarmctl implementations for command line interaction with swarmd. I can split that off into another PR if needed.
This implements the Get/Create/Remove/List functionality for secrets in the control API as specified in #1511.