From 6baf1f04224a6c296968fc7d10b9ceb24e8d7996 Mon Sep 17 00:00:00 2001 From: Yoriyasu Yano <430092+yorinasub17@users.noreply.github.com> Date: Fri, 16 Dec 2022 11:07:10 -0600 Subject: [PATCH] Add support for injecting meta-arguments (#20) Signed-off-by: Yoriyasu Yano <430092+yorinasub17@users.noreply.github.com> --- src/_custom/helpers.libsonnet | 23 +++ src/_custom/main.libsonnet | 1 + src/_custom/meta.libsonnet | 179 ++++++++++++++++++ src/_custom/root.libsonnet | 31 ++- .../helpers/is_string_array/expected.json | 11 ++ .../helpers/is_string_array/test.jsonnet | 21 ++ test/fixtures/tfunit/meta/main.tf.jsonnet | 9 + test/unit_test.go | 15 ++ 8 files changed, 284 insertions(+), 6 deletions(-) create mode 100644 src/_custom/meta.libsonnet create mode 100644 test/fixtures/helpers/is_string_array/expected.json create mode 100644 test/fixtures/helpers/is_string_array/test.jsonnet create mode 100644 test/fixtures/tfunit/meta/main.tf.jsonnet diff --git a/src/_custom/helpers.libsonnet b/src/_custom/helpers.libsonnet index f91b05e..0a2a18a 100644 --- a/src/_custom/helpers.libsonnet +++ b/src/_custom/helpers.libsonnet @@ -54,8 +54,31 @@ local objItemsAll(obj) = ]; +// isStringArray returns true if the given value is an array with all elements as string. +// +// Args: +// v (any): The value being evaluated. +// +// Returns: +// A boolean indicating whether the given arg is a string array. +local isStringArray(v) = + std.isArray(v) + && ( + // We temporarily avoid using std.all since the linter does not support it. + std.foldl( + function(x, y) (x && y), + [ + std.isString(i) + for i in v + ], + true, + ) + ); + + { mergeAll:: mergeAll, objItems:: objItems, objItemsAll:: objItemsAll, + isStringArray:: isStringArray, } diff --git a/src/_custom/main.libsonnet b/src/_custom/main.libsonnet index 56c8885..6a90950 100644 --- a/src/_custom/main.libsonnet +++ b/src/_custom/main.libsonnet @@ -1,2 +1,3 @@ (import './root.libsonnet') ++ (import './meta.libsonnet') + (import './helpers.libsonnet') diff --git a/src/_custom/meta.libsonnet b/src/_custom/meta.libsonnet new file mode 100644 index 0000000..c27d0be --- /dev/null +++ b/src/_custom/meta.libsonnet @@ -0,0 +1,179 @@ +// Meta-argument constructor functions. Refer to the meta-arguments tab on +// https://developer.hashicorp.com/terraform/language for more information. +// +// This can be used as arguments to the `_meta` parameter for any resource +// or data source constructor generated by libgenerator. + +local h = import './helpers.libsonnet'; + +// newMeta will generate an object that can be mixed into any resource or data +// source to set the Terraform meta arguments. +local newMeta(count=null, depends_on=null, for_each=null, provider=null, lifecycle=null) = + local maybeCount = + if count != null then + { count: count } + else + {}; + + local maybeDependsOn = + if depends_on != null then + { depends_on: depends_on } + else + {}; + + local maybeForEach = + if for_each != null then + { for_each: for_each } + else + {}; + + local maybeProvider = + if provider != null then + { provider: provider } + else + {}; + + local maybeLifecycle = + if lifecycle != null then + { lifecycle: lifecycle } + else + {}; + + maybeCount + + maybeDependsOn + + maybeForEach + + maybeProvider + + maybeLifecycle; + + +// newModuleMeta will generate an object that can be mixed into any module call to set the Terraform meta arguments. +local newModuleMeta(count=null, depends_on=null, for_each=null, providers=null) = + local maybeCount = + if count != null then + { count: count } + else + {}; + + local maybeDependsOn = + if depends_on != null then + { depends_on: depends_on } + else + {}; + + local maybeForEach = + if for_each != null then + { for_each: for_each } + else + {}; + + local maybeProviders = + if providers != null then + if std.isObject(providers) then + { providers: providers } + else + error 'providers meta argument must be a map' + else + {}; + + maybeCount + + maybeDependsOn + + maybeForEach + + maybeProviders; + + +// newLifecycle will generate a new lifecycle block. Note that unlike the other functions, this includes type checking +// due to the Terraform requirement that the lifecycle block only supports literal values only. As such, it is easier to +// do a type check on the args since there is no possibility to use complex Terraform expressions (which will reduce to +// a string type in jsonnet). +local newLifecycle( + create_before_destroy=null, + prevent_destroy=null, + ignore_changes=null, + replace_triggered_by=null, + precondition=null, + postcondition=null, + ) = + local maybeCreateBeforeDestroy = + if create_before_destroy != null then + if std.isBoolean(create_before_destroy) then + { create_before_destroy: create_before_destroy } + else + error 'lifecycle meta argument attr create_before_destroy must be a boolean' + else + {}; + + local maybePreventDestroy = + if prevent_destroy != null then + if std.isBoolean(prevent_destroy) then + { prevent_destroy: prevent_destroy } + else + error 'lifecycle meta argument attr prevent_destroy must be a boolean' + else + {}; + + local maybeIgnoreChanges = + if ignore_changes != null then + if h.isStringArray(ignore_changes) then + { ignore_changes: ignore_changes } + else + error 'lifecycle meta argument attr ignore_changes must be a string array' + else + {}; + + local maybeReplaceTriggeredBy = + if replace_triggered_by != null then + if h.isStringArray(replace_triggered_by) then + { replace_triggered_by: replace_triggered_by } + else + error 'lifecycle meta argument attr replace_triggered_by must be a string array' + else + {}; + + local maybePrecondition = + if precondition != null then + if std.isArray(precondition) then + { precondition: precondition } + else + error 'lifecycle meta argument attr precondition must be an array of condition blocks' + else + {}; + + local maybePostcondition = + if postcondition != null then + if std.isArray(postcondition) then + { postcondition: postcondition } + else + error 'lifecycle meta argument attr postcondition must be an array of condition blocks' + else + {}; + + maybeCreateBeforeDestroy + + maybePreventDestroy + + maybeIgnoreChanges + + maybeReplaceTriggeredBy + + maybePrecondition + + maybePostcondition; + + +// newCondition will generate a new condition block that can be used as part of precondition or postcondition in the +// lifecycle block. +local newCondition(condition, error_message) = + { + condition: condition, + error_message: error_message, + }; + + +// root object +{ + meta:: { + new:: newMeta, + newForModule:: newModuleMeta, + lifecycle:: { + new:: newLifecycle, + condition:: { + new:: newCondition, + }, + }, + }, +} diff --git a/src/_custom/root.libsonnet b/src/_custom/root.libsonnet index 24ee5b0..f607f58 100644 --- a/src/_custom/root.libsonnet +++ b/src/_custom/root.libsonnet @@ -71,13 +71,20 @@ local withProvider(name, attrs, alias=null, src=null, version=null) = // type (string): The resource type to create (e.g., aws_instance, null_resource, etc). // label (string): The label to apply to the instance of the resource. // attrs (obj): The attributes for the instance of the resource being created. +// _meta (obj): An optional meta-argument object that (see meta.libsonnet). Note that while technically you can set +// the meta-arguments on the attrs object, it is recommended to use the `_meta` arg to highlight the +// meta-arguments. +// TODO: add type checking // // Returns: // A mixin object that injects the new resource into the root Terraform configuration. -local withResource(type, label, attrs) = { +local withResource(type, label, attrs, _meta={}) = { resource+: { [type]+: { - [label]: attrs, + [label]: ( + attrs + + _meta + ), }, }, @@ -114,13 +121,20 @@ local withResource(type, label, attrs) = { // type (string): The data source type to create (e.g., aws_instance, local_file, etc). // label (string): The label to apply to the instance of the data source. // attrs (obj): The attributes for the instance of the data source to read. +// _meta (obj): An optional meta-argument object that (see meta.libsonnet). Note that while technically you can set +// the meta-arguments on the attrs object, it is recommended to use the `_meta` arg to highlight the +// meta-arguments. +// TODO: add type checking // // Returns: // A mixin object that injects the new data source into the root Terraform configuration. -local withData(type, label, attrs) = { +local withData(type, label, attrs, _meta={}) = { data+: { [type]+: { - [label]: attrs, + [label]: ( + attrs + + _meta + ), }, }, @@ -161,10 +175,14 @@ local withData(type, label, attrs) = { // inputs (obj): The input values to pass into the module block. // version (string): The version of the module source to pull in, if the module source references a registry. When // null, the version field is omitted from the resulting module block. +// _meta (obj): An optional meta-argument object that (see meta.libsonnet). Note that while technically you can set +// the meta-arguments on the inputs object, it is recommended to use the `_meta` arg to highlight the +// meta-arguments. +// TODO: add type checking // // Returns: // A mixin object that injects the new module block into the root Terraform configuration. -local withModule(name, source, inputs, version=null) = +local withModule(name, source, inputs, version=null, _meta={}) = local maybeVersion = if version != null then { version: version } @@ -176,7 +194,8 @@ local withModule(name, source, inputs, version=null) = [name]: { source: source } + maybeVersion - + inputs, + + inputs + + _meta, }, _ref+:: { diff --git a/test/fixtures/helpers/is_string_array/expected.json b/test/fixtures/helpers/is_string_array/expected.json new file mode 100644 index 0000000..36356f9 --- /dev/null +++ b/test/fixtures/helpers/is_string_array/expected.json @@ -0,0 +1,11 @@ +{ + "arrayNestedString": false, + "arrayNestedStringFlattened": true, + "arrayNumber": false, + "arrayObject": false, + "arrayString": true, + "emptyArray": true, + "number": false, + "object": false, + "string": false +} diff --git a/test/fixtures/helpers/is_string_array/test.jsonnet b/test/fixtures/helpers/is_string_array/test.jsonnet new file mode 100644 index 0000000..966d800 --- /dev/null +++ b/test/fixtures/helpers/is_string_array/test.jsonnet @@ -0,0 +1,21 @@ +local h = import 'src/_custom/helpers.libsonnet'; + +{ + number: h.isStringArray(42), + string: h.isStringArray('hello world'), + object: h.isStringArray({ msg: 'hello world' }), + emptyArray: h.isStringArray([]), + arrayNumber: h.isStringArray([42]), + arrayObject: h.isStringArray([{ msg: 'hello world' }]), + arrayString: h.isStringArray(['hello', 'world']), + arrayNestedString: h.isStringArray([ + ['hello'], + ['world'], + ]), + arrayNestedStringFlattened: h.isStringArray( + std.flattenArrays([ + ['hello'], + ['world'], + ]), + ), +} diff --git a/test/fixtures/tfunit/meta/main.tf.jsonnet b/test/fixtures/tfunit/meta/main.tf.jsonnet new file mode 100644 index 0000000..cf51d7c --- /dev/null +++ b/test/fixtures/tfunit/meta/main.tf.jsonnet @@ -0,0 +1,9 @@ +local tf = import 'main.libsonnet'; + +tf.withResource( + 'null_resource', + 'foo', + {}, + _meta=tf.meta.new(count=5), +) ++ tf.withOutput('output', { num_created: '${length(null_resource.foo)}' }) diff --git a/test/unit_test.go b/test/unit_test.go index 14ace0d..eefe536 100644 --- a/test/unit_test.go +++ b/test/unit_test.go @@ -142,6 +142,21 @@ func TestUnitRef(t *testing.T) { g.Expect(out.NullResourceID).NotTo(Equal("")) } +func TestUnitMeta(t *testing.T) { + t.Parallel() + g := NewGomegaWithT(t) + + var out struct { + NumCreated int `json:"num_created"` + } + + metaPath := filepath.Join(unitTestFixtureDir, "meta") + jsonnetFPath := filepath.Join(metaPath, "main.tf.jsonnet") + err := renderAndApplyE(t, jsonnetFPath, nil, &out) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(out.NumCreated).To(Equal(5)) +} + func renderAndApplyE( t *testing.T, jsonnetFPath string,