Move mutator interface to top level bundle package (#105)

While working on artifact upload and workspace interrogation I realized
this mutator interface needs to:
1. Operate at the whole bundle level so it can apply to both
configuration and internal state
2. Include a `context.Context` parameter for a) long running operations
and b) progress reporting

Previous interface:
```
Apply(*config.Root) ([]Mutator, error)
```

New interface:
```
Apply(context.Context, *Bundle) ([]Mutator, error)
```
This commit is contained in:
Pieter Noordhuis 2022-11-28 10:59:43 +01:00 committed by GitHub
parent 5c916a6fb4
commit b88b35a510
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 308 additions and 213 deletions

View File

@ -1,12 +1,10 @@
package bundle package bundle
import ( import (
"context"
"path/filepath" "path/filepath"
"sync" "sync"
"github.com/databricks/bricks/bundle/config" "github.com/databricks/bricks/bundle/config"
"github.com/databricks/bricks/bundle/config/mutator"
"github.com/databricks/databricks-sdk-go" "github.com/databricks/databricks-sdk-go"
) )
@ -19,10 +17,6 @@ type Bundle struct {
client *databricks.WorkspaceClient client *databricks.WorkspaceClient
} }
func (b *Bundle) MutateForEnvironment(env string) error {
return mutator.Apply(&b.Config, mutator.DefaultMutatorsForEnvironment(env))
}
func Load(path string) (*Bundle, error) { func Load(path string) (*Bundle, error) {
bundle := &Bundle{ bundle := &Bundle{
Config: config.Root{ Config: config.Root{
@ -45,20 +39,6 @@ func LoadFromRoot() (*Bundle, error) {
return Load(root) return Load(root)
} }
func ConfigureForEnvironment(ctx context.Context, env string) (context.Context, error) {
b, err := LoadFromRoot()
if err != nil {
return nil, err
}
err = b.MutateForEnvironment(env)
if err != nil {
return nil, err
}
return Context(ctx, b), nil
}
func (b *Bundle) WorkspaceClient() *databricks.WorkspaceClient { func (b *Bundle) WorkspaceClient() *databricks.WorkspaceClient {
b.clientOnce.Do(func() { b.clientOnce.Do(func() {
var err error var err error

View File

@ -15,7 +15,7 @@ func TestLoadNotExists(t *testing.T) {
} }
func TestLoadExists(t *testing.T) { func TestLoadExists(t *testing.T) {
b, err := Load("./config/tests/basic") b, err := Load("./tests/basic")
require.Nil(t, err) require.Nil(t, err)
assert.Equal(t, "basic", b.Config.Bundle.Name) assert.Equal(t, "basic", b.Config.Bundle.Name)
} }

View File

@ -1,8 +1,10 @@
package mutator package mutator
import ( import (
"context"
"fmt" "fmt"
"github.com/databricks/bricks/bundle"
"github.com/databricks/bricks/bundle/config" "github.com/databricks/bricks/bundle/config"
) )
@ -12,7 +14,7 @@ type defineDefaultEnvironment struct {
// DefineDefaultEnvironment adds an environment named "default" // DefineDefaultEnvironment adds an environment named "default"
// to the configuration if none have been defined. // to the configuration if none have been defined.
func DefineDefaultEnvironment() Mutator { func DefineDefaultEnvironment() bundle.Mutator {
return &defineDefaultEnvironment{ return &defineDefaultEnvironment{
name: "default", name: "default",
} }
@ -22,14 +24,14 @@ func (m *defineDefaultEnvironment) Name() string {
return fmt.Sprintf("DefineDefaultEnvironment(%s)", m.name) return fmt.Sprintf("DefineDefaultEnvironment(%s)", m.name)
} }
func (m *defineDefaultEnvironment) Apply(root *config.Root) ([]Mutator, error) { func (m *defineDefaultEnvironment) Apply(_ context.Context, b *bundle.Bundle) ([]bundle.Mutator, error) {
// Nothing to do if the configuration has at least 1 environment. // Nothing to do if the configuration has at least 1 environment.
if root.Environments != nil || len(root.Environments) > 0 { if b.Config.Environments != nil || len(b.Config.Environments) > 0 {
return nil, nil return nil, nil
} }
// Define default environment. // Define default environment.
root.Environments = make(map[string]*config.Environment) b.Config.Environments = make(map[string]*config.Environment)
root.Environments[m.name] = &config.Environment{} b.Config.Environments[m.name] = &config.Environment{}
return nil, nil return nil, nil
} }

View File

@ -1,8 +1,10 @@
package mutator_test package mutator_test
import ( import (
"context"
"testing" "testing"
"github.com/databricks/bricks/bundle"
"github.com/databricks/bricks/bundle/config" "github.com/databricks/bricks/bundle/config"
"github.com/databricks/bricks/bundle/config/mutator" "github.com/databricks/bricks/bundle/config/mutator"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -10,22 +12,24 @@ import (
) )
func TestDefaultEnvironment(t *testing.T) { func TestDefaultEnvironment(t *testing.T) {
root := &config.Root{} bundle := &bundle.Bundle{}
_, err := mutator.DefineDefaultEnvironment().Apply(root) _, err := mutator.DefineDefaultEnvironment().Apply(context.Background(), bundle)
require.NoError(t, err) require.NoError(t, err)
env, ok := root.Environments["default"] env, ok := bundle.Config.Environments["default"]
assert.True(t, ok) assert.True(t, ok)
assert.Equal(t, &config.Environment{}, env) assert.Equal(t, &config.Environment{}, env)
} }
func TestDefaultEnvironmentAlreadySpecified(t *testing.T) { func TestDefaultEnvironmentAlreadySpecified(t *testing.T) {
root := &config.Root{ bundle := &bundle.Bundle{
Environments: map[string]*config.Environment{ Config: config.Root{
"development": {}, Environments: map[string]*config.Environment{
"development": {},
},
}, },
} }
_, err := mutator.DefineDefaultEnvironment().Apply(root) _, err := mutator.DefineDefaultEnvironment().Apply(context.Background(), bundle)
require.NoError(t, err) require.NoError(t, err)
_, ok := root.Environments["default"] _, ok := bundle.Config.Environments["default"]
assert.False(t, ok) assert.False(t, ok)
} }

View File

@ -1,7 +1,9 @@
package mutator package mutator
import ( import (
"github.com/databricks/bricks/bundle/config" "context"
"github.com/databricks/bricks/bundle"
"golang.org/x/exp/slices" "golang.org/x/exp/slices"
) )
@ -10,7 +12,7 @@ type defineDefaultInclude struct {
} }
// DefineDefaultInclude sets the list of includes to a default if it hasn't been set. // DefineDefaultInclude sets the list of includes to a default if it hasn't been set.
func DefineDefaultInclude() Mutator { func DefineDefaultInclude() bundle.Mutator {
return &defineDefaultInclude{ return &defineDefaultInclude{
// When we support globstar we can collapse below into a single line. // When we support globstar we can collapse below into a single line.
include: []string{ include: []string{
@ -26,9 +28,9 @@ func (m *defineDefaultInclude) Name() string {
return "DefineDefaultInclude" return "DefineDefaultInclude"
} }
func (m *defineDefaultInclude) Apply(root *config.Root) ([]Mutator, error) { func (m *defineDefaultInclude) Apply(_ context.Context, b *bundle.Bundle) ([]bundle.Mutator, error) {
if len(root.Include) == 0 { if len(b.Config.Include) == 0 {
root.Include = slices.Clone(m.include) b.Config.Include = slices.Clone(m.include)
} }
return nil, nil return nil, nil
} }

View File

@ -1,17 +1,18 @@
package mutator_test package mutator_test
import ( import (
"context"
"testing" "testing"
"github.com/databricks/bricks/bundle/config" "github.com/databricks/bricks/bundle"
"github.com/databricks/bricks/bundle/config/mutator" "github.com/databricks/bricks/bundle/config/mutator"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestDefaultInclude(t *testing.T) { func TestDefaultInclude(t *testing.T) {
root := &config.Root{} bundle := &bundle.Bundle{}
_, err := mutator.DefineDefaultInclude().Apply(root) _, err := mutator.DefineDefaultInclude().Apply(context.Background(), bundle)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, []string{"*.yml", "*/*.yml"}, root.Include) assert.Equal(t, []string{"*.yml", "*/*.yml"}, bundle.Config.Include)
} }

View File

@ -1,46 +1,17 @@
package mutator package mutator
import "github.com/databricks/bricks/bundle/config" import (
"github.com/databricks/bricks/bundle"
)
// Mutator is the interface types that mutate the bundle configuration. func DefaultMutators() []bundle.Mutator {
// This makes every mutation observable and debuggable. return []bundle.Mutator{
type Mutator interface {
// Name returns the mutators name.
Name() string
// Apply mutates the specified configuration object.
// It optionally returns a list of mutators to invoke immediately after this mutator.
// This is used when processing all configuration files in the tree; each file gets
// its own mutator instance.
Apply(*config.Root) ([]Mutator, error)
}
func DefaultMutators() []Mutator {
return []Mutator{
DefineDefaultInclude(), DefineDefaultInclude(),
ProcessRootIncludes(), ProcessRootIncludes(),
DefineDefaultEnvironment(), DefineDefaultEnvironment(),
} }
} }
func DefaultMutatorsForEnvironment(env string) []Mutator { func DefaultMutatorsForEnvironment(env string) []bundle.Mutator {
return append(DefaultMutators(), SelectEnvironment(env)) return append(DefaultMutators(), SelectEnvironment(env))
} }
func Apply(root *config.Root, ms []Mutator) error {
if len(ms) == 0 {
return nil
}
for _, m := range ms {
ms_, err := m.Apply(root)
if err != nil {
return err
}
// Apply recursively.
err = Apply(root, ms_)
if err != nil {
return err
}
}
return nil
}

View File

@ -1,8 +1,10 @@
package mutator package mutator
import ( import (
"context"
"fmt" "fmt"
"github.com/databricks/bricks/bundle"
"github.com/databricks/bricks/bundle/config" "github.com/databricks/bricks/bundle/config"
) )
@ -12,7 +14,7 @@ type processInclude struct {
} }
// ProcessInclude loads the configuration at [fullPath] and merges it into the configuration. // ProcessInclude loads the configuration at [fullPath] and merges it into the configuration.
func ProcessInclude(fullPath, relPath string) Mutator { func ProcessInclude(fullPath, relPath string) bundle.Mutator {
return &processInclude{ return &processInclude{
fullPath: fullPath, fullPath: fullPath,
relPath: relPath, relPath: relPath,
@ -23,10 +25,10 @@ func (m *processInclude) Name() string {
return fmt.Sprintf("ProcessInclude(%s)", m.relPath) return fmt.Sprintf("ProcessInclude(%s)", m.relPath)
} }
func (m *processInclude) Apply(root *config.Root) ([]Mutator, error) { func (m *processInclude) Apply(_ context.Context, b *bundle.Bundle) ([]bundle.Mutator, error) {
this, err := config.Load(m.fullPath) this, err := config.Load(m.fullPath)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return nil, root.Merge(this) return nil, b.Config.Merge(this)
} }

View File

@ -1,11 +1,13 @@
package mutator_test package mutator_test
import ( import (
"context"
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"testing" "testing"
"github.com/databricks/bricks/bundle"
"github.com/databricks/bricks/bundle/config" "github.com/databricks/bricks/bundle/config"
"github.com/databricks/bricks/bundle/config/mutator" "github.com/databricks/bricks/bundle/config/mutator"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -13,22 +15,24 @@ import (
) )
func TestProcessInclude(t *testing.T) { func TestProcessInclude(t *testing.T) {
root := &config.Root{ bundle := &bundle.Bundle{
Path: t.TempDir(), Config: config.Root{
Workspace: config.Workspace{ Path: t.TempDir(),
Host: "foo", Workspace: config.Workspace{
Host: "foo",
},
}, },
} }
relPath := "./file.yml" relPath := "./file.yml"
fullPath := filepath.Join(root.Path, relPath) fullPath := filepath.Join(bundle.Config.Path, relPath)
f, err := os.Create(fullPath) f, err := os.Create(fullPath)
require.NoError(t, err) require.NoError(t, err)
fmt.Fprint(f, "workspace:\n host: bar\n") fmt.Fprint(f, "workspace:\n host: bar\n")
f.Close() f.Close()
assert.Equal(t, "foo", root.Workspace.Host) assert.Equal(t, "foo", bundle.Config.Workspace.Host)
_, err = mutator.ProcessInclude(fullPath, relPath).Apply(root) _, err = mutator.ProcessInclude(fullPath, relPath).Apply(context.Background(), bundle)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, "bar", root.Workspace.Host) assert.Equal(t, "bar", bundle.Config.Workspace.Host)
} }

View File

@ -1,9 +1,11 @@
package mutator package mutator
import ( import (
"context"
"fmt" "fmt"
"path/filepath" "path/filepath"
"github.com/databricks/bricks/bundle"
"github.com/databricks/bricks/bundle/config" "github.com/databricks/bricks/bundle/config"
"golang.org/x/exp/slices" "golang.org/x/exp/slices"
) )
@ -12,7 +14,7 @@ type processRootIncludes struct{}
// ProcessRootIncludes expands the patterns in the configuration's include list // ProcessRootIncludes expands the patterns in the configuration's include list
// into a list of mutators for each matching file. // into a list of mutators for each matching file.
func ProcessRootIncludes() Mutator { func ProcessRootIncludes() bundle.Mutator {
return &processRootIncludes{} return &processRootIncludes{}
} }
@ -20,8 +22,8 @@ func (m *processRootIncludes) Name() string {
return "ProcessRootIncludes" return "ProcessRootIncludes"
} }
func (m *processRootIncludes) Apply(root *config.Root) ([]Mutator, error) { func (m *processRootIncludes) Apply(_ context.Context, b *bundle.Bundle) ([]bundle.Mutator, error) {
var out []Mutator var out []bundle.Mutator
// Map with files we've already seen to avoid loading them twice. // Map with files we've already seen to avoid loading them twice.
var seen = map[string]bool{ var seen = map[string]bool{
@ -31,14 +33,14 @@ func (m *processRootIncludes) Apply(root *config.Root) ([]Mutator, error) {
// For each glob, find all files to load. // For each glob, find all files to load.
// Ordering of the list of globs is maintained in the output. // Ordering of the list of globs is maintained in the output.
// For matches that appear in multiple globs, only the first is kept. // For matches that appear in multiple globs, only the first is kept.
for _, entry := range root.Include { for _, entry := range b.Config.Include {
// Include paths must be relative. // Include paths must be relative.
if filepath.IsAbs(entry) { if filepath.IsAbs(entry) {
return nil, fmt.Errorf("%s: includes must be relative paths", entry) return nil, fmt.Errorf("%s: includes must be relative paths", entry)
} }
// Anchor includes to the bundle root path. // Anchor includes to the bundle root path.
matches, err := filepath.Glob(filepath.Join(root.Path, entry)) matches, err := filepath.Glob(filepath.Join(b.Config.Path, entry))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -46,7 +48,7 @@ func (m *processRootIncludes) Apply(root *config.Root) ([]Mutator, error) {
// Filter matches to ones we haven't seen yet. // Filter matches to ones we haven't seen yet.
var includes []string var includes []string
for _, match := range matches { for _, match := range matches {
rel, err := filepath.Rel(root.Path, match) rel, err := filepath.Rel(b.Config.Path, match)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -60,7 +62,7 @@ func (m *processRootIncludes) Apply(root *config.Root) ([]Mutator, error) {
// Add matches to list of mutators to return. // Add matches to list of mutators to return.
slices.Sort(includes) slices.Sort(includes)
for _, include := range includes { for _, include := range includes {
out = append(out, ProcessInclude(filepath.Join(root.Path, include), include)) out = append(out, ProcessInclude(filepath.Join(b.Config.Path, include), include))
} }
} }

View File

@ -1,10 +1,12 @@
package mutator_test package mutator_test
import ( import (
"context"
"os" "os"
"path/filepath" "path/filepath"
"testing" "testing"
"github.com/databricks/bricks/bundle"
"github.com/databricks/bricks/bundle/config" "github.com/databricks/bricks/bundle/config"
"github.com/databricks/bricks/bundle/config/mutator" "github.com/databricks/bricks/bundle/config/mutator"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -18,36 +20,44 @@ func touch(t *testing.T, path, file string) {
} }
func TestProcessRootIncludesEmpty(t *testing.T) { func TestProcessRootIncludesEmpty(t *testing.T) {
root := &config.Root{Path: "."} bundle := &bundle.Bundle{
_, err := mutator.ProcessRootIncludes().Apply(root) Config: config.Root{
Path: ".",
},
}
_, err := mutator.ProcessRootIncludes().Apply(context.Background(), bundle)
require.NoError(t, err) require.NoError(t, err)
} }
func TestProcessRootIncludesAbs(t *testing.T) { func TestProcessRootIncludesAbs(t *testing.T) {
root := &config.Root{ bundle := &bundle.Bundle{
Path: ".", Config: config.Root{
Include: []string{ Path: ".",
"/tmp/*.yml", Include: []string{
"/tmp/*.yml",
},
}, },
} }
_, err := mutator.ProcessRootIncludes().Apply(root) _, err := mutator.ProcessRootIncludes().Apply(context.Background(), bundle)
require.Error(t, err) require.Error(t, err)
assert.Contains(t, err.Error(), "must be relative paths") assert.Contains(t, err.Error(), "must be relative paths")
} }
func TestProcessRootIncludesSingleGlob(t *testing.T) { func TestProcessRootIncludesSingleGlob(t *testing.T) {
root := &config.Root{ bundle := &bundle.Bundle{
Path: t.TempDir(), Config: config.Root{
Include: []string{ Path: t.TempDir(),
"*.yml", Include: []string{
"*.yml",
},
}, },
} }
touch(t, root.Path, "bundle.yml") touch(t, bundle.Config.Path, "bundle.yml")
touch(t, root.Path, "a.yml") touch(t, bundle.Config.Path, "a.yml")
touch(t, root.Path, "b.yml") touch(t, bundle.Config.Path, "b.yml")
ms, err := mutator.ProcessRootIncludes().Apply(root) ms, err := mutator.ProcessRootIncludes().Apply(context.Background(), bundle)
require.NoError(t, err) require.NoError(t, err)
var names []string var names []string
@ -61,18 +71,20 @@ func TestProcessRootIncludesSingleGlob(t *testing.T) {
} }
func TestProcessRootIncludesMultiGlob(t *testing.T) { func TestProcessRootIncludesMultiGlob(t *testing.T) {
root := &config.Root{ bundle := &bundle.Bundle{
Path: t.TempDir(), Config: config.Root{
Include: []string{ Path: t.TempDir(),
"a*.yml", Include: []string{
"b*.yml", "a*.yml",
"b*.yml",
},
}, },
} }
touch(t, root.Path, "a1.yml") touch(t, bundle.Config.Path, "a1.yml")
touch(t, root.Path, "b1.yml") touch(t, bundle.Config.Path, "b1.yml")
ms, err := mutator.ProcessRootIncludes().Apply(root) ms, err := mutator.ProcessRootIncludes().Apply(context.Background(), bundle)
require.NoError(t, err) require.NoError(t, err)
var names []string var names []string
@ -85,17 +97,19 @@ func TestProcessRootIncludesMultiGlob(t *testing.T) {
} }
func TestProcessRootIncludesRemoveDups(t *testing.T) { func TestProcessRootIncludesRemoveDups(t *testing.T) {
root := &config.Root{ bundle := &bundle.Bundle{
Path: t.TempDir(), Config: config.Root{
Include: []string{ Path: t.TempDir(),
"*.yml", Include: []string{
"*.yml", "*.yml",
"*.yml",
},
}, },
} }
touch(t, root.Path, "a.yml") touch(t, bundle.Config.Path, "a.yml")
ms, err := mutator.ProcessRootIncludes().Apply(root) ms, err := mutator.ProcessRootIncludes().Apply(context.Background(), bundle)
require.NoError(t, err) require.NoError(t, err)
assert.Len(t, ms, 1) assert.Len(t, ms, 1)
assert.Equal(t, "ProcessInclude(a.yml)", ms[0].Name()) assert.Equal(t, "ProcessInclude(a.yml)", ms[0].Name())

View File

@ -1,9 +1,10 @@
package mutator package mutator
import ( import (
"context"
"fmt" "fmt"
"github.com/databricks/bricks/bundle/config" "github.com/databricks/bricks/bundle"
) )
type selectEnvironment struct { type selectEnvironment struct {
@ -11,7 +12,7 @@ type selectEnvironment struct {
} }
// SelectEnvironment merges the specified environment into the root configuration. // SelectEnvironment merges the specified environment into the root configuration.
func SelectEnvironment(name string) Mutator { func SelectEnvironment(name string) bundle.Mutator {
return &selectEnvironment{ return &selectEnvironment{
name: name, name: name,
} }
@ -21,27 +22,27 @@ func (m *selectEnvironment) Name() string {
return fmt.Sprintf("SelectEnvironment(%s)", m.name) return fmt.Sprintf("SelectEnvironment(%s)", m.name)
} }
func (m *selectEnvironment) Apply(root *config.Root) ([]Mutator, error) { func (m *selectEnvironment) Apply(_ context.Context, b *bundle.Bundle) ([]bundle.Mutator, error) {
if root.Environments == nil { if b.Config.Environments == nil {
return nil, fmt.Errorf("no environments defined") return nil, fmt.Errorf("no environments defined")
} }
// Get specified environment // Get specified environment
env, ok := root.Environments[m.name] env, ok := b.Config.Environments[m.name]
if !ok { if !ok {
return nil, fmt.Errorf("%s: no such environment", m.name) return nil, fmt.Errorf("%s: no such environment", m.name)
} }
// Merge specified environment into root configuration structure. // Merge specified environment into root configuration structure.
err := root.MergeEnvironment(env) err := b.Config.MergeEnvironment(env)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// Store specified environment in configuration for reference. // Store specified environment in configuration for reference.
root.Bundle.Environment = m.name b.Config.Bundle.Environment = m.name
// Clear environments after loading. // Clear environments after loading.
root.Environments = nil b.Config.Environments = nil
return nil, nil return nil, nil
} }

View File

@ -1,8 +1,10 @@
package mutator_test package mutator_test
import ( import (
"context"
"testing" "testing"
"github.com/databricks/bricks/bundle"
"github.com/databricks/bricks/bundle/config" "github.com/databricks/bricks/bundle/config"
"github.com/databricks/bricks/bundle/config/mutator" "github.com/databricks/bricks/bundle/config/mutator"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -10,29 +12,33 @@ import (
) )
func TestSelectEnvironment(t *testing.T) { func TestSelectEnvironment(t *testing.T) {
root := &config.Root{ bundle := &bundle.Bundle{
Workspace: config.Workspace{ Config: config.Root{
Host: "foo", Workspace: config.Workspace{
}, Host: "foo",
Environments: map[string]*config.Environment{ },
"default": { Environments: map[string]*config.Environment{
Workspace: &config.Workspace{ "default": {
Host: "bar", Workspace: &config.Workspace{
Host: "bar",
},
}, },
}, },
}, },
} }
_, err := mutator.SelectEnvironment("default").Apply(root) _, err := mutator.SelectEnvironment("default").Apply(context.Background(), bundle)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, "bar", root.Workspace.Host) assert.Equal(t, "bar", bundle.Config.Workspace.Host)
} }
func TestSelectEnvironmentNotFound(t *testing.T) { func TestSelectEnvironmentNotFound(t *testing.T) {
root := &config.Root{ bundle := &bundle.Bundle{
Environments: map[string]*config.Environment{ Config: config.Root{
"default": {}, Environments: map[string]*config.Environment{
"default": {},
},
}, },
} }
_, err := mutator.SelectEnvironment("doesnt-exist").Apply(root) _, err := mutator.SelectEnvironment("doesnt-exist").Apply(context.Background(), bundle)
require.Error(t, err, "no environments defined") require.Error(t, err, "no environments defined")
} }

View File

@ -25,7 +25,7 @@ func TestRootMarshalUnmarshal(t *testing.T) {
func TestRootLoad(t *testing.T) { func TestRootLoad(t *testing.T) {
root := &Root{} root := &Root{}
err := root.Load("./tests/basic/bundle.yml") err := root.Load("../tests/basic/bundle.yml")
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, "basic", root.Bundle.Name) assert.Equal(t, "basic", root.Bundle.Name)
} }

View File

@ -1,14 +0,0 @@
package config_tests
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestEnvironmentOverridesDev(t *testing.T) {
development := loadEnvironment(t, "./environment_overrides", "development")
assert.Equal(t, "https://development.acme.cloud.databricks.com/", development.Workspace.Host)
staging := loadEnvironment(t, "./environment_overrides", "staging")
assert.Equal(t, "https://staging.acme.cloud.databricks.com/", staging.Workspace.Host)
}

View File

@ -1,24 +0,0 @@
package config_tests
import (
"testing"
"github.com/databricks/bricks/bundle/config"
"github.com/databricks/bricks/bundle/config/mutator"
"github.com/stretchr/testify/require"
)
func load(t *testing.T, path string) *config.Root {
root, err := config.Load(path)
require.NoError(t, err)
err = mutator.Apply(root, mutator.DefaultMutators())
require.NoError(t, err)
return root
}
func loadEnvironment(t *testing.T, path, env string) *config.Root {
root := load(t, path)
err := mutator.Apply(root, []mutator.Mutator{mutator.SelectEnvironment(env)})
require.NoError(t, err)
return root
}

22
bundle/loader/loader.go Normal file
View File

@ -0,0 +1,22 @@
package loader
import (
"context"
"github.com/databricks/bricks/bundle"
"github.com/databricks/bricks/bundle/config/mutator"
)
func ConfigureForEnvironment(ctx context.Context, env string) (context.Context, error) {
b, err := bundle.LoadFromRoot()
if err != nil {
return nil, err
}
err = bundle.Apply(ctx, b, mutator.DefaultMutatorsForEnvironment(env))
if err != nil {
return nil, err
}
return bundle.Context(ctx, b), nil
}

36
bundle/mutator.go Normal file
View File

@ -0,0 +1,36 @@
package bundle
import (
"context"
)
// Mutator is the interface type that mutates a bundle's configuration or internal state.
// This makes every mutation or action observable and debuggable.
type Mutator interface {
// Name returns the mutators name.
Name() string
// Apply mutates the specified bundle object.
// It may return a list of mutators to apply immediately after this mutator.
// For example: when processing all configuration files in the tree; each file gets
// its own mutator instance.
Apply(context.Context, *Bundle) ([]Mutator, error)
}
func Apply(ctx context.Context, b *Bundle, ms []Mutator) error {
if len(ms) == 0 {
return nil
}
for _, m := range ms {
ms_, err := m.Apply(ctx, b)
if err != nil {
return err
}
// Apply recursively.
err = Apply(ctx, b, ms_)
if err != nil {
return err
}
}
return nil
}

44
bundle/mutator_test.go Normal file
View File

@ -0,0 +1,44 @@
package bundle
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
)
type testMutator struct {
applyCalled int
nestedMutators []Mutator
}
func (t *testMutator) Name() string {
return "test"
}
func (t *testMutator) Apply(_ context.Context, b *Bundle) ([]Mutator, error) {
t.applyCalled++
return t.nestedMutators, nil
}
func TestMutator(t *testing.T) {
nested := []*testMutator{
{},
{},
}
m := &testMutator{
nestedMutators: []Mutator{
nested[0],
nested[1],
},
}
bundle := &Bundle{}
err := Apply(context.Background(), bundle, []Mutator{m})
assert.NoError(t, err)
assert.Equal(t, 1, m.applyCalled)
assert.Equal(t, 1, nested[0].applyCalled)
assert.Equal(t, 1, nested[1].applyCalled)
}

View File

@ -7,6 +7,6 @@ import (
) )
func TestBasic(t *testing.T) { func TestBasic(t *testing.T) {
root := load(t, "./basic") b := load(t, "./basic")
assert.Equal(t, "basic", root.Bundle.Name) assert.Equal(t, "basic", b.Config.Bundle.Name)
} }

View File

@ -0,0 +1,17 @@
package config_tests
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestEnvironmentOverridesDev(t *testing.T) {
b := loadEnvironment(t, "./environment_overrides", "development")
assert.Equal(t, "https://development.acme.cloud.databricks.com/", b.Config.Workspace.Host)
}
func TestEnvironmentOverridesStaging(t *testing.T) {
b := loadEnvironment(t, "./environment_overrides", "staging")
assert.Equal(t, "https://staging.acme.cloud.databricks.com/", b.Config.Workspace.Host)
}

View File

@ -9,12 +9,12 @@ import (
) )
func TestIncludeDefault(t *testing.T) { func TestIncludeDefault(t *testing.T) {
root := load(t, "./include_default") b := load(t, "./include_default")
// Test that both jobs were loaded. // Test that both jobs were loaded.
keys := maps.Keys(root.Resources.Jobs) keys := maps.Keys(b.Config.Resources.Jobs)
sort.Strings(keys) sort.Strings(keys)
assert.Equal(t, []string{"my_first_job", "my_second_job"}, keys) assert.Equal(t, []string{"my_first_job", "my_second_job"}, keys)
assert.Equal(t, "1", root.Resources.Jobs["my_first_job"].ID) assert.Equal(t, "1", b.Config.Resources.Jobs["my_first_job"].ID)
assert.Equal(t, "2", root.Resources.Jobs["my_second_job"].ID) assert.Equal(t, "2", b.Config.Resources.Jobs["my_second_job"].ID)
} }

View File

@ -7,6 +7,6 @@ import (
) )
func TestIncludeOverride(t *testing.T) { func TestIncludeOverride(t *testing.T) {
root := load(t, "./include_override") b := load(t, "./include_override")
assert.Empty(t, root.Resources.Jobs) assert.Empty(t, b.Config.Resources.Jobs)
} }

View File

@ -8,11 +8,11 @@ import (
) )
func TestJobAndPipelineDevelopment(t *testing.T) { func TestJobAndPipelineDevelopment(t *testing.T) {
root := loadEnvironment(t, "./job_and_pipeline", "development") b := loadEnvironment(t, "./job_and_pipeline", "development")
assert.Len(t, root.Resources.Jobs, 0) assert.Len(t, b.Config.Resources.Jobs, 0)
assert.Len(t, root.Resources.Pipelines, 1) assert.Len(t, b.Config.Resources.Pipelines, 1)
p := root.Resources.Pipelines["nyc_taxi_pipeline"] p := b.Config.Resources.Pipelines["nyc_taxi_pipeline"]
assert.True(t, p.Development) assert.True(t, p.Development)
require.Len(t, p.Libraries, 1) require.Len(t, p.Libraries, 1)
assert.Equal(t, "./dlt/nyc_taxi_loader", p.Libraries[0].Notebook.Path) assert.Equal(t, "./dlt/nyc_taxi_loader", p.Libraries[0].Notebook.Path)
@ -20,11 +20,11 @@ func TestJobAndPipelineDevelopment(t *testing.T) {
} }
func TestJobAndPipelineStaging(t *testing.T) { func TestJobAndPipelineStaging(t *testing.T) {
root := loadEnvironment(t, "./job_and_pipeline", "staging") b := loadEnvironment(t, "./job_and_pipeline", "staging")
assert.Len(t, root.Resources.Jobs, 0) assert.Len(t, b.Config.Resources.Jobs, 0)
assert.Len(t, root.Resources.Pipelines, 1) assert.Len(t, b.Config.Resources.Pipelines, 1)
p := root.Resources.Pipelines["nyc_taxi_pipeline"] p := b.Config.Resources.Pipelines["nyc_taxi_pipeline"]
assert.False(t, p.Development) assert.False(t, p.Development)
require.Len(t, p.Libraries, 1) require.Len(t, p.Libraries, 1)
assert.Equal(t, "./dlt/nyc_taxi_loader", p.Libraries[0].Notebook.Path) assert.Equal(t, "./dlt/nyc_taxi_loader", p.Libraries[0].Notebook.Path)
@ -32,17 +32,17 @@ func TestJobAndPipelineStaging(t *testing.T) {
} }
func TestJobAndPipelineProduction(t *testing.T) { func TestJobAndPipelineProduction(t *testing.T) {
root := loadEnvironment(t, "./job_and_pipeline", "production") b := loadEnvironment(t, "./job_and_pipeline", "production")
assert.Len(t, root.Resources.Jobs, 1) assert.Len(t, b.Config.Resources.Jobs, 1)
assert.Len(t, root.Resources.Pipelines, 1) assert.Len(t, b.Config.Resources.Pipelines, 1)
p := root.Resources.Pipelines["nyc_taxi_pipeline"] p := b.Config.Resources.Pipelines["nyc_taxi_pipeline"]
assert.False(t, p.Development) assert.False(t, p.Development)
require.Len(t, p.Libraries, 1) require.Len(t, p.Libraries, 1)
assert.Equal(t, "./dlt/nyc_taxi_loader", p.Libraries[0].Notebook.Path) assert.Equal(t, "./dlt/nyc_taxi_loader", p.Libraries[0].Notebook.Path)
assert.Equal(t, "nyc_taxi_production", p.Target) assert.Equal(t, "nyc_taxi_production", p.Target)
j := root.Resources.Jobs["pipeline_schedule"] j := b.Config.Resources.Jobs["pipeline_schedule"]
assert.Equal(t, "Daily refresh of production pipeline", j.Name) assert.Equal(t, "Daily refresh of production pipeline", j.Name)
require.Len(t, j.Tasks, 1) require.Len(t, j.Tasks, 1)
assert.NotEmpty(t, j.Tasks[0].PipelineTask.PipelineId) assert.NotEmpty(t, j.Tasks[0].PipelineTask.PipelineId)

25
bundle/tests/loader.go Normal file
View File

@ -0,0 +1,25 @@
package config_tests
import (
"context"
"testing"
"github.com/databricks/bricks/bundle"
"github.com/databricks/bricks/bundle/config/mutator"
"github.com/stretchr/testify/require"
)
func load(t *testing.T, path string) *bundle.Bundle {
b, err := bundle.Load(path)
require.NoError(t, err)
err = bundle.Apply(context.Background(), b, mutator.DefaultMutators())
require.NoError(t, err)
return b
}
func loadEnvironment(t *testing.T, path, env string) *bundle.Bundle {
b := load(t, path)
err := bundle.Apply(context.Background(), b, []bundle.Mutator{mutator.SelectEnvironment(env)})
require.NoError(t, err)
return b
}

View File

@ -8,10 +8,10 @@ import (
) )
func TestYAMLAnchors(t *testing.T) { func TestYAMLAnchors(t *testing.T) {
root := load(t, "./yaml_anchors") b := load(t, "./yaml_anchors")
assert.Len(t, root.Resources.Jobs, 1) assert.Len(t, b.Config.Resources.Jobs, 1)
j := root.Resources.Jobs["my_job"] j := b.Config.Resources.Jobs["my_job"]
require.Len(t, j.Tasks, 2) require.Len(t, j.Tasks, 2)
t0 := j.Tasks[0] t0 := j.Tasks[0]

View File

@ -1,7 +1,7 @@
package bundle package bundle
import ( import (
"github.com/databricks/bricks/bundle" "github.com/databricks/bricks/bundle/loader"
"github.com/databricks/bricks/cmd/root" "github.com/databricks/bricks/cmd/root"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -15,7 +15,7 @@ var rootCmd = &cobra.Command{
// ConfigureBundle loads the bundle configuration // ConfigureBundle loads the bundle configuration
// and configures it on the command's context. // and configures it on the command's context.
func ConfigureBundle(cmd *cobra.Command, args []string) error { func ConfigureBundle(cmd *cobra.Command, args []string) error {
ctx, err := bundle.ConfigureForEnvironment(cmd.Context(), getEnvironment(cmd)) ctx, err := loader.ConfigureForEnvironment(cmd.Context(), getEnvironment(cmd))
if err != nil { if err != nil {
return err return err
} }