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

feat: inline Environment #403

Merged
merged 16 commits into from
Nov 26, 2020
Merged

feat: inline Environment #403

merged 16 commits into from
Nov 26, 2020

Conversation

Duologic
Copy link
Member

@Duologic Duologic commented Oct 11, 2020

Building further on #389, with implementing the environment inline there is no more need to encapsulate a Tanka Environment in a directory.

This proposes a solution for #390.

Might also help for #394 as the "spec" is now evaluated in Jsonnet.

This PR will allow us to use the Environment inline, like this:

{
  apiVersion: 'tanka.dev/v1alpha1',
  kind: 'Environment',
  metadata: {
    name: 'myenv',
  },
  spec: {
    apiServer: 'https://localhost',
    namespace: 'myns',
  },
  data: {
    // put here what used to be in main.jsonnet
    // or just (import 'main.jsonnet') and call this file with tanka directly
  },
}

It is fully backwards compatible but has a few drawbacks:

  • The tanka.dev/Environment extCode is based on the premise that the env is availabe before runtime, it is now generated at runtime, it doesn't make much sense to read env from tk.env now as the env is now available inside your jsonnet anyway.
  • tk env is build upon the spec.json, it will be hard to support inline environments with this see below
  • tk complete might need some adjustments

On tk env:

  • tk env add and tk env remove will be fairly easy to support, just need to decide on what the default layout is for add.
  • tk env list can become significantly slower as the jsonnet has to be evaluated for an Environment object to exist, we can improve on speed with go routines and only evaluate if spec.json doesn't exists I've fixed this by wrapping a snippet around the jsonnet removing the data: object in 2ee1696
  • tk env set would be really hard to port over, you'd be modifying someone's jsonnet code.

@Duologic Duologic changed the title feat: allow inline Environment feat: inline Environment Oct 11, 2020
@Duologic Duologic marked this pull request as ready for review October 11, 2020 16:18
@Duologic
Copy link
Member Author

rebased.

pkg/spec/v1alpha1/config.go Outdated Show resolved Hide resolved
@Duologic
Copy link
Member Author

rebased.

@jdbaldry
Copy link
Member

Also, I think the current main.jsonnet/spec.json approach is actually very beginner friendly
I agree, I don't think we should change the defaults.

My first thought is that option 1. is closer to existing behavior which I like but I'm also keen on how 2. looks aesthetically. 2. adjusts the relationship between the environment and data (environment contains data rather than being composed with it), and I'm not sure if there is enough value to justify confusing that relationship.

That being said I'm not sure which of the two relationships is objectively better out of:

  • environment contains data
  • environment and data are composed together

@malcolmholmes
Copy link
Collaborator

malcolmholmes commented Oct 26, 2020

That being said I'm not sure which of the two relationships is objectively better out of:

  • environment contains data
  • environment and data are composed together

To really get a sense of where this is going, it helps to look at the next step that @Duologic has in mind, where you can have multiple environments within one setup. That I think will drive exactly how this is expressed.

@Duologic
Copy link
Member Author

I'm wondering whether we should do it in one go but I'm worried this PR becomes to big.

@Duologic
Copy link
Member Author

Please have a look at #414, which can replace this PR.

@captncraig
Copy link
Contributor

I really like the possibilities that this brings. A few thoughts:

  1. I feel it is fair to assume there will be either a spec.json or a main.jsonnet that evaluates to a single environment having spec and data fields. I like enforced structure and prefer checked assumptions to supporting any desired style.
  2. If we don't detect a spec.json, we can assume we are in the "new" mode and do a first pass that evaluates .spec to get all of the metadata we need. That will let us support tk env and friends with less overhead, as well as keeping the extCodes working and things.
  3. I am still not sold on feat: multiple inline environments #414, since running multiple environments is a much larger paradigm shift in the workflows around tanka. It is a lot more involved than slightly refactoring the input format.

@sh0rez
Copy link
Member

sh0rez commented Nov 2, 2020

I'm a bit out here and surely didn't attend all discussions, so I might miss obvious things.

I'm generally on the lines of @captncraig, where I am in favor of a slight refactor / workflow change (this PR), but #414 seems more far-reaching to me. I do not fully understand the use-case for #414, but I am sure @Duologic can explain that to me.

Some random thoughts I had while reading along:

If we don't detect a spec.json, we can assume we are in the "new" mode

Existing behavior explicitly supports not having a spec.json, resulting in namespace=default and an empty apiServer url. We need to take care not to break users in unexpected ways.

either a spec.json or a main.jsonnet

We introduce more than one way to do the same thing here, mostly for compatibility reasons.

I'd really like to avoid a "basic" and "advanced" mode. Could we instead identify what makes the Jsonnet mode harder and make it much simpler so we can ship it as the one and only way?

@Duologic
Copy link
Member Author

Duologic commented Nov 2, 2020

I'm generally on the lines of @captncraig, where I am in favor of a slight refactor / workflow change (this PR), but #414 seems more far-reaching to me. I do not fully understand the use-case for #414, but I am sure @Duologic can explain that to me.

As mentioned in #414, it proposes an answer to the question "What if the jsonnet has multiple environments?" This can also be answered with an error message saying Tanka won't handle multiple environments at once, that seems a bit dense to me as it is perfectly possible to handle multiple environments at once.

I'd really like to avoid a "basic" and "advanced" mode. Could we instead identify what makes the Jsonnet mode harder and make it much simpler so we can ship it as the one and only way?

The "advanced mode" does not necessarily reflect on Tanka, the way of working (basic/advanced) becomes a matter of applying conventions to your jsonnet code base instead of imposing restrictions by the runtime (Tanka). With saying basic, I mean defining the current approach as the simplest convention possible on how to use Tanka. If we want to bring out a different convention as the basic use case, then we can, for both the single environment and multiple environments pull requests.

There are some implementation details I've put into place on #414 (like finding the environments), I'd like to backport to this PR in case #414 doesn't get accepted. This would allow us to simply pull all Environments out of the jsonnet code, and if there is more than 1, we can return aforementioned error instead of trying to apply them. done

@Duologic
Copy link
Member Author

Duologic commented Nov 4, 2020

re data::

As I said before, the argument for making data part of the Environment object is to ensure the jsonnet is
deterministic about what it represents.

Any other the Jsonnet objects can exist as siblings to the Environment object, removing the assumption that all visible
objects are Kubernetes objects. This brings opportunities here for other tools to jump less hoops for the same jsonnet
codebase. (grizzly hacks in making $.grafanaDashboards visible at runtime, for example)

re spec.json behavior:

The code currently looks for an Environment object, if that doesn't exist it assumes the old spec.json behavior. This is
required to keep backwards compatibility. We can deprecate that way of working over time, but that doesn't need to be
decided here.

This moves the naming to the ParseDir() function as naming based on
directory name only makes sense in the context of spec.json. Inline
environments can technically be run form anywhere, only convention keeps
them in this assumed directory structure.
pkg/tanka/parse.go Outdated Show resolved Hide resolved
pkg/tanka/parse.go Outdated Show resolved Hide resolved
pkg/spec/v1alpha1/config.go Outdated Show resolved Hide resolved
type evaluateFunc func(path string, opts jsonnet.Opts) (string, error)

// parseEnv finds the Environment object at the given path
func parseEnv(path string, opts jsonnet.Opts, evalFn evaluateFunc) (interface{}, *v1alpha1.Config, error) {
Copy link
Member

Choose a reason for hiding this comment

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

Injecting big amounts of logic as a anonymous function is very hard to debug (behavior no longer obvious from function body). I will work with @Duologic to find a better way here. I wish to hold this PR until that happened, if that's okay (@malcolmholmes)

Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm happy with the functionality of this PR as it is. I agree with the code being a bit more complex than it needs to be, but am also happy to see that addressed in a subsequent PR.

Copy link
Member

@sh0rez sh0rez left a comment

Choose a reason for hiding this comment

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

Looks good! Some minor findings and this is good to be merged

pkg/spec/v1alpha1/environment.go Outdated Show resolved Hide resolved
Comment on lines 186 to 189
func Eval(dir string, opts Opts) (raw interface{}, err error) {
r, _, err := eval(dir, opts.JsonnetOpts)
if err != nil {
return nil, err
}
return r, nil
return r, err
}
Copy link
Member

Choose a reason for hiding this comment

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

What's the reason for removing this? Well behaved functions should not return data in case of errors

Copy link
Member Author

Choose a reason for hiding this comment

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

If r is not nil, then the evaluation succeeded, the error could still be that there is no Environment but still evaluate the jsonnet fine. Do you think I should catch that err type and only return r, nil if it isn't about the missing environment?

Copy link
Member Author

Choose a reason for hiding this comment

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

PTAL, I catch a few errors that have no effect on the eval itself and print them as log messages instead.

pkg/tanka/parse.go Outdated Show resolved Hide resolved
}

// ErrMultipleEnvs means that the given jsonnet has multiple Environment objects
type ErrMultipleEnvs struct {
Copy link
Member

Choose a reason for hiding this comment

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

Could this error message include the names of the environments? Makes it far easier to debug then

Copy link
Member Author

Choose a reason for hiding this comment

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

config := v1alpha1.New()
if err := json.Unmarshal(data, config); err != nil {
return nil, errors.Wrap(err, "parsing spec.json")
}

// set the name field
config.Metadata.Name = name
Copy link
Member

Choose a reason for hiding this comment

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

If we no longer set the name ourselves, we should enforce some name is set

Copy link
Member Author

Choose a reason for hiding this comment

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

I've covered for this in 4ee8de7

@Duologic Duologic requested a review from sh0rez November 20, 2020 09:27
if err != nil {
switch err.(type) {
case ErrMultipleEnvs:
log.Println(err)
Copy link
Member

Choose a reason for hiding this comment

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

I must admit I can't quite follow this code. To me this seems as encountering multiple environments is no longer a fatal thing, but only a warning, given no error is returned.

I'd expect Eval to behave like so:

  • return an error everytime an error occured (e.g. invalid Jsonnet, no Environment, multiple Environments, etc)
  • as you need to handle the "no inline but maybe spec.json" case further down the road, this specific case should also return data

This allows a naive callee to automatically abort when any error occurs. Callees aware of inline vs spec.json can react to receiving ErrNoEnv

Copy link
Member Author

Choose a reason for hiding this comment

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

That is what I did before, simply return both data and err from upstream to downstream, no handling here.

Copy link
Member

Choose a reason for hiding this comment

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

Sure, but this is different from what I just described.

Only the specific case of "no inline environment" has a reason to return data alongside an error. Afaict, all other errors that occur here are fatal and thus return nil, err

Copy link
Member Author

Choose a reason for hiding this comment

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

Okay, so here you say return data+err? Gotcha.

Copy link
Member

Choose a reason for hiding this comment

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

Yes, which you could simplify along those lines:

// Eval returns the raw evaluated Jsonnet output (without any transformations)
func Eval(dir string, opts Opts) (raw interface{}, err error) {
	r, _, err := eval(dir, opts.JsonnetOpts)
        if errors.Is(err, ErrNoEnv) {
                return r, err
        } else if err != nil {
                return nil, err
        }

	return r, nil
}

@Duologic Duologic merged commit 12f7716 into master Nov 26, 2020
@Duologic Duologic deleted the duologic/inline_environment branch November 26, 2020 21:07
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.

None yet

5 participants