2023-07-30 07:19:49 +00:00
package mutator
2023-07-12 06:51:54 +00:00
import (
"context"
2023-07-30 07:19:49 +00:00
"reflect"
"strings"
2023-07-12 06:51:54 +00:00
"testing"
"github.com/databricks/cli/bundle"
"github.com/databricks/cli/bundle/config"
"github.com/databricks/cli/bundle/config/resources"
2024-08-19 18:18:50 +00:00
"github.com/databricks/cli/libs/diag"
2023-10-02 06:58:51 +00:00
"github.com/databricks/cli/libs/tags"
sdkconfig "github.com/databricks/databricks-sdk-go/config"
2023-10-16 15:32:49 +00:00
"github.com/databricks/databricks-sdk-go/service/catalog"
2023-07-30 07:19:49 +00:00
"github.com/databricks/databricks-sdk-go/service/iam"
2023-07-12 06:51:54 +00:00
"github.com/databricks/databricks-sdk-go/service/jobs"
"github.com/databricks/databricks-sdk-go/service/ml"
"github.com/databricks/databricks-sdk-go/service/pipelines"
2023-09-07 21:54:31 +00:00
"github.com/databricks/databricks-sdk-go/service/serving"
2023-07-12 06:51:54 +00:00
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
2023-07-30 07:19:49 +00:00
func mockBundle ( mode config . Mode ) * bundle . Bundle {
return & bundle . Bundle {
2023-07-12 06:51:54 +00:00
Config : config . Root {
Bundle : config . Bundle {
2023-07-30 07:19:49 +00:00
Mode : mode ,
2023-07-30 12:44:33 +00:00
Git : config . Git {
OriginURL : "http://origin" ,
Branch : "main" ,
} ,
2023-07-30 07:19:49 +00:00
} ,
2024-08-22 20:09:18 +00:00
Targets : map [ string ] * config . Target {
"" : { } ,
} ,
2023-07-30 07:19:49 +00:00
Workspace : config . Workspace {
CurrentUser : & config . User {
ShortName : "lennart" ,
User : & iam . User {
UserName : "lennart@company.com" ,
Id : "1" ,
} ,
} ,
2023-11-15 13:37:26 +00:00
StatePath : "/Users/lennart@company.com/.bundle/x/y/state" ,
ArtifactPath : "/Users/lennart@company.com/.bundle/x/y/artifacts" ,
FilePath : "/Users/lennart@company.com/.bundle/x/y/files" ,
2023-07-12 06:51:54 +00:00
} ,
Resources : config . Resources {
Jobs : map [ string ] * resources . Job {
2023-11-13 19:50:39 +00:00
"job1" : {
JobSettings : & jobs . JobSettings {
Name : "job1" ,
Schedule : & jobs . CronSchedule {
QuartzCronExpression : "* * * * *" ,
} ,
2024-08-19 18:18:50 +00:00
Tags : map [ string ] string { "existing" : "tag" } ,
2023-11-13 19:50:39 +00:00
} ,
} ,
"job2" : {
JobSettings : & jobs . JobSettings {
Name : "job2" ,
Schedule : & jobs . CronSchedule {
QuartzCronExpression : "* * * * *" ,
PauseStatus : jobs . PauseStatusUnpaused ,
} ,
} ,
} ,
2023-11-29 16:32:42 +00:00
"job3" : {
JobSettings : & jobs . JobSettings {
Name : "job3" ,
Trigger : & jobs . TriggerSettings {
FileArrival : & jobs . FileArrivalTriggerConfiguration {
Url : "test.com" ,
} ,
} ,
} ,
} ,
"job4" : {
JobSettings : & jobs . JobSettings {
Name : "job4" ,
Continuous : & jobs . Continuous {
PauseStatus : jobs . PauseStatusPaused ,
} ,
} ,
} ,
2023-07-12 06:51:54 +00:00
} ,
Pipelines : map [ string ] * resources . Pipeline {
2024-08-19 16:27:57 +00:00
"pipeline1" : { PipelineSpec : & pipelines . PipelineSpec { Name : "pipeline1" , Continuous : true } } ,
2023-07-12 06:51:54 +00:00
} ,
Experiments : map [ string ] * resources . MlflowExperiment {
"experiment1" : { Experiment : & ml . Experiment { Name : "/Users/lennart.kats@databricks.com/experiment1" } } ,
"experiment2" : { Experiment : & ml . Experiment { Name : "experiment2" } } ,
} ,
Models : map [ string ] * resources . MlflowModel {
"model1" : { Model : & ml . Model { Name : "model1" } } ,
} ,
2023-09-07 21:54:31 +00:00
ModelServingEndpoints : map [ string ] * resources . ModelServingEndpoint {
"servingendpoint1" : { CreateServingEndpoint : & serving . CreateServingEndpoint { Name : "servingendpoint1" } } ,
} ,
2023-10-16 15:32:49 +00:00
RegisteredModels : map [ string ] * resources . RegisteredModel {
"registeredmodel1" : { CreateRegisteredModelRequest : & catalog . CreateRegisteredModelRequest { Name : "registeredmodel1" } } ,
} ,
2024-05-31 09:42:25 +00:00
QualityMonitors : map [ string ] * resources . QualityMonitor {
"qualityMonitor1" : { CreateMonitor : & catalog . CreateMonitor { TableName : "qualityMonitor1" } } ,
2024-06-19 13:54:35 +00:00
"qualityMonitor2" : {
CreateMonitor : & catalog . CreateMonitor {
TableName : "qualityMonitor2" ,
Schedule : & catalog . MonitorCronSchedule { } ,
} ,
} ,
"qualityMonitor3" : {
CreateMonitor : & catalog . CreateMonitor {
TableName : "qualityMonitor3" ,
Schedule : & catalog . MonitorCronSchedule {
PauseStatus : catalog . MonitorCronSchedulePauseStatusUnpaused ,
} ,
} ,
} ,
2024-05-31 09:42:25 +00:00
} ,
2024-07-31 12:16:28 +00:00
Schemas : map [ string ] * resources . Schema {
"schema1" : { CreateSchema : & catalog . CreateSchema { Name : "schema1" } } ,
} ,
2023-07-12 06:51:54 +00:00
} ,
} ,
2023-10-02 06:58:51 +00:00
// Use AWS implementation for testing.
Tagging : tags . ForCloud ( & sdkconfig . Config {
Host : "https://company.cloud.databricks.com" ,
} ) ,
2023-07-12 06:51:54 +00:00
}
2023-07-30 07:19:49 +00:00
}
2023-07-12 06:51:54 +00:00
2023-08-17 15:22:32 +00:00
func TestProcessTargetModeDevelopment ( t * testing . T ) {
2023-11-15 14:03:36 +00:00
b := mockBundle ( config . Development )
2023-07-30 07:19:49 +00:00
2024-08-19 18:18:50 +00:00
m := bundle . Seq ( ProcessTargetMode ( ) , ApplyPresets ( ) )
2024-03-25 14:18:47 +00:00
diags := bundle . Apply ( context . Background ( ) , b , m )
require . NoError ( t , diags . Error ( ) )
2023-10-02 06:58:51 +00:00
// Job 1
2023-11-15 14:03:36 +00:00
assert . Equal ( t , "[dev lennart] job1" , b . Config . Resources . Jobs [ "job1" ] . Name )
2024-08-19 18:18:50 +00:00
assert . Equal ( t , b . Config . Resources . Jobs [ "job1" ] . Tags [ "existing" ] , "tag" )
2023-11-15 14:03:36 +00:00
assert . Equal ( t , b . Config . Resources . Jobs [ "job1" ] . Tags [ "dev" ] , "lennart" )
assert . Equal ( t , b . Config . Resources . Jobs [ "job1" ] . Schedule . PauseStatus , jobs . PauseStatusPaused )
2023-11-13 19:50:39 +00:00
// Job 2
2023-11-15 14:03:36 +00:00
assert . Equal ( t , "[dev lennart] job2" , b . Config . Resources . Jobs [ "job2" ] . Name )
assert . Equal ( t , b . Config . Resources . Jobs [ "job2" ] . Tags [ "dev" ] , "lennart" )
assert . Equal ( t , b . Config . Resources . Jobs [ "job2" ] . Schedule . PauseStatus , jobs . PauseStatusUnpaused )
2023-10-02 06:58:51 +00:00
// Pipeline 1
2023-11-15 14:03:36 +00:00
assert . Equal ( t , "[dev lennart] pipeline1" , b . Config . Resources . Pipelines [ "pipeline1" ] . Name )
2024-08-19 16:27:57 +00:00
assert . Equal ( t , false , b . Config . Resources . Pipelines [ "pipeline1" ] . Continuous )
2023-11-15 14:03:36 +00:00
assert . True ( t , b . Config . Resources . Pipelines [ "pipeline1" ] . PipelineSpec . Development )
2023-10-02 06:58:51 +00:00
// Experiment 1
2023-11-15 14:03:36 +00:00
assert . Equal ( t , "/Users/lennart.kats@databricks.com/[dev lennart] experiment1" , b . Config . Resources . Experiments [ "experiment1" ] . Name )
assert . Contains ( t , b . Config . Resources . Experiments [ "experiment1" ] . Experiment . Tags , ml . ExperimentTag { Key : "dev" , Value : "lennart" } )
assert . Equal ( t , "dev" , b . Config . Resources . Experiments [ "experiment1" ] . Experiment . Tags [ 0 ] . Key )
2023-10-02 06:58:51 +00:00
// Experiment 2
2023-11-15 14:03:36 +00:00
assert . Equal ( t , "[dev lennart] experiment2" , b . Config . Resources . Experiments [ "experiment2" ] . Name )
assert . Contains ( t , b . Config . Resources . Experiments [ "experiment2" ] . Experiment . Tags , ml . ExperimentTag { Key : "dev" , Value : "lennart" } )
2023-10-02 06:58:51 +00:00
// Model 1
2023-11-15 14:03:36 +00:00
assert . Equal ( t , "[dev lennart] model1" , b . Config . Resources . Models [ "model1" ] . Name )
2024-02-22 14:52:49 +00:00
assert . Contains ( t , b . Config . Resources . Models [ "model1" ] . Tags , ml . ModelTag { Key : "dev" , Value : "lennart" } )
2023-10-02 06:58:51 +00:00
// Model serving endpoint 1
2023-11-15 14:03:36 +00:00
assert . Equal ( t , "dev_lennart_servingendpoint1" , b . Config . Resources . ModelServingEndpoints [ "servingendpoint1" ] . Name )
2023-10-16 15:32:49 +00:00
// Registered model 1
2023-11-15 14:03:36 +00:00
assert . Equal ( t , "dev_lennart_registeredmodel1" , b . Config . Resources . RegisteredModels [ "registeredmodel1" ] . Name )
2024-05-31 09:42:25 +00:00
// Quality Monitor 1
assert . Equal ( t , "qualityMonitor1" , b . Config . Resources . QualityMonitors [ "qualityMonitor1" ] . TableName )
2024-06-19 13:54:35 +00:00
assert . Nil ( t , b . Config . Resources . QualityMonitors [ "qualityMonitor2" ] . Schedule )
assert . Equal ( t , catalog . MonitorCronSchedulePauseStatusUnpaused , b . Config . Resources . QualityMonitors [ "qualityMonitor3" ] . Schedule . PauseStatus )
2024-07-31 12:16:28 +00:00
// Schema 1
assert . Equal ( t , "dev_lennart_schema1" , b . Config . Resources . Schemas [ "schema1" ] . Name )
2023-10-02 06:58:51 +00:00
}
func TestProcessTargetModeDevelopmentTagNormalizationForAws ( t * testing . T ) {
2023-11-15 14:03:36 +00:00
b := mockBundle ( config . Development )
b . Tagging = tags . ForCloud ( & sdkconfig . Config {
2023-10-02 06:58:51 +00:00
Host : "https://dbc-XXXXXXXX-YYYY.cloud.databricks.com/" ,
} )
2023-11-15 14:03:36 +00:00
b . Config . Workspace . CurrentUser . ShortName = "Héllö wörld?!"
2024-08-19 18:18:50 +00:00
m := bundle . Seq ( ProcessTargetMode ( ) , ApplyPresets ( ) )
diags := bundle . Apply ( context . Background ( ) , b , m )
2024-03-25 14:18:47 +00:00
require . NoError ( t , diags . Error ( ) )
2023-10-02 06:58:51 +00:00
// Assert that tag normalization took place.
2023-11-15 14:03:36 +00:00
assert . Equal ( t , "Hello world__" , b . Config . Resources . Jobs [ "job1" ] . Tags [ "dev" ] )
2023-10-02 06:58:51 +00:00
}
func TestProcessTargetModeDevelopmentTagNormalizationForAzure ( t * testing . T ) {
2023-11-15 14:03:36 +00:00
b := mockBundle ( config . Development )
b . Tagging = tags . ForCloud ( & sdkconfig . Config {
2023-10-02 06:58:51 +00:00
Host : "https://adb-xxx.y.azuredatabricks.net/" ,
} )
2023-11-15 14:03:36 +00:00
b . Config . Workspace . CurrentUser . ShortName = "Héllö wörld?!"
2024-08-19 18:18:50 +00:00
m := bundle . Seq ( ProcessTargetMode ( ) , ApplyPresets ( ) )
diags := bundle . Apply ( context . Background ( ) , b , m )
2024-03-25 14:18:47 +00:00
require . NoError ( t , diags . Error ( ) )
2023-10-02 06:58:51 +00:00
// Assert that tag normalization took place (Azure allows more characters than AWS).
2023-11-15 14:03:36 +00:00
assert . Equal ( t , "Héllö wörld?!" , b . Config . Resources . Jobs [ "job1" ] . Tags [ "dev" ] )
2023-10-02 06:58:51 +00:00
}
func TestProcessTargetModeDevelopmentTagNormalizationForGcp ( t * testing . T ) {
2023-11-15 14:03:36 +00:00
b := mockBundle ( config . Development )
b . Tagging = tags . ForCloud ( & sdkconfig . Config {
2023-10-02 06:58:51 +00:00
Host : "https://123.4.gcp.databricks.com/" ,
} )
2023-11-15 14:03:36 +00:00
b . Config . Workspace . CurrentUser . ShortName = "Héllö wörld?!"
2024-08-19 18:18:50 +00:00
m := bundle . Seq ( ProcessTargetMode ( ) , ApplyPresets ( ) )
diags := bundle . Apply ( context . Background ( ) , b , m )
2024-03-25 14:18:47 +00:00
require . NoError ( t , diags . Error ( ) )
2023-10-02 06:58:51 +00:00
// Assert that tag normalization took place.
2023-11-15 14:03:36 +00:00
assert . Equal ( t , "Hello_world" , b . Config . Resources . Jobs [ "job1" ] . Tags [ "dev" ] )
2023-07-12 06:51:54 +00:00
}
2024-08-19 18:18:50 +00:00
func TestValidateDevelopmentMode ( t * testing . T ) {
// Test with a valid development mode bundle
b := mockBundle ( config . Development )
diags := validateDevelopmentMode ( b )
require . NoError ( t , diags . Error ( ) )
// Test with a bundle that has a non-user path
b . Config . Workspace . RootPath = "/Shared/.bundle/x/y/state"
diags = validateDevelopmentMode ( b )
require . ErrorContains ( t , diags . Error ( ) , "root_path" )
// Test with a bundle that has an unpaused trigger pause status
b = mockBundle ( config . Development )
b . Config . Presets . TriggerPauseStatus = config . Unpaused
diags = validateDevelopmentMode ( b )
require . ErrorContains ( t , diags . Error ( ) , "UNPAUSED" )
// Test with a bundle that has a prefix not containing the username or short name
b = mockBundle ( config . Development )
b . Config . Presets . NamePrefix = "[prod]"
diags = validateDevelopmentMode ( b )
require . Len ( t , diags , 1 )
assert . Equal ( t , diag . Error , diags [ 0 ] . Severity )
assert . Contains ( t , diags [ 0 ] . Summary , "" )
// Test with a bundle that has valid user paths
b = mockBundle ( config . Development )
b . Config . Workspace . RootPath = "/Users/lennart@company.com/.bundle/x/y/state"
b . Config . Workspace . StatePath = "/Users/lennart@company.com/.bundle/x/y/state"
b . Config . Workspace . FilePath = "/Users/lennart@company.com/.bundle/x/y/files"
b . Config . Workspace . ArtifactPath = "/Users/lennart@company.com/.bundle/x/y/artifacts"
diags = validateDevelopmentMode ( b )
require . NoError ( t , diags . Error ( ) )
}
2023-08-17 15:22:32 +00:00
func TestProcessTargetModeDefault ( t * testing . T ) {
2023-11-15 14:03:36 +00:00
b := mockBundle ( "" )
2023-07-30 07:19:49 +00:00
2024-08-19 18:18:50 +00:00
m := bundle . Seq ( ProcessTargetMode ( ) , ApplyPresets ( ) )
2024-03-25 14:18:47 +00:00
diags := bundle . Apply ( context . Background ( ) , b , m )
require . NoError ( t , diags . Error ( ) )
2023-11-15 14:03:36 +00:00
assert . Equal ( t , "job1" , b . Config . Resources . Jobs [ "job1" ] . Name )
assert . Equal ( t , "pipeline1" , b . Config . Resources . Pipelines [ "pipeline1" ] . Name )
assert . False ( t , b . Config . Resources . Pipelines [ "pipeline1" ] . PipelineSpec . Development )
assert . Equal ( t , "servingendpoint1" , b . Config . Resources . ModelServingEndpoints [ "servingendpoint1" ] . Name )
assert . Equal ( t , "registeredmodel1" , b . Config . Resources . RegisteredModels [ "registeredmodel1" ] . Name )
2024-05-31 09:42:25 +00:00
assert . Equal ( t , "qualityMonitor1" , b . Config . Resources . QualityMonitors [ "qualityMonitor1" ] . TableName )
2023-07-30 07:19:49 +00:00
}
2023-08-17 15:22:32 +00:00
func TestProcessTargetModeProduction ( t * testing . T ) {
2023-11-15 14:03:36 +00:00
b := mockBundle ( config . Production )
2023-07-30 07:19:49 +00:00
2024-03-25 14:18:47 +00:00
diags := validateProductionMode ( context . Background ( ) , b , false )
2024-08-22 20:09:18 +00:00
require . ErrorContains ( t , diags . Error ( ) , "target with 'mode: production' must specify explicit 'workspace.root_path' to make sure only one copy is deployed" )
2023-07-30 07:19:49 +00:00
2023-11-15 14:03:36 +00:00
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"
2023-07-30 07:19:49 +00:00
2024-03-25 14:18:47 +00:00
diags = validateProductionMode ( context . Background ( ) , b , false )
2024-08-22 20:09:18 +00:00
require . ErrorContains ( t , diags . Error ( ) , "target with 'mode: production' must specify explicit 'workspace.root_path' to make sure only one copy is deployed" )
2023-07-30 07:19:49 +00:00
permissions := [ ] resources . Permission {
{
Level : "CAN_MANAGE" ,
UserName : "user@company.com" ,
2023-07-12 06:51:54 +00:00
} ,
}
2023-11-15 14:03:36 +00:00
b . Config . Resources . Jobs [ "job1" ] . Permissions = permissions
b . Config . Resources . Jobs [ "job1" ] . RunAs = & jobs . JobRunAs { UserName : "user@company.com" }
b . Config . Resources . Jobs [ "job2" ] . RunAs = & jobs . JobRunAs { UserName : "user@company.com" }
2023-11-29 16:32:42 +00:00
b . Config . Resources . Jobs [ "job3" ] . RunAs = & jobs . JobRunAs { UserName : "user@company.com" }
b . Config . Resources . Jobs [ "job4" ] . RunAs = & jobs . JobRunAs { UserName : "user@company.com" }
2023-11-15 14:03:36 +00:00
b . Config . Resources . Pipelines [ "pipeline1" ] . Permissions = permissions
b . Config . Resources . Experiments [ "experiment1" ] . Permissions = permissions
b . Config . Resources . Experiments [ "experiment2" ] . Permissions = permissions
b . Config . Resources . Models [ "model1" ] . Permissions = permissions
b . Config . Resources . ModelServingEndpoints [ "servingendpoint1" ] . Permissions = permissions
2024-03-25 14:18:47 +00:00
diags = validateProductionMode ( context . Background ( ) , b , false )
require . NoError ( t , diags . Error ( ) )
2023-07-30 07:19:49 +00:00
2023-11-15 14:03:36 +00:00
assert . Equal ( t , "job1" , b . Config . Resources . Jobs [ "job1" ] . Name )
assert . Equal ( t , "pipeline1" , b . Config . Resources . Pipelines [ "pipeline1" ] . Name )
assert . False ( t , b . Config . Resources . Pipelines [ "pipeline1" ] . PipelineSpec . Development )
assert . Equal ( t , "servingendpoint1" , b . Config . Resources . ModelServingEndpoints [ "servingendpoint1" ] . Name )
assert . Equal ( t , "registeredmodel1" , b . Config . Resources . RegisteredModels [ "registeredmodel1" ] . Name )
2024-05-31 09:42:25 +00:00
assert . Equal ( t , "qualityMonitor1" , b . Config . Resources . QualityMonitors [ "qualityMonitor1" ] . TableName )
2023-07-12 06:51:54 +00:00
}
2023-07-30 07:19:49 +00:00
2023-08-17 15:22:32 +00:00
func TestProcessTargetModeProductionOkForPrincipal ( t * testing . T ) {
2023-11-15 14:03:36 +00:00
b := mockBundle ( config . Production )
2023-07-30 07:19:49 +00:00
2023-08-17 15:22:32 +00:00
// Our target has all kinds of problems when not using service principals ...
2024-03-25 14:18:47 +00:00
diags := validateProductionMode ( context . Background ( ) , b , false )
require . Error ( t , diags . Error ( ) )
2023-07-30 07:19:49 +00:00
// ... but we're much less strict when a principal is used
2024-03-25 14:18:47 +00:00
diags = validateProductionMode ( context . Background ( ) , b , true )
require . NoError ( t , diags . Error ( ) )
2023-07-30 07:19:49 +00:00
}
2024-08-22 20:09:18 +00:00
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 . Targets [ "" ] . Workspace = & config . Workspace {
RootPath : "some-root-path" ,
}
diags = validateProductionMode ( context . Background ( ) , b , false )
require . NoError ( t , diags . Error ( ) )
}
2023-07-30 07:19:49 +00:00
// Make sure that we have test coverage for all resource types
func TestAllResourcesMocked ( t * testing . T ) {
2023-11-15 14:03:36 +00:00
b := mockBundle ( config . Development )
resources := reflect . ValueOf ( b . Config . Resources )
2023-07-30 07:19:49 +00:00
for i := 0 ; i < resources . NumField ( ) ; i ++ {
field := resources . Field ( i )
if field . Kind ( ) == reflect . Map {
assert . True (
t ,
! field . IsNil ( ) && field . Len ( ) > 0 ,
2023-08-17 15:22:32 +00:00
"process_target_mode should support '%s' (please add it to process_target_mode.go and extend the test suite)" ,
2023-07-30 07:19:49 +00:00
resources . Type ( ) . Field ( i ) . Name ,
)
}
}
}
// Make sure that we at least rename all resources
func TestAllResourcesRenamed ( t * testing . T ) {
2023-11-15 14:03:36 +00:00
b := mockBundle ( config . Development )
2023-07-30 07:19:49 +00:00
2024-08-19 18:18:50 +00:00
m := bundle . Seq ( ProcessTargetMode ( ) , ApplyPresets ( ) )
2024-03-25 14:18:47 +00:00
diags := bundle . Apply ( context . Background ( ) , b , m )
require . NoError ( t , diags . Error ( ) )
2023-07-30 07:19:49 +00:00
Use dynamic configuration model in bundles (#1098)
## Changes
This is a fundamental change to how we load and process bundle
configuration. We now depend on the configuration being represented as a
`dyn.Value`. This representation is functionally equivalent to Go's
`any` (it is variadic) and allows us to capture metadata associated with
a value, such as where it was defined (e.g. file, line, and column). It
also allows us to represent Go's zero values properly (e.g. empty
string, integer equal to 0, or boolean false).
Using this representation allows us to let the configuration model
deviate from the typed structure we have been relying on so far
(`config.Root`). We need to deviate from these types when using
variables for fields that are not a string themselves. For example,
using `${var.num_workers}` for an integer `workers` field was impossible
until now (though not implemented in this change).
The loader for a `dyn.Value` includes functionality to capture any and
all type mismatches between the user-defined configuration and the
expected types. These mismatches can be surfaced as validation errors in
future PRs.
Given that many mutators expect the typed struct to be the source of
truth, this change converts between the dynamic representation and the
typed representation on mutator entry and exit. Existing mutators can
continue to modify the typed representation and these modifications are
reflected in the dynamic representation (see `MarkMutatorEntry` and
`MarkMutatorExit` in `bundle/config/root.go`).
Required changes included in this change:
* The existing interpolation package is removed in favor of
`libs/dyn/dynvar`.
* Functionality to merge job clusters, job tasks, and pipeline clusters
are now all broken out into their own mutators.
To be implemented later:
* Allow variable references for non-string types.
* Surface diagnostics about the configuration provided by the user in
the validation output.
* Some mutators use a resource's configuration file path to resolve
related relative paths. These depend on `bundle/config/paths.Path` being
set and populated through `ConfigureConfigFilePath`. Instead, they
should interact with the dynamically typed configuration directly. Doing
this also unlocks being able to differentiate different base paths used
within a job (e.g. a task override with a relative path defined in a
directory other than the base job).
## Tests
* Existing unit tests pass (some have been modified to accommodate)
* Integration tests pass
2024-02-16 19:41:58 +00:00
resources := reflect . ValueOf ( b . Config . Resources )
2023-07-30 07:19:49 +00:00
for i := 0 ; i < resources . NumField ( ) ; i ++ {
field := resources . Field ( i )
if field . Kind ( ) == reflect . Map {
for _ , key := range field . MapKeys ( ) {
resource := field . MapIndex ( key )
nameField := resource . Elem ( ) . FieldByName ( "Name" )
if nameField . IsValid ( ) && nameField . Kind ( ) == reflect . String {
assert . True (
t ,
strings . Contains ( nameField . String ( ) , "dev" ) ,
2023-08-17 15:22:32 +00:00
"process_target_mode should rename '%s' in '%s'" ,
2023-07-30 07:19:49 +00:00
key ,
resources . Type ( ) . Field ( i ) . Name ,
)
}
}
}
}
}
2024-04-18 01:59:39 +00:00
func TestDisableLocking ( t * testing . T ) {
ctx := context . Background ( )
b := mockBundle ( config . Development )
2024-08-19 18:18:50 +00:00
transformDevelopmentMode ( ctx , b )
2024-04-18 01:59:39 +00:00
assert . False ( t , b . Config . Bundle . Deployment . Lock . IsEnabled ( ) )
}
func TestDisableLockingDisabled ( t * testing . T ) {
ctx := context . Background ( )
b := mockBundle ( config . Development )
explicitlyEnabled := true
b . Config . Bundle . Deployment . Lock . Enabled = & explicitlyEnabled
2024-08-19 18:18:50 +00:00
transformDevelopmentMode ( ctx , b )
2024-04-18 01:59:39 +00:00
assert . True ( t , b . Config . Bundle . Deployment . Lock . IsEnabled ( ) , "Deployment lock should remain enabled in development mode when explicitly enabled" )
}
2024-08-19 18:18:50 +00:00
func TestPrefixAlreadySet ( t * testing . T ) {
b := mockBundle ( config . Development )
b . Config . Presets . NamePrefix = "custom_lennart_deploy_"
m := bundle . Seq ( ProcessTargetMode ( ) , ApplyPresets ( ) )
diags := bundle . Apply ( context . Background ( ) , b , m )
require . NoError ( t , diags . Error ( ) )
assert . Equal ( t , "custom_lennart_deploy_job1" , b . Config . Resources . Jobs [ "job1" ] . Name )
}
func TestTagsAlreadySet ( t * testing . T ) {
b := mockBundle ( config . Development )
b . Config . Presets . Tags = map [ string ] string {
"custom" : "tag" ,
"dev" : "foo" ,
}
m := bundle . Seq ( ProcessTargetMode ( ) , ApplyPresets ( ) )
diags := bundle . Apply ( context . Background ( ) , b , m )
require . NoError ( t , diags . Error ( ) )
assert . Equal ( t , "tag" , b . Config . Resources . Jobs [ "job1" ] . Tags [ "custom" ] )
assert . Equal ( t , "foo" , b . Config . Resources . Jobs [ "job1" ] . Tags [ "dev" ] )
}
func TestTagsNil ( t * testing . T ) {
b := mockBundle ( config . Development )
b . Config . Presets . Tags = nil
m := bundle . Seq ( ProcessTargetMode ( ) , ApplyPresets ( ) )
diags := bundle . Apply ( context . Background ( ) , b , m )
require . NoError ( t , diags . Error ( ) )
assert . Equal ( t , "lennart" , b . Config . Resources . Jobs [ "job2" ] . Tags [ "dev" ] )
}
func TestTagsEmptySet ( t * testing . T ) {
b := mockBundle ( config . Development )
b . Config . Presets . Tags = map [ string ] string { }
m := bundle . Seq ( ProcessTargetMode ( ) , ApplyPresets ( ) )
diags := bundle . Apply ( context . Background ( ) , b , m )
require . NoError ( t , diags . Error ( ) )
assert . Equal ( t , "lennart" , b . Config . Resources . Jobs [ "job2" ] . Tags [ "dev" ] )
}
func TestJobsMaxConcurrentRunsAlreadySet ( t * testing . T ) {
b := mockBundle ( config . Development )
b . Config . Presets . JobsMaxConcurrentRuns = 10
m := bundle . Seq ( ProcessTargetMode ( ) , ApplyPresets ( ) )
diags := bundle . Apply ( context . Background ( ) , b , m )
require . NoError ( t , diags . Error ( ) )
assert . Equal ( t , 10 , b . Config . Resources . Jobs [ "job1" ] . MaxConcurrentRuns )
}
func TestJobsMaxConcurrentRunsDisabled ( t * testing . T ) {
b := mockBundle ( config . Development )
b . Config . Presets . JobsMaxConcurrentRuns = 1
m := bundle . Seq ( ProcessTargetMode ( ) , ApplyPresets ( ) )
diags := bundle . Apply ( context . Background ( ) , b , m )
require . NoError ( t , diags . Error ( ) )
assert . Equal ( t , 1 , b . Config . Resources . Jobs [ "job1" ] . MaxConcurrentRuns )
}
func TestTriggerPauseStatusWhenUnpaused ( t * testing . T ) {
b := mockBundle ( config . Development )
b . Config . Presets . TriggerPauseStatus = config . Unpaused
m := bundle . Seq ( ProcessTargetMode ( ) , ApplyPresets ( ) )
diags := bundle . Apply ( context . Background ( ) , b , m )
require . ErrorContains ( t , diags . Error ( ) , "target with 'mode: development' cannot set trigger pause status to UNPAUSED by default" )
}
func TestPipelinesDevelopmentDisabled ( t * testing . T ) {
b := mockBundle ( config . Development )
notEnabled := false
b . Config . Presets . PipelinesDevelopment = & notEnabled
m := bundle . Seq ( ProcessTargetMode ( ) , ApplyPresets ( ) )
diags := bundle . Apply ( context . Background ( ) , b , m )
require . NoError ( t , diags . Error ( ) )
assert . False ( t , b . Config . Resources . Pipelines [ "pipeline1" ] . PipelineSpec . Development )
}