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

secrets control-api implementation #1567

Merged
merged 4 commits into from
Sep 28, 2016
Merged

Conversation

cyli
Copy link
Contributor

@cyli cyli commented Sep 24, 2016

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.

@codecov-io
Copy link

codecov-io commented Sep 24, 2016

Current coverage is 54.00% (diff: 52.31%)

Merging #1567 into master will decrease coverage by 0.02%

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

Sunburst

Powered by Codecov. Last update a126286...aebced0

@cyli cyli changed the title [WIP] secrets control-api implementation secrets control-api implementation Sep 24, 2016
@cyli cyli force-pushed the secrets-implementation branch from a805231 to 733f210 Compare September 24, 2016 03:36
Copy link
Collaborator

@aaronlehmann aaronlehmann left a comment

Choose a reason for hiding this comment

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

Overall LGTM! Just one comment about digest formatting.

ID: identity.NewID(),
Spec: *spec,
SecretSize: uint32(len(spec.Data)),
Digest: hex.EncodeToString(checksumBytes[:]),
Copy link
Collaborator

Choose a reason for hiding this comment

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

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

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!


var (
inspectCmd = &cobra.Command{
Use: "inspect <secret ID orname>",
Copy link
Member

Choose a reason for hiding this comment

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

orname --> or name

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Nice catch, thanks!

"with:colon",
"with;semicolon",
"snowman☃",
"o_______________________________________________________________o", // exactly 65 characters
Copy link
Contributor

Choose a reason for hiding this comment

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

strings.Repeat?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Aww, I liked the giant frowny face. :) But yes strings.Repeat would make it clearer, thanks.

@@ -12,13 +17,40 @@ import (
// 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])*$`)
Copy link
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't have any strong feelings about it, but any particular reason to not start with numbers?

Copy link
Contributor Author

@cyli cyli Sep 27, 2016

Choose a reason for hiding this comment

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

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)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

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.

return &api.Secret{
ID: identity.NewID(),
Spec: *spec,
SecretSize: uint32(len(spec.Data)),
Copy link
Contributor

Choose a reason for hiding this comment

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

Please change the type of this field to int64. No need to use unsigned here: we aren't interested in the field-arithmetic properties.

Copy link
Collaborator

Choose a reason for hiding this comment

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

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.

Copy link
Contributor Author

@cyli cyli Sep 27, 2016

Choose a reason for hiding this comment

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

@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(),
Copy link
Contributor

Choose a reason for hiding this comment

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

You can use a custom type in the protobuf to avoid the string conversion.

Copy link
Contributor Author

@cyli cyli Sep 26, 2016

Choose a reason for hiding this comment

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

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

Copy link
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

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.

Copy link
Member

@aluzzardi aluzzardi left a comment

Choose a reason for hiding this comment

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

Overall looks good, a few nits in the CLI but the rest of the code (store/control API) looks great

@@ -6,6 +6,7 @@ import (
"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"
Copy link
Member

Choose a reason for hiding this comment

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

nit: This should be named secret to be consistent with the rest (e.g. node, network, ...)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Moved

@@ -54,5 +55,6 @@ func init() {
version.Cmd,
network.Cmd,
cluster.Cmd,
secrets.Cmd,
Copy link
Member

Choose a reason for hiding this comment

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

secret

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Moved

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

Choose a reason for hiding this comment

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

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think @diogomonica proposed value in the doc, too. I'm +1 on this.

Copy link
Collaborator

Choose a reason for hiding this comment

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

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

Copy link
Member

Choose a reason for hiding this comment

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

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.

  1. echo blah | docker secret create would show up in the process list / shell history as well
  2. If you have local root access, you can grab the secrets from memory anyway
  3. stdin can be messy for the users, especially since we don't display the secrets back to the user. Having a trailing \n will be a pretty common problem
  4. All the secret stores I've played with so far allow to provide secrets through the command line
  5. A manager is not "the enemy". We're not trying to protect secrets against the manager machine itself

Copy link
Contributor Author

Choose a reason for hiding this comment

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

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)

Copy link
Contributor

Choose a reason for hiding this comment

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

swarmctl is good practice for the big show...

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

Choose a reason for hiding this comment

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

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

Copy link
Contributor Author

@cyli cyli Sep 26, 2016

Choose a reason for hiding this comment

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

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?

Copy link
Collaborator

Choose a reason for hiding this comment

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

You need to use "github.com/docker/swarmkit/protobuf/ptypes"

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oops, that's what I get for depending on autocomplete and gofmt. :D Thanks!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed, thanks!

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

Choose a reason for hiding this comment

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

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)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed, thanks!

}()
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))
Copy link
Member

Choose a reason for hiding this comment

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

created, err := ptypes.Timestamp(s.Meta.CreatedAt)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed, thanks!

@cyli cyli force-pushed the secrets-implementation branch from e53ed9e to 8777042 Compare September 27, 2016 01:23
Copy link
Contributor

@diogomonica diogomonica left a comment

Choose a reason for hiding this comment

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

LGTM, small nits.

@@ -245,7 +245,7 @@ message Secret {
// is calculated from the data contained in `Secret.Spec.data`.
string digest = 4;

// 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`..
Copy link
Contributor

Choose a reason for hiding this comment

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

two ".."

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed, thanks

return nil, grpc.Errorf(codes.NotFound, "secret %s not found", request.SecretID)
}

secret.Spec.Data = nil
Copy link
Contributor

Choose a reason for hiding this comment

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

Add comment as to why.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

s.store.View(func(tx store.ReadTx) {
secrets, err = store.FindSecrets(tx, by)
})

Copy link
Contributor

Choose a reason for hiding this comment

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

remove extra \n

if request.SecretID == "" {
return nil, grpc.Errorf(codes.InvalidArgument, "secret ID must be provided")
}

Copy link
Contributor

Choose a reason for hiding this comment

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

extra \n

Copy link
Contributor Author

Choose a reason for hiding this comment

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

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

Choose a reason for hiding this comment

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

should secret response include the secret that was removed?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

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?

}

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

Choose a reason for hiding this comment

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

Secret data must be larger than 0 and smaller than MaxSecret bytes

Copy link
Contributor Author

@cyli cyli Sep 27, 2016

Choose a reason for hiding this comment

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

Updated, and also changed the comparison for MaxSecretSize to >=.

@cyli cyli force-pushed the secrets-implementation branch from 8777042 to 2f2b67c Compare September 27, 2016 17:00
@aluzzardi
Copy link
Member

Anything else or can we go ahead and merge this PR?

@cyli
Copy link
Contributor Author

cyli commented Sep 27, 2016

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

@aluzzardi
Copy link
Member

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

@cyli cyli force-pushed the secrets-implementation branch from 2f2b67c to aebced0 Compare September 27, 2016 23:59
@cyli
Copy link
Contributor Author

cyli commented Sep 27, 2016

(have rebased)

@ehazlett ehazlett mentioned this pull request Oct 26, 2016
4 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants