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

kubectl -f-style support for deploy task #514

Merged
merged 21 commits into from
Sep 4, 2019

Conversation

timothysmith0609
Copy link
Contributor

@timothysmith0609 timothysmith0609 commented Jul 23, 2019

What are you trying to accomplish with this PR?

  • Implement a new flag, -f, with similar semantics to kubectl -f
    • -f accepts a comma-separated list of filenames and/or directories
    • Must support proper partials scoping for both file and directory arguments
    • Must work with templates passed from stdin (tbh this is mostly for free)
  • Currently this is only for deploy_task, but the -f parsing is done in such a way as to be reusable (via TemplateDiscovery#templates)
  • Required some changes to method signatures in the tests. I tried to make these as minor as possible.
  • --template-dir is still supported (both at the CLI and API level), but should be considered deprecated once this PR merges

Update (Aug 15, 2019)
The scope of this PR expanded from just supporting multiple template directory to full fledged support for passing in both files and directories. This approach necessitated a few changes to some internal abstractions, namely TemplateDiscovery and the new LocalResourceDiscovery.

TemplateDiscover::templates(paths) returns a mapping of the form dir -> [filenames]. As an example, let's say the paths argument is [my/dir, other/foo.yml] and the contents of my/dir and other are [bar.yml, baz.yml] and [foo.yml, more.yml]. The return value of templates will thus be:

{
  "my/dir": ["bar.yml", "baz.yml"],
  "other": ["foo.yml"]
}

TemplateDiscovery performs proper aggregation of resources and eliminates the possibility of duplicate values existing. Furthermore, proper partial scoping is preserved since we maintain a mapping of directories for all kinds of resources (both dirs and files).

How is this accomplished?
Initially, I tried passing multiple template directories around deploy_task, but this became cumbersome: I had to create a mapping of template directories to their respective partials dirs, wrap them together, and pass them to and from TemplateContext objects and back to the (somewhat-global) renderer.

A cleaner approach was to simply instantiate a separate renderer for each template directory passed in, then, when appropriate, concatenate the result of each template directory rendering operation and proceed through the deploy as normal. This way, the changes to the codebase are much smaller and the logic for each individual element stays simpler.

Edit: What I ended up doing in the end is a bit of a mix of the first 2 ideas. In the deploy task constructor, we declare a mapping of renderers to template directories. The renderers are then lazily instantiated directly at the point of rendering (in #split_templates), leveraging the closure provided by the default block of the @renderers Hash (thank you Blake for the great suggestion).

Please see the unresolved comments from my review for more fine-grained issues regarding this PR.

What could go wrong?
How defensive should we be? I think that giving people the ability to specify multiple template directories places the responsibility of ensuring they end up with a sane and collision-free experience squarely on them.

TODO

  • Add tests

Copy link
Contributor Author

@timothysmith0609 timothysmith0609 left a comment

Choose a reason for hiding this comment

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

A few points for discussion

lib/kubernetes-deploy/options_helper.rb Outdated Show resolved Hide resolved
lib/kubernetes-deploy/renderer.rb Outdated Show resolved Hide resolved
lib/kubernetes-deploy/renderer.rb Outdated Show resolved Hide resolved
@timothysmith0609 timothysmith0609 changed the title Initial implementation of multiple template dirs support -WIP- Initial implementation of multiple template dirs support Jul 23, 2019
@timothysmith0609 timothysmith0609 force-pushed the support_multiple_template_dirs branch 2 times, most recently from 57cabbe to 79d77dc Compare July 23, 2019 19:49
lib/kubernetes-deploy/deploy_task.rb Show resolved Hide resolved
lib/kubernetes-deploy/deploy_task.rb Outdated Show resolved Hide resolved
lib/kubernetes-deploy/renderer.rb Outdated Show resolved Hide resolved
Copy link
Contributor Author

@timothysmith0609 timothysmith0609 left a comment

Choose a reason for hiding this comment

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

Some tidying up and implementation of Blake's suggestion + a few more comments for discussion

@@ -29,7 +29,7 @@ def run!(stream, only_filenames = [])
@logger.phase_heading("Initializing render task")

filenames = if only_filenames.empty?
TemplateDiscovery.new(@template_dir).templates
TemplateDiscovery.new([@template_dir]).templates[@template_dir]
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is the result of the change in contract of TemplateDiscovery#new requiring an array of template dirs.

  • Do we want the render task to accept multiple template dirs. Note that the render task can also accept an arbitrary list of filenames as a filter. For multiple template dirs, we'll just apply the filter to each directory.
  • If we do want the render task to accept multiple template directories as well, do we want to tackle that in this PR or in a follow-up?

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think that accepting multiple directories makes sense the way the renderer's CLI is currently implemented. For example, imagine you have a setup like this:

dirA/
  one.yml
  two.yml
dirB/
  one.yml
  two.yml

And what you want to render is dirA/one.yml and dirB/two.yml. You... couldn't?

According to our design for the Krane CLI, we will want both commands to use a -f argument that behaves similarly to kubectl's. That strikes me as incompatible with a directory/filter system: in that design, we'd get a mixed list of files and directories, and have to group individual files given to us by (inferred) deploy directory. Given the importance of the directory to us, maybe the -f plan doesn't make sense after all, because it would lead to unintuitive behaviour. WDYT?

In any case, I'm thinking it would be a breaking API change for the render CLI (unlike the deploy CLI), and if that's the case, we should save it for the transition to Krane.

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 you're absolutely right. The current semantics of kubernetes-render are incompatible with a kubectl -f-like model. For instance, there would be no way to know which bare file names provided to kubernetes-render point to which provided template dirs :/

To me, this means we need to make a fundamental change to the CLI contract of kubernetes-render. In particular, dropping support for providing bare filenames as arguments and instead force the (conventional) semantics of the -f flag. From your above example, this would be:
krane render -f dirA/one.yml,dirB/two.yml (not that bad).

In light of this, I'd like to keep kubernetes-render as-is (only supporting a single template dir), and make a follow-up PR to make krane render behave with -f semantics.

# Exposed for direct use only when deploy_fixtures cannot be used because the template cannot be loaded pre-deploy,
# for example because it contains an intentional syntax error
def deploy_dir(dir, **args)
def deploy_dir(*dirs, **args)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Multiple template directories are exceptional, so it seemed more straightforward to change the signature to accept an arbitrary number of dirs as opposed to enforcing an actual array. I can do the latter if that's what we ultimately want.

end

def test_deploy_successful_with_multiple_template_dirs_multiple_partials
result = deploy_dir(fixture_path("test-partials"), fixture_path("test-partials2"),
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The goal of this test is to ensure that partials with identical names are actually rendered according to the relative path of the given template directory. This was done by using an identically-named partial that renders different parts of the spec; if the 'wrong' partial is used, the deploy would fail. This convinces me enough that the renderer for a given template directory is behaving as expected (e.g. properly scoped to the right dir)

@timothysmith0609 timothysmith0609 changed the title -WIP- Initial implementation of multiple template dirs support Initial implementation of multiple template dirs support Jul 24, 2019
@timothysmith0609 timothysmith0609 changed the title Initial implementation of multiple template dirs support Multiple template dirs support for deploy task Jul 24, 2019
@timothysmith0609 timothysmith0609 self-assigned this Jul 24, 2019
@timothysmith0609 timothysmith0609 added the 🆕 feature Makes something new possible label Jul 24, 2019
@dturn
Copy link
Contributor

dturn commented Jul 24, 2019

My original concern with this PR was how will it interact with our goal of supporting a -f flag in the Krane CLI https://docs.google.com/document/d/1oInUsKplYGNWTymPY48xtDCx3x4Cwc-phcEKUkt2v4U/edit#. I think this actually makes it easier @timothysmith0609 thoughts?

@timothysmith0609
Copy link
Contributor Author

My original concern with this PR was how will it interact with our goal of supporting a -f flag in the Krane CLI https://docs.google.com/document/d/1oInUsKplYGNWTymPY48xtDCx3x4Cwc-phcEKUkt2v4U/edit#. I think this actually makes it easier @timothysmith0609 thoughts?

At the very least, I don't think this PR makes things any more difficult in achieving that end. The sole complicating factor between kubectl -f and krane deploy -f is our support of partials. I think there's a few ways we can tackle that:

  • When given a directory/directories, proceed as we currently do (this PR actually enables this behaviour)
  • When given concrete filenames (not directories), collect them together by directory, and treat those collections as more template_dirs (this will require more thought than just this, of course).

@timothysmith0609 timothysmith0609 force-pushed the support_multiple_template_dirs branch 2 times, most recently from 001722e to 78b72bd Compare July 26, 2019 12:39
lib/kubernetes-deploy/deploy_task.rb Outdated Show resolved Hide resolved
lib/kubernetes-deploy/options_helper.rb Outdated Show resolved Hide resolved
lib/kubernetes-deploy/options_helper.rb Outdated Show resolved Hide resolved
lib/kubernetes-deploy/options_helper.rb Show resolved Hide resolved
@@ -29,7 +29,7 @@ def run!(stream, only_filenames = [])
@logger.phase_heading("Initializing render task")

filenames = if only_filenames.empty?
TemplateDiscovery.new(@template_dir).templates
TemplateDiscovery.new([@template_dir]).templates[@template_dir]
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think that accepting multiple directories makes sense the way the renderer's CLI is currently implemented. For example, imagine you have a setup like this:

dirA/
  one.yml
  two.yml
dirB/
  one.yml
  two.yml

And what you want to render is dirA/one.yml and dirB/two.yml. You... couldn't?

According to our design for the Krane CLI, we will want both commands to use a -f argument that behaves similarly to kubectl's. That strikes me as incompatible with a directory/filter system: in that design, we'd get a mixed list of files and directories, and have to group individual files given to us by (inferred) deploy directory. Given the importance of the directory to us, maybe the -f plan doesn't make sense after all, because it would lead to unintuitive behaviour. WDYT?

In any case, I'm thinking it would be a breaking API change for the render CLI (unlike the deploy CLI), and if that's the case, we should save it for the transition to Krane.

lib/kubernetes-deploy/deploy_task.rb Outdated Show resolved Hide resolved
name: web-from-partial
spec:
replicas: 1
template:
Copy link
Contributor

Choose a reason for hiding this comment

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

Nitpick: only this part is a podspec, despite the name of the file.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The goal of the test is to ensure that it's not loading the identically named partial in the other directory, so I kept the partial name the same but ensured the render would fail to validate if it grabbed the wrong partial

@@ -102,22 +102,24 @@ def server_version
kubectl.server_version
end

def initialize(namespace:, context:, current_sha:, template_dir:, logger:, kubectl_instance: nil, bindings: {},
def initialize(namespace:, context:, current_sha:, template_dirs:, logger:, kubectl_instance: nil, bindings: {},
Copy link
Contributor

Choose a reason for hiding this comment

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

I was trying to think of ways to avoid this breaking API change, but the options that occur to me aren't pretty:

  • Keep template_dir:, but let it accept an array and do @template_dirs = Array(template_dir) in the initializer
  • have both template_dir:, and template_dirs:, either the initializer merges them or it raises an error if they're both specified

It kinda irks me to make a breaking API change like this soon before we make another one (with Krane, which is a single clean cutover in a way). I'd be kinda ok with something like the second option above as a result. If we keep it breaking though, let's make it very prominent in the changelog.

@dturn
Copy link
Contributor

dturn commented Jul 26, 2019

A cleaner approach was to simply instantiate a separate renderer for each template directory passed in, then, when appropriate, concatenate the result of each template directory rendering operation and proceed through the deploy as normal. This way, the changes to the codebase are much smaller and the logic for each individual element stays simpler.

Edit: What I ended up doing in the end is a bit of a mix of the first 2 ideas.

Can you comment on why the cleaner approach didn't work out?

@timothysmith0609
Copy link
Contributor Author

Can you comment on why the cleaner approach didn't work out?

The renderer takes a few arguments that are easily grabbed in the task initializer. In the end, it seemed easier to wrap those in the closure provided by the default hash block than try to pass them into TemplateDiscovery further down the stack. That's really the only difference

@dturn
Copy link
Contributor

dturn commented Aug 14, 2019

Perhaps TemplateDiscovery is poorly named, and should instead be something like ResourceDiscovery. ResourceDiscovery is then responsible for finding all relevant resource files in a template directories, splitting the templates in each file, calling the renderer on them, and returning a list of fully realized KubernetesResources.

I can't comment in the thread but, I like the idea. As an fyi we have a class called ClusterResourceDiscovery so I'd prefer something more specific LocalResouceDiscovery?

Copy link
Contributor

@dturn dturn left a comment

Choose a reason for hiding this comment

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

I like this a lot better, thanks for putting in the work to refactor it!

lib/kubernetes-deploy/deploy_task.rb Outdated Show resolved Hide resolved
test/fixtures/ejson-cloud2/secrets.ejson Outdated Show resolved Hide resolved
@dturn dturn mentioned this pull request Aug 14, 2019
Copy link
Contributor Author

@timothysmith0609 timothysmith0609 left a comment

Choose a reason for hiding this comment

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

This is ready for a (hopefully final) review

@namespace = namespace
@namespace_tags = []
@context = context
@current_sha = current_sha
@template_dir = File.expand_path(template_dir)
@template_dirs = (template_dirs.map { |dir| File.expand_path(dir) } << template_dir).compact
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We don't need to worry about the case of template_dir and template_dirs both existing when invoking via CLI, but I've done it this way to make it accommodate the case of someone using the API itself.

errors << "Template directory `#{template_dir}` doesn't exist"
elsif Dir.entries(template_dir).none? { |file| file =~ /(\.ya?ml(\.erb)?)$|(secrets\.ejson)$/ }
errors << "`#{template_dir}` doesn't contain valid templates (secrets.ejson or postfix .yml, .yml.erb)"
end
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This whole block is another thing that should be extracted out of DeployTask if possible

lib/kubernetes-deploy/local_resource_discovery.rb Outdated Show resolved Hide resolved
hash[dir_name] << File.basename(filename) unless hash[dir_name].include?(filename)
end

template_paths
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Now any task should be able to call TemplateDiscovery#templates and receive back a mapping of dirname: [files]. The input of templates is an array whose elements are either file paths or directories (abs and/or relative). The nice thing here is that there's zero other dependencies for generating the dir->files mappings

@timothysmith0609 timothysmith0609 changed the title Multiple template dirs support for deploy task kubectl -f-style support for deploy task Aug 15, 2019
Copy link
Contributor Author

@timothysmith0609 timothysmith0609 left a comment

Choose a reason for hiding this comment

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

Implemented the TemplateSets abstraction I talked about with @dturn . It actually ends up cleaning things up quite a bit since we now have a single API interface to deal with arbitrary numbers of template paths.

I've added unit tests and fleshed out the validation for TemplateSets.

Also realized a silly error I made and corrected it: -f flag can now be invoked multiple times

README.md Outdated Show resolved Hide resolved
lib/kubernetes-deploy/template_sets.rb Outdated Show resolved Hide resolved
lib/kubernetes-deploy/template_sets.rb Outdated Show resolved Hide resolved
Copy link
Contributor

@dturn dturn left a comment

Choose a reason for hiding this comment

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

I really like this refactor, my only real question is the params to new_from_dirs_and_files.

lib/kubernetes-deploy/deploy_task.rb Outdated Show resolved Hide resolved
lib/kubernetes-deploy/options_helper.rb Outdated Show resolved Hide resolved
lib/kubernetes-deploy/template_sets.rb Outdated Show resolved Hide resolved
lib/kubernetes-deploy/template_sets.rb Outdated Show resolved Hide resolved
lib/kubernetes-deploy/template_sets.rb Outdated Show resolved Hide resolved
lib/kubernetes-deploy/template_sets.rb Outdated Show resolved Hide resolved
lib/kubernetes-deploy/template_sets.rb Outdated Show resolved Hide resolved
Copy link
Contributor

@dturn dturn left a comment

Choose a reason for hiding this comment

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

Let's be sure to 🎩 this before merging

lib/kubernetes-deploy/template_sets.rb Outdated Show resolved Hide resolved
Different approach with multiple renderers

rubocop

change ejson_secret_provisioner to handle multiple template dirs

rejig tests due to method signature/name changes

more test updates

actually a tab:

use \t instead of actual tab (for linter)

fixup

refactor to avoid passing template_dirs all around

hacky fix for render task, add test for partial naming collision

test result, not forced failure

deploy_dir -> deploy_dirs

Add assert_logs_match tests to multi-template-dir tests

ensure ejson secrets from multiple template directories are created

rubocop fixes

avoid nilref error possibility in OptionsHelper#default_template_dir

force unique template dirs

support template_dir (deprecated) and template_dirs

TemplateDiscovery -> ResourceDiscovery

rubocop

pass namespace

properly pass filename from raise InvalidTemplateException

reraise Psych::SyntaxError as InvalidTemplateError

error handling dance

yet more error handling

ResourceDiscovery -> LocalResourceDiscovery

whitespace

tidy up DeployTask#discover_resources

reject(&nil?) -> compact

optionshelper test + refactor

rubocop

tidy up exe/kubernetes-deploy for easier deprecation + checking

the return of template discovery + proper -f handling (files + dirs)

tests for filename handling as arg

lots of renaming

deal with ejsonsecretprovisioner case. more naming fixes

TemplateDiscovery cleanup

testing partials, secrets, with filenames

lint test

make render cli work. rename some flag fields

CHANGELOG

handling ejsonsecretprovisioning with new template args

statsh

Move back to multiple ejsonprovisioners. EjsonProvisioner must be supplied a secret, it no longer fetches one itself

fix File.join bug

VALID_EXTENSIONS shouldn't include secrets.ejson

remove unused memo for TemplateDicovery::resource_templates (method is now a class method, not instance)

use fixture_path instead of relative path for test. Remove redundant condition from default_template_dir in options_helper

rubocop

README

some pr review

fix render task no-template-dir-provided case. Pass ejson file, not simply dirname

stash changes

stash

working template_set implementation (need to rejig tests)

rubocop + basename(filename) fix for record_invalid_template"

dealing with secrets, however sloppily

validations beginnings

stash

fix rendering error not propogating message properly

more dir validation

fix test

remove period

code cleanup

remove unused code

ensure we use config/deploy/$ENVIRONMENT if no template_dir(s) given
…ling name as a file, not a dir. Need to fail fast on OptionsHelper instead
@timothysmith0609 timothysmith0609 dismissed KnVerey’s stale review September 4, 2019 17:10

Katrina is off on vacation and this has gone through peer review in her absence

@timothysmith0609 timothysmith0609 merged commit c2e92f1 into master Sep 4, 2019
@timothysmith0609 timothysmith0609 deleted the support_multiple_template_dirs branch September 4, 2019 17:19
@timothysmith0609 timothysmith0609 mentioned this pull request Sep 4, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
🆕 feature Makes something new possible
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants