-
Notifications
You must be signed in to change notification settings - Fork 9.6k
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
Environments #12182
Environments #12182
Conversation
4986ce6
to
5b6df06
Compare
After discussing with @apparentlymart, we agree that subcommands are a cleaner solution than command flags and flag-specific flags. The awkwardness seems to be around the bare How about the following command set?
I'm not opposed to |
This subcommand-based interface makes good sense to me. Perhaps a reasonable compromise on the bare
This way the output tells us where we are and where we can go from here. I presume here that running e.g. Some of the subcommand flags could be progressively discovered through good error messages. For example:
The |
6dfc6b9
to
4714299
Compare
Pushed with the new subcommands, but I'm retracting the current env output from the bare |
@jbardin this is a really interesting concept; you mentioned an RFC in your original description. Is that something shareable here? I'd personally love to read a bit more about the backstory of this and intended use cases etc. Thanks! |
Sorry, I don't have anything I can share, but at this point this PR probably contains more relevant info than the original RFC ;). The immediate use case for most developers is replacing the workflow of having multiple state files manually managed and specified at runtime, to a workflow of using environments to manage those state files in a less error-prone way. I think the only missing concept here is that "Enhanced Backends" will have the ability to extend the idea of an "environment" further, as they handle the Terraform operations directly. What this means precisely will depend on the backend implementation. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The CLI looks super good, very little to no comments there. Some seemingly major but probably not so bad feedback on the main implementation. I think overall its probably mostly just bit shuffling a bit but important for the semantics.
We can chat in slack this week too! Almost there, looks really good.
backend/local/backend.go
Outdated
|
||
// workingDir is where the State* paths should be relative to. | ||
// This is currently only used for tests. | ||
workingDir string |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd avoid this and use testChdir
. We actually USED to have a field like this but I've tried to make helpers to make sure we're executing code identical as much as possible (we're not perfect yet).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I agree. This was solely to check be able to differentiate 2 backends in a test, where I should have made a test implementation instead.
backend/backend.go
Outdated
@@ -27,6 +29,22 @@ type Backend interface { | |||
State() (state.State, error) | |||
} | |||
|
|||
// MultiState is an interface that a backend can implement to allow changing | |||
// between named states depending on the configured environment. | |||
type MultiState interface { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd prefer to make this required for all states (make it part of Backend).
For legacy remote state we can return an error in the actual remote-state.Backend
implementation instead.
The reason being is that going forward I don't want this to be optional and introduce more optional interfaces. It is difficult now due to backwards compatibility but if we force all new remote implementations to support it and only error for legacy then we can slowly fix them over one by one as we convert to the new API.
I believe that should be fully backwards compatible and simplifies all the checking.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like to err on the side of smaller interfaces (isn't there some tao of Go quote about small interfaces? 😉 ) Though if we do want to enforce this it's not too unwieldy and not nearly the largest in terraform.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It may be, but I also think an interface should represent the full set of functionality required to make it useful. In the context of Terraform, I believe that is loading AND changing states, so splitting it into two required interface seems more of a mental burden than having one required interface.
So I think it should be part of the one interface.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yup, advocating the alternative if only to keep ourselves in line ;)
backend/backend.go
Outdated
// between named states depending on the configured environment. | ||
type MultiState interface { | ||
// States returns a list of configured named states and the current state. | ||
States() ([]string, string, error) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The backend shouldn't be responsible for "current" state since that should be local to the user (i.e. with the same remote backend on two different machines one user can point to one env and another can point to another. The backend itself has no concept of "current").
The "current" state should be stored locally in the terraform cache dir (.terraform so it is VCS ignored).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, this makes sense, and I clears up part of what I wanted to cover in this first review. The original spec called for a remote ChangeState method, which implied the backend also managed the current state. Deferring to the backend was much less intrusive in the terraform code, not even accounting for possible synchronization issues between the local environment and backend. Removing the backend from being aware of "current" at all makes this work much better.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah I can see how that is confusing. Sorry!
Given that API, maybe the behavior should be to remove ChangeState
and just have State(name)
, DeleteState(name)
, States() []string error
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, that's what I was about to implement. I can't think of a reason to require a separate CreateState(string)
method, when State
could handle it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed, and state existence when switching to it can live in the CLI itself. I think thats still important to avoid a typo when switching states.
backend/local/backend.go
Outdated
// the listing always start with "default" | ||
envs := []string{backend.DefaultStateName} | ||
|
||
current, err := b.currentStateName() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as above, "current" state should be a TF core thing and backends should just be told directly what state to load. This will simplify backend implementation a lot and shift that complexity into core but at least its shared.
backend/local/backend.go
Outdated
} | ||
|
||
// if we're deleting the current state, we change back to the default | ||
if name == current { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Deleting the current state isn't allowed (as per the spec, we have to show an error and tell the user to switch to another state first). This protects the user by making it so they 1.) must have another state to go to and 2.) can't easily delete states.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh yeas, I missed that and figured it OK since state was protected if it's not empty. Easy change.
backend/remote-state/backend.go
Outdated
@@ -46,6 +47,14 @@ func (b *Backend) Configure(rc *terraform.ResourceConfig) error { | |||
return b.Backend.Configure(rc) | |||
} | |||
|
|||
func (b *Backend) States() ([]string, string, error) { | |||
return []string{backend.DefaultStateName}, backend.DefaultStateName, nil |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As per the comments above: this is where I'd move the "multiple states not supported" errors.
Add the missing methods/arguments to handle Terraform environments in Backends. Extra functionality simply returns defaults for now.
Add the functionality required for terraform environments
The Local backend is now responsible for handling the paths to the local state files, since they are dependent on the current environment.
Split the interface to change environments out from the minimal Backend interface, to make it optional for backend implementations. If backend.MultiState isn't implemented, return a "not implemented" from environment related methods. Have the Local backend delegate the MultiState methods to the proper backend.
Ensure that when MultiState methods are properly delegated when there is a defined Local.Backend.
Used a single command with flags for now. We may refactor this out to subcommands.
In order to operate in parity with other commands, the env command should take a path argument to locate the configuration. This however introduces the issue of a possible name conflict between a path and subcommand, or printing an incorrect current environment for the bare `env` command. In favor of simplicity this removes the current env output and only prints usage when no subcommand is provided.
Forgot to remove the currentState field, which was not always set. The current state should always just be read from the environment file. Always return the default state name when we can't determine the state.
Destroying a terraform state can't always create an empty state, as outputs and the root module may remain. Use HasResources to warn about deleting an environment with resources.
These were left from the initial implementation, but are not used.
What will hopfully be the final version of the Backend interface. This combines the MultiState interface into Backend since it will be required to implement, and simplifies the interface because the Backend is no longer responsible for tracking the current state.
Update the methods, remove the handling of "current", and make tests pass.
f97866d
to
c30eee1
Compare
move the unsupported error value to backend.ErrNamedStatesNotSupported to be used by any backend implementation.
Add Env and SetEnv methods to command.Meta to retrieve the current environment name inside any command. Make sure all calls to Backend.State contain an environment name, and make the package compile against the update backend package.
Add an `environment` field to the terraform remote state data source.
c30eee1
to
dc67554
Compare
add a test to ensure we have consistent output
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One nitpick.
Looks great! I think we got it. :)
One question: why did we have to change States to embed StateMeta vs. Meta?
command/env_delete.go
Outdated
|
||
// Lock the state if we can | ||
lockInfo := state.NewLockInfo() | ||
lockInfo.Operation = "env new" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"env delete" probably
command/state_meta.go
Outdated
@@ -23,8 +25,9 @@ func (c *StateMeta) State(m *Meta) (state.State, error) { | |||
return nil, err | |||
} | |||
|
|||
env := c.Env() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
May want to be using m.Env()
here. Practically I don't think it makes a difference currently but maybe one day we may pass in a differently configured meta here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, ok. This was the reason for your question above, in that StateMeta was calling the embedded Meta.Env method, and embedding Meta in StateMeta was just the cleanest way to ensure all State commands also had the same method.
Looking at this though, I think we can also just drop the Meta param, since StateMeta
would now have the correctly configured Meta for the command embedded.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, actually there is no more Meta.State with the new backends, so this structure probably isn't needed at all.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I finally see what the second Meta is for now. I'm going to split the Meta back out just to alleviate the confusion, since StateMeta
doesn't it need.
Removing the call to StateMeta.Env, so that it doesn't need an embedded Meta field. Embed Meta and StateMeta separately in all State commands.
d8c9523
to
39a5ddd
Compare
Fixed lock reason for |
I'm going to lock this issue because it has been closed for 30 days ⏳. This helps our maintainers find and focus on the active issues. If you have found a problem that seems similar to this, please open a new issue and complete the issue template so we can capture all the details necessary to investigate further. |
This starts support for "environments" in the backends.
Environments, aka "named states", are optionally supported by Backend implementations. Because they are optional, the corresponding method set has been split out into its own interface:
This diverges slightly from the RFC in a few ways.
name
argument added to the State method. This would change a lot of code, and since the backend must already know about the current state, it is mostly superfluous. The primary drawback is that it requires a call toChangeState
to fetch a particular state.States()
returns the list of known states, and the current state. The alleviates thestate.State
from having to know about its name.DeleteState
.An "environment" corresponds to a named state. Environments can be manipulated through the new
terraform env
command. This is implemented as a single command with flags for convenience, but could be refactored into subcommands if it improves the UI.