diff --git a/Makefile b/Makefile index 66435284af..1f914dc5d9 100644 --- a/Makefile +++ b/Makefile @@ -57,7 +57,7 @@ sweep: ## destroy the whole architecture; USE ONLY FOR DEVELOPMENT ACCOUNTS @echo "Are you sure? [y/n]" >&2 @read -r REPLY; \ if echo "$$REPLY" | grep -qG "^[yY]$$"; then \ - TEST_SF_TF_ENABLE_SWEEP=1 go test -timeout 300s -run "^(TestSweepAll|Test_Sweeper_NukeStaleObjects)" ./pkg/sdk -v; \ + TEST_SF_TF_ENABLE_SWEEP=1 go test -timeout=10m -run "^(TestSweepAll|Test_Sweeper_NukeStaleObjects)" ./pkg/sdk -v; \ else echo "Aborting..."; \ fi; @@ -162,8 +162,20 @@ generate-resource-model-builders: ## Generate resource model builders clean-resource-model-builders: ## Clean resource model builders rm -f ./pkg/acceptance/bettertestspoc/config/model/*_gen.go -clean-all-assertions-and-config-models: clean-snowflake-object-assertions clean-snowflake-object-parameters-assertions clean-resource-assertions clean-resource-parameters-assertions clean-resource-show-output-assertions clean-resource-model-builders ## clean all generated assertions and config models +generate-provider-model-builders: ## Generate provider model builders + go generate ./pkg/acceptance/bettertestspoc/config/providermodel/generate.go -generate-all-assertions-and-config-models: generate-snowflake-object-assertions generate-snowflake-object-parameters-assertions generate-resource-assertions generate-resource-parameters-assertions generate-resource-show-output-assertions generate-resource-model-builders ## generate all assertions and config models +clean-provider-model-builders: ## Clean provider model builders + rm -f ./pkg/acceptance/bettertestspoc/config/providermodel/*_gen.go + +generate-datasource-model-builders: ## Generate datasource model builders + go generate ./pkg/acceptance/bettertestspoc/config/datasourcemodel/generate.go + +clean-datasource-model-builders: ## Clean datasource model builders + rm -f ./pkg/acceptance/bettertestspoc/config/datasourcemodel/*_gen.go + +clean-all-assertions-and-config-models: clean-snowflake-object-assertions clean-snowflake-object-parameters-assertions clean-resource-assertions clean-resource-parameters-assertions clean-resource-show-output-assertions clean-resource-model-builders clean-provider-model-builders clean-datasource-model-builders ## clean all generated assertions and config models + +generate-all-assertions-and-config-models: generate-snowflake-object-assertions generate-snowflake-object-parameters-assertions generate-resource-assertions generate-resource-parameters-assertions generate-resource-show-output-assertions generate-resource-model-builders generate-provider-model-builders generate-datasource-model-builders ## generate all assertions and config models .PHONY: build-local clean-generator-poc dev-setup dev-cleanup docs docs-check fmt fmt-check fumpt help install lint lint-fix mod mod-check pre-push pre-push-check sweep test test-acceptance uninstall-tf diff --git a/go.mod b/go.mod index 7f42d3fbd8..d599d2ef2c 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/gookit/color v1.5.4 github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 github.com/hashicorp/go-uuid v1.0.3 + github.com/hashicorp/hcl v1.0.0 github.com/hashicorp/terraform-json v0.21.0 github.com/hashicorp/terraform-plugin-framework v1.8.0 github.com/hashicorp/terraform-plugin-framework-validators v0.12.0 diff --git a/go.sum b/go.sum index f73906bfd7..7693a11e05 100644 --- a/go.sum +++ b/go.sum @@ -157,6 +157,8 @@ github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mO github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/hc-install v0.6.3 h1:yE/r1yJvWbtrJ0STwScgEnCanb0U9v7zp0Gbkmcoxqs= github.com/hashicorp/hc-install v0.6.3/go.mod h1:KamGdbodYzlufbWh4r9NRo8y6GLHWZP2GBtdnms1Ln0= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/hcl/v2 v2.19.1 h1://i05Jqznmb2EXqa39Nsvyan2o5XyMowW5fnCKW5RPI= github.com/hashicorp/hcl/v2 v2.19.1/go.mod h1:ThLC89FV4p9MPW804KVbe/cEXoQ8NZEh+JtMeeGErHE= github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= diff --git a/pkg/acceptance/asserts.go b/pkg/acceptance/asserts.go deleted file mode 100644 index 4ce547dfa9..0000000000 --- a/pkg/acceptance/asserts.go +++ /dev/null @@ -1,23 +0,0 @@ -package acceptance - -import ( - "fmt" - "strconv" - - "github.com/hashicorp/terraform-plugin-testing/helper/resource" -) - -func IsGreaterOrEqualTo(greaterOrEqualValue int) resource.CheckResourceAttrWithFunc { - return func(value string) error { - intValue, err := strconv.Atoi(value) - if err != nil { - return fmt.Errorf("unable to parse value %s as integer, err = %w", value, err) - } - - if intValue < greaterOrEqualValue { - return fmt.Errorf("expected value %d to be greater or equal to %d", intValue, greaterOrEqualValue) - } - - return nil - } -} diff --git a/pkg/acceptance/asserts_test.go b/pkg/acceptance/asserts_test.go deleted file mode 100644 index 6d7e488d7f..0000000000 --- a/pkg/acceptance/asserts_test.go +++ /dev/null @@ -1,73 +0,0 @@ -package acceptance - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestIsGreaterOrEqualTo(t *testing.T) { - testCases := []struct { - Name string - GreaterOrEqualTo int - Actual string - Error string - }{ - { - Name: "validation: smaller than expected", - GreaterOrEqualTo: 20, - Actual: "10", - Error: "expected value 10 to be greater or equal to 20", - }, - { - Name: "validation: zero actual value", - GreaterOrEqualTo: 20, - Actual: "0", - Error: "expected value 0 to be greater or equal to 20", - }, - { - Name: "validation: zero greater value", - GreaterOrEqualTo: 0, - Actual: "-10", - Error: "expected value -10 to be greater or equal to 0", - }, - { - Name: "validation: negative value", - GreaterOrEqualTo: -20, - Actual: "-30", - Error: "expected value -30 to be greater or equal to -20", - }, - { - Name: "validation: not int value", - GreaterOrEqualTo: 20, - Actual: "not_int", - Error: "unable to parse value not_int as integer, err = strconv.Atoi: parsing \"not_int\": invalid syntax", - }, - { - Name: "validation: equal value", - GreaterOrEqualTo: 20, - Actual: "20", - }, - { - Name: "validation: greater value", - GreaterOrEqualTo: 20, - Actual: "30", - }, - { - Name: "validation: greater value with expected negative value", - GreaterOrEqualTo: -20, - Actual: "30", - }, - } - - for _, testCase := range testCases { - t.Run(testCase.Name, func(t *testing.T) { - err := IsGreaterOrEqualTo(testCase.GreaterOrEqualTo)(testCase.Actual) - if testCase.Error != "" { - assert.ErrorContains(t, err, testCase.Error) - } else { - assert.NoError(t, err) - } - }) - } -} diff --git a/pkg/acceptance/bettertestspoc/README.md b/pkg/acceptance/bettertestspoc/README.md index 80fa4be4dc..a7c4d4eec0 100644 --- a/pkg/acceptance/bettertestspoc/README.md +++ b/pkg/acceptance/bettertestspoc/README.md @@ -23,7 +23,7 @@ It contains the following packages: - resource parameters assertions (generated in subpackage `resourceparametersassert`) - show output assertions (generated in subpackage `resourceshowoutputassert`) -- `config` - the new `ResourceModel` abstraction resides here. It provides models for objects and the builder methods allowing better config preparation in the acceptance tests. +- `config` - the new model abstractions (`ResourceModel`, `DatasourceModel`, and `ProviderModel`) reside here. They provide models for objects and the builder methods allowing better config preparation in the acceptance tests. It aims to be more readable than using `Config:` with hardcoded string or `ConfigFile:` for file that is not directly reachable from the test body. Also, it should be easier to reuse the models and prepare convenience extension methods. The models are already generated. ## How it works @@ -97,18 +97,58 @@ Resource config model builders can be generated automatically. For object `abc` - add object you want to generate to `allResourceSchemaDefs` slice in the `assert/resourceassert/gen/resource_schema_def.go` - to add custom (not generated) config builder methods create file `warehouse_model_ext` in the `config/model` package. Example would be: ```go -func BasicWarehouseModel( +func BasicAbcModel( name string, comment string, -) *WarehouseModel { - return WarehouseWithDefaultMeta(name).WithComment(comment) +) *AbcModel { + return AbcWithDefaultMeta(name).WithComment(comment) } -func (w *WarehouseModel) WithWarehouseSizeEnum(warehouseSize sdk.WarehouseSize) *WarehouseModel { +func (w *AbcModel) WithWarehouseSizeEnum(warehouseSize sdk.WarehouseSize) *AbcModel { return w.WithWarehouseSize(string(warehouseSize)) } ``` +### Adding new datasource config model builders +Data source config model builders can be generated automatically. For object `abc` do the following: +- add object you want to generate to `allDatasourcesSchemaDefs` slice in the `config/datasourcemodel/gen/datasource_schema_def.go` +- to add custom (not generated) config builder methods create file `abc_model_ext` in the `config/datasourcemodel` package. Example would be: +```go +func BasicAbcModel( + name string, + comment string, +) *AbcModel { + return AbcWithDefaultMeta(name).WithComment(comment) +} + +func (d *AbcModel) WithLimit(rows int) *AbcModel { + return d.WithLimitValue( + tfconfig.ObjectVariable(map[string]tfconfig.Variable{ + "rows": tfconfig.IntegerVariable(rows), + }), + ) +} +``` + +### Adding new provider config model builders +Provider config model builders can be generated automatically. For object `abc` do the following: +- add object you want to generate to `allProviderSchemaDefs` slice in the `config/providermodel/gen/provider_schema_def.go` +- to add custom (not generated) config builder methods create file `abc_model_ext` in the `config/providermodel` package. Example would be: +```go +func BasicAbcModel( + name string, + comment string, +) *AbcModel { + return AbcWithDefaultMeta(name).WithComment(comment) +} + +func (w *AbcModel) WithWarehouseSizeEnum(warehouseSize sdk.WarehouseSize) *AbcModel { + return w.WithWarehouseSize(string(warehouseSize)) +} +``` + +*Note*: our provider's config is already generated, so there should not be a need to generate any more providers (the regeneration or adding custom methods are still expected). + ### Running the generators Each of the above assertion types/config models has its own generator and cleanup entry in our Makefile. You can generate config models with: @@ -310,12 +350,12 @@ it will result in: Test: TestInt_Warehouses/create:_complete ``` -## Known limitations/planned improvements +## Planned improvements - Test all the utilities for assertion/model construction (public interfaces, methods, functions). - Verify if all the config types are supported. - Consider a better implementation for the model conversion to config (TODO left in `config/config.go`). - Support additional methods for references in models (TODO left in `config/config.go`). -- Support depends_on in models so that it can be chained like other resource fields (TODO left in `config/config.go`). +- Generate depends_on for all compatible models. Consider exporting it in meta (discussion: https://github.com/Snowflake-Labs/terraform-provider-snowflake/pull/3207#discussion_r1850053618). - Add a convenience function to concatenate multiple models (TODO left in `config/config.go`). - Add function to support using `ConfigFile:` in the acceptance tests (TODO left in `config/config.go`). - Replace `acceptance/snowflakechecks` with the new proposed Snowflake objects assertions. @@ -354,4 +394,32 @@ func (w *WarehouseDatasourceShowOutputAssert) IsEmpty() { - utilize `ContainsExactlyInAnyOrder` function in `pkg/acceptance/bettertestspoc/assert/commons.go` to create asserts on collections that are order independent - Additional asserts for sets and lists that wouldn't rely on the order of items saved to the state (SNOW-1706544) - support generating provider config and use generated configs in `pkg/provider/provider_acceptance_test.go` +- add config builders for other block types (Variable, Output, Locals, Module, Terraform) +- add provider to resource/datasource models (use in the grant_ownership_acceptance_test) +- explore HCL v2 in more detail (especially struct tags generation; probably with migration to plugin framework because of schema models); ref: https://github.com/hashicorp/hcl/blob/bee2dc2e75f7528ad85777b7a013c13796426bd6/gohcl/encode_test.go#L48 +- introduce some common interface for all three existing models (ResourceModel, DatasourceModel, and ProviderModel) +- rename ResourceSchemaDetails (because it is used for the datasources and provider too) +- consider duplicating the builders template from resource (currently same template used for datasources and provider which limits the customization possibilities for just one block type) +- consider merging ResourceModel with DatasourceModel (currently the implementation is really similar) +- remove schema.TypeMap workaround or make it wiser (e.g. during generation we could programmatically gather all schema.TypeMap and use this workaround only for them) - support asserting resource id in `assert/resourceassert/*_gen.go` + +## Known limitations +- generating provider config may misbehave when used only with one object/map paramter (like `params`), e.g.: +```json +{ + "provider": { + "snowflake": { + "params": { + "statement_timeout_in_seconds": 31337 + } + } + } +} +``` +will be converted to HCL: +```hcl +provider "snowflake" "params" { + statement_timeout_in_seconds = 31337 +} +``` diff --git a/pkg/acceptance/bettertestspoc/config/config.go b/pkg/acceptance/bettertestspoc/config/config.go index 1e256a811c..3174d20fca 100644 --- a/pkg/acceptance/bettertestspoc/config/config.go +++ b/pkg/acceptance/bettertestspoc/config/config.go @@ -9,70 +9,95 @@ import ( tfconfig "github.com/hashicorp/terraform-plugin-testing/config" - "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/provider/resources" "github.com/stretchr/testify/require" ) -// TODO [SNOW-1501905]: add possibility to have reference to another object (e.g. WithResourceMonitorReference); new config.Variable impl? -// TODO [SNOW-1501905]: generate With/SetDependsOn for the resources to preserve builder pattern -// TODO [SNOW-1501905]: add a convenience method to use multiple configs from multiple models - -// ResourceModel is the base interface all of our config models will implement. -// To allow easy implementation, ResourceModelMeta can be embedded inside the struct (and the struct will automatically implement it). -type ResourceModel interface { - Resource() resources.Resource - ResourceName() string - SetResourceName(name string) - ResourceReference() string - DependsOn() []string - SetDependsOn(values ...string) -} +// ResourceFromModel should be used in terraform acceptance tests for Config attribute to get string config from ResourceModel. +// Current implementation is an improved implementation using two steps: +// - .tf.json generation +// - conversion to HCL using hcl v1 lib +// It is still not ideal. HCL v2 should be considered. +func ResourceFromModel(t *testing.T, model ResourceModel) string { + t.Helper() -type ResourceModelMeta struct { - name string - resource resources.Resource - dependsOn []string -} + resourceJson, err := DefaultJsonConfigProvider.ResourceJsonFromModel(model) + require.NoError(t, err) -func (m *ResourceModelMeta) Resource() resources.Resource { - return m.resource -} + hcl, err := DefaultHclConfigProvider.HclFromJson(resourceJson) + require.NoError(t, err) + t.Logf("Generated config:\n%s", hcl) -func (m *ResourceModelMeta) ResourceName() string { - return m.name + return hcl } -func (m *ResourceModelMeta) SetResourceName(name string) { - m.name = name -} +// DatasourceFromModel should be used in terraform acceptance tests for Config attribute to get string config from DatasourceModel. +// Current implementation is an improved implementation using two steps: +// - .tf.json generation +// - conversion to HCL using hcl v1 lib +// It is still not ideal. HCL v2 should be considered. +func DatasourceFromModel(t *testing.T, model DatasourceModel) string { + t.Helper() -func (m *ResourceModelMeta) ResourceReference() string { - return fmt.Sprintf(`%s.%s`, m.resource, m.name) -} + datasourceJson, err := DefaultJsonConfigProvider.DatasourceJsonFromModel(model) + require.NoError(t, err) -func (m *ResourceModelMeta) DependsOn() []string { - return m.dependsOn -} + hcl, err := DefaultHclConfigProvider.HclFromJson(datasourceJson) + require.NoError(t, err) + t.Logf("Generated config:\n%s", hcl) -func (m *ResourceModelMeta) SetDependsOn(values ...string) { - m.dependsOn = values + return hcl } -// DefaultResourceName is exported to allow assertions against the resources using the default name. -const DefaultResourceName = "test" +// ProviderFromModel should be used in terraform acceptance tests for Config attribute to get string config from ProviderModel. +// Current implementation is an improved implementation using two steps: +// - .tf.json generation +// - conversion to HCL using hcl v1 lib +// It is still not ideal. HCL v2 should be considered. +func ProviderFromModel(t *testing.T, model ProviderModel) string { + t.Helper() + + providerJson, err := DefaultJsonConfigProvider.ProviderJsonFromModel(model) + require.NoError(t, err) + + hcl, err := DefaultHclConfigProvider.HclFromJson(providerJson) + require.NoError(t, err) + hcl, err = revertEqualSignForMapTypeAttributes(hcl) + require.NoError(t, err) -func DefaultMeta(resource resources.Resource) *ResourceModelMeta { - return &ResourceModelMeta{name: DefaultResourceName, resource: resource} + return hcl } -func Meta(resourceName string, resource resources.Resource) *ResourceModelMeta { - return &ResourceModelMeta{name: resourceName, resource: resource} +// FromModels allows to combine multiple models. +// TODO [SNOW-1501905]: introduce some common interface for all three existing models (ResourceModel, DatasourceModel, and ProviderModel) +func FromModels(t *testing.T, models ...any) string { + t.Helper() + + var sb strings.Builder + for i, model := range models { + switch m := model.(type) { + case ResourceModel: + sb.WriteString(ResourceFromModel(t, m)) + case DatasourceModel: + sb.WriteString(DatasourceFromModel(t, m)) + case ProviderModel: + sb.WriteString(ProviderFromModel(t, m)) + default: + t.Fatalf("unknown model: %T", model) + } + if i < len(models)-1 { + sb.WriteString("\n") + } + } + return sb.String() } // FromModel should be used in terraform acceptance tests for Config attribute to get string config from ResourceModel. // Current implementation is really straightforward but it could be improved and tested. It may not handle all cases (like objects, lists, sets) correctly. // TODO [SNOW-1501905]: use reflection to build config directly from model struct (or some other different way) // TODO [SNOW-1501905]: add support for config.TestStepConfigFunc (to use as ConfigFile); the naive implementation would be to just create a tmp directory and save file there +// TODO [SNOW-1501905]: add generating MarshalJSON() function +// TODO [SNOW-1501905]: migrate resources to new config generation method (above needed first) +// Use ResourceFromModel, DatasourceFromModel, ProviderFromModel, and FromModels instead. func FromModel(t *testing.T, model ResourceModel) string { t.Helper() @@ -99,7 +124,9 @@ func FromModel(t *testing.T, model ResourceModel) string { return s } -func FromModels(t *testing.T, models ...ResourceModel) string { +// FromModelsDeprecated allows to combine multiple resource models. +// Use FromModels instead. +func FromModelsDeprecated(t *testing.T, models ...ResourceModel) string { t.Helper() var sb strings.Builder for _, model := range models { @@ -110,6 +137,7 @@ func FromModels(t *testing.T, models ...ResourceModel) string { // ConfigVariablesFromModel constructs config.Variables needed in acceptance tests that are using ConfigVariables in // combination with ConfigDirectory. It's necessary for cases not supported by FromModel, like lists of objects. +// Use ResourceFromModel, DatasourceFromModel, ProviderFromModel, and FromModels instead. func ConfigVariablesFromModel(t *testing.T, model ResourceModel) tfconfig.Variables { t.Helper() variables := make(tfconfig.Variables) @@ -139,15 +167,3 @@ func ConfigVariablesFromModels(t *testing.T, variableName string, models ...Reso variableName: tfconfig.ListVariable(allVariables...), } } - -type nullVariable struct{} - -// MarshalJSON returns the JSON encoding of nullVariable. -func (v nullVariable) MarshalJSON() ([]byte, error) { - return json.Marshal(nil) -} - -// NullVariable returns nullVariable which implements Variable. -func NullVariable() nullVariable { - return nullVariable{} -} diff --git a/pkg/acceptance/bettertestspoc/config/config_test.go b/pkg/acceptance/bettertestspoc/config/config_test.go new file mode 100644 index 0000000000..f061058adc --- /dev/null +++ b/pkg/acceptance/bettertestspoc/config/config_test.go @@ -0,0 +1,189 @@ +package config_test + +import ( + "strings" + "testing" + + tfconfig "github.com/hashicorp/terraform-plugin-testing/config" + + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/config" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/config/datasourcemodel" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/config/providermodel" + "github.com/stretchr/testify/require" +) + +func Test_ResourceFromModelPoc(t *testing.T) { + t.Run("test basic", func(t *testing.T) { + someModel := Some("test", "Some Name") + expectedOutput := strings.TrimPrefix(` +resource "snowflake_share" "test" { + name = "Some Name" +} +`, "\n") + result := config.ResourceFromModel(t, someModel) + + require.Equal(t, expectedOutput, result) + }) + + t.Run("test full", func(t *testing.T) { + someModel := Some("test", "Some Name"). + WithComment("Some Comment"). + WithStringList("a", "b", "a"). + WithStringSet("a", "b", "c"). + WithObjectList( + Item{IntField: 1, StringField: "first item"}, + Item{IntField: 2, StringField: "second item"}, + ). + WithSingleObject("one", 2). + WithDependsOn("some_other_resource.some_name", "other_resource.some_other_name", "third_resource.third_name") + expectedOutput := strings.TrimPrefix(` +resource "snowflake_share" "test" { + comment = "Some Comment" + name = "Some Name" + string_list = ["a", "b", "a"] + string_set = ["a", "b", "c"] + object_list { + int_field = 1 + string_field = "first item" + } + object_list { + int_field = 2 + string_field = "second item" + } + single_object { + a = "one" + b = 2 + } + depends_on = [some_other_resource.some_name, other_resource.some_other_name, third_resource.third_name] +} +`, "\n") + + result := config.ResourceFromModel(t, someModel) + + require.Equal(t, expectedOutput, result) + }) +} + +func Test_DatasourceFromModelPoc(t *testing.T) { + t.Run("test basic", func(t *testing.T) { + datasourceModel := datasourcemodel.Databases("test") + expectedOutput := strings.TrimPrefix(` +data "snowflake_databases" "test" {} +`, "\n") + result := config.DatasourceFromModel(t, datasourceModel) + + require.Equal(t, expectedOutput, result) + }) + + t.Run("test with some arguments", func(t *testing.T) { + datasourceModel := datasourcemodel.Databases("test").WithLike("some").WithLimit(1) + expectedOutput := strings.TrimPrefix(` +data "snowflake_databases" "test" { + like = "some" + limit { + rows = 1 + } +} +`, "\n") + result := config.DatasourceFromModel(t, datasourceModel) + + require.Equal(t, expectedOutput, result) + }) + + t.Run("test with depends on", func(t *testing.T) { + datasourceModel := datasourcemodel.Databases("test"). + WithDependsOn("some_other_resource.some_name", "other_resource.some_other_name", "third_resource.third_name") + expectedOutput := strings.TrimPrefix(` +data "snowflake_databases" "test" { + depends_on = [some_other_resource.some_name, other_resource.some_other_name, third_resource.third_name] +} +`, "\n") + result := config.DatasourceFromModel(t, datasourceModel) + + require.Equal(t, expectedOutput, result) + }) +} + +func Test_ProviderFromModelPoc(t *testing.T) { + t.Run("test basic", func(t *testing.T) { + providerModel := providermodel.SnowflakeProvider() + expectedOutput := strings.TrimPrefix(` +provider "snowflake" {} +`, "\n") + result := config.ProviderFromModel(t, providerModel) + + require.Equal(t, expectedOutput, result) + }) + + t.Run("test with alias", func(t *testing.T) { + providerModel := providermodel.SnowflakeProviderAlias("other_name") + expectedOutput := strings.TrimPrefix(` +provider "snowflake" { + alias = "other_name" +} +`, "\n") + result := config.ProviderFromModel(t, providerModel) + + require.Equal(t, expectedOutput, result) + }) + + t.Run("test with some attributes", func(t *testing.T) { + providerModel := providermodel.SnowflakeProvider().WithProfile("some_profile").WithUser("some user") + expectedOutput := strings.TrimPrefix(` +provider "snowflake" { + profile = "some_profile" + user = "some user" +} +`, "\n") + result := config.ProviderFromModel(t, providerModel) + + require.Equal(t, expectedOutput, result) + }) + + t.Run("test with parameters map", func(t *testing.T) { + providerModel := providermodel.SnowflakeProvider().WithProfile("some_profile").WithParamsValue( + tfconfig.MapVariable(map[string]tfconfig.Variable{ + "statement_timeout_in_seconds": tfconfig.IntegerVariable(31337), + }), + ) + expectedOutput := strings.TrimPrefix(` +provider "snowflake" { + params = { + statement_timeout_in_seconds = 31337 + } + profile = "some_profile" +} +`, "\n") + result := config.ProviderFromModel(t, providerModel) + + require.Equal(t, expectedOutput, result) + }) +} + +func Test_ConfigFromModelsPoc(t *testing.T) { + t.Run("test basic", func(t *testing.T) { + providerModel := providermodel.SnowflakeProvider() + someModel := Some("test", "Some Name") + datasourceModel := datasourcemodel.Databases("test").WithDependsOn(someModel.ResourceReference()) + someOtherModel := Some("test2", "Some Name 2").WithDependsOn(datasourceModel.DatasourceReference()) + expectedOutput := strings.TrimPrefix(` +provider "snowflake" {} + +resource "snowflake_share" "test" { + name = "Some Name" +} + +data "snowflake_databases" "test" { + depends_on = [snowflake_share.test] +} + +resource "snowflake_share" "test2" { + name = "Some Name 2" + depends_on = [data.snowflake_databases.test] +} +`, "\n") + result := config.FromModels(t, providerModel, someModel, datasourceModel, someOtherModel) + + require.Equal(t, expectedOutput, result) + }) +} diff --git a/pkg/acceptance/bettertestspoc/config/custom_variables.go b/pkg/acceptance/bettertestspoc/config/custom_variables.go new file mode 100644 index 0000000000..4fb26cc040 --- /dev/null +++ b/pkg/acceptance/bettertestspoc/config/custom_variables.go @@ -0,0 +1,15 @@ +package config + +import "encoding/json" + +type nullVariable struct{} + +// MarshalJSON returns the JSON encoding of nullVariable. +func (v nullVariable) MarshalJSON() ([]byte, error) { + return json.Marshal(nil) +} + +// NullVariable returns nullVariable which implements Variable. +func NullVariable() nullVariable { + return nullVariable{} +} diff --git a/pkg/acceptance/bettertestspoc/config/datasource_model.go b/pkg/acceptance/bettertestspoc/config/datasource_model.go new file mode 100644 index 0000000000..4fada6ae84 --- /dev/null +++ b/pkg/acceptance/bettertestspoc/config/datasource_model.go @@ -0,0 +1,60 @@ +package config + +import ( + "fmt" + + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/provider/datasources" +) + +// DatasourceModel is the base interface all of our datasource config models will implement. +// To allow easy implementation, DatasourceModelMeta can be embedded inside the struct (and the struct will automatically implement it). +// TODO [SNOW-1501905]: consider merging ResourceModel with DatasourceModel (currently the implementation is really similar) +type DatasourceModel interface { + Datasource() datasources.Datasource + DatasourceName() string + SetDatasourceName(name string) + DatasourceReference() string + DependsOn() []string + SetDependsOn(values ...string) +} + +type DatasourceModelMeta struct { + name string + datasource datasources.Datasource + dependsOn []string +} + +func (m *DatasourceModelMeta) Datasource() datasources.Datasource { + return m.datasource +} + +func (m *DatasourceModelMeta) DatasourceName() string { + return m.name +} + +func (m *DatasourceModelMeta) SetDatasourceName(name string) { + m.name = name +} + +func (m *DatasourceModelMeta) DatasourceReference() string { + return fmt.Sprintf(`data.%s.%s`, m.datasource, m.name) +} + +func (m *DatasourceModelMeta) DependsOn() []string { + return m.dependsOn +} + +func (m *DatasourceModelMeta) SetDependsOn(values ...string) { + m.dependsOn = values +} + +// DefaultDatasourceName is exported to allow assertions against the resources using the default name. +const DefaultDatasourceName = "test" + +func DatasourceDefaultMeta(datasource datasources.Datasource) *DatasourceModelMeta { + return &DatasourceModelMeta{name: DefaultDatasourceName, datasource: datasource} +} + +func DatasourceMeta(resourceName string, datasource datasources.Datasource) *DatasourceModelMeta { + return &DatasourceModelMeta{name: resourceName, datasource: datasource} +} diff --git a/pkg/acceptance/bettertestspoc/config/datasourcemodel/database_model_ext.go b/pkg/acceptance/bettertestspoc/config/datasourcemodel/database_model_ext.go new file mode 100644 index 0000000000..96e4ec2d28 --- /dev/null +++ b/pkg/acceptance/bettertestspoc/config/datasourcemodel/database_model_ext.go @@ -0,0 +1,17 @@ +package datasourcemodel + +import ( + "encoding/json" +) + +// Based on https://medium.com/picus-security-engineering/custom-json-marshaller-in-go-and-common-pitfalls-c43fa774db05. +func (d *DatabaseModel) MarshalJSON() ([]byte, error) { + type Alias DatabaseModel + return json.Marshal(&struct { + *Alias + DependsOn []string `json:"depends_on,omitempty"` + }{ + Alias: (*Alias)(d), + DependsOn: d.DependsOn(), + }) +} diff --git a/pkg/acceptance/bettertestspoc/config/datasourcemodel/database_model_gen.go b/pkg/acceptance/bettertestspoc/config/datasourcemodel/database_model_gen.go new file mode 100644 index 0000000000..dd99002397 --- /dev/null +++ b/pkg/acceptance/bettertestspoc/config/datasourcemodel/database_model_gen.go @@ -0,0 +1,143 @@ +// Code generated by config model builder generator; DO NOT EDIT. + +package datasourcemodel + +import ( + tfconfig "github.com/hashicorp/terraform-plugin-testing/config" + + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/config" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/provider/datasources" +) + +type DatabaseModel struct { + Comment tfconfig.Variable `json:"comment,omitempty"` + CreatedOn tfconfig.Variable `json:"created_on,omitempty"` + IsCurrent tfconfig.Variable `json:"is_current,omitempty"` + IsDefault tfconfig.Variable `json:"is_default,omitempty"` + Name tfconfig.Variable `json:"name,omitempty"` + Options tfconfig.Variable `json:"options,omitempty"` + Origin tfconfig.Variable `json:"origin,omitempty"` + Owner tfconfig.Variable `json:"owner,omitempty"` + RetentionTime tfconfig.Variable `json:"retention_time,omitempty"` + + *config.DatasourceModelMeta +} + +///////////////////////////////////////////////// +// Basic builders (resource name and required) // +///////////////////////////////////////////////// + +func Database( + datasourceName string, + name string, +) *DatabaseModel { + d := &DatabaseModel{DatasourceModelMeta: config.DatasourceMeta(datasourceName, datasources.Database)} + d.WithName(name) + return d +} + +func DatabaseWithDefaultMeta( + name string, +) *DatabaseModel { + d := &DatabaseModel{DatasourceModelMeta: config.DatasourceDefaultMeta(datasources.Database)} + d.WithName(name) + return d +} + +///////////////////////////////// +// below all the proper values // +///////////////////////////////// + +func (d *DatabaseModel) WithComment(comment string) *DatabaseModel { + d.Comment = tfconfig.StringVariable(comment) + return d +} + +func (d *DatabaseModel) WithCreatedOn(createdOn string) *DatabaseModel { + d.CreatedOn = tfconfig.StringVariable(createdOn) + return d +} + +func (d *DatabaseModel) WithIsCurrent(isCurrent bool) *DatabaseModel { + d.IsCurrent = tfconfig.BoolVariable(isCurrent) + return d +} + +func (d *DatabaseModel) WithIsDefault(isDefault bool) *DatabaseModel { + d.IsDefault = tfconfig.BoolVariable(isDefault) + return d +} + +func (d *DatabaseModel) WithName(name string) *DatabaseModel { + d.Name = tfconfig.StringVariable(name) + return d +} + +func (d *DatabaseModel) WithOptions(options string) *DatabaseModel { + d.Options = tfconfig.StringVariable(options) + return d +} + +func (d *DatabaseModel) WithOrigin(origin string) *DatabaseModel { + d.Origin = tfconfig.StringVariable(origin) + return d +} + +func (d *DatabaseModel) WithOwner(owner string) *DatabaseModel { + d.Owner = tfconfig.StringVariable(owner) + return d +} + +func (d *DatabaseModel) WithRetentionTime(retentionTime int) *DatabaseModel { + d.RetentionTime = tfconfig.IntegerVariable(retentionTime) + return d +} + +////////////////////////////////////////// +// below it's possible to set any value // +////////////////////////////////////////// + +func (d *DatabaseModel) WithCommentValue(value tfconfig.Variable) *DatabaseModel { + d.Comment = value + return d +} + +func (d *DatabaseModel) WithCreatedOnValue(value tfconfig.Variable) *DatabaseModel { + d.CreatedOn = value + return d +} + +func (d *DatabaseModel) WithIsCurrentValue(value tfconfig.Variable) *DatabaseModel { + d.IsCurrent = value + return d +} + +func (d *DatabaseModel) WithIsDefaultValue(value tfconfig.Variable) *DatabaseModel { + d.IsDefault = value + return d +} + +func (d *DatabaseModel) WithNameValue(value tfconfig.Variable) *DatabaseModel { + d.Name = value + return d +} + +func (d *DatabaseModel) WithOptionsValue(value tfconfig.Variable) *DatabaseModel { + d.Options = value + return d +} + +func (d *DatabaseModel) WithOriginValue(value tfconfig.Variable) *DatabaseModel { + d.Origin = value + return d +} + +func (d *DatabaseModel) WithOwnerValue(value tfconfig.Variable) *DatabaseModel { + d.Owner = value + return d +} + +func (d *DatabaseModel) WithRetentionTimeValue(value tfconfig.Variable) *DatabaseModel { + d.RetentionTime = value + return d +} diff --git a/pkg/acceptance/bettertestspoc/config/datasourcemodel/databases_model_ext.go b/pkg/acceptance/bettertestspoc/config/datasourcemodel/databases_model_ext.go new file mode 100644 index 0000000000..1e19163087 --- /dev/null +++ b/pkg/acceptance/bettertestspoc/config/datasourcemodel/databases_model_ext.go @@ -0,0 +1,32 @@ +package datasourcemodel + +import ( + "encoding/json" + + tfconfig "github.com/hashicorp/terraform-plugin-testing/config" +) + +// Based on https://medium.com/picus-security-engineering/custom-json-marshaller-in-go-and-common-pitfalls-c43fa774db05. +func (d *DatabasesModel) MarshalJSON() ([]byte, error) { + type Alias DatabasesModel + return json.Marshal(&struct { + *Alias + DependsOn []string `json:"depends_on,omitempty"` + }{ + Alias: (*Alias)(d), + DependsOn: d.DependsOn(), + }) +} + +func (d *DatabasesModel) WithDependsOn(values ...string) *DatabasesModel { + d.SetDependsOn(values...) + return d +} + +func (d *DatabasesModel) WithLimit(rows int) *DatabasesModel { + return d.WithLimitValue( + tfconfig.ObjectVariable(map[string]tfconfig.Variable{ + "rows": tfconfig.IntegerVariable(rows), + }), + ) +} diff --git a/pkg/acceptance/bettertestspoc/config/datasourcemodel/databases_model_gen.go b/pkg/acceptance/bettertestspoc/config/datasourcemodel/databases_model_gen.go new file mode 100644 index 0000000000..f2adf41b91 --- /dev/null +++ b/pkg/acceptance/bettertestspoc/config/datasourcemodel/databases_model_gen.go @@ -0,0 +1,99 @@ +// Code generated by config model builder generator; DO NOT EDIT. + +package datasourcemodel + +import ( + tfconfig "github.com/hashicorp/terraform-plugin-testing/config" + + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/config" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/provider/datasources" +) + +type DatabasesModel struct { + Databases tfconfig.Variable `json:"databases,omitempty"` + Like tfconfig.Variable `json:"like,omitempty"` + Limit tfconfig.Variable `json:"limit,omitempty"` + StartsWith tfconfig.Variable `json:"starts_with,omitempty"` + WithDescribe tfconfig.Variable `json:"with_describe,omitempty"` + WithParameters tfconfig.Variable `json:"with_parameters,omitempty"` + + *config.DatasourceModelMeta +} + +///////////////////////////////////////////////// +// Basic builders (resource name and required) // +///////////////////////////////////////////////// + +func Databases( + datasourceName string, +) *DatabasesModel { + d := &DatabasesModel{DatasourceModelMeta: config.DatasourceMeta(datasourceName, datasources.Databases)} + return d +} + +func DatabasesWithDefaultMeta() *DatabasesModel { + d := &DatabasesModel{DatasourceModelMeta: config.DatasourceDefaultMeta(datasources.Databases)} + return d +} + +///////////////////////////////// +// below all the proper values // +///////////////////////////////// + +// databases attribute type is not yet supported, so WithDatabases can't be generated + +func (d *DatabasesModel) WithLike(like string) *DatabasesModel { + d.Like = tfconfig.StringVariable(like) + return d +} + +// limit attribute type is not yet supported, so WithLimit can't be generated + +func (d *DatabasesModel) WithStartsWith(startsWith string) *DatabasesModel { + d.StartsWith = tfconfig.StringVariable(startsWith) + return d +} + +func (d *DatabasesModel) WithWithDescribe(withDescribe bool) *DatabasesModel { + d.WithDescribe = tfconfig.BoolVariable(withDescribe) + return d +} + +func (d *DatabasesModel) WithWithParameters(withParameters bool) *DatabasesModel { + d.WithParameters = tfconfig.BoolVariable(withParameters) + return d +} + +////////////////////////////////////////// +// below it's possible to set any value // +////////////////////////////////////////// + +func (d *DatabasesModel) WithDatabasesValue(value tfconfig.Variable) *DatabasesModel { + d.Databases = value + return d +} + +func (d *DatabasesModel) WithLikeValue(value tfconfig.Variable) *DatabasesModel { + d.Like = value + return d +} + +func (d *DatabasesModel) WithLimitValue(value tfconfig.Variable) *DatabasesModel { + d.Limit = value + return d +} + +func (d *DatabasesModel) WithStartsWithValue(value tfconfig.Variable) *DatabasesModel { + d.StartsWith = value + return d +} + +func (d *DatabasesModel) WithWithDescribeValue(value tfconfig.Variable) *DatabasesModel { + d.WithDescribe = value + return d +} + +func (d *DatabasesModel) WithWithParametersValue(value tfconfig.Variable) *DatabasesModel { + d.WithParameters = value + return d +} diff --git a/pkg/acceptance/bettertestspoc/config/datasourcemodel/gen/datasource_schema_def.go b/pkg/acceptance/bettertestspoc/config/datasourcemodel/gen/datasource_schema_def.go new file mode 100644 index 0000000000..e507ea2800 --- /dev/null +++ b/pkg/acceptance/bettertestspoc/config/datasourcemodel/gen/datasource_schema_def.go @@ -0,0 +1,33 @@ +package gen + +import ( + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/datasources" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/genhelpers" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +type DatasourceSchemaDef struct { + name string + schema map[string]*schema.Schema +} + +// TODO [SNOW-1501905]: rename ResourceSchemaDetails (because it is used for the datasources and provider too) +func GetDatasourceSchemaDetails() []genhelpers.ResourceSchemaDetails { + allDatasourcesSchemas := allDatasourcesSchemaDefs + allDatasourcesSchemasDetails := make([]genhelpers.ResourceSchemaDetails, len(allDatasourcesSchemas)) + for idx, s := range allDatasourcesSchemas { + allDatasourcesSchemasDetails[idx] = genhelpers.ExtractResourceSchemaDetails(s.name, s.schema) + } + return allDatasourcesSchemasDetails +} + +var allDatasourcesSchemaDefs = []DatasourceSchemaDef{ + { + name: "Database", + schema: datasources.Database().Schema, + }, + { + name: "Databases", + schema: datasources.Databases().Schema, + }, +} diff --git a/pkg/acceptance/bettertestspoc/config/datasourcemodel/gen/main/main.go b/pkg/acceptance/bettertestspoc/config/datasourcemodel/gen/main/main.go new file mode 100644 index 0000000000..0099d8a413 --- /dev/null +++ b/pkg/acceptance/bettertestspoc/config/datasourcemodel/gen/main/main.go @@ -0,0 +1,22 @@ +package main + +import ( + resourcemodelgen "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/config/model/gen" + + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/config/datasourcemodel/gen" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/genhelpers" +) + +func main() { + genhelpers.NewGenerator( + gen.GetDatasourceSchemaDetails, + resourcemodelgen.ModelFromResourceSchemaDetails, + getFilename, + gen.AllTemplates, + ). + RunAndHandleOsReturn() +} + +func getFilename(_ genhelpers.ResourceSchemaDetails, model resourcemodelgen.ResourceConfigBuilderModel) string { + return genhelpers.ToSnakeCase(model.Name) + "_model" + "_gen.go" +} diff --git a/pkg/acceptance/bettertestspoc/config/datasourcemodel/gen/templates.go b/pkg/acceptance/bettertestspoc/config/datasourcemodel/gen/templates.go new file mode 100644 index 0000000000..0c0da8794d --- /dev/null +++ b/pkg/acceptance/bettertestspoc/config/datasourcemodel/gen/templates.go @@ -0,0 +1,28 @@ +package gen + +import ( + "text/template" + + _ "embed" + + resourcemodel "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/config/model/gen" + + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/internal/genhelpers" +) + +var ( + //go:embed templates/preamble.tmpl + preambleTemplateContent string + PreambleTemplate, _ = template.New("preambleTemplate").Parse(preambleTemplateContent) + + //go:embed templates/definition.tmpl + definitionTemplateContent string + DefinitionTemplate, _ = template.New("definitionTemplate").Funcs(genhelpers.BuildTemplateFuncMap( + genhelpers.FirstLetterLowercase, + genhelpers.FirstLetter, + genhelpers.SnakeCaseToCamel, + )).Parse(definitionTemplateContent) + + // TODO [SNOW-1501905]: consider duplicating the builders template from resource (currently same template used for datasources and provider which limits the customization possibilities for just one block type) + AllTemplates = []*template.Template{PreambleTemplate, DefinitionTemplate, resourcemodel.BuildersTemplate} +) diff --git a/pkg/acceptance/bettertestspoc/config/datasourcemodel/gen/templates/definition.tmpl b/pkg/acceptance/bettertestspoc/config/datasourcemodel/gen/templates/definition.tmpl new file mode 100644 index 0000000000..0eff2ab707 --- /dev/null +++ b/pkg/acceptance/bettertestspoc/config/datasourcemodel/gen/templates/definition.tmpl @@ -0,0 +1,53 @@ +{{- /*gotype: github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/config/model/gen.ResourceConfigBuilderModel*/ -}} + +{{- $modelName := .Name | printf "%sModel" -}} +{{- $nameLowerCase := FirstLetterLowercase .Name -}} +{{- $modelVar := FirstLetter $nameLowerCase }} + +type {{ $modelName }} struct { + {{ range .Attributes -}} + {{ SnakeCaseToCamel .Name }} tfconfig.Variable `json:"{{ .Name }},omitempty"` + {{ end }} + *config.DatasourceModelMeta +} + +///////////////////////////////////////////////// +// Basic builders (resource name and required) // +///////////////////////////////////////////////// + +func {{ .Name }}( + datasourceName string, + {{ range .Attributes -}} + {{- $attributeNameCamel := SnakeCaseToCamel .Name -}} + {{ if .Required -}} + {{ FirstLetterLowercase $attributeNameCamel }} {{ .AttributeType }}, + {{ end }} + {{- end -}} +) *{{ $modelName }} { + {{ $modelVar }} := &{{ $modelName }}{DatasourceModelMeta: config.DatasourceMeta(datasourceName, datasources.{{ .Name }})} + {{ range .Attributes -}} + {{- $attributeNameCamel := SnakeCaseToCamel .Name -}} + {{ if .Required -}} + {{ $modelVar }}.With{{ $attributeNameCamel }}({{ FirstLetterLowercase $attributeNameCamel }}) + {{ end }} + {{- end -}} + return {{ $modelVar }} +} + +func {{ .Name }}WithDefaultMeta( + {{ range .Attributes -}} + {{- $attributeNameCamel := SnakeCaseToCamel .Name -}} + {{ if .Required -}} + {{ FirstLetterLowercase $attributeNameCamel }} {{ .AttributeType }}, + {{ end }} + {{- end -}} +) *{{ $modelName }} { + {{ $modelVar }} := &{{ $modelName }}{DatasourceModelMeta: config.DatasourceDefaultMeta(datasources.{{ .Name }})} + {{ range .Attributes -}} + {{- $attributeNameCamel := SnakeCaseToCamel .Name -}} + {{ if .Required -}} + {{ $modelVar }}.With{{ $attributeNameCamel }}({{ FirstLetterLowercase $attributeNameCamel }}) + {{ end }} + {{- end -}} + return {{ $modelVar }} +} diff --git a/pkg/acceptance/bettertestspoc/config/datasourcemodel/gen/templates/preamble.tmpl b/pkg/acceptance/bettertestspoc/config/datasourcemodel/gen/templates/preamble.tmpl new file mode 100644 index 0000000000..ebd7566570 --- /dev/null +++ b/pkg/acceptance/bettertestspoc/config/datasourcemodel/gen/templates/preamble.tmpl @@ -0,0 +1,16 @@ +{{- /*gotype: github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/config/model/gen.PreambleModel*/ -}} + +// Code generated by config model builder generator; DO NOT EDIT. + +package {{ .PackageName }} + +import ( + {{- range .AdditionalStandardImports }} + "{{- . }}" + {{- end }} + + tfconfig "github.com/hashicorp/terraform-plugin-testing/config" + + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/acceptance/bettertestspoc/config" + "github.com/Snowflake-Labs/terraform-provider-snowflake/pkg/provider/datasources" +) diff --git a/pkg/acceptance/bettertestspoc/config/datasourcemodel/generate.go b/pkg/acceptance/bettertestspoc/config/datasourcemodel/generate.go new file mode 100644 index 0000000000..383a77c127 --- /dev/null +++ b/pkg/acceptance/bettertestspoc/config/datasourcemodel/generate.go @@ -0,0 +1,3 @@ +package datasourcemodel + +//go:generate go run ./gen/main/main.go $SF_TF_GENERATOR_ARGS diff --git a/pkg/acceptance/bettertestspoc/config/experiments/hcl_v1_test.go b/pkg/acceptance/bettertestspoc/config/experiments/hcl_v1_test.go new file mode 100644 index 0000000000..c01280c5cb --- /dev/null +++ b/pkg/acceptance/bettertestspoc/config/experiments/hcl_v1_test.go @@ -0,0 +1,126 @@ +package experiments_test + +import ( + "bytes" + "fmt" + "testing" + + hclv1printer "github.com/hashicorp/hcl/hcl/printer" + hclv1parser "github.com/hashicorp/hcl/json/parser" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// Test_exploreHclV1 shows conversion from .tf.json to .tf using HCL v1 lib. +// The main takes: +// - block types (e.g. resource) stay quoted (while they should be unquoted) +// - attribute names stay quoted (while they should be unquoted) +// - objects and object lists have equal sign (what should not be there) +// - references in depends_on stay quoted (while they should be unquoted) +// - there are two newlines between every attribute +// +// Because of the above, the current implementation using json generation and later conversion to HCL using hcl v1 +// introduces formatters tackling the issues above. This is not the perfect solution but it's fast and will work in the meantime. +// Check: config.DefaultHclConfigProvider. +// +// References: +// - https://developer.hashicorp.com/terraform/language/syntax/json +// - https://github.com/hashicorp/hcl/blob/56a9aee5207dbaed7f061cd926b96fc159d26ea0/json/spec.md +// - https://developer.hashicorp.com/terraform/language/resources/syntax +// - https://developer.hashicorp.com/terraform/language/meta-arguments/depends_on +// TODO [SNOW-1501905]: explore HCL v2 in more detail (especially struct tags generation; probably with migration to plugin framework because of schema models); ref: https://github.com/hashicorp/hcl/blob/bee2dc2e75f7528ad85777b7a013c13796426bd6/gohcl/encode_test.go#L48 +func Test_exploreHclV1(t *testing.T) { + convertJsonToHclStringV1 := func(json string) (string, error) { + parsed, err := hclv1parser.Parse([]byte(json)) + if err != nil { + return "", err + } + + var buffer bytes.Buffer + err = hclv1printer.Fprint(&buffer, parsed) + if err != nil { + return "", err + } + + formatted, err := hclv1printer.Format(buffer.Bytes()) + if err != nil { + return "", err + } + + return string(formatted), nil + } + + t.Run("test HCL v1", func(t *testing.T) { + resourceJson := `{ + "resource": { + "snowflake_share": { + "test": { + "attribute_int": 1, + "attribute_bool": true, + "attribute_string": "some string", + "string_template": "${resource.name.attribute}", + "string_list": ["a", "b", "a"], + "object_list": [ + { + "int_field": 1, + "string_field": "first item" + }, + { + "int_field": 2, + "string_field": "second item" + } + ], + "single_object": { + "prop1": 1, + "prop2": "two" + }, + "depends_on": [ + "some_other_resource.some_name", + "other_resource.some_other_name", + "data.some_datasource.some_fancy_datasource" + ] + } + } + } + }` + expectedResult := `"resource" "snowflake_share" "test" { + "attribute_int" = 1 + + "attribute_bool" = true + + "attribute_string" = "some string" + + "string_template" = "${resource.name.attribute}" + + "string_list" = ["a", "b", "a"] + + "object_list" = { + "int_field" = 1 + + "string_field" = "first item" + } + + "object_list" = { + "int_field" = 2 + + "string_field" = "second item" + } + + "single_object" = { + "prop1" = 1 + + "prop2" = "two" + } + + "depends_on" = ["some_other_resource.some_name", "other_resource.some_other_name", "data.some_datasource.some_fancy_datasource"] +} +` + + result, err := convertJsonToHclStringV1(resourceJson) + require.NoError(t, err) + assert.Equal(t, expectedResult, result) + + fmt.Printf("%s", result) + }) +} diff --git a/pkg/acceptance/bettertestspoc/config/hcl_config_provider.go b/pkg/acceptance/bettertestspoc/config/hcl_config_provider.go new file mode 100644 index 0000000000..632e2d502e --- /dev/null +++ b/pkg/acceptance/bettertestspoc/config/hcl_config_provider.go @@ -0,0 +1,128 @@ +package config + +import ( + "bytes" + "fmt" + "regexp" + "strings" + + hclv1printer "github.com/hashicorp/hcl/hcl/printer" + hclv1parser "github.com/hashicorp/hcl/json/parser" +) + +var DefaultHclConfigProvider = NewHclV1ConfigProvider(unquoteBlockType, fixBlockArguments, fixMultilinePrivateKey, unquoteArguments, unquoteArguments, removeDoubleNewlines, unquoteDependsOnReferences) + +// HclConfigProvider defines methods to generate .tf config from .tf.json configs. +type HclConfigProvider interface { + HclFromJson(json []byte) (string, error) +} + +type HclFormatter func(string) (string, error) + +type hclV1ConfigProvider struct { + formatters []HclFormatter +} + +func NewHclV1ConfigProvider(formatters ...HclFormatter) HclConfigProvider { + return &hclV1ConfigProvider{ + formatters: formatters, + } +} + +func (h *hclV1ConfigProvider) HclFromJson(json []byte) (string, error) { + hcl, err := convertJsonToHclStringV1(json) + if err != nil { + return "", err + } + + for _, formatter := range h.formatters { + hcl, err = formatter(hcl) + if err != nil { + return "", err + } + } + + return hcl, nil +} + +func convertJsonToHclStringV1(jsonBytes []byte) (string, error) { + parsed, err := hclv1parser.Parse(jsonBytes) + if err != nil { + return "", err + } + + var buffer bytes.Buffer + err = hclv1printer.Fprint(&buffer, parsed) + if err != nil { + return "", err + } + + formatted, err := hclv1printer.Format(buffer.Bytes()) + if err != nil { + return "", err + } + + return string(formatted), nil +} + +// Conversion to HCL using hcl v1 does not unquote block types (i.e. `"resource"` instead of expected `resource`). +// Check experiments subpackage for details. +func unquoteBlockType(s string) (string, error) { + blockTypeRegex := regexp.MustCompile(`"(resource|data|provider)"(( "\w+"){1,2} {)`) + return blockTypeRegex.ReplaceAllString(s, `$1$2`), nil +} + +// Conversion to HCL using hcl v1 uses `=` sign for objects and lists of objects. +// Check experiments subpackage for details. +func fixBlockArguments(s string) (string, error) { + argumentRegex := regexp.MustCompile(`( +)"(\w+)"( +)= ({\n)`) + return argumentRegex.ReplaceAllString(s, `$1$2$3$4`), nil +} + +// TODO [SNOW-1501905]: fix new lines replacement totally in this method, consider better placeholders (now there can't be multilinekeys for private key) +func fixMultilinePrivateKey(s string) (string, error) { + argumentRegex := regexp.MustCompile(fmt.Sprintf(`"%[1]s(.*)%[1]s"`, SnowflakeProviderConfigPrivateKey)) + submatches := argumentRegex.FindStringSubmatch(s) + if len(submatches) < 1 { + return s, nil + } else { + return argumentRegex.ReplaceAllString(s, fmt.Sprintf(`<