-
Notifications
You must be signed in to change notification settings - Fork 57
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
Encourage the use of root_path in production to ensure single deployment #1712
base: main
Are you sure you want to change the base?
Changes from 7 commits
2dad625
595beed
821239e
dc63c7c
5436123
3f45f71
6ea5306
c5fa11c
293d780
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,6 +2,7 @@ package mutator | |
|
||
import ( | ||
"context" | ||
"fmt" | ||
"strings" | ||
|
||
"github.com/databricks/cli/bundle" | ||
|
@@ -146,8 +147,21 @@ func validateProductionMode(ctx context.Context, b *bundle.Bundle, isPrincipalUs | |
} | ||
} | ||
|
||
if !isPrincipalUsed && !isRunAsSet(r) { | ||
return diag.Errorf("'run_as' must be set for all jobs when using 'mode: production'") | ||
// We need to verify that there is only a single deployment of the current target. | ||
// The best way to enforce this is to explicitly set root_path. | ||
advice := fmt.Sprintf( | ||
"set 'workspace.root_path' to make sure only one copy is deployed. A common practice is to use a username or principal name in this path, i.e. root_path: /Users/%s/.bundle/${bundle.name}/${bundle.target}", | ||
lennartkats-db marked this conversation as resolved.
Show resolved
Hide resolved
|
||
b.Config.Workspace.CurrentUser.UserName, | ||
) | ||
if !isExplicitRootSet(b) { | ||
if isRunAsSet(r) || isPrincipalUsed { | ||
// Just setting run_as is not enough to guarantee a single deployment, | ||
// and neither is setting a principal. | ||
// We only show a warning for these cases since we didn't historically | ||
// report an error for them. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So this is breaking IFF:
? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We previously showed an error under those conditions ( There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you think we should continue to comment setting If a user goes in and looks at defaults and configures it to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I do think it's a good practice to set There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you think we should de-emphasize There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
return diag.Warningf("target with 'mode: production' should " + advice) | ||
} | ||
return diag.Errorf("target with 'mode: production' must " + advice) | ||
} | ||
return nil | ||
} | ||
|
@@ -164,6 +178,17 @@ func isRunAsSet(r config.Resources) bool { | |
return true | ||
} | ||
|
||
func isExplicitRootSet(b *bundle.Bundle) bool { | ||
lennartkats-db marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if b.Config.Bundle.TargetConfig == nil { | ||
return false | ||
} | ||
targetConfig := b.Config.Bundle.TargetConfig | ||
if targetConfig.Workspace == nil { | ||
return false | ||
} | ||
return targetConfig.Workspace.RootPath != "" | ||
} | ||
|
||
func (m *processTargetMode) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics { | ||
switch b.Config.Bundle.Mode { | ||
case config.Development: | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -295,14 +295,14 @@ func TestProcessTargetModeProduction(t *testing.T) { | |
b := mockBundle(config.Production) | ||
|
||
diags := validateProductionMode(context.Background(), b, false) | ||
require.ErrorContains(t, diags.Error(), "run_as") | ||
require.ErrorContains(t, diags.Error(), "target with 'mode: production' must set 'workspace.root_path' to make sure only one copy is deployed. A common practice is to use a username or principal name in this path, i.e. root_path: /Users/[email protected]/.bundle/${bundle.name}/${bundle.target}") | ||
|
||
b.Config.Workspace.StatePath = "/Shared/.bundle/x/y/state" | ||
b.Config.Workspace.ArtifactPath = "/Shared/.bundle/x/y/artifacts" | ||
b.Config.Workspace.FilePath = "/Shared/.bundle/x/y/files" | ||
|
||
diags = validateProductionMode(context.Background(), b, false) | ||
require.ErrorContains(t, diags.Error(), "production") | ||
require.ErrorContains(t, diags.Error(), "target with 'mode: production' must set 'workspace.root_path' to make sure only one copy is deployed. A common practice is to use a username or principal name in this path, i.e. root_path: /Users/[email protected]/.bundle/${bundle.name}/${bundle.target}") | ||
|
||
permissions := []resources.Permission{ | ||
{ | ||
|
@@ -346,6 +346,23 @@ func TestProcessTargetModeProductionOkForPrincipal(t *testing.T) { | |
require.NoError(t, diags.Error()) | ||
} | ||
|
||
func TestProcessTargetModeProductionOkWithRootPath(t *testing.T) { | ||
b := mockBundle(config.Production) | ||
|
||
// Our target has all kinds of problems when not using service principals ... | ||
diags := validateProductionMode(context.Background(), b, false) | ||
require.Error(t, diags.Error()) | ||
|
||
// ... but we're okay if we specify a root path | ||
b.Config.Bundle.TargetConfig = &config.Target{ | ||
Workspace: &config.Workspace{ | ||
RootPath: "some-root-path", | ||
}, | ||
} | ||
diags = validateProductionMode(context.Background(), b, false) | ||
require.NoError(t, diags.Error()) | ||
} | ||
|
||
// Make sure that we have test coverage for all resource types | ||
func TestAllResourcesMocked(t *testing.T) { | ||
b := mockBundle(config.Development) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -15,6 +15,7 @@ type selectTarget struct { | |
} | ||
|
||
// SelectTarget merges the specified target into the root configuration. | ||
// After merging, it removes the 'Targets' section from the configuration. | ||
func SelectTarget(name string) bundle.Mutator { | ||
return &selectTarget{ | ||
name: name, | ||
|
@@ -31,7 +32,7 @@ func (m *selectTarget) Apply(_ context.Context, b *bundle.Bundle) diag.Diagnosti | |
} | ||
|
||
// Get specified target | ||
_, ok := b.Config.Targets[m.name] | ||
targetConfig, ok := b.Config.Targets[m.name] | ||
if !ok { | ||
return diag.Errorf("%s: no such target. Available targets: %s", m.name, strings.Join(maps.Keys(b.Config.Targets), ", ")) | ||
} | ||
|
@@ -43,13 +44,15 @@ func (m *selectTarget) Apply(_ context.Context, b *bundle.Bundle) diag.Diagnosti | |
} | ||
|
||
// Store specified target in configuration for reference. | ||
b.Config.Bundle.TargetConfig = targetConfig | ||
b.Config.Bundle.Target = m.name | ||
|
||
// We do this for backward compatibility. | ||
// TODO: remove when Environments section is not supported anymore. | ||
b.Config.Bundle.Environment = b.Config.Bundle.Target | ||
|
||
// Clear targets after loading. | ||
// Cleanup the original targets and environments sections since they | ||
// show up in the JSON output of the 'summary' and 'validate' commands. | ||
b.Config.Targets = nil | ||
b.Config.Environments = nil | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Instead of keeping these around here, could you break out a field on the bundle struct where we can keep a snapshot of the selected target? Then you can interrogate it and there's no risk of other mutators changing it after selection. The targets in the configuration have no significance beyond this point. E.g. // Target stores a snapshot of the target configuration when it was selected.
Target *config.Target There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Isn't it cleaner to just remove the side effect from There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What I see as a risk is that keeping them around means another location that new mutators can go and look at, even though everything under targets no longer has any effect. Variable interpolation won't run either, so values under it shouldn't be used. I see how this is most convenient though. @andrewnester What do you think? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's just that over the past year I ran into this problem twice. You need to use a step through debugger to find where this property is secretly deleted. And the code that deletes it includes no rationale and is just meant to select the default target. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Alright, I don't want to leave this PR open just because we can't make a decision here. I added 6ea5306 which merges the cleanup behavior back into SelectTarget and adds a few comments about the behavior for maintainers. |
||
|
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.
If located here, it is a part of the configuration, therefore the schema, the output of validate, etc.
Please move this to be a property on the
bundle.Bundle
type to avoid this.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, yes, you're right. I moved it.
Do you still think it's the right balance to have that extra property there. It's an extra field that copies the bundle config but it only has a value depending on which state the deployment is in. I'm okay with the proposed approach but it seems cleaner to me to have fewer properties and to do a cleanup step just before summary/validate.