Skip to content

Conversation

@aaronlehmann
Copy link
Collaborator

@aaronlehmann aaronlehmann commented Apr 21, 2017

Add the ability for secrets and configs to expand templates, which can
reference the following fields:

  • The same set of fields available to ContainerSpec templates
    ({{.Service.Name}}, {{.Node.ID}}, etc)
  • Environment variables: {{Env "envvar"}}
  • Other secrets: {{Secret "sometarget"}}
  • Other configs: {{Config "sometarget"}}

Secrets and configs can be accessed either by source (secret name) or
target (file name).

Templating of secrets/configs is optional, and is disabled by default
because it's very possible for Go template syntax to conflict with the
syntax of the payload.

The only change that will be necessary in the executor is wrapping the
DependencyGetter in NewTemplatedDependencyGetter before handing it off
to the execution backend.

cc @aluzzardi @stevvooe @diogomonica @cyli

t *api.Task
}

func (sg secretPayloadGetter) GetBySource(source string) (string, error) {
Copy link
Contributor

Choose a reason for hiding this comment

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

In the theme of brevity, could this just be ByTarget?

// invoke those methods.
type PayloadContext struct {
Context
Secrets secretPayloadGetter
Copy link
Contributor

Choose a reason for hiding this comment

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

Why not add this to the base context? It will keep the scope of what is available in a template a lot clearer.

Copy link
Contributor

Choose a reason for hiding this comment

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

Presumably, this particular context is only available for contexts after env variables have been evaluated?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I decided to avoid adding these to the base Context type because I wanted to avoid exposing secrets during template substitution on labels, environment variables, etc. This might be a desirable feature to have in its own right, but I was worried there might be more opportunities for injection here, so didn't want to bundle it into this PR.

@diogomonica: Any thoughts on where templates should be able to use secrets or not?

That said, I think when I change this to use the func map as you suggested, this structure will no longer be necessary. There will just be Context with the func map injected (in places where we allow secret/config substitution), and Context without the func map (where we disallow secret/config substitution). This is probably a cleaner result.

@stevvooe
Copy link
Contributor

@aaronlehmann This looks like it has the correct layout, but I am slightly worried about exposing methods on the injected objects. Would it be safer for us to inject these into the func map?

For example, we'd have the following, syntactically, as a result:

{{secret "foo"}}
{{config "bar"}}

Such a change would allow us to have a "no method" policy on context objects.

I'm not sure I understand the distinction behind source and target (other than being the "origin of the secret"). I know we do this in the command line to allow one to select the secret but does it make sense here when the target is constant?

@aaronlehmann
Copy link
Collaborator Author

This looks like it has the correct layout, but I am slightly worried about exposing methods on the injected objects. Would it be safer for us to inject these into the func map?

I definitely agree. I missed the func map in my cursory read of the template docs. I'll update the PR to work like this instead. Thanks for the suggestion.

I'm not sure I understand the distinction behind source and target (other than being the "origin of the secret"). I know we do this in the command line to allow one to select the secret but does it make sense here when the target is constant?

The target is considered more "portable" - rotating a secret would involve creating a new one with a different name, and updating the service to mount that new secret under the same target (filename). However, I felt it might be weird to only be able to reference secrets by target, and not by secret name - so I included both options here. If people think one or the other makes sense as a canonical method of reference, I'm fine with just choosing one.

@stevvooe
Copy link
Contributor

If people think one or the other makes sense as a canonical method of reference, I'm fine with just choosing one.

Let's make the default the syntactically nice option, then have an alternate for the other. It sounds like target is the "right" option, but I can see the need for referencing source.

@codecov
Copy link

codecov bot commented Apr 25, 2017

Codecov Report

Merging #2133 into master will increase coverage by 0.21%.
The diff coverage is 71.87%.

@@            Coverage Diff             @@
##           master    #2133      +/-   ##
==========================================
+ Coverage   60.06%   60.28%   +0.21%     
==========================================
  Files         119      120       +1     
  Lines       19847    19972     +125     
==========================================
+ Hits        11921    12040     +119     
+ Misses       6568     6556      -12     
- Partials     1358     1376      +18

@aaronlehmann
Copy link
Collaborator Author

I've updated the PR to inject functions into the template instead of relying on methods. This also allowed switching to more usable syntax. The injected functions are:

{{Secret "secretname"}}     # by target
{{SecretBySource "secretname"}}
{{Config "configname"}}     # by target
{{ConfigBySource "configname"}}
{{Env "envvar"}}

I kept the PayloadContext for now, but can unify it with Context if this still makes sense. I just wasn't sure about adding secret and config related fields to a structure which may be used in a context where we (currently) don't want to support secret/config substitution.

@diogomonica
Copy link
Contributor

@aaronlehmann since by default in docker secret create NAME NAME is the source, shouldn't we invert SecretBySource to be SecretByTarget?

@aaronlehmann
Copy link
Collaborator Author

I guess the question is which way should be the default. Specifying a secret by its target is more "portable", since the source may change as the secret data is rotated. In the original version of the PR I called the functions SecretBySource and SecretByTarget to be very explicit, but @stevvooe suggested having a simple function and, and an alternate one for the less common use case:

Let's make the default the syntactically nice option, then have an alternate for the other. It sounds like target is the "right" option, but I can see the need for referencing source.

Another thing I can think of is making the function take two arguments:

{{Secret "source" "mysecret"}}

or borrowing the k/v syntax we use in the CLI to specify a source or target:

{{Secret "source=mysecret"}}

I'm not sure what's best here.

"Secret": ctx.secretGetterByTarget,
"ConfigBySource": ctx.configGetterBySource,
"Config": ctx.configGetterByTarget,
"Env": ctx.envGetter,
Copy link
Contributor

Choose a reason for hiding this comment

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

I think the convention for template functions is to be lowercase.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I made them uppercase for consistency with the existing template variables such as .Service.Name, but I'm happy to change them to lowercase if that's better.

Copy link
Contributor

Choose a reason for hiding this comment

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

The "best practice" seems to be that variables are uppercase and functions are lowercase. This was followed with join.

Here is the builtin function set: https://golang.org/pkg/text/template/#hdr-Functions.

I think we follow this convention in docker, as well: https://github.com/moby/moby/blob/master/pkg/templates/templates.go#L11.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

No problem, I'll make them lowercase.

func newTemplate(s string) (*template.Template, error) {
return template.New("expansion").Option("missingkey=error").Funcs(funcMap).Parse(s)
func newTemplate(s string, extraFuncs template.FuncMap) (*template.Template, error) {
tmpl := template.New("expansion").Option("missingkey=error").Funcs(funcMap)
Copy link
Contributor

Choose a reason for hiding this comment

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

Does this merge the func maps?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Does this merge the func maps?

Yeah.

@stevvooe
Copy link
Contributor

stevvooe commented Apr 28, 2017

What about this?

{{secret "mysecret" }} // target secret
{{secret "mysecret" "bysource=true"}} // source secret

This will allow us to parameterize secret access in templates in the future?

@aaronlehmann
Copy link
Collaborator Author

What about this?

{{secret "mysecret" }} // target secret
{{secret "mysecret" "bysource=true"}} // source secret

SGTM. I'll wait for Diogo and Andrea to chime in before making any changes.

@diogomonica
Copy link
Contributor

@aaronlehmann I like it

@aaronlehmann
Copy link
Collaborator Author

Pushed some updates:

  • Changed function names to lowercase
  • Replaced SecretBySource and ConfigBySource with an optional bysource=true. @aluzzardi suggested dropping support for referencing by source entirely, so we could do that instead if the access by source is not useful.
  • Changed SecretGetter and ConfigGetter interfaces to return an error from their Get methods, so a template error can propagate up to the executor and fail the task.

@diogomonica
Copy link
Contributor

@aaronlehmann agreed on dropping it by source.

@stevvooe
Copy link
Contributor

stevvooe commented May 3, 2017

@aaronlehmann agreed on dropping it by source.

Seconded. We can always add the parameter later.

@aaronlehmann
Copy link
Collaborator Author

I've removed the bysource option. PTAL.

@stevvooe
Copy link
Contributor

stevvooe commented May 3, 2017

LGTM

1 similar comment
@diogomonica
Copy link
Contributor

LGTM

@aaronlehmann
Copy link
Collaborator Author

ping @aluzzardi

@aluzzardi
Copy link
Member

Overall looks good. My 2 cents:

  • I'm questioning the usefulness of secret templating. The same could be achieved by importing a secret inside a config template
  • If we go that route anyway, what happens when including a templatized secret? Do we apply the template? If so, do we run into recursive issues?
  • Rather than having a Templating option, wouldn't it be more future proof to have a Type (defaults to PLAIN, can be GO_TEMPLATE)? Maybe I'm overthinking it
  • I'm not sure about best practices regarding templating. I know InfraKit is using lowercase functions (https://github.com/infrakit/examples/blob/master/swarm/worker-init.sh), either way it's fine.

@aaronlehmann
Copy link
Collaborator Author

I'm questioning the usefulness of secret templating. The same could be achieved by importing a secret inside a config template

The only reason I can think of is that the actual config files are not backed by tmpfs, but secrets are. @diogomonica any thoughts?

If we go that route anyway, what happens when including a templatized secret? Do we apply the template? If so, do we run into recursive issues?

In this PR, only top-level templates are evaluated, to avoid recursion. There is a unit test related to this. I suppose we could support multiple levels of templating, and have some kind of cycle detector, but only applying one template seemed like the simplest thing to do for now.

Rather than having a Templating option, wouldn't it be more future proof to have a Type (defaults to PLAIN, can be GO_TEMPLATE)? Maybe I'm overthinking it

Sounds okay to me, any opinions for/against @diogomonica @stevvooe? I feel like in the future we might add other properties that are not mutually exclusive with templating, which is why I made the field specific to templating, but then again we might add properties that are mutually exclusive with templating...

I'm not sure about best practices regarding templating. I know InfraKit is using lowercase functions (https://github.com/infrakit/examples/blob/master/swarm/worker-init.sh), either way it's fine.

There was some discussion above that resulted in the functions becoming lowercase: #2133 (comment)

@stevvooe
Copy link
Contributor

stevvooe commented May 9, 2017

If we go that route anyway, what happens when including a templatized secret? Do we apply the template? If so, do we run into recursive issues?

I think it is clear that secrets are evaluated in line. This must be a dag, in that a template can consume a secret, but not vice versa.

Rather than having a Templating option, wouldn't it be more future proof to have a Type (defaults to PLAIN, can be GO_TEMPLATE)? Maybe I'm overthinking it

For consistency with secret, we may be past this point. We may have wanted "external types" at some point, but we just have to deal with sibling fields for that case. I actually think a bool here for template or not would be fine (template_enable). If we have other types, we can add them later.

I'm not sure about best practices regarding templating. I know InfraKit is using lowercase functions (https://github.com/infrakit/examples/blob/master/swarm/worker-init.sh), either way it's fine.

I hunted this down. In docker, we use lowercase for functions and that seems to be the best practice across the Go community.

@aaronlehmann
Copy link
Collaborator Author

I'm tempted to just keep the protos as they are in this PR. It feels a bit simpler to have a single field related to templating than a bool and a template type field. And if we leave the template type field for the future, existing implementations wouldn't be aware of it, and they would try to evaluate everything as a Go template.

But I really don't feel strongly. If there is a preference for switching to a bool, I'm fine with doing that. If there's a preference for renaming the field to Type, I'm fine with that as well.

@aluzzardi

@stevvooe
Copy link
Contributor

@aaronlehmann I would prefer the bool. I'm only bringing it up because that is unusual.

@aaronlehmann
Copy link
Collaborator Author

I've updated the protobufs to use the Driver type to specify the templating engine, instead of an enum. This will allow us to potentially support templating plugins in the future, and to pass options to templating engines.

PTAL

@stevvooe
Copy link
Contributor

LGTM

@aaronlehmann
Copy link
Collaborator Author

Rebased

Aaron Lehmann added 5 commits May 18, 2017 14:52
Add the ability for secrets and configs to expand templates, which can
reference the following fields:

- The same set of fields available to ContainerSpec templates
  ({{.Service.Name}}, {{.Node.ID}}, etc)
- Environment variables: {{Env "envvar"}}
- Other secrets: {{Secret "sometarget"}}
- Other configs: {{Config "sometarget"}}

Secrets and configs can be accessed either by source (secret name) or
target (file name).

Templating of secrets/configs is optional, and is disabled by default
because it's very possible for Go template syntax to conflict with the
syntax of the payload.

The only change that will be necessary in the executor is wrapping the
DependencyGetter in NewTemplatedDependencyGetter before handing it off
to the execution backend.

Signed-off-by: Aaron Lehmann <aaron.lehmann@docker.com>
This allows a templating error to fail a task.

Signed-off-by: Aaron Lehmann <aaron.lehmann@docker.com>
Rename Secret, Config, and Env to lowercase variants.

Replace SecretBySource/ConfigBySource with optional bysource=true
arguments to "secret" and "config".

Signed-off-by: Aaron Lehmann <aaron.lehmann@docker.com>
Signed-off-by: Aaron Lehmann <aaron.lehmann@docker.com>
Signed-off-by: Aaron Lehmann <aaron.lehmann@docker.com>
@aaronlehmann
Copy link
Collaborator Author

Rebased

@diogomonica
Copy link
Contributor

LGTM!

@aluzzardi
Copy link
Member

LGTM

/cc @dhiltgen

Copy link

@wsong wsong left a comment

Choose a reason for hiding this comment

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

I'm not really qualified to make specific comments on the code, but it LGTM overall. Design seems sensible.

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.

5 participants