From 28f4719e711c568f2e6216f37a0afc7d576f4a98 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Mon, 24 Feb 2025 09:42:02 -0500 Subject: [PATCH 01/20] new attribute additions (breaks build for existing internal schema attributes) --- go.mod | 10 ++++----- go.sum | 40 +++++++++++++++++----------------- internal/fwschema/attribute.go | 18 +++++++++++++++ 3 files changed, 43 insertions(+), 25 deletions(-) diff --git a/go.mod b/go.mod index 207c0347e..a6671ad84 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ toolchain go1.22.7 require ( github.com/google/go-cmp v0.6.0 - github.com/hashicorp/terraform-plugin-go v0.26.0 + github.com/hashicorp/terraform-plugin-go v0.26.1-0.20250219133416-0561ec7f0caf github.com/hashicorp/terraform-plugin-log v0.9.0 ) @@ -14,7 +14,7 @@ require ( github.com/fatih/color v1.13.0 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/hashicorp/go-hclog v1.5.0 // indirect - github.com/hashicorp/go-plugin v1.6.2 // indirect + github.com/hashicorp/go-plugin v1.6.3 // indirect github.com/hashicorp/go-uuid v1.0.3 // indirect github.com/hashicorp/terraform-registry-address v0.2.4 // indirect github.com/hashicorp/terraform-svchost v0.1.1 // indirect @@ -28,7 +28,7 @@ require ( golang.org/x/net v0.34.0 // indirect golang.org/x/sys v0.29.0 // indirect golang.org/x/text v0.21.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 // indirect - google.golang.org/grpc v1.69.4 // indirect - google.golang.org/protobuf v1.36.3 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20241202173237-19429a94021a // indirect + google.golang.org/grpc v1.70.0 // indirect + google.golang.org/protobuf v1.36.5 // indirect ) diff --git a/go.sum b/go.sum index 5a586522d..e44889059 100644 --- a/go.sum +++ b/go.sum @@ -17,12 +17,12 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c= github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= -github.com/hashicorp/go-plugin v1.6.2 h1:zdGAEd0V1lCaU0u+MxWQhtSDQmahpkwOun8U8EiRVog= -github.com/hashicorp/go-plugin v1.6.2/go.mod h1:CkgLQ5CZqNmdL9U9JzM532t8ZiYQ35+pj3b1FD37R0Q= +github.com/hashicorp/go-plugin v1.6.3 h1:xgHB+ZUSYeuJi96WtxEjzi23uh7YQpznjGh0U0UUrwg= +github.com/hashicorp/go-plugin v1.6.3/go.mod h1:MRobyh+Wc/nYy1V4KAXUiYfzxoYhs7V1mlH1Z7iY2h0= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/terraform-plugin-go v0.26.0 h1:cuIzCv4qwigug3OS7iKhpGAbZTiypAfFQmw8aE65O2M= -github.com/hashicorp/terraform-plugin-go v0.26.0/go.mod h1:+CXjuLDiFgqR+GcrM5a2E2Kal5t5q2jb0E3D57tTdNY= +github.com/hashicorp/terraform-plugin-go v0.26.1-0.20250219133416-0561ec7f0caf h1:qsHBfGoRp15P8vc95eAVMVO6erkMfpLKZ/6A5lLcR1w= +github.com/hashicorp/terraform-plugin-go v0.26.1-0.20250219133416-0561ec7f0caf/go.mod h1:dUu1RU16sOSKn6w4g+xaAnV0S0wHsVBEY/XH8jv1kx4= github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0= github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow= github.com/hashicorp/terraform-registry-address v0.2.4 h1:JXu/zHB2Ymg/TGVCRu10XqNa4Sh2bWcqCNyKWjnCPJA= @@ -54,16 +54,16 @@ github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IU github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= -go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY= -go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE= -go.opentelemetry.io/otel/metric v1.31.0 h1:FSErL0ATQAmYHUIzSezZibnyVlft1ybhy4ozRPcF2fE= -go.opentelemetry.io/otel/metric v1.31.0/go.mod h1:C3dEloVbLuYoX41KpmAhOqNriGbA+qqH6PQ5E5mUfnY= -go.opentelemetry.io/otel/sdk v1.31.0 h1:xLY3abVHYZ5HSfOg3l2E5LUj2Cwva5Y7yGxnSW9H5Gk= -go.opentelemetry.io/otel/sdk v1.31.0/go.mod h1:TfRbMdhvxIIr/B2N2LQW2S5v9m3gOQ/08KsbbO5BPT0= -go.opentelemetry.io/otel/sdk/metric v1.31.0 h1:i9hxxLJF/9kkvfHppyLL55aW7iIJz4JjxTeYusH7zMc= -go.opentelemetry.io/otel/sdk/metric v1.31.0/go.mod h1:CRInTMVvNhUKgSAMbKyTMxqOBC0zgyxzW55lZzX43Y8= -go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HYdmJys= -go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A= +go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U= +go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg= +go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M= +go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8= +go.opentelemetry.io/otel/sdk v1.32.0 h1:RNxepc9vK59A8XsgZQouW8ue8Gkb4jpWtJm9ge5lEG4= +go.opentelemetry.io/otel/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJCAP9LlJXEjU= +go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU= +go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ= +go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM= +go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8= golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -76,12 +76,12 @@ golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 h1:X58yt85/IXCx0Y3ZwN6sEIKZzQtDEYaBWrDvErdXrRE= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= -google.golang.org/grpc v1.69.4 h1:MF5TftSMkd8GLw/m0KM6V8CMOCY6NZ1NQDPGFgbTt4A= -google.golang.org/grpc v1.69.4/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4= -google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU= -google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241202173237-19429a94021a h1:hgh8P4EuoxpsuKMXX/To36nOFD7vixReXgn8lPGnt+o= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241202173237-19429a94021a/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU= +google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ= +google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/fwschema/attribute.go b/internal/fwschema/attribute.go index 6c440b319..b9fe31129 100644 --- a/internal/fwschema/attribute.go +++ b/internal/fwschema/attribute.go @@ -71,6 +71,16 @@ type Attribute interface { // // Write-only attributes are a managed-resource schema concept only. IsWriteOnly() bool + + // IsOptionalForImport should return true if the identity attribute is optional to be set by + // the practitioner when importing by identity. This is named differently than OptionalForImport + // to prevent a conflict with the relevant field name. + IsOptionalForImport() bool + + // IsRequiredForImport should return true if the identity attribute must be set by + // the practitioner when importing by identity. This is named differently than RequiredForImport + // to prevent a conflict with the relevant field name. + IsRequiredForImport() bool } // AttributesEqual is a helper function to perform equality testing on two @@ -113,5 +123,13 @@ func AttributesEqual(a, b Attribute) bool { return false } + if a.IsOptionalForImport() != b.IsOptionalForImport() { + return false + } + + if a.IsRequiredForImport() != b.IsRequiredForImport() { + return false + } + return true } From c4ad0c5bb9729f62d5a27dc2f6fd2f97b2069fe6 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Mon, 24 Feb 2025 09:42:29 -0500 Subject: [PATCH 02/20] update internal testing/testschema --- .../testing/testschema/attributewithbooldefault.go | 12 ++++++++++++ .../testschema/attributewithboolplanmodifiers.go | 12 ++++++++++++ .../testschema/attributewithboolvalidators.go | 12 ++++++++++++ .../testschema/attributewithdynamicdefault.go | 12 ++++++++++++ .../testschema/attributewithdynamicplanmodifiers.go | 12 ++++++++++++ .../testschema/attributewithdynamicvalidators.go | 12 ++++++++++++ .../testschema/attributewithfloat32default.go | 12 ++++++++++++ .../testschema/attributewithfloat32planmodifiers.go | 12 ++++++++++++ .../testschema/attributewithfloat32validators.go | 12 ++++++++++++ .../testschema/attributewithfloat64default.go | 12 ++++++++++++ .../testschema/attributewithfloat64planmodifiers.go | 12 ++++++++++++ .../testschema/attributewithfloat64validators.go | 12 ++++++++++++ .../testing/testschema/attributewithint32default.go | 12 ++++++++++++ .../testschema/attributewithint32planmodifiers.go | 12 ++++++++++++ .../testschema/attributewithint32validators.go | 12 ++++++++++++ .../testing/testschema/attributewithint64default.go | 12 ++++++++++++ .../testschema/attributewithint64planmodifiers.go | 12 ++++++++++++ .../testschema/attributewithint64validators.go | 12 ++++++++++++ .../testing/testschema/attributewithlistdefault.go | 12 ++++++++++++ .../testschema/attributewithlistplanmodifiers.go | 12 ++++++++++++ .../testschema/attributewithlistvalidators.go | 12 ++++++++++++ .../testing/testschema/attributewithmapdefault.go | 12 ++++++++++++ .../testschema/attributewithmapplanmodifiers.go | 12 ++++++++++++ .../testing/testschema/attributewithmapvalidators.go | 12 ++++++++++++ .../testing/testschema/attributewithnumberdefault.go | 12 ++++++++++++ .../testschema/attributewithnumberplanmodifiers.go | 12 ++++++++++++ .../testschema/attributewithnumbervalidators.go | 12 ++++++++++++ .../testing/testschema/attributewithobjectdefault.go | 12 ++++++++++++ .../testschema/attributewithobjectplanmodifiers.go | 12 ++++++++++++ .../testschema/attributewithobjectvalidators.go | 12 ++++++++++++ .../testing/testschema/attributewithsetdefault.go | 12 ++++++++++++ .../testschema/attributewithsetplanmodifiers.go | 12 ++++++++++++ .../testing/testschema/attributewithsetvalidators.go | 12 ++++++++++++ .../testing/testschema/attributewithstringdefault.go | 12 ++++++++++++ .../testschema/attributewithstringplanmodifiers.go | 12 ++++++++++++ .../testschema/attributewithstringvalidators.go | 12 ++++++++++++ internal/testing/testschema/nested_attribute.go | 12 ++++++++++++ .../testschema/nested_attribute_with_list_default.go | 12 ++++++++++++ .../nested_attribute_with_list_plan_modifiers.go | 12 ++++++++++++ .../testschema/nested_attribute_with_map_default.go | 12 ++++++++++++ .../nested_attribute_with_map_plan_modifiers.go | 12 ++++++++++++ .../nested_attribute_with_object_default.go | 12 ++++++++++++ .../nested_attribute_with_object_plan_modifiers.go | 12 ++++++++++++ .../testschema/nested_attribute_with_set_default.go | 12 ++++++++++++ .../nested_attribute_with_set_plan_modifiers.go | 12 ++++++++++++ 45 files changed, 540 insertions(+) diff --git a/internal/testing/testschema/attributewithbooldefault.go b/internal/testing/testschema/attributewithbooldefault.go index 66edc07e5..e1ca0627a 100644 --- a/internal/testing/testschema/attributewithbooldefault.go +++ b/internal/testing/testschema/attributewithbooldefault.go @@ -23,6 +23,8 @@ type AttributeWithBoolDefaultValue struct { Required bool Sensitive bool WriteOnly bool + RequiredForImport bool + OptionalForImport bool Default defaults.Bool } @@ -91,3 +93,13 @@ func (a AttributeWithBoolDefaultValue) IsSensitive() bool { func (a AttributeWithBoolDefaultValue) IsWriteOnly() bool { return a.WriteOnly } + +// IsRequiredForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithBoolDefaultValue) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithBoolDefaultValue) IsOptionalForImport() bool { + return a.OptionalForImport +} diff --git a/internal/testing/testschema/attributewithboolplanmodifiers.go b/internal/testing/testschema/attributewithboolplanmodifiers.go index 397f18186..812697fb2 100644 --- a/internal/testing/testschema/attributewithboolplanmodifiers.go +++ b/internal/testing/testschema/attributewithboolplanmodifiers.go @@ -24,6 +24,8 @@ type AttributeWithBoolPlanModifiers struct { Required bool Sensitive bool WriteOnly bool + RequiredForImport bool + OptionalForImport bool PlanModifiers []planmodifier.Bool } @@ -92,3 +94,13 @@ func (a AttributeWithBoolPlanModifiers) IsSensitive() bool { func (a AttributeWithBoolPlanModifiers) IsWriteOnly() bool { return a.WriteOnly } + +// IsRequiredForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithBoolPlanModifiers) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithBoolPlanModifiers) IsOptionalForImport() bool { + return a.OptionalForImport +} diff --git a/internal/testing/testschema/attributewithboolvalidators.go b/internal/testing/testschema/attributewithboolvalidators.go index 044c25cf4..5da143524 100644 --- a/internal/testing/testschema/attributewithboolvalidators.go +++ b/internal/testing/testschema/attributewithboolvalidators.go @@ -24,6 +24,8 @@ type AttributeWithBoolValidators struct { Required bool Sensitive bool WriteOnly bool + RequiredForImport bool + OptionalForImport bool Validators []validator.Bool } @@ -92,3 +94,13 @@ func (a AttributeWithBoolValidators) IsSensitive() bool { func (a AttributeWithBoolValidators) IsWriteOnly() bool { return a.WriteOnly } + +// IsRequiredForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithBoolValidators) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithBoolValidators) IsOptionalForImport() bool { + return a.OptionalForImport +} diff --git a/internal/testing/testschema/attributewithdynamicdefault.go b/internal/testing/testschema/attributewithdynamicdefault.go index b562132a8..ae3690aa0 100644 --- a/internal/testing/testschema/attributewithdynamicdefault.go +++ b/internal/testing/testschema/attributewithdynamicdefault.go @@ -23,6 +23,8 @@ type AttributeWithDynamicDefaultValue struct { Required bool Sensitive bool WriteOnly bool + RequiredForImport bool + OptionalForImport bool Default defaults.Dynamic } @@ -91,3 +93,13 @@ func (a AttributeWithDynamicDefaultValue) IsSensitive() bool { func (a AttributeWithDynamicDefaultValue) IsWriteOnly() bool { return a.WriteOnly } + +// IsRequiredForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithDynamicDefaultValue) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithDynamicDefaultValue) IsOptionalForImport() bool { + return a.OptionalForImport +} diff --git a/internal/testing/testschema/attributewithdynamicplanmodifiers.go b/internal/testing/testschema/attributewithdynamicplanmodifiers.go index 74a80587a..f510ba12c 100644 --- a/internal/testing/testschema/attributewithdynamicplanmodifiers.go +++ b/internal/testing/testschema/attributewithdynamicplanmodifiers.go @@ -23,6 +23,8 @@ type AttributeWithDynamicPlanModifiers struct { Required bool Sensitive bool WriteOnly bool + RequiredForImport bool + OptionalForImport bool PlanModifiers []planmodifier.Dynamic } @@ -91,3 +93,13 @@ func (a AttributeWithDynamicPlanModifiers) DynamicPlanModifiers() []planmodifier func (a AttributeWithDynamicPlanModifiers) IsWriteOnly() bool { return a.WriteOnly } + +// IsRequiredForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithDynamicPlanModifiers) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithDynamicPlanModifiers) IsOptionalForImport() bool { + return a.OptionalForImport +} diff --git a/internal/testing/testschema/attributewithdynamicvalidators.go b/internal/testing/testschema/attributewithdynamicvalidators.go index e4ef1024e..cca5e5fc6 100644 --- a/internal/testing/testschema/attributewithdynamicvalidators.go +++ b/internal/testing/testschema/attributewithdynamicvalidators.go @@ -24,6 +24,8 @@ type AttributeWithDynamicValidators struct { Required bool Sensitive bool WriteOnly bool + RequiredForImport bool + OptionalForImport bool Validators []validator.Dynamic } @@ -92,3 +94,13 @@ func (a AttributeWithDynamicValidators) DynamicValidators() []validator.Dynamic func (a AttributeWithDynamicValidators) IsWriteOnly() bool { return a.WriteOnly } + +// IsRequiredForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithDynamicValidators) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithDynamicValidators) IsOptionalForImport() bool { + return a.OptionalForImport +} diff --git a/internal/testing/testschema/attributewithfloat32default.go b/internal/testing/testschema/attributewithfloat32default.go index c3aeb627d..6798180f1 100644 --- a/internal/testing/testschema/attributewithfloat32default.go +++ b/internal/testing/testschema/attributewithfloat32default.go @@ -23,6 +23,8 @@ type AttributeWithFloat32DefaultValue struct { Required bool Sensitive bool WriteOnly bool + RequiredForImport bool + OptionalForImport bool Default defaults.Float32 } @@ -91,3 +93,13 @@ func (a AttributeWithFloat32DefaultValue) IsSensitive() bool { func (a AttributeWithFloat32DefaultValue) IsWriteOnly() bool { return a.WriteOnly } + +// IsRequiredForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithFloat32DefaultValue) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithFloat32DefaultValue) IsOptionalForImport() bool { + return a.OptionalForImport +} diff --git a/internal/testing/testschema/attributewithfloat32planmodifiers.go b/internal/testing/testschema/attributewithfloat32planmodifiers.go index f93d87229..d01c9ca8d 100644 --- a/internal/testing/testschema/attributewithfloat32planmodifiers.go +++ b/internal/testing/testschema/attributewithfloat32planmodifiers.go @@ -24,6 +24,8 @@ type AttributeWithFloat32PlanModifiers struct { Required bool Sensitive bool WriteOnly bool + RequiredForImport bool + OptionalForImport bool PlanModifiers []planmodifier.Float32 } @@ -92,3 +94,13 @@ func (a AttributeWithFloat32PlanModifiers) IsSensitive() bool { func (a AttributeWithFloat32PlanModifiers) IsWriteOnly() bool { return a.WriteOnly } + +// IsRequiredForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithFloat32PlanModifiers) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithFloat32PlanModifiers) IsOptionalForImport() bool { + return a.OptionalForImport +} diff --git a/internal/testing/testschema/attributewithfloat32validators.go b/internal/testing/testschema/attributewithfloat32validators.go index 7fb02a5ad..fbf460685 100644 --- a/internal/testing/testschema/attributewithfloat32validators.go +++ b/internal/testing/testschema/attributewithfloat32validators.go @@ -24,6 +24,8 @@ type AttributeWithFloat32Validators struct { Required bool Sensitive bool WriteOnly bool + RequiredForImport bool + OptionalForImport bool Validators []validator.Float32 } @@ -92,3 +94,13 @@ func (a AttributeWithFloat32Validators) IsSensitive() bool { func (a AttributeWithFloat32Validators) IsWriteOnly() bool { return a.WriteOnly } + +// IsRequiredForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithFloat32Validators) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithFloat32Validators) IsOptionalForImport() bool { + return a.OptionalForImport +} diff --git a/internal/testing/testschema/attributewithfloat64default.go b/internal/testing/testschema/attributewithfloat64default.go index 484ec37d9..9a9cca0a6 100644 --- a/internal/testing/testschema/attributewithfloat64default.go +++ b/internal/testing/testschema/attributewithfloat64default.go @@ -23,6 +23,8 @@ type AttributeWithFloat64DefaultValue struct { Required bool Sensitive bool WriteOnly bool + RequiredForImport bool + OptionalForImport bool Default defaults.Float64 } @@ -91,3 +93,13 @@ func (a AttributeWithFloat64DefaultValue) IsSensitive() bool { func (a AttributeWithFloat64DefaultValue) IsWriteOnly() bool { return a.WriteOnly } + +// IsRequiredForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithFloat64DefaultValue) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithFloat64DefaultValue) IsOptionalForImport() bool { + return a.OptionalForImport +} diff --git a/internal/testing/testschema/attributewithfloat64planmodifiers.go b/internal/testing/testschema/attributewithfloat64planmodifiers.go index ba04291da..fe9a0077e 100644 --- a/internal/testing/testschema/attributewithfloat64planmodifiers.go +++ b/internal/testing/testschema/attributewithfloat64planmodifiers.go @@ -23,6 +23,8 @@ type AttributeWithFloat64PlanModifiers struct { Required bool Sensitive bool WriteOnly bool + RequiredForImport bool + OptionalForImport bool PlanModifiers []planmodifier.Float64 } @@ -91,3 +93,13 @@ func (a AttributeWithFloat64PlanModifiers) IsSensitive() bool { func (a AttributeWithFloat64PlanModifiers) IsWriteOnly() bool { return a.WriteOnly } + +// IsRequiredForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithFloat64PlanModifiers) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithFloat64PlanModifiers) IsOptionalForImport() bool { + return a.OptionalForImport +} diff --git a/internal/testing/testschema/attributewithfloat64validators.go b/internal/testing/testschema/attributewithfloat64validators.go index 02ef17704..309dbd1bc 100644 --- a/internal/testing/testschema/attributewithfloat64validators.go +++ b/internal/testing/testschema/attributewithfloat64validators.go @@ -23,6 +23,8 @@ type AttributeWithFloat64Validators struct { Required bool Sensitive bool WriteOnly bool + RequiredForImport bool + OptionalForImport bool Validators []validator.Float64 } @@ -91,3 +93,13 @@ func (a AttributeWithFloat64Validators) IsSensitive() bool { func (a AttributeWithFloat64Validators) IsWriteOnly() bool { return a.WriteOnly } + +// IsRequiredForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithFloat64Validators) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithFloat64Validators) IsOptionalForImport() bool { + return a.OptionalForImport +} diff --git a/internal/testing/testschema/attributewithint32default.go b/internal/testing/testschema/attributewithint32default.go index f332bae41..e9d6a8e9b 100644 --- a/internal/testing/testschema/attributewithint32default.go +++ b/internal/testing/testschema/attributewithint32default.go @@ -23,6 +23,8 @@ type AttributeWithInt32DefaultValue struct { Required bool Sensitive bool WriteOnly bool + RequiredForImport bool + OptionalForImport bool Default defaults.Int32 } @@ -91,3 +93,13 @@ func (a AttributeWithInt32DefaultValue) IsSensitive() bool { func (a AttributeWithInt32DefaultValue) IsWriteOnly() bool { return a.WriteOnly } + +// IsRequiredForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithInt32DefaultValue) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithInt32DefaultValue) IsOptionalForImport() bool { + return a.OptionalForImport +} diff --git a/internal/testing/testschema/attributewithint32planmodifiers.go b/internal/testing/testschema/attributewithint32planmodifiers.go index 7f131df58..4cef7d2cd 100644 --- a/internal/testing/testschema/attributewithint32planmodifiers.go +++ b/internal/testing/testschema/attributewithint32planmodifiers.go @@ -24,6 +24,8 @@ type AttributeWithInt32PlanModifiers struct { Required bool Sensitive bool WriteOnly bool + RequiredForImport bool + OptionalForImport bool PlanModifiers []planmodifier.Int32 } @@ -92,3 +94,13 @@ func (a AttributeWithInt32PlanModifiers) IsSensitive() bool { func (a AttributeWithInt32PlanModifiers) IsWriteOnly() bool { return a.WriteOnly } + +// IsRequiredForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithInt32PlanModifiers) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithInt32PlanModifiers) IsOptionalForImport() bool { + return a.OptionalForImport +} diff --git a/internal/testing/testschema/attributewithint32validators.go b/internal/testing/testschema/attributewithint32validators.go index 8a4546e9e..62366de90 100644 --- a/internal/testing/testschema/attributewithint32validators.go +++ b/internal/testing/testschema/attributewithint32validators.go @@ -24,6 +24,8 @@ type AttributeWithInt32Validators struct { Required bool Sensitive bool WriteOnly bool + RequiredForImport bool + OptionalForImport bool Validators []validator.Int32 } @@ -92,3 +94,13 @@ func (a AttributeWithInt32Validators) IsSensitive() bool { func (a AttributeWithInt32Validators) IsWriteOnly() bool { return a.WriteOnly } + +// IsRequiredForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithInt32Validators) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithInt32Validators) IsOptionalForImport() bool { + return a.OptionalForImport +} diff --git a/internal/testing/testschema/attributewithint64default.go b/internal/testing/testschema/attributewithint64default.go index 574a88a58..bb3f12f00 100644 --- a/internal/testing/testschema/attributewithint64default.go +++ b/internal/testing/testschema/attributewithint64default.go @@ -23,6 +23,8 @@ type AttributeWithInt64DefaultValue struct { Required bool Sensitive bool WriteOnly bool + RequiredForImport bool + OptionalForImport bool Default defaults.Int64 } @@ -91,3 +93,13 @@ func (a AttributeWithInt64DefaultValue) IsSensitive() bool { func (a AttributeWithInt64DefaultValue) IsWriteOnly() bool { return a.WriteOnly } + +// IsRequiredForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithInt64DefaultValue) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithInt64DefaultValue) IsOptionalForImport() bool { + return a.OptionalForImport +} diff --git a/internal/testing/testschema/attributewithint64planmodifiers.go b/internal/testing/testschema/attributewithint64planmodifiers.go index e21a43251..ef315b3ae 100644 --- a/internal/testing/testschema/attributewithint64planmodifiers.go +++ b/internal/testing/testschema/attributewithint64planmodifiers.go @@ -23,6 +23,8 @@ type AttributeWithInt64PlanModifiers struct { Required bool Sensitive bool WriteOnly bool + RequiredForImport bool + OptionalForImport bool PlanModifiers []planmodifier.Int64 } @@ -91,3 +93,13 @@ func (a AttributeWithInt64PlanModifiers) IsSensitive() bool { func (a AttributeWithInt64PlanModifiers) IsWriteOnly() bool { return a.WriteOnly } + +// IsRequiredForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithInt64PlanModifiers) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithInt64PlanModifiers) IsOptionalForImport() bool { + return a.OptionalForImport +} diff --git a/internal/testing/testschema/attributewithint64validators.go b/internal/testing/testschema/attributewithint64validators.go index c4e23e166..5ccb40aa7 100644 --- a/internal/testing/testschema/attributewithint64validators.go +++ b/internal/testing/testschema/attributewithint64validators.go @@ -23,6 +23,8 @@ type AttributeWithInt64Validators struct { Required bool Sensitive bool WriteOnly bool + RequiredForImport bool + OptionalForImport bool Validators []validator.Int64 } @@ -91,3 +93,13 @@ func (a AttributeWithInt64Validators) IsSensitive() bool { func (a AttributeWithInt64Validators) IsWriteOnly() bool { return a.WriteOnly } + +// IsRequiredForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithInt64Validators) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithInt64Validators) IsOptionalForImport() bool { + return a.OptionalForImport +} diff --git a/internal/testing/testschema/attributewithlistdefault.go b/internal/testing/testschema/attributewithlistdefault.go index ff23c5215..3676ca7ba 100644 --- a/internal/testing/testschema/attributewithlistdefault.go +++ b/internal/testing/testschema/attributewithlistdefault.go @@ -24,6 +24,8 @@ type AttributeWithListDefaultValue struct { Required bool Sensitive bool WriteOnly bool + RequiredForImport bool + OptionalForImport bool Default defaults.List } @@ -94,3 +96,13 @@ func (a AttributeWithListDefaultValue) IsSensitive() bool { func (a AttributeWithListDefaultValue) IsWriteOnly() bool { return a.WriteOnly } + +// IsRequiredForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithListDefaultValue) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithListDefaultValue) IsOptionalForImport() bool { + return a.OptionalForImport +} diff --git a/internal/testing/testschema/attributewithlistplanmodifiers.go b/internal/testing/testschema/attributewithlistplanmodifiers.go index fbb50c332..eafa1093b 100644 --- a/internal/testing/testschema/attributewithlistplanmodifiers.go +++ b/internal/testing/testschema/attributewithlistplanmodifiers.go @@ -24,6 +24,8 @@ type AttributeWithListPlanModifiers struct { Required bool Sensitive bool WriteOnly bool + RequiredForImport bool + OptionalForImport bool PlanModifiers []planmodifier.List } @@ -94,3 +96,13 @@ func (a AttributeWithListPlanModifiers) IsWriteOnly() bool { func (a AttributeWithListPlanModifiers) ListPlanModifiers() []planmodifier.List { return a.PlanModifiers } + +// IsRequiredForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithListPlanModifiers) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithListPlanModifiers) IsOptionalForImport() bool { + return a.OptionalForImport +} diff --git a/internal/testing/testschema/attributewithlistvalidators.go b/internal/testing/testschema/attributewithlistvalidators.go index fefa2eb02..36182c4a0 100644 --- a/internal/testing/testschema/attributewithlistvalidators.go +++ b/internal/testing/testschema/attributewithlistvalidators.go @@ -24,6 +24,8 @@ type AttributeWithListValidators struct { Required bool Sensitive bool WriteOnly bool + RequiredForImport bool + OptionalForImport bool Validators []validator.List } @@ -94,3 +96,13 @@ func (a AttributeWithListValidators) IsWriteOnly() bool { func (a AttributeWithListValidators) ListValidators() []validator.List { return a.Validators } + +// IsRequiredForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithListValidators) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithListValidators) IsOptionalForImport() bool { + return a.OptionalForImport +} diff --git a/internal/testing/testschema/attributewithmapdefault.go b/internal/testing/testschema/attributewithmapdefault.go index 2b223cd1d..0ca52972c 100644 --- a/internal/testing/testschema/attributewithmapdefault.go +++ b/internal/testing/testschema/attributewithmapdefault.go @@ -24,6 +24,8 @@ type AttributeWithMapDefaultValue struct { Required bool Sensitive bool WriteOnly bool + RequiredForImport bool + OptionalForImport bool Default defaults.Map } @@ -94,3 +96,13 @@ func (a AttributeWithMapDefaultValue) IsSensitive() bool { func (a AttributeWithMapDefaultValue) IsWriteOnly() bool { return a.WriteOnly } + +// IsRequiredForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithMapDefaultValue) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithMapDefaultValue) IsOptionalForImport() bool { + return a.OptionalForImport +} diff --git a/internal/testing/testschema/attributewithmapplanmodifiers.go b/internal/testing/testschema/attributewithmapplanmodifiers.go index 06c7b2fe4..b57d597ce 100644 --- a/internal/testing/testschema/attributewithmapplanmodifiers.go +++ b/internal/testing/testschema/attributewithmapplanmodifiers.go @@ -24,6 +24,8 @@ type AttributeWithMapPlanModifiers struct { Required bool Sensitive bool WriteOnly bool + RequiredForImport bool + OptionalForImport bool PlanModifiers []planmodifier.Map } @@ -94,3 +96,13 @@ func (a AttributeWithMapPlanModifiers) IsWriteOnly() bool { func (a AttributeWithMapPlanModifiers) MapPlanModifiers() []planmodifier.Map { return a.PlanModifiers } + +// IsRequiredForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithMapPlanModifiers) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithMapPlanModifiers) IsOptionalForImport() bool { + return a.OptionalForImport +} diff --git a/internal/testing/testschema/attributewithmapvalidators.go b/internal/testing/testschema/attributewithmapvalidators.go index 5e3a9dac8..8b2ec6b14 100644 --- a/internal/testing/testschema/attributewithmapvalidators.go +++ b/internal/testing/testschema/attributewithmapvalidators.go @@ -24,6 +24,8 @@ type AttributeWithMapValidators struct { Required bool Sensitive bool WriteOnly bool + RequiredForImport bool + OptionalForImport bool Validators []validator.Map } @@ -90,6 +92,16 @@ func (a AttributeWithMapValidators) IsWriteOnly() bool { return a.WriteOnly } +// IsRequiredForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithMapValidators) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithMapValidators) IsOptionalForImport() bool { + return a.OptionalForImport +} + // MapValidators satisfies the fwxschema.AttributeWithMapValidators interface. func (a AttributeWithMapValidators) MapValidators() []validator.Map { return a.Validators diff --git a/internal/testing/testschema/attributewithnumberdefault.go b/internal/testing/testschema/attributewithnumberdefault.go index effa79507..bdb16149d 100644 --- a/internal/testing/testschema/attributewithnumberdefault.go +++ b/internal/testing/testschema/attributewithnumberdefault.go @@ -23,6 +23,8 @@ type AttributeWithNumberDefaultValue struct { Required bool Sensitive bool WriteOnly bool + RequiredForImport bool + OptionalForImport bool Default defaults.Number } @@ -91,3 +93,13 @@ func (a AttributeWithNumberDefaultValue) IsSensitive() bool { func (a AttributeWithNumberDefaultValue) IsWriteOnly() bool { return a.WriteOnly } + +// IsRequiredForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithNumberDefaultValue) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithNumberDefaultValue) IsOptionalForImport() bool { + return a.OptionalForImport +} diff --git a/internal/testing/testschema/attributewithnumberplanmodifiers.go b/internal/testing/testschema/attributewithnumberplanmodifiers.go index cf9299778..db6bf3d46 100644 --- a/internal/testing/testschema/attributewithnumberplanmodifiers.go +++ b/internal/testing/testschema/attributewithnumberplanmodifiers.go @@ -23,6 +23,8 @@ type AttributeWithNumberPlanModifiers struct { Required bool Sensitive bool WriteOnly bool + RequiredForImport bool + OptionalForImport bool PlanModifiers []planmodifier.Number } @@ -87,6 +89,16 @@ func (a AttributeWithNumberPlanModifiers) IsWriteOnly() bool { return a.WriteOnly } +// IsRequiredForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithNumberPlanModifiers) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithNumberPlanModifiers) IsOptionalForImport() bool { + return a.OptionalForImport +} + // NumberPlanModifiers satisfies the fwxschema.AttributeWithNumberPlanModifiers interface. func (a AttributeWithNumberPlanModifiers) NumberPlanModifiers() []planmodifier.Number { return a.PlanModifiers diff --git a/internal/testing/testschema/attributewithnumbervalidators.go b/internal/testing/testschema/attributewithnumbervalidators.go index af1869d38..d5ba9bc7c 100644 --- a/internal/testing/testschema/attributewithnumbervalidators.go +++ b/internal/testing/testschema/attributewithnumbervalidators.go @@ -23,6 +23,8 @@ type AttributeWithNumberValidators struct { Required bool Sensitive bool WriteOnly bool + RequiredForImport bool + OptionalForImport bool Validators []validator.Number } @@ -87,6 +89,16 @@ func (a AttributeWithNumberValidators) IsWriteOnly() bool { return a.WriteOnly } +// IsRequiredForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithNumberValidators) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithNumberValidators) IsOptionalForImport() bool { + return a.OptionalForImport +} + // NumberValidators satisfies the fwxschema.AttributeWithNumberValidators interface. func (a AttributeWithNumberValidators) NumberValidators() []validator.Number { return a.Validators diff --git a/internal/testing/testschema/attributewithobjectdefault.go b/internal/testing/testschema/attributewithobjectdefault.go index ed25e0a90..c8ae9e047 100644 --- a/internal/testing/testschema/attributewithobjectdefault.go +++ b/internal/testing/testschema/attributewithobjectdefault.go @@ -24,6 +24,8 @@ type AttributeWithObjectDefaultValue struct { Required bool Sensitive bool WriteOnly bool + RequiredForImport bool + OptionalForImport bool Default defaults.Object } @@ -94,3 +96,13 @@ func (a AttributeWithObjectDefaultValue) IsSensitive() bool { func (a AttributeWithObjectDefaultValue) IsWriteOnly() bool { return a.WriteOnly } + +// IsRequiredForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithObjectDefaultValue) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithObjectDefaultValue) IsOptionalForImport() bool { + return a.OptionalForImport +} diff --git a/internal/testing/testschema/attributewithobjectplanmodifiers.go b/internal/testing/testschema/attributewithobjectplanmodifiers.go index e4ec023bd..625746b47 100644 --- a/internal/testing/testschema/attributewithobjectplanmodifiers.go +++ b/internal/testing/testschema/attributewithobjectplanmodifiers.go @@ -24,6 +24,8 @@ type AttributeWithObjectPlanModifiers struct { Required bool Sensitive bool WriteOnly bool + RequiredForImport bool + OptionalForImport bool PlanModifiers []planmodifier.Object } @@ -90,6 +92,16 @@ func (a AttributeWithObjectPlanModifiers) IsWriteOnly() bool { return a.WriteOnly } +// IsRequiredForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithObjectPlanModifiers) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithObjectPlanModifiers) IsOptionalForImport() bool { + return a.OptionalForImport +} + // ObjectPlanModifiers satisfies the fwxschema.AttributeWithObjectPlanModifiers interface. func (a AttributeWithObjectPlanModifiers) ObjectPlanModifiers() []planmodifier.Object { return a.PlanModifiers diff --git a/internal/testing/testschema/attributewithobjectvalidators.go b/internal/testing/testschema/attributewithobjectvalidators.go index 534c47cf6..03bab115f 100644 --- a/internal/testing/testschema/attributewithobjectvalidators.go +++ b/internal/testing/testschema/attributewithobjectvalidators.go @@ -24,6 +24,8 @@ type AttributeWithObjectValidators struct { Required bool Sensitive bool WriteOnly bool + RequiredForImport bool + OptionalForImport bool Validators []validator.Object } @@ -90,6 +92,16 @@ func (a AttributeWithObjectValidators) IsWriteOnly() bool { return a.WriteOnly } +// IsRequiredForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithObjectValidators) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithObjectValidators) IsOptionalForImport() bool { + return a.OptionalForImport +} + // ObjectValidators satisfies the fwxschema.AttributeWithObjectValidators interface. func (a AttributeWithObjectValidators) ObjectValidators() []validator.Object { return a.Validators diff --git a/internal/testing/testschema/attributewithsetdefault.go b/internal/testing/testschema/attributewithsetdefault.go index f770f956c..d5d557cec 100644 --- a/internal/testing/testschema/attributewithsetdefault.go +++ b/internal/testing/testschema/attributewithsetdefault.go @@ -24,6 +24,8 @@ type AttributeWithSetDefaultValue struct { Required bool Sensitive bool WriteOnly bool + RequiredForImport bool + OptionalForImport bool Default defaults.Set } @@ -94,3 +96,13 @@ func (a AttributeWithSetDefaultValue) IsSensitive() bool { func (a AttributeWithSetDefaultValue) IsWriteOnly() bool { return a.WriteOnly } + +// IsRequiredForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithSetDefaultValue) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithSetDefaultValue) IsOptionalForImport() bool { + return a.OptionalForImport +} diff --git a/internal/testing/testschema/attributewithsetplanmodifiers.go b/internal/testing/testschema/attributewithsetplanmodifiers.go index a36f5adc4..685709121 100644 --- a/internal/testing/testschema/attributewithsetplanmodifiers.go +++ b/internal/testing/testschema/attributewithsetplanmodifiers.go @@ -25,6 +25,8 @@ type AttributeWithSetPlanModifiers struct { Required bool Sensitive bool WriteOnly bool + RequiredForImport bool + OptionalForImport bool PlanModifiers []planmodifier.Set } @@ -91,6 +93,16 @@ func (a AttributeWithSetPlanModifiers) IsWriteOnly() bool { return a.WriteOnly } +// IsRequiredForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithSetPlanModifiers) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithSetPlanModifiers) IsOptionalForImport() bool { + return a.OptionalForImport +} + // SetPlanModifiers satisfies the fwxschema.AttributeWithSetPlanModifiers interface. func (a AttributeWithSetPlanModifiers) SetPlanModifiers() []planmodifier.Set { return a.PlanModifiers diff --git a/internal/testing/testschema/attributewithsetvalidators.go b/internal/testing/testschema/attributewithsetvalidators.go index 32bc5256f..ed18808cc 100644 --- a/internal/testing/testschema/attributewithsetvalidators.go +++ b/internal/testing/testschema/attributewithsetvalidators.go @@ -24,6 +24,8 @@ type AttributeWithSetValidators struct { Required bool Sensitive bool WriteOnly bool + RequiredForImport bool + OptionalForImport bool Validators []validator.Set } @@ -90,6 +92,16 @@ func (a AttributeWithSetValidators) IsWriteOnly() bool { return a.WriteOnly } +// IsRequiredForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithSetValidators) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithSetValidators) IsOptionalForImport() bool { + return a.OptionalForImport +} + // SetValidators satisfies the fwxschema.AttributeWithSetValidators interface. func (a AttributeWithSetValidators) SetValidators() []validator.Set { return a.Validators diff --git a/internal/testing/testschema/attributewithstringdefault.go b/internal/testing/testschema/attributewithstringdefault.go index 01e1e6f4e..28131c39a 100644 --- a/internal/testing/testschema/attributewithstringdefault.go +++ b/internal/testing/testschema/attributewithstringdefault.go @@ -23,6 +23,8 @@ type AttributeWithStringDefaultValue struct { Required bool Sensitive bool WriteOnly bool + RequiredForImport bool + OptionalForImport bool Default defaults.String } @@ -91,3 +93,13 @@ func (a AttributeWithStringDefaultValue) IsSensitive() bool { func (a AttributeWithStringDefaultValue) IsWriteOnly() bool { return a.WriteOnly } + +// IsRequiredForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithStringDefaultValue) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithStringDefaultValue) IsOptionalForImport() bool { + return a.OptionalForImport +} diff --git a/internal/testing/testschema/attributewithstringplanmodifiers.go b/internal/testing/testschema/attributewithstringplanmodifiers.go index d1b5af8d5..f88a93cf7 100644 --- a/internal/testing/testschema/attributewithstringplanmodifiers.go +++ b/internal/testing/testschema/attributewithstringplanmodifiers.go @@ -23,6 +23,8 @@ type AttributeWithStringPlanModifiers struct { Required bool Sensitive bool WriteOnly bool + RequiredForImport bool + OptionalForImport bool PlanModifiers []planmodifier.String } @@ -87,6 +89,16 @@ func (a AttributeWithStringPlanModifiers) IsWriteOnly() bool { return a.WriteOnly } +// IsRequiredForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithStringPlanModifiers) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithStringPlanModifiers) IsOptionalForImport() bool { + return a.OptionalForImport +} + // StringPlanModifiers satisfies the fwxschema.AttributeWithStringPlanModifiers interface. func (a AttributeWithStringPlanModifiers) StringPlanModifiers() []planmodifier.String { return a.PlanModifiers diff --git a/internal/testing/testschema/attributewithstringvalidators.go b/internal/testing/testschema/attributewithstringvalidators.go index e1ddf7e59..21efefe3d 100644 --- a/internal/testing/testschema/attributewithstringvalidators.go +++ b/internal/testing/testschema/attributewithstringvalidators.go @@ -23,6 +23,8 @@ type AttributeWithStringValidators struct { Required bool Sensitive bool WriteOnly bool + RequiredForImport bool + OptionalForImport bool Validators []validator.String } @@ -87,6 +89,16 @@ func (a AttributeWithStringValidators) IsWriteOnly() bool { return a.WriteOnly } +// IsRequiredForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithStringValidators) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport satisfies the fwschema.Attribute interface. +func (a AttributeWithStringValidators) IsOptionalForImport() bool { + return a.OptionalForImport +} + // StringValidators satisfies the fwxschema.AttributeWithStringValidators interface. func (a AttributeWithStringValidators) StringValidators() []validator.String { return a.Validators diff --git a/internal/testing/testschema/nested_attribute.go b/internal/testing/testschema/nested_attribute.go index f37301359..2a638bcb4 100644 --- a/internal/testing/testschema/nested_attribute.go +++ b/internal/testing/testschema/nested_attribute.go @@ -28,6 +28,8 @@ type NestedAttribute struct { Required bool Sensitive bool WriteOnly bool + RequiredForImport bool + OptionalForImport bool Type attr.Type } @@ -163,3 +165,13 @@ func (a NestedAttribute) IsSensitive() bool { func (a NestedAttribute) IsWriteOnly() bool { return a.WriteOnly } + +// IsRequiredForImport satisfies the fwschema.Attribute interface. +func (a NestedAttribute) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport satisfies the fwschema.Attribute interface. +func (a NestedAttribute) IsOptionalForImport() bool { + return a.OptionalForImport +} diff --git a/internal/testing/testschema/nested_attribute_with_list_default.go b/internal/testing/testschema/nested_attribute_with_list_default.go index a1b70cd87..2621e9d39 100644 --- a/internal/testing/testschema/nested_attribute_with_list_default.go +++ b/internal/testing/testschema/nested_attribute_with_list_default.go @@ -28,6 +28,8 @@ type NestedAttributeWithListDefaultValue struct { Required bool Sensitive bool WriteOnly bool + RequiredForImport bool + OptionalForImport bool Type attr.Type } @@ -108,6 +110,16 @@ func (a NestedAttributeWithListDefaultValue) IsWriteOnly() bool { return a.WriteOnly } +// IsRequiredForImport satisfies the fwschema.Attribute interface. +func (a NestedAttributeWithListDefaultValue) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport satisfies the fwschema.Attribute interface. +func (a NestedAttributeWithListDefaultValue) IsOptionalForImport() bool { + return a.OptionalForImport +} + // ListDefaultValue satisfies the fwschema.AttributeWithListDefaultValue interface. func (a NestedAttributeWithListDefaultValue) ListDefaultValue() defaults.List { return a.Default diff --git a/internal/testing/testschema/nested_attribute_with_list_plan_modifiers.go b/internal/testing/testschema/nested_attribute_with_list_plan_modifiers.go index 2904c21ee..e754a7a6d 100644 --- a/internal/testing/testschema/nested_attribute_with_list_plan_modifiers.go +++ b/internal/testing/testschema/nested_attribute_with_list_plan_modifiers.go @@ -28,6 +28,8 @@ type NestedAttributeWithListPlanModifiers struct { Required bool Sensitive bool WriteOnly bool + RequiredForImport bool + OptionalForImport bool Type attr.Type } @@ -108,6 +110,16 @@ func (a NestedAttributeWithListPlanModifiers) IsWriteOnly() bool { return a.WriteOnly } +// IsRequiredForImport satisfies the fwschema.Attribute interface. +func (a NestedAttributeWithListPlanModifiers) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport satisfies the fwschema.Attribute interface. +func (a NestedAttributeWithListPlanModifiers) IsOptionalForImport() bool { + return a.OptionalForImport +} + // ListPlanModifiers satisfies the fwxschema.AttributeWithListPlanModifiers interface. func (a NestedAttributeWithListPlanModifiers) ListPlanModifiers() []planmodifier.List { return a.PlanModifiers diff --git a/internal/testing/testschema/nested_attribute_with_map_default.go b/internal/testing/testschema/nested_attribute_with_map_default.go index 6eac9d3ff..be51d0a36 100644 --- a/internal/testing/testschema/nested_attribute_with_map_default.go +++ b/internal/testing/testschema/nested_attribute_with_map_default.go @@ -28,6 +28,8 @@ type NestedAttributeWithMapDefaultValue struct { Required bool Sensitive bool WriteOnly bool + RequiredForImport bool + OptionalForImport bool Type attr.Type } @@ -108,6 +110,16 @@ func (a NestedAttributeWithMapDefaultValue) IsWriteOnly() bool { return a.WriteOnly } +// IsRequiredForImport satisfies the fwschema.Attribute interface. +func (a NestedAttributeWithMapDefaultValue) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport satisfies the fwschema.Attribute interface. +func (a NestedAttributeWithMapDefaultValue) IsOptionalForImport() bool { + return a.OptionalForImport +} + // MapDefaultValue satisfies the fwschema.AttributeWithMapDefaultValue interface. func (a NestedAttributeWithMapDefaultValue) MapDefaultValue() defaults.Map { return a.Default diff --git a/internal/testing/testschema/nested_attribute_with_map_plan_modifiers.go b/internal/testing/testschema/nested_attribute_with_map_plan_modifiers.go index 35f2a0732..855ef49df 100644 --- a/internal/testing/testschema/nested_attribute_with_map_plan_modifiers.go +++ b/internal/testing/testschema/nested_attribute_with_map_plan_modifiers.go @@ -28,6 +28,8 @@ type NestedAttributeWithMapPlanModifiers struct { Required bool Sensitive bool WriteOnly bool + RequiredForImport bool + OptionalForImport bool Type attr.Type } @@ -108,6 +110,16 @@ func (a NestedAttributeWithMapPlanModifiers) IsWriteOnly() bool { return a.WriteOnly } +// IsRequiredForImport satisfies the fwschema.Attribute interface. +func (a NestedAttributeWithMapPlanModifiers) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport satisfies the fwschema.Attribute interface. +func (a NestedAttributeWithMapPlanModifiers) IsOptionalForImport() bool { + return a.OptionalForImport +} + // MapPlanModifiers satisfies the fwxschema.AttributeWithMapPlanModifiers interface. func (a NestedAttributeWithMapPlanModifiers) MapPlanModifiers() []planmodifier.Map { return a.PlanModifiers diff --git a/internal/testing/testschema/nested_attribute_with_object_default.go b/internal/testing/testschema/nested_attribute_with_object_default.go index e8579c2ac..5d83fb9d9 100644 --- a/internal/testing/testschema/nested_attribute_with_object_default.go +++ b/internal/testing/testschema/nested_attribute_with_object_default.go @@ -29,6 +29,8 @@ type NestedAttributeWithObjectDefaultValue struct { Required bool Sensitive bool WriteOnly bool + RequiredForImport bool + OptionalForImport bool Type attr.Type } @@ -111,6 +113,16 @@ func (a NestedAttributeWithObjectDefaultValue) IsWriteOnly() bool { return a.WriteOnly } +// IsRequiredForImport satisfies the fwschema.Attribute interface. +func (a NestedAttributeWithObjectDefaultValue) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport satisfies the fwschema.Attribute interface. +func (a NestedAttributeWithObjectDefaultValue) IsOptionalForImport() bool { + return a.OptionalForImport +} + // ObjectDefaultValue satisfies the fwschema.AttributeWithListDefaultValue interface. func (a NestedAttributeWithObjectDefaultValue) ObjectDefaultValue() defaults.Object { return a.Default diff --git a/internal/testing/testschema/nested_attribute_with_object_plan_modifiers.go b/internal/testing/testschema/nested_attribute_with_object_plan_modifiers.go index 2d86af989..5f70c1fda 100644 --- a/internal/testing/testschema/nested_attribute_with_object_plan_modifiers.go +++ b/internal/testing/testschema/nested_attribute_with_object_plan_modifiers.go @@ -27,6 +27,8 @@ type NestedAttributeWithObjectPlanModifiers struct { Required bool Sensitive bool WriteOnly bool + RequiredForImport bool + OptionalForImport bool Type attr.Type } @@ -105,6 +107,16 @@ func (a NestedAttributeWithObjectPlanModifiers) IsWriteOnly() bool { return a.WriteOnly } +// IsRequiredForImport satisfies the fwschema.Attribute interface. +func (a NestedAttributeWithObjectPlanModifiers) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport satisfies the fwschema.Attribute interface. +func (a NestedAttributeWithObjectPlanModifiers) IsOptionalForImport() bool { + return a.OptionalForImport +} + // ObjectPlanModifiers satisfies the fwxschema.AttributeWithObjectPlanModifiers interface. func (a NestedAttributeWithObjectPlanModifiers) ObjectPlanModifiers() []planmodifier.Object { return a.PlanModifiers diff --git a/internal/testing/testschema/nested_attribute_with_set_default.go b/internal/testing/testschema/nested_attribute_with_set_default.go index 3a80c28a7..16fc98d1c 100644 --- a/internal/testing/testschema/nested_attribute_with_set_default.go +++ b/internal/testing/testschema/nested_attribute_with_set_default.go @@ -28,6 +28,8 @@ type NestedAttributeWithSetDefaultValue struct { Required bool Sensitive bool WriteOnly bool + RequiredForImport bool + OptionalForImport bool Type attr.Type } @@ -108,6 +110,16 @@ func (a NestedAttributeWithSetDefaultValue) IsWriteOnly() bool { return a.WriteOnly } +// IsRequiredForImport satisfies the fwschema.Attribute interface. +func (a NestedAttributeWithSetDefaultValue) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport satisfies the fwschema.Attribute interface. +func (a NestedAttributeWithSetDefaultValue) IsOptionalForImport() bool { + return a.OptionalForImport +} + // MapDefaultValue satisfies the fwschema.AttributeWithMapDefaultValue interface. func (a NestedAttributeWithSetDefaultValue) SetDefaultValue() defaults.Set { return a.Default diff --git a/internal/testing/testschema/nested_attribute_with_set_plan_modifiers.go b/internal/testing/testschema/nested_attribute_with_set_plan_modifiers.go index 1d886e70e..a92acd392 100644 --- a/internal/testing/testschema/nested_attribute_with_set_plan_modifiers.go +++ b/internal/testing/testschema/nested_attribute_with_set_plan_modifiers.go @@ -28,6 +28,8 @@ type NestedAttributeWithSetPlanModifiers struct { Required bool Sensitive bool WriteOnly bool + RequiredForImport bool + OptionalForImport bool Type attr.Type } @@ -108,6 +110,16 @@ func (a NestedAttributeWithSetPlanModifiers) IsWriteOnly() bool { return a.WriteOnly } +// IsRequiredForImport satisfies the fwschema.Attribute interface. +func (a NestedAttributeWithSetPlanModifiers) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport satisfies the fwschema.Attribute interface. +func (a NestedAttributeWithSetPlanModifiers) IsOptionalForImport() bool { + return a.OptionalForImport +} + // SetPlanModifiers satisfies the fwxschema.AttributeWithSetPlanModifiers interface. func (a NestedAttributeWithSetPlanModifiers) SetPlanModifiers() []planmodifier.Set { return a.PlanModifiers From 2950f20148adbbb50cde9244b19afc153bd5d991 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Mon, 24 Feb 2025 09:52:48 -0500 Subject: [PATCH 03/20] implement in datasource/schema --- datasource/schema/bool_attribute.go | 12 +++++ datasource/schema/bool_attribute_test.go | 52 +++++++++++++++++++ datasource/schema/dynamic_attribute.go | 12 +++++ datasource/schema/dynamic_attribute_test.go | 52 +++++++++++++++++++ datasource/schema/float32_attribute.go | 12 +++++ datasource/schema/float32_attribute_test.go | 52 +++++++++++++++++++ datasource/schema/float64_attribute.go | 12 +++++ datasource/schema/float64_attribute_test.go | 52 +++++++++++++++++++ datasource/schema/int32_attribute.go | 12 +++++ datasource/schema/int32_attribute_test.go | 52 +++++++++++++++++++ datasource/schema/int64_attribute.go | 12 +++++ datasource/schema/int64_attribute_test.go | 52 +++++++++++++++++++ datasource/schema/list_attribute.go | 12 +++++ datasource/schema/list_attribute_test.go | 52 +++++++++++++++++++ datasource/schema/list_nested_attribute.go | 12 +++++ .../schema/list_nested_attribute_test.go | 52 +++++++++++++++++++ datasource/schema/map_attribute.go | 12 +++++ datasource/schema/map_attribute_test.go | 52 +++++++++++++++++++ datasource/schema/map_nested_attribute.go | 12 +++++ .../schema/map_nested_attribute_test.go | 52 +++++++++++++++++++ datasource/schema/number_attribute.go | 12 +++++ datasource/schema/number_attribute_test.go | 52 +++++++++++++++++++ datasource/schema/object_attribute.go | 12 +++++ datasource/schema/object_attribute_test.go | 52 +++++++++++++++++++ datasource/schema/set_attribute.go | 12 +++++ datasource/schema/set_attribute_test.go | 52 +++++++++++++++++++ datasource/schema/set_nested_attribute.go | 12 +++++ .../schema/set_nested_attribute_test.go | 52 +++++++++++++++++++ datasource/schema/single_nested_attribute.go | 12 +++++ .../schema/single_nested_attribute_test.go | 52 +++++++++++++++++++ datasource/schema/string_attribute.go | 12 +++++ datasource/schema/string_attribute_test.go | 52 +++++++++++++++++++ 32 files changed, 1024 insertions(+) diff --git a/datasource/schema/bool_attribute.go b/datasource/schema/bool_attribute.go index 1d984a8db..55a982573 100644 --- a/datasource/schema/bool_attribute.go +++ b/datasource/schema/bool_attribute.go @@ -191,3 +191,15 @@ func (a BoolAttribute) IsWriteOnly() bool { func (a BoolAttribute) IsSensitive() bool { return a.Sensitive } + +// IsRequiredForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a BoolAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a BoolAttribute) IsOptionalForImport() bool { + return false +} diff --git a/datasource/schema/bool_attribute_test.go b/datasource/schema/bool_attribute_test.go index ea22a21fc..e2fa2d2a6 100644 --- a/datasource/schema/bool_attribute_test.go +++ b/datasource/schema/bool_attribute_test.go @@ -428,3 +428,55 @@ func TestBoolAttributeIsWriteOnly(t *testing.T) { }) } } + +func TestBoolAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.BoolAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.BoolAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestBoolAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.BoolAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.BoolAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/datasource/schema/dynamic_attribute.go b/datasource/schema/dynamic_attribute.go index 4659cadf6..526490383 100644 --- a/datasource/schema/dynamic_attribute.go +++ b/datasource/schema/dynamic_attribute.go @@ -188,6 +188,18 @@ func (a DynamicAttribute) IsWriteOnly() bool { return false } +// IsRequiredForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a DynamicAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a DynamicAttribute) IsOptionalForImport() bool { + return false +} + // DynamicValidators returns the Validators field value. func (a DynamicAttribute) DynamicValidators() []validator.Dynamic { return a.Validators diff --git a/datasource/schema/dynamic_attribute_test.go b/datasource/schema/dynamic_attribute_test.go index 7166ae9cb..05cd9e340 100644 --- a/datasource/schema/dynamic_attribute_test.go +++ b/datasource/schema/dynamic_attribute_test.go @@ -428,3 +428,55 @@ func TestDynamicAttributeDynamicValidators(t *testing.T) { }) } } + +func TestDynamicAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.DynamicAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.DynamicAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestDynamicAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.DynamicAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.DynamicAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/datasource/schema/float32_attribute.go b/datasource/schema/float32_attribute.go index d2510f5c3..4ad4203ca 100644 --- a/datasource/schema/float32_attribute.go +++ b/datasource/schema/float32_attribute.go @@ -194,3 +194,15 @@ func (a Float32Attribute) IsSensitive() bool { func (a Float32Attribute) IsWriteOnly() bool { return false } + +// IsRequiredForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a Float32Attribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a Float32Attribute) IsOptionalForImport() bool { + return false +} diff --git a/datasource/schema/float32_attribute_test.go b/datasource/schema/float32_attribute_test.go index 3149803e0..152ef32c0 100644 --- a/datasource/schema/float32_attribute_test.go +++ b/datasource/schema/float32_attribute_test.go @@ -428,3 +428,55 @@ func TestFloat32AttributeIsWriteOnly(t *testing.T) { }) } } + +func TestFloat32AttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.Float32Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.Float32Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/datasource/schema/float64_attribute.go b/datasource/schema/float64_attribute.go index 1d893dd33..6e965edfb 100644 --- a/datasource/schema/float64_attribute.go +++ b/datasource/schema/float64_attribute.go @@ -194,3 +194,15 @@ func (a Float64Attribute) IsSensitive() bool { func (a Float64Attribute) IsWriteOnly() bool { return false } + +// IsRequiredForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a Float64Attribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a Float64Attribute) IsOptionalForImport() bool { + return false +} diff --git a/datasource/schema/float64_attribute_test.go b/datasource/schema/float64_attribute_test.go index c73193387..a99d9154c 100644 --- a/datasource/schema/float64_attribute_test.go +++ b/datasource/schema/float64_attribute_test.go @@ -428,3 +428,55 @@ func TestFloat64AttributeIsWriteOnly(t *testing.T) { }) } } + +func TestFloat64AttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float64Attribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.Float64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat64AttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float64Attribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.Float64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/datasource/schema/int32_attribute.go b/datasource/schema/int32_attribute.go index d06a9b8ce..67b355d29 100644 --- a/datasource/schema/int32_attribute.go +++ b/datasource/schema/int32_attribute.go @@ -194,3 +194,15 @@ func (a Int32Attribute) IsSensitive() bool { func (a Int32Attribute) IsWriteOnly() bool { return false } + +// IsRequiredForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a Int32Attribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a Int32Attribute) IsOptionalForImport() bool { + return false +} diff --git a/datasource/schema/int32_attribute_test.go b/datasource/schema/int32_attribute_test.go index 15404c9e8..2e38dc919 100644 --- a/datasource/schema/int32_attribute_test.go +++ b/datasource/schema/int32_attribute_test.go @@ -428,3 +428,55 @@ func TestInt32AttributeIsWriteOnly(t *testing.T) { }) } } + +func TestInt32AttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int32Attribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.Int32Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt32AttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int32Attribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.Int32Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/datasource/schema/int64_attribute.go b/datasource/schema/int64_attribute.go index 85bd4a445..535f1f8a1 100644 --- a/datasource/schema/int64_attribute.go +++ b/datasource/schema/int64_attribute.go @@ -194,3 +194,15 @@ func (a Int64Attribute) IsSensitive() bool { func (a Int64Attribute) IsWriteOnly() bool { return false } + +// IsRequiredForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a Int64Attribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a Int64Attribute) IsOptionalForImport() bool { + return false +} diff --git a/datasource/schema/int64_attribute_test.go b/datasource/schema/int64_attribute_test.go index ed0544c62..c692c2b3f 100644 --- a/datasource/schema/int64_attribute_test.go +++ b/datasource/schema/int64_attribute_test.go @@ -428,3 +428,55 @@ func TestInt64AttributeIsWriteOnly(t *testing.T) { }) } } + +func TestInt64AttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int64Attribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.Int64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt64AttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int64Attribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.Int64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/datasource/schema/list_attribute.go b/datasource/schema/list_attribute.go index 2e5140602..de211ed62 100644 --- a/datasource/schema/list_attribute.go +++ b/datasource/schema/list_attribute.go @@ -213,6 +213,18 @@ func (a ListAttribute) ListValidators() []validator.List { return a.Validators } +// IsRequiredForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a ListAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a ListAttribute) IsOptionalForImport() bool { + return false +} + // ValidateImplementation contains logic for validating the // provider-defined implementation of the attribute to prevent unexpected // errors or panics. This logic runs during the GetProviderSchema RPC diff --git a/datasource/schema/list_attribute_test.go b/datasource/schema/list_attribute_test.go index c38c41e2f..d28a3732d 100644 --- a/datasource/schema/list_attribute_test.go +++ b/datasource/schema/list_attribute_test.go @@ -524,3 +524,55 @@ func TestListAttributeValidateImplementation(t *testing.T) { }) } } + +func TestListAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.ListAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.ListAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/datasource/schema/list_nested_attribute.go b/datasource/schema/list_nested_attribute.go index 922320627..4e844574d 100644 --- a/datasource/schema/list_nested_attribute.go +++ b/datasource/schema/list_nested_attribute.go @@ -236,6 +236,18 @@ func (a ListNestedAttribute) IsWriteOnly() bool { return false } +// IsRequiredForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a ListNestedAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a ListNestedAttribute) IsOptionalForImport() bool { + return false +} + // ListValidators returns the Validators field value. func (a ListNestedAttribute) ListValidators() []validator.List { return a.Validators diff --git a/datasource/schema/list_nested_attribute_test.go b/datasource/schema/list_nested_attribute_test.go index b7ea5be20..70725f0eb 100644 --- a/datasource/schema/list_nested_attribute_test.go +++ b/datasource/schema/list_nested_attribute_test.go @@ -689,3 +689,55 @@ func TestListNestedAttributeValidateImplementation(t *testing.T) { }) } } + +func TestListNestedAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListNestedAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.ListNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListNestedAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListNestedAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.ListNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/datasource/schema/map_attribute.go b/datasource/schema/map_attribute.go index 3d6c57680..8e7030888 100644 --- a/datasource/schema/map_attribute.go +++ b/datasource/schema/map_attribute.go @@ -211,6 +211,18 @@ func (a MapAttribute) IsWriteOnly() bool { return false } +// IsRequiredForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a MapAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a MapAttribute) IsOptionalForImport() bool { + return false +} + // MapValidators returns the Validators field value. func (a MapAttribute) MapValidators() []validator.Map { return a.Validators diff --git a/datasource/schema/map_attribute_test.go b/datasource/schema/map_attribute_test.go index af361a636..f72a8b496 100644 --- a/datasource/schema/map_attribute_test.go +++ b/datasource/schema/map_attribute_test.go @@ -524,3 +524,55 @@ func TestMapAttributeValidateImplementation(t *testing.T) { }) } } + +func TestMapAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.MapAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.MapAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/datasource/schema/map_nested_attribute.go b/datasource/schema/map_nested_attribute.go index 9bf1bb957..b9a8bb905 100644 --- a/datasource/schema/map_nested_attribute.go +++ b/datasource/schema/map_nested_attribute.go @@ -236,6 +236,18 @@ func (a MapNestedAttribute) IsWriteOnly() bool { return false } +// IsRequiredForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a MapNestedAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a MapNestedAttribute) IsOptionalForImport() bool { + return false +} + // MapValidators returns the Validators field value. func (a MapNestedAttribute) MapValidators() []validator.Map { return a.Validators diff --git a/datasource/schema/map_nested_attribute_test.go b/datasource/schema/map_nested_attribute_test.go index d691cda82..e5db0a5c2 100644 --- a/datasource/schema/map_nested_attribute_test.go +++ b/datasource/schema/map_nested_attribute_test.go @@ -689,3 +689,55 @@ func TestMapNestedAttributeValidateImplementation(t *testing.T) { }) } } + +func TestMapNestedAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapNestedAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.MapNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapNestedAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapNestedAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.MapNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/datasource/schema/number_attribute.go b/datasource/schema/number_attribute.go index c21f74a15..61e9184ea 100644 --- a/datasource/schema/number_attribute.go +++ b/datasource/schema/number_attribute.go @@ -191,6 +191,18 @@ func (a NumberAttribute) IsWriteOnly() bool { return false } +// IsRequiredForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a NumberAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a NumberAttribute) IsOptionalForImport() bool { + return false +} + // NumberValidators returns the Validators field value. func (a NumberAttribute) NumberValidators() []validator.Number { return a.Validators diff --git a/datasource/schema/number_attribute_test.go b/datasource/schema/number_attribute_test.go index c30b711eb..87c10c323 100644 --- a/datasource/schema/number_attribute_test.go +++ b/datasource/schema/number_attribute_test.go @@ -428,3 +428,55 @@ func TestNumberAttributeNumberValidators(t *testing.T) { }) } } + +func TestNumberAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.NumberAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.NumberAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNumberAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.NumberAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.NumberAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/datasource/schema/object_attribute.go b/datasource/schema/object_attribute.go index a004329ac..981285181 100644 --- a/datasource/schema/object_attribute.go +++ b/datasource/schema/object_attribute.go @@ -210,6 +210,18 @@ func (a ObjectAttribute) IsWriteOnly() bool { return false } +// IsRequiredForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a ObjectAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a ObjectAttribute) IsOptionalForImport() bool { + return false +} + // ObjectValidators returns the Validators field value. func (a ObjectAttribute) ObjectValidators() []validator.Object { return a.Validators diff --git a/datasource/schema/object_attribute_test.go b/datasource/schema/object_attribute_test.go index d5c45ee64..a4b30d2fe 100644 --- a/datasource/schema/object_attribute_test.go +++ b/datasource/schema/object_attribute_test.go @@ -557,3 +557,55 @@ func TestObjectAttributeValidateImplementation(t *testing.T) { }) } } + +func TestObjectAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ObjectAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.ObjectAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestObjectAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ObjectAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.ObjectAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/datasource/schema/set_attribute.go b/datasource/schema/set_attribute.go index 859b0558d..714a96022 100644 --- a/datasource/schema/set_attribute.go +++ b/datasource/schema/set_attribute.go @@ -206,6 +206,18 @@ func (a SetAttribute) IsWriteOnly() bool { return false } +// IsRequiredForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a SetAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a SetAttribute) IsOptionalForImport() bool { + return false +} + // SetValidators returns the Validators field value. func (a SetAttribute) SetValidators() []validator.Set { return a.Validators diff --git a/datasource/schema/set_attribute_test.go b/datasource/schema/set_attribute_test.go index 02fcb830c..29f2effe1 100644 --- a/datasource/schema/set_attribute_test.go +++ b/datasource/schema/set_attribute_test.go @@ -524,3 +524,55 @@ func TestSetAttributeValidateImplementation(t *testing.T) { }) } } + +func TestSetAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.SetAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.SetAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/datasource/schema/set_nested_attribute.go b/datasource/schema/set_nested_attribute.go index 26b14e449..30ed7573d 100644 --- a/datasource/schema/set_nested_attribute.go +++ b/datasource/schema/set_nested_attribute.go @@ -231,6 +231,18 @@ func (a SetNestedAttribute) IsWriteOnly() bool { return false } +// IsRequiredForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a SetNestedAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a SetNestedAttribute) IsOptionalForImport() bool { + return false +} + // SetValidators returns the Validators field value. func (a SetNestedAttribute) SetValidators() []validator.Set { return a.Validators diff --git a/datasource/schema/set_nested_attribute_test.go b/datasource/schema/set_nested_attribute_test.go index 1a50cac03..083eba851 100644 --- a/datasource/schema/set_nested_attribute_test.go +++ b/datasource/schema/set_nested_attribute_test.go @@ -689,3 +689,55 @@ func TestSetNestedAttributeValidateImplementation(t *testing.T) { }) } } + +func TestSetNestedAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetNestedAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.SetNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetNestedAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetNestedAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.SetNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/datasource/schema/single_nested_attribute.go b/datasource/schema/single_nested_attribute.go index b6f4e7f32..290d21075 100644 --- a/datasource/schema/single_nested_attribute.go +++ b/datasource/schema/single_nested_attribute.go @@ -245,6 +245,18 @@ func (a SingleNestedAttribute) IsWriteOnly() bool { return false } +// IsRequiredForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a SingleNestedAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a SingleNestedAttribute) IsOptionalForImport() bool { + return false +} + // ObjectValidators returns the Validators field value. func (a SingleNestedAttribute) ObjectValidators() []validator.Object { return a.Validators diff --git a/datasource/schema/single_nested_attribute_test.go b/datasource/schema/single_nested_attribute_test.go index 0a7a0b66f..73a007390 100644 --- a/datasource/schema/single_nested_attribute_test.go +++ b/datasource/schema/single_nested_attribute_test.go @@ -570,3 +570,55 @@ func TestSingleNestedAttributeObjectValidators(t *testing.T) { }) } } + +func TestSingleNestedAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SingleNestedAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.SingleNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSingleNestedAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SingleNestedAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.SingleNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/datasource/schema/string_attribute.go b/datasource/schema/string_attribute.go index 95534fece..250fb5ff2 100644 --- a/datasource/schema/string_attribute.go +++ b/datasource/schema/string_attribute.go @@ -187,6 +187,18 @@ func (a StringAttribute) IsWriteOnly() bool { return false } +// IsRequiredForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a StringAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a StringAttribute) IsOptionalForImport() bool { + return false +} + // StringValidators returns the Validators field value. func (a StringAttribute) StringValidators() []validator.String { return a.Validators diff --git a/datasource/schema/string_attribute_test.go b/datasource/schema/string_attribute_test.go index 09ea194f2..6ccf2a9bf 100644 --- a/datasource/schema/string_attribute_test.go +++ b/datasource/schema/string_attribute_test.go @@ -428,3 +428,55 @@ func TestStringAttributeStringValidators(t *testing.T) { }) } } + +func TestStringAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.StringAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.StringAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestStringAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.StringAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.StringAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} From f2243bade5fb89fe49df06d9dd025ebea996ff28 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Mon, 24 Feb 2025 10:04:06 -0500 Subject: [PATCH 04/20] implement in ephemeral/schema --- ephemeral/schema/bool_attribute.go | 12 +++++ ephemeral/schema/bool_attribute_test.go | 52 +++++++++++++++++++ ephemeral/schema/dynamic_attribute.go | 12 +++++ ephemeral/schema/dynamic_attribute_test.go | 52 +++++++++++++++++++ ephemeral/schema/float32_attribute.go | 12 +++++ ephemeral/schema/float32_attribute_test.go | 52 +++++++++++++++++++ ephemeral/schema/float64_attribute.go | 12 +++++ ephemeral/schema/float64_attribute_test.go | 52 +++++++++++++++++++ ephemeral/schema/int32_attribute.go | 12 +++++ ephemeral/schema/int32_attribute_test.go | 52 +++++++++++++++++++ ephemeral/schema/int64_attribute.go | 12 +++++ ephemeral/schema/int64_attribute_test.go | 52 +++++++++++++++++++ ephemeral/schema/list_attribute.go | 12 +++++ ephemeral/schema/list_attribute_test.go | 52 +++++++++++++++++++ ephemeral/schema/list_nested_attribute.go | 12 +++++ .../schema/list_nested_attribute_test.go | 52 +++++++++++++++++++ ephemeral/schema/map_attribute.go | 12 +++++ ephemeral/schema/map_attribute_test.go | 52 +++++++++++++++++++ ephemeral/schema/map_nested_attribute.go | 12 +++++ ephemeral/schema/map_nested_attribute_test.go | 52 +++++++++++++++++++ ephemeral/schema/number_attribute.go | 12 +++++ ephemeral/schema/number_attribute_test.go | 52 +++++++++++++++++++ ephemeral/schema/object_attribute.go | 12 +++++ ephemeral/schema/object_attribute_test.go | 52 +++++++++++++++++++ ephemeral/schema/set_attribute.go | 12 +++++ ephemeral/schema/set_attribute_test.go | 52 +++++++++++++++++++ ephemeral/schema/set_nested_attribute.go | 12 +++++ ephemeral/schema/set_nested_attribute_test.go | 52 +++++++++++++++++++ ephemeral/schema/single_nested_attribute.go | 12 +++++ .../schema/single_nested_attribute_test.go | 52 +++++++++++++++++++ ephemeral/schema/string_attribute.go | 12 +++++ ephemeral/schema/string_attribute_test.go | 52 +++++++++++++++++++ 32 files changed, 1024 insertions(+) diff --git a/ephemeral/schema/bool_attribute.go b/ephemeral/schema/bool_attribute.go index 56790dee2..4ff3b43bc 100644 --- a/ephemeral/schema/bool_attribute.go +++ b/ephemeral/schema/bool_attribute.go @@ -190,3 +190,15 @@ func (a BoolAttribute) IsSensitive() bool { func (a BoolAttribute) IsWriteOnly() bool { return false } + +// IsRequiredForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a BoolAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a BoolAttribute) IsOptionalForImport() bool { + return false +} diff --git a/ephemeral/schema/bool_attribute_test.go b/ephemeral/schema/bool_attribute_test.go index 30cc54aa2..7a9b27f63 100644 --- a/ephemeral/schema/bool_attribute_test.go +++ b/ephemeral/schema/bool_attribute_test.go @@ -428,3 +428,55 @@ func TestBoolAttributeIsWriteOnly(t *testing.T) { }) } } + +func TestBoolAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.BoolAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.BoolAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestBoolAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.BoolAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.BoolAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/ephemeral/schema/dynamic_attribute.go b/ephemeral/schema/dynamic_attribute.go index 9cd22e70a..205c73ea2 100644 --- a/ephemeral/schema/dynamic_attribute.go +++ b/ephemeral/schema/dynamic_attribute.go @@ -191,3 +191,15 @@ func (a DynamicAttribute) IsWriteOnly() bool { func (a DynamicAttribute) DynamicValidators() []validator.Dynamic { return a.Validators } + +// IsRequiredForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a DynamicAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a DynamicAttribute) IsOptionalForImport() bool { + return false +} diff --git a/ephemeral/schema/dynamic_attribute_test.go b/ephemeral/schema/dynamic_attribute_test.go index 4d01e95a0..48fc4da08 100644 --- a/ephemeral/schema/dynamic_attribute_test.go +++ b/ephemeral/schema/dynamic_attribute_test.go @@ -427,3 +427,55 @@ func TestDynamicAttributeDynamicValidators(t *testing.T) { }) } } + +func TestDynamicAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.DynamicAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.DynamicAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestDynamicAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.DynamicAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.DynamicAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/ephemeral/schema/float32_attribute.go b/ephemeral/schema/float32_attribute.go index 46af5bae2..cb5de0fb7 100644 --- a/ephemeral/schema/float32_attribute.go +++ b/ephemeral/schema/float32_attribute.go @@ -193,3 +193,15 @@ func (a Float32Attribute) IsSensitive() bool { func (a Float32Attribute) IsWriteOnly() bool { return false } + +// IsRequiredForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a Float32Attribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a Float32Attribute) IsOptionalForImport() bool { + return false +} diff --git a/ephemeral/schema/float32_attribute_test.go b/ephemeral/schema/float32_attribute_test.go index 1617d12af..90f9afe99 100644 --- a/ephemeral/schema/float32_attribute_test.go +++ b/ephemeral/schema/float32_attribute_test.go @@ -428,3 +428,55 @@ func TestFloat32AttributeIsWriteOnly(t *testing.T) { }) } } + +func TestFloat32AttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.Float32Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.Float32Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/ephemeral/schema/float64_attribute.go b/ephemeral/schema/float64_attribute.go index f4699e210..a295ace36 100644 --- a/ephemeral/schema/float64_attribute.go +++ b/ephemeral/schema/float64_attribute.go @@ -193,3 +193,15 @@ func (a Float64Attribute) IsSensitive() bool { func (a Float64Attribute) IsWriteOnly() bool { return false } + +// IsRequiredForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a Float64Attribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a Float64Attribute) IsOptionalForImport() bool { + return false +} diff --git a/ephemeral/schema/float64_attribute_test.go b/ephemeral/schema/float64_attribute_test.go index 4bfb2ebd0..29b240f7c 100644 --- a/ephemeral/schema/float64_attribute_test.go +++ b/ephemeral/schema/float64_attribute_test.go @@ -428,3 +428,55 @@ func TestFloat54AttributeIsWriteOnly(t *testing.T) { }) } } + +func TestFloat64AttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float64Attribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.Float64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat64AttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float64Attribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.Float64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/ephemeral/schema/int32_attribute.go b/ephemeral/schema/int32_attribute.go index 76f6e0194..1f948eaf1 100644 --- a/ephemeral/schema/int32_attribute.go +++ b/ephemeral/schema/int32_attribute.go @@ -193,3 +193,15 @@ func (a Int32Attribute) IsSensitive() bool { func (a Int32Attribute) IsWriteOnly() bool { return false } + +// IsRequiredForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a Int32Attribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a Int32Attribute) IsOptionalForImport() bool { + return false +} diff --git a/ephemeral/schema/int32_attribute_test.go b/ephemeral/schema/int32_attribute_test.go index b4d3cc6dc..3e88ea4d6 100644 --- a/ephemeral/schema/int32_attribute_test.go +++ b/ephemeral/schema/int32_attribute_test.go @@ -428,3 +428,55 @@ func TestInt2AttributeIsWriteOnly(t *testing.T) { }) } } + +func TestInt32AttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int32Attribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.Int32Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt32AttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int32Attribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.Int32Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/ephemeral/schema/int64_attribute.go b/ephemeral/schema/int64_attribute.go index 27cb2dd23..95b96fe2b 100644 --- a/ephemeral/schema/int64_attribute.go +++ b/ephemeral/schema/int64_attribute.go @@ -193,3 +193,15 @@ func (a Int64Attribute) IsSensitive() bool { func (a Int64Attribute) IsWriteOnly() bool { return false } + +// IsRequiredForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a Int64Attribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a Int64Attribute) IsOptionalForImport() bool { + return false +} diff --git a/ephemeral/schema/int64_attribute_test.go b/ephemeral/schema/int64_attribute_test.go index 6b7c1849f..ef637140f 100644 --- a/ephemeral/schema/int64_attribute_test.go +++ b/ephemeral/schema/int64_attribute_test.go @@ -428,3 +428,55 @@ func TestInt64AttributeIsWriteOnly(t *testing.T) { }) } } + +func TestInt64AttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int64Attribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.Int64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt64AttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int64Attribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.Int64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/ephemeral/schema/list_attribute.go b/ephemeral/schema/list_attribute.go index 258757f99..05cc68955 100644 --- a/ephemeral/schema/list_attribute.go +++ b/ephemeral/schema/list_attribute.go @@ -206,6 +206,18 @@ func (a ListAttribute) IsWriteOnly() bool { return false } +// IsRequiredForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a ListAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a ListAttribute) IsOptionalForImport() bool { + return false +} + // ListValidators returns the Validators field value. func (a ListAttribute) ListValidators() []validator.List { return a.Validators diff --git a/ephemeral/schema/list_attribute_test.go b/ephemeral/schema/list_attribute_test.go index cdbc9feb6..9ee0e5c85 100644 --- a/ephemeral/schema/list_attribute_test.go +++ b/ephemeral/schema/list_attribute_test.go @@ -523,3 +523,55 @@ func TestListAttributeIsWriteOnly(t *testing.T) { }) } } + +func TestListAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.ListAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.ListAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/ephemeral/schema/list_nested_attribute.go b/ephemeral/schema/list_nested_attribute.go index a4478fb48..ea4954da9 100644 --- a/ephemeral/schema/list_nested_attribute.go +++ b/ephemeral/schema/list_nested_attribute.go @@ -235,6 +235,18 @@ func (a ListNestedAttribute) IsWriteOnly() bool { return false } +// IsRequiredForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a ListNestedAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a ListNestedAttribute) IsOptionalForImport() bool { + return false +} + // ListValidators returns the Validators field value. func (a ListNestedAttribute) ListValidators() []validator.List { return a.Validators diff --git a/ephemeral/schema/list_nested_attribute_test.go b/ephemeral/schema/list_nested_attribute_test.go index 914af418c..daba7e113 100644 --- a/ephemeral/schema/list_nested_attribute_test.go +++ b/ephemeral/schema/list_nested_attribute_test.go @@ -688,3 +688,55 @@ func TestListNestedAttributeValidateImplementation(t *testing.T) { }) } } + +func TestListNestedAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListNestedAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.ListNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListNestedAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListNestedAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.ListNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/ephemeral/schema/map_attribute.go b/ephemeral/schema/map_attribute.go index 0741747a4..018e83c40 100644 --- a/ephemeral/schema/map_attribute.go +++ b/ephemeral/schema/map_attribute.go @@ -210,6 +210,18 @@ func (a MapAttribute) IsWriteOnly() bool { return false } +// IsRequiredForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a MapAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a MapAttribute) IsOptionalForImport() bool { + return false +} + // MapValidators returns the Validators field value. func (a MapAttribute) MapValidators() []validator.Map { return a.Validators diff --git a/ephemeral/schema/map_attribute_test.go b/ephemeral/schema/map_attribute_test.go index f750b5f5d..ed2348551 100644 --- a/ephemeral/schema/map_attribute_test.go +++ b/ephemeral/schema/map_attribute_test.go @@ -523,3 +523,55 @@ func TestMapAttributeValidateImplementation(t *testing.T) { }) } } + +func TestMapAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.MapAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.MapAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/ephemeral/schema/map_nested_attribute.go b/ephemeral/schema/map_nested_attribute.go index de057e106..f935c12c0 100644 --- a/ephemeral/schema/map_nested_attribute.go +++ b/ephemeral/schema/map_nested_attribute.go @@ -235,6 +235,18 @@ func (a MapNestedAttribute) IsWriteOnly() bool { return false } +// IsRequiredForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a MapNestedAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a MapNestedAttribute) IsOptionalForImport() bool { + return false +} + // MapValidators returns the Validators field value. func (a MapNestedAttribute) MapValidators() []validator.Map { return a.Validators diff --git a/ephemeral/schema/map_nested_attribute_test.go b/ephemeral/schema/map_nested_attribute_test.go index 37c2f1aa2..2bbaa15d5 100644 --- a/ephemeral/schema/map_nested_attribute_test.go +++ b/ephemeral/schema/map_nested_attribute_test.go @@ -688,3 +688,55 @@ func TestMapNestedAttributeValidateImplementation(t *testing.T) { }) } } + +func TestMapNestedAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapNestedAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.MapNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapNestedAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapNestedAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.MapNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/ephemeral/schema/number_attribute.go b/ephemeral/schema/number_attribute.go index 17d557398..6b5de920e 100644 --- a/ephemeral/schema/number_attribute.go +++ b/ephemeral/schema/number_attribute.go @@ -190,6 +190,18 @@ func (a NumberAttribute) IsWriteOnly() bool { return false } +// IsRequiredForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a NumberAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a NumberAttribute) IsOptionalForImport() bool { + return false +} + // NumberValidators returns the Validators field value. func (a NumberAttribute) NumberValidators() []validator.Number { return a.Validators diff --git a/ephemeral/schema/number_attribute_test.go b/ephemeral/schema/number_attribute_test.go index a15142afa..2b07635a6 100644 --- a/ephemeral/schema/number_attribute_test.go +++ b/ephemeral/schema/number_attribute_test.go @@ -428,3 +428,55 @@ func TestNumberAttributeNumberValidators(t *testing.T) { }) } } + +func TestNumberAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.NumberAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.NumberAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNumberAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.NumberAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.NumberAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/ephemeral/schema/object_attribute.go b/ephemeral/schema/object_attribute.go index fa808e4ba..327769075 100644 --- a/ephemeral/schema/object_attribute.go +++ b/ephemeral/schema/object_attribute.go @@ -208,6 +208,18 @@ func (a ObjectAttribute) IsWriteOnly() bool { return false } +// IsRequiredForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a ObjectAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a ObjectAttribute) IsOptionalForImport() bool { + return false +} + // ObjectValidators returns the Validators field value. func (a ObjectAttribute) ObjectValidators() []validator.Object { return a.Validators diff --git a/ephemeral/schema/object_attribute_test.go b/ephemeral/schema/object_attribute_test.go index f49e4afca..cff690c6e 100644 --- a/ephemeral/schema/object_attribute_test.go +++ b/ephemeral/schema/object_attribute_test.go @@ -557,3 +557,55 @@ func TestObjectAttributeValidateImplementation(t *testing.T) { }) } } + +func TestObjectAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ObjectAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.ObjectAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestObjectAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ObjectAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.ObjectAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/ephemeral/schema/set_attribute.go b/ephemeral/schema/set_attribute.go index 7ecd08ffd..a05235023 100644 --- a/ephemeral/schema/set_attribute.go +++ b/ephemeral/schema/set_attribute.go @@ -205,6 +205,18 @@ func (a SetAttribute) IsWriteOnly() bool { return false } +// IsRequiredForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a SetAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a SetAttribute) IsOptionalForImport() bool { + return false +} + // SetValidators returns the Validators field value. func (a SetAttribute) SetValidators() []validator.Set { return a.Validators diff --git a/ephemeral/schema/set_attribute_test.go b/ephemeral/schema/set_attribute_test.go index d3c158fda..af56ab903 100644 --- a/ephemeral/schema/set_attribute_test.go +++ b/ephemeral/schema/set_attribute_test.go @@ -523,3 +523,55 @@ func TestSetAttributeValidateImplementation(t *testing.T) { }) } } + +func TestSetAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.SetAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.SetAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/ephemeral/schema/set_nested_attribute.go b/ephemeral/schema/set_nested_attribute.go index 658dc0df7..f5740cd57 100644 --- a/ephemeral/schema/set_nested_attribute.go +++ b/ephemeral/schema/set_nested_attribute.go @@ -230,6 +230,18 @@ func (a SetNestedAttribute) IsWriteOnly() bool { return false } +// IsRequiredForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a SetNestedAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a SetNestedAttribute) IsOptionalForImport() bool { + return false +} + // SetValidators returns the Validators field value. func (a SetNestedAttribute) SetValidators() []validator.Set { return a.Validators diff --git a/ephemeral/schema/set_nested_attribute_test.go b/ephemeral/schema/set_nested_attribute_test.go index 3abd59839..a049ade74 100644 --- a/ephemeral/schema/set_nested_attribute_test.go +++ b/ephemeral/schema/set_nested_attribute_test.go @@ -689,3 +689,55 @@ func TestSetNestedAttributeValidateImplementation(t *testing.T) { }) } } + +func TestSetNestedAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetNestedAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.SetNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetNestedAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetNestedAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.SetNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/ephemeral/schema/single_nested_attribute.go b/ephemeral/schema/single_nested_attribute.go index 167b8c136..16cc31fc9 100644 --- a/ephemeral/schema/single_nested_attribute.go +++ b/ephemeral/schema/single_nested_attribute.go @@ -244,6 +244,18 @@ func (a SingleNestedAttribute) IsWriteOnly() bool { return false } +// IsRequiredForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a SingleNestedAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a SingleNestedAttribute) IsOptionalForImport() bool { + return false +} + // ObjectValidators returns the Validators field value. func (a SingleNestedAttribute) ObjectValidators() []validator.Object { return a.Validators diff --git a/ephemeral/schema/single_nested_attribute_test.go b/ephemeral/schema/single_nested_attribute_test.go index 8f4e4e783..f97ba872b 100644 --- a/ephemeral/schema/single_nested_attribute_test.go +++ b/ephemeral/schema/single_nested_attribute_test.go @@ -569,3 +569,55 @@ func TestSingleNestedAttributeObjectValidators(t *testing.T) { }) } } + +func TestSingleNestedAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SingleNestedAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.SingleNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSingleNestedAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SingleNestedAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.SingleNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/ephemeral/schema/string_attribute.go b/ephemeral/schema/string_attribute.go index 3cfefe97c..c751c9872 100644 --- a/ephemeral/schema/string_attribute.go +++ b/ephemeral/schema/string_attribute.go @@ -186,6 +186,18 @@ func (a StringAttribute) IsWriteOnly() bool { return false } +// IsRequiredForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a StringAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a StringAttribute) IsOptionalForImport() bool { + return false +} + // StringValidators returns the Validators field value. func (a StringAttribute) StringValidators() []validator.String { return a.Validators diff --git a/ephemeral/schema/string_attribute_test.go b/ephemeral/schema/string_attribute_test.go index 9f8532f34..95a34c24c 100644 --- a/ephemeral/schema/string_attribute_test.go +++ b/ephemeral/schema/string_attribute_test.go @@ -427,3 +427,55 @@ func TestStringAttributeStringValidators(t *testing.T) { }) } } + +func TestStringAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.StringAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.StringAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestStringAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.StringAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.StringAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} From 057093a4987a1dc48740415a26dca731c109e9a7 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Mon, 24 Feb 2025 10:13:51 -0500 Subject: [PATCH 05/20] implement in provider/metaschema --- provider/metaschema/bool_attribute.go | 12 +++++ provider/metaschema/bool_attribute_test.go | 52 +++++++++++++++++++ provider/metaschema/float64_attribute.go | 12 +++++ provider/metaschema/float64_attribute_test.go | 52 +++++++++++++++++++ provider/metaschema/int64_attribute.go | 12 +++++ provider/metaschema/int64_attribute_test.go | 52 +++++++++++++++++++ provider/metaschema/list_attribute.go | 12 +++++ provider/metaschema/list_attribute_test.go | 52 +++++++++++++++++++ provider/metaschema/list_nested_attribute.go | 12 +++++ .../metaschema/list_nested_attribute_test.go | 52 +++++++++++++++++++ provider/metaschema/map_attribute.go | 12 +++++ provider/metaschema/map_attribute_test.go | 52 +++++++++++++++++++ provider/metaschema/map_nested_attribute.go | 12 +++++ .../metaschema/map_nested_attribute_test.go | 52 +++++++++++++++++++ provider/metaschema/number_attribute.go | 12 +++++ provider/metaschema/number_attribute_test.go | 52 +++++++++++++++++++ provider/metaschema/object_attribute.go | 12 +++++ provider/metaschema/object_attribute_test.go | 52 +++++++++++++++++++ provider/metaschema/set_attribute.go | 12 +++++ provider/metaschema/set_attribute_test.go | 52 +++++++++++++++++++ provider/metaschema/set_nested_attribute.go | 12 +++++ .../metaschema/set_nested_attribute_test.go | 52 +++++++++++++++++++ .../metaschema/single_nested_attribute.go | 12 +++++ .../single_nested_attribute_test.go | 52 +++++++++++++++++++ provider/metaschema/string_attribute.go | 12 +++++ provider/metaschema/string_attribute_test.go | 52 +++++++++++++++++++ 26 files changed, 832 insertions(+) diff --git a/provider/metaschema/bool_attribute.go b/provider/metaschema/bool_attribute.go index 374296341..811a94e34 100644 --- a/provider/metaschema/bool_attribute.go +++ b/provider/metaschema/bool_attribute.go @@ -124,3 +124,15 @@ func (a BoolAttribute) IsSensitive() bool { func (a BoolAttribute) IsWriteOnly() bool { return false } + +// IsRequiredForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a BoolAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a BoolAttribute) IsOptionalForImport() bool { + return false +} diff --git a/provider/metaschema/bool_attribute_test.go b/provider/metaschema/bool_attribute_test.go index f679fc1a5..caeefaeb1 100644 --- a/provider/metaschema/bool_attribute_test.go +++ b/provider/metaschema/bool_attribute_test.go @@ -377,3 +377,55 @@ func TestBoolAttributeIsWriteOnly(t *testing.T) { }) } } + +func TestBoolAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.BoolAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: metaschema.BoolAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestBoolAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.BoolAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: metaschema.BoolAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/metaschema/float64_attribute.go b/provider/metaschema/float64_attribute.go index ac2b79b0a..a4eb9e9d0 100644 --- a/provider/metaschema/float64_attribute.go +++ b/provider/metaschema/float64_attribute.go @@ -127,3 +127,15 @@ func (a Float64Attribute) IsSensitive() bool { func (a Float64Attribute) IsWriteOnly() bool { return false } + +// IsRequiredForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a Float64Attribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a Float64Attribute) IsOptionalForImport() bool { + return false +} diff --git a/provider/metaschema/float64_attribute_test.go b/provider/metaschema/float64_attribute_test.go index 8a73c9225..0ee1b3d42 100644 --- a/provider/metaschema/float64_attribute_test.go +++ b/provider/metaschema/float64_attribute_test.go @@ -377,3 +377,55 @@ func TestFloat64AttributeIsWriteOnly(t *testing.T) { }) } } + +func TestFloat64AttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.Float64Attribute + expected bool + }{ + "not-requiredForImport": { + attribute: metaschema.Float64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat64AttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.Float64Attribute + expected bool + }{ + "not-optionalForImport": { + attribute: metaschema.Float64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/metaschema/int64_attribute.go b/provider/metaschema/int64_attribute.go index aeccd7030..25a214934 100644 --- a/provider/metaschema/int64_attribute.go +++ b/provider/metaschema/int64_attribute.go @@ -127,3 +127,15 @@ func (a Int64Attribute) IsSensitive() bool { func (a Int64Attribute) IsWriteOnly() bool { return false } + +// IsRequiredForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a Int64Attribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a Int64Attribute) IsOptionalForImport() bool { + return false +} diff --git a/provider/metaschema/int64_attribute_test.go b/provider/metaschema/int64_attribute_test.go index 69439d021..ab7a5bdf6 100644 --- a/provider/metaschema/int64_attribute_test.go +++ b/provider/metaschema/int64_attribute_test.go @@ -377,3 +377,55 @@ func TestInt64AttributeIsWriteOnly(t *testing.T) { }) } } + +func TestInt64AttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.Int64Attribute + expected bool + }{ + "not-requiredForImport": { + attribute: metaschema.Int64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt64AttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.Int64Attribute + expected bool + }{ + "not-optionalForImport": { + attribute: metaschema.Int64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/metaschema/list_attribute.go b/provider/metaschema/list_attribute.go index 187d9c47c..2149c212c 100644 --- a/provider/metaschema/list_attribute.go +++ b/provider/metaschema/list_attribute.go @@ -141,6 +141,18 @@ func (a ListAttribute) IsWriteOnly() bool { return false } +// IsRequiredForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a ListAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a ListAttribute) IsOptionalForImport() bool { + return false +} + // ValidateImplementation contains logic for validating the // provider-defined implementation of the attribute to prevent unexpected // errors or panics. This logic runs during the GetProviderSchema RPC diff --git a/provider/metaschema/list_attribute_test.go b/provider/metaschema/list_attribute_test.go index 93f213ce0..66f5e6b66 100644 --- a/provider/metaschema/list_attribute_test.go +++ b/provider/metaschema/list_attribute_test.go @@ -451,3 +451,55 @@ func TestListAttributeValidateImplementation(t *testing.T) { }) } } + +func TestListAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.ListAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: metaschema.ListAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.ListAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: metaschema.ListAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/metaschema/list_nested_attribute.go b/provider/metaschema/list_nested_attribute.go index 0fa1b8221..bf9b2f948 100644 --- a/provider/metaschema/list_nested_attribute.go +++ b/provider/metaschema/list_nested_attribute.go @@ -166,3 +166,15 @@ func (a ListNestedAttribute) IsSensitive() bool { func (a ListNestedAttribute) IsWriteOnly() bool { return false } + +// IsRequiredForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a ListNestedAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a ListNestedAttribute) IsOptionalForImport() bool { + return false +} diff --git a/provider/metaschema/list_nested_attribute_test.go b/provider/metaschema/list_nested_attribute_test.go index 1c3576a30..fe587af0d 100644 --- a/provider/metaschema/list_nested_attribute_test.go +++ b/provider/metaschema/list_nested_attribute_test.go @@ -551,3 +551,55 @@ func TestListNestedAttributeIsWriteOnly(t *testing.T) { }) } } + +func TestListNestedAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.ListNestedAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: metaschema.ListNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListNestedAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.ListNestedAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: metaschema.ListNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/metaschema/map_attribute.go b/provider/metaschema/map_attribute.go index 9103231ff..18a3e9866 100644 --- a/provider/metaschema/map_attribute.go +++ b/provider/metaschema/map_attribute.go @@ -144,6 +144,18 @@ func (a MapAttribute) IsWriteOnly() bool { return false } +// IsRequiredForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a MapAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a MapAttribute) IsOptionalForImport() bool { + return false +} + // ValidateImplementation contains logic for validating the // provider-defined implementation of the attribute to prevent unexpected // errors or panics. This logic runs during the GetProviderSchema RPC diff --git a/provider/metaschema/map_attribute_test.go b/provider/metaschema/map_attribute_test.go index 7d137ef1d..11ec28d01 100644 --- a/provider/metaschema/map_attribute_test.go +++ b/provider/metaschema/map_attribute_test.go @@ -451,3 +451,55 @@ func TestMapAttributeValidateImplementation(t *testing.T) { }) } } + +func TestMapAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.MapAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: metaschema.MapAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.MapAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: metaschema.MapAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/metaschema/map_nested_attribute.go b/provider/metaschema/map_nested_attribute.go index 587c56c0a..5962d38a4 100644 --- a/provider/metaschema/map_nested_attribute.go +++ b/provider/metaschema/map_nested_attribute.go @@ -166,3 +166,15 @@ func (a MapNestedAttribute) IsSensitive() bool { func (a MapNestedAttribute) IsWriteOnly() bool { return false } + +// IsRequiredForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a MapNestedAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a MapNestedAttribute) IsOptionalForImport() bool { + return false +} diff --git a/provider/metaschema/map_nested_attribute_test.go b/provider/metaschema/map_nested_attribute_test.go index 99c5ae558..b2cc9a219 100644 --- a/provider/metaschema/map_nested_attribute_test.go +++ b/provider/metaschema/map_nested_attribute_test.go @@ -551,3 +551,55 @@ func TestMapNestedAttributeIsWriteOnly(t *testing.T) { }) } } + +func TestMapNestedAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.MapNestedAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: metaschema.MapNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapNestedAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.MapNestedAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: metaschema.MapNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/metaschema/number_attribute.go b/provider/metaschema/number_attribute.go index 511e7000a..829c0a504 100644 --- a/provider/metaschema/number_attribute.go +++ b/provider/metaschema/number_attribute.go @@ -128,3 +128,15 @@ func (a NumberAttribute) IsSensitive() bool { func (a NumberAttribute) IsWriteOnly() bool { return false } + +// IsRequiredForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a NumberAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a NumberAttribute) IsOptionalForImport() bool { + return false +} diff --git a/provider/metaschema/number_attribute_test.go b/provider/metaschema/number_attribute_test.go index 676db8e1b..53a094126 100644 --- a/provider/metaschema/number_attribute_test.go +++ b/provider/metaschema/number_attribute_test.go @@ -377,3 +377,55 @@ func TestNumberAttributeIsWriteOnly(t *testing.T) { }) } } + +func TestNumberAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.NumberAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: metaschema.NumberAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNumberAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.NumberAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: metaschema.NumberAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/metaschema/object_attribute.go b/provider/metaschema/object_attribute.go index aabe40d4c..091fff810 100644 --- a/provider/metaschema/object_attribute.go +++ b/provider/metaschema/object_attribute.go @@ -143,6 +143,18 @@ func (a ObjectAttribute) IsWriteOnly() bool { return false } +// IsRequiredForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a ObjectAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a ObjectAttribute) IsOptionalForImport() bool { + return false +} + // ValidateImplementation contains logic for validating the // provider-defined implementation of the attribute to prevent unexpected // errors or panics. This logic runs during the GetProviderSchema RPC diff --git a/provider/metaschema/object_attribute_test.go b/provider/metaschema/object_attribute_test.go index 6b3bb4479..7b47aa5c9 100644 --- a/provider/metaschema/object_attribute_test.go +++ b/provider/metaschema/object_attribute_test.go @@ -459,3 +459,55 @@ func TestObjectAttributeValidateImplementation(t *testing.T) { }) } } + +func TestObjectAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.ObjectAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: metaschema.ObjectAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestObjectAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.ObjectAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: metaschema.ObjectAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/metaschema/set_attribute.go b/provider/metaschema/set_attribute.go index f7d3e4112..a4b8365c0 100644 --- a/provider/metaschema/set_attribute.go +++ b/provider/metaschema/set_attribute.go @@ -139,6 +139,18 @@ func (a SetAttribute) IsWriteOnly() bool { return false } +// IsRequiredForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a SetAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a SetAttribute) IsOptionalForImport() bool { + return false +} + // ValidateImplementation contains logic for validating the // provider-defined implementation of the attribute to prevent unexpected // errors or panics. This logic runs during the GetProviderSchema RPC diff --git a/provider/metaschema/set_attribute_test.go b/provider/metaschema/set_attribute_test.go index 975b3b719..94bf6346b 100644 --- a/provider/metaschema/set_attribute_test.go +++ b/provider/metaschema/set_attribute_test.go @@ -451,3 +451,55 @@ func TestSetAttributeValidateImplementation(t *testing.T) { }) } } + +func TestSetAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.SetAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: metaschema.SetAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.SetAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: metaschema.SetAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/metaschema/set_nested_attribute.go b/provider/metaschema/set_nested_attribute.go index a3c6fbf9e..a137dedac 100644 --- a/provider/metaschema/set_nested_attribute.go +++ b/provider/metaschema/set_nested_attribute.go @@ -161,3 +161,15 @@ func (a SetNestedAttribute) IsSensitive() bool { func (a SetNestedAttribute) IsWriteOnly() bool { return false } + +// IsRequiredForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a SetNestedAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a SetNestedAttribute) IsOptionalForImport() bool { + return false +} diff --git a/provider/metaschema/set_nested_attribute_test.go b/provider/metaschema/set_nested_attribute_test.go index 225a936f9..5e7c46f66 100644 --- a/provider/metaschema/set_nested_attribute_test.go +++ b/provider/metaschema/set_nested_attribute_test.go @@ -551,3 +551,55 @@ func TestSetNestedAttributeIsWriteOnly(t *testing.T) { }) } } + +func TestSetNestedAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.SetNestedAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: metaschema.SetNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetNestedAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.SetNestedAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: metaschema.SetNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/metaschema/single_nested_attribute.go b/provider/metaschema/single_nested_attribute.go index 160fb1c80..a10d23685 100644 --- a/provider/metaschema/single_nested_attribute.go +++ b/provider/metaschema/single_nested_attribute.go @@ -181,3 +181,15 @@ func (a SingleNestedAttribute) IsSensitive() bool { func (a SingleNestedAttribute) IsWriteOnly() bool { return false } + +// IsRequiredForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a SingleNestedAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a SingleNestedAttribute) IsOptionalForImport() bool { + return false +} diff --git a/provider/metaschema/single_nested_attribute_test.go b/provider/metaschema/single_nested_attribute_test.go index d590cbba8..82b57349f 100644 --- a/provider/metaschema/single_nested_attribute_test.go +++ b/provider/metaschema/single_nested_attribute_test.go @@ -515,3 +515,55 @@ func TestSingleNestedAttributeIsWriteOnly(t *testing.T) { }) } } + +func TestSingleNestedAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.SingleNestedAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: metaschema.SingleNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSingleNestedAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.SingleNestedAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: metaschema.SingleNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/metaschema/string_attribute.go b/provider/metaschema/string_attribute.go index fe25c5014..5efcba288 100644 --- a/provider/metaschema/string_attribute.go +++ b/provider/metaschema/string_attribute.go @@ -124,3 +124,15 @@ func (a StringAttribute) IsSensitive() bool { func (a StringAttribute) IsWriteOnly() bool { return false } + +// IsRequiredForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a StringAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a StringAttribute) IsOptionalForImport() bool { + return false +} diff --git a/provider/metaschema/string_attribute_test.go b/provider/metaschema/string_attribute_test.go index c360462a1..2d12a2f58 100644 --- a/provider/metaschema/string_attribute_test.go +++ b/provider/metaschema/string_attribute_test.go @@ -377,3 +377,55 @@ func TestStringAttributeIsWriteOnly(t *testing.T) { }) } } + +func TestStringAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.StringAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: metaschema.StringAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestStringAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute metaschema.StringAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: metaschema.StringAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} From 2fed09114c3a15a0289c7cf7dfd4f0c15ce594a6 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Mon, 24 Feb 2025 10:23:12 -0500 Subject: [PATCH 06/20] implement in provider/schema --- provider/schema/bool_attribute.go | 12 +++++ provider/schema/bool_attribute_test.go | 52 +++++++++++++++++++ provider/schema/dynamic_attribute.go | 12 +++++ provider/schema/dynamic_attribute_test.go | 52 +++++++++++++++++++ provider/schema/float32_attribute.go | 12 +++++ provider/schema/float32_attribute_test.go | 52 +++++++++++++++++++ provider/schema/float64_attribute.go | 12 +++++ provider/schema/float64_attribute_test.go | 52 +++++++++++++++++++ provider/schema/int32_attribute.go | 12 +++++ provider/schema/int32_attribute_test.go | 52 +++++++++++++++++++ provider/schema/int64_attribute.go | 12 +++++ provider/schema/int64_attribute_test.go | 52 +++++++++++++++++++ provider/schema/list_attribute.go | 12 +++++ provider/schema/list_attribute_test.go | 52 +++++++++++++++++++ provider/schema/list_nested_attribute.go | 12 +++++ provider/schema/list_nested_attribute_test.go | 52 +++++++++++++++++++ provider/schema/map_attribute.go | 12 +++++ provider/schema/map_attribute_test.go | 52 +++++++++++++++++++ provider/schema/map_nested_attribute.go | 12 +++++ provider/schema/map_nested_attribute_test.go | 52 +++++++++++++++++++ provider/schema/number_attribute.go | 12 +++++ provider/schema/number_attribute_test.go | 52 +++++++++++++++++++ provider/schema/object_attribute.go | 12 +++++ provider/schema/object_attribute_test.go | 52 +++++++++++++++++++ provider/schema/set_attribute.go | 12 +++++ provider/schema/set_attribute_test.go | 52 +++++++++++++++++++ provider/schema/set_nested_attribute.go | 12 +++++ provider/schema/set_nested_attribute_test.go | 52 +++++++++++++++++++ provider/schema/single_nested_attribute.go | 12 +++++ .../schema/single_nested_attribute_test.go | 52 +++++++++++++++++++ provider/schema/string_attribute.go | 12 +++++ provider/schema/string_attribute_test.go | 52 +++++++++++++++++++ 32 files changed, 1024 insertions(+) diff --git a/provider/schema/bool_attribute.go b/provider/schema/bool_attribute.go index 0502821ca..ea1eb8f9c 100644 --- a/provider/schema/bool_attribute.go +++ b/provider/schema/bool_attribute.go @@ -185,3 +185,15 @@ func (a BoolAttribute) IsSensitive() bool { func (a BoolAttribute) IsWriteOnly() bool { return false } + +// IsRequiredForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a BoolAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a BoolAttribute) IsOptionalForImport() bool { + return false +} diff --git a/provider/schema/bool_attribute_test.go b/provider/schema/bool_attribute_test.go index 3e134f214..07291287b 100644 --- a/provider/schema/bool_attribute_test.go +++ b/provider/schema/bool_attribute_test.go @@ -422,3 +422,55 @@ func TestBoolAttributeIsWriteOnly(t *testing.T) { }) } } + +func TestBoolAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.BoolAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.BoolAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestBoolAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.BoolAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.BoolAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/schema/dynamic_attribute.go b/provider/schema/dynamic_attribute.go index 4b31279f8..5e5c07edc 100644 --- a/provider/schema/dynamic_attribute.go +++ b/provider/schema/dynamic_attribute.go @@ -182,3 +182,15 @@ func (a DynamicAttribute) DynamicValidators() []validator.Dynamic { func (a DynamicAttribute) IsWriteOnly() bool { return false } + +// IsRequiredForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a DynamicAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a DynamicAttribute) IsOptionalForImport() bool { + return false +} diff --git a/provider/schema/dynamic_attribute_test.go b/provider/schema/dynamic_attribute_test.go index 66c7d855e..bd6ec94a7 100644 --- a/provider/schema/dynamic_attribute_test.go +++ b/provider/schema/dynamic_attribute_test.go @@ -422,3 +422,55 @@ func TestDynamicAttributeDynamicValidators(t *testing.T) { }) } } + +func TestDynamicAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.DynamicAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.DynamicAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestDynamicAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.DynamicAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.DynamicAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/schema/float32_attribute.go b/provider/schema/float32_attribute.go index 8e62dc96d..f272539b9 100644 --- a/provider/schema/float32_attribute.go +++ b/provider/schema/float32_attribute.go @@ -188,3 +188,15 @@ func (a Float32Attribute) IsSensitive() bool { func (a Float32Attribute) IsWriteOnly() bool { return false } + +// IsRequiredForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a Float32Attribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a Float32Attribute) IsOptionalForImport() bool { + return false +} diff --git a/provider/schema/float32_attribute_test.go b/provider/schema/float32_attribute_test.go index 70d13db04..2793464d7 100644 --- a/provider/schema/float32_attribute_test.go +++ b/provider/schema/float32_attribute_test.go @@ -422,3 +422,55 @@ func TestFloat32AttributeIsWriteOnly(t *testing.T) { }) } } + +func TestFloat32AttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.Float32Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.Float32Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/schema/float64_attribute.go b/provider/schema/float64_attribute.go index f0c0cc008..336154468 100644 --- a/provider/schema/float64_attribute.go +++ b/provider/schema/float64_attribute.go @@ -188,3 +188,15 @@ func (a Float64Attribute) IsSensitive() bool { func (a Float64Attribute) IsWriteOnly() bool { return false } + +// IsRequiredForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a Float64Attribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a Float64Attribute) IsOptionalForImport() bool { + return false +} diff --git a/provider/schema/float64_attribute_test.go b/provider/schema/float64_attribute_test.go index 8ff796cdb..01db35d43 100644 --- a/provider/schema/float64_attribute_test.go +++ b/provider/schema/float64_attribute_test.go @@ -422,3 +422,55 @@ func TestFloat64AttributeIsWriteOnly(t *testing.T) { }) } } + +func TestFloat64AttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float64Attribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.Float64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat64AttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float64Attribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.Float64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/schema/int32_attribute.go b/provider/schema/int32_attribute.go index 1f8c60c39..3b97a0f37 100644 --- a/provider/schema/int32_attribute.go +++ b/provider/schema/int32_attribute.go @@ -188,3 +188,15 @@ func (a Int32Attribute) IsSensitive() bool { func (a Int32Attribute) IsWriteOnly() bool { return false } + +// IsRequiredForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a Int32Attribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a Int32Attribute) IsOptionalForImport() bool { + return false +} diff --git a/provider/schema/int32_attribute_test.go b/provider/schema/int32_attribute_test.go index b9fa878b7..f93d28ef0 100644 --- a/provider/schema/int32_attribute_test.go +++ b/provider/schema/int32_attribute_test.go @@ -422,3 +422,55 @@ func TestInt32AttributeIsWriteOnly(t *testing.T) { }) } } + +func TestInt32AttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int32Attribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.Int32Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt32AttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int32Attribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.Int32Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/schema/int64_attribute.go b/provider/schema/int64_attribute.go index 25243bbf1..1dc1f85eb 100644 --- a/provider/schema/int64_attribute.go +++ b/provider/schema/int64_attribute.go @@ -188,3 +188,15 @@ func (a Int64Attribute) IsSensitive() bool { func (a Int64Attribute) IsWriteOnly() bool { return false } + +// IsRequiredForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a Int64Attribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a Int64Attribute) IsOptionalForImport() bool { + return false +} diff --git a/provider/schema/int64_attribute_test.go b/provider/schema/int64_attribute_test.go index c5ebec88a..c2bdc7d1d 100644 --- a/provider/schema/int64_attribute_test.go +++ b/provider/schema/int64_attribute_test.go @@ -422,3 +422,55 @@ func TestInt64AttributeIsWriteOnly(t *testing.T) { }) } } + +func TestInt64AttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int64Attribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.Int64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt64AttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int64Attribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.Int64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/schema/list_attribute.go b/provider/schema/list_attribute.go index b85848b58..e94825973 100644 --- a/provider/schema/list_attribute.go +++ b/provider/schema/list_attribute.go @@ -202,6 +202,18 @@ func (a ListAttribute) IsWriteOnly() bool { return false } +// IsRequiredForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a ListAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a ListAttribute) IsOptionalForImport() bool { + return false +} + // ListValidators returns the Validators field value. func (a ListAttribute) ListValidators() []validator.List { return a.Validators diff --git a/provider/schema/list_attribute_test.go b/provider/schema/list_attribute_test.go index d0ab91c58..bb8451841 100644 --- a/provider/schema/list_attribute_test.go +++ b/provider/schema/list_attribute_test.go @@ -518,3 +518,55 @@ func TestListAttributeValidateImplementation(t *testing.T) { }) } } + +func TestListAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.ListAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.ListAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/schema/list_nested_attribute.go b/provider/schema/list_nested_attribute.go index 700299c05..07d6ebd45 100644 --- a/provider/schema/list_nested_attribute.go +++ b/provider/schema/list_nested_attribute.go @@ -230,6 +230,18 @@ func (a ListNestedAttribute) IsWriteOnly() bool { return false } +// IsRequiredForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a ListNestedAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a ListNestedAttribute) IsOptionalForImport() bool { + return false +} + // ListValidators returns the Validators field value. func (a ListNestedAttribute) ListValidators() []validator.List { return a.Validators diff --git a/provider/schema/list_nested_attribute_test.go b/provider/schema/list_nested_attribute_test.go index 1f3715c73..b6130073e 100644 --- a/provider/schema/list_nested_attribute_test.go +++ b/provider/schema/list_nested_attribute_test.go @@ -683,3 +683,55 @@ func TestListNestedAttributeValidateImplementation(t *testing.T) { }) } } + +func TestListNestedAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListNestedAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.ListNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListNestedAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListNestedAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.ListNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/schema/map_attribute.go b/provider/schema/map_attribute.go index 82b5a05d7..a43108d13 100644 --- a/provider/schema/map_attribute.go +++ b/provider/schema/map_attribute.go @@ -205,6 +205,18 @@ func (a MapAttribute) IsWriteOnly() bool { return false } +// IsRequiredForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a MapAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a MapAttribute) IsOptionalForImport() bool { + return false +} + // MapValidators returns the Validators field value. func (a MapAttribute) MapValidators() []validator.Map { return a.Validators diff --git a/provider/schema/map_attribute_test.go b/provider/schema/map_attribute_test.go index f2e1e6a25..bf46b6c5a 100644 --- a/provider/schema/map_attribute_test.go +++ b/provider/schema/map_attribute_test.go @@ -518,3 +518,55 @@ func TestMapAttributeValidateImplementation(t *testing.T) { }) } } + +func TestMapAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.MapAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.MapAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/schema/map_nested_attribute.go b/provider/schema/map_nested_attribute.go index 14fb4092b..d2ee6ece2 100644 --- a/provider/schema/map_nested_attribute.go +++ b/provider/schema/map_nested_attribute.go @@ -229,6 +229,18 @@ func (a MapNestedAttribute) IsWriteOnly() bool { return false } +// IsRequiredForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a MapNestedAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a MapNestedAttribute) IsOptionalForImport() bool { + return false +} + // MapValidators returns the Validators field value. func (a MapNestedAttribute) MapValidators() []validator.Map { return a.Validators diff --git a/provider/schema/map_nested_attribute_test.go b/provider/schema/map_nested_attribute_test.go index bfbdb86c9..26812fc03 100644 --- a/provider/schema/map_nested_attribute_test.go +++ b/provider/schema/map_nested_attribute_test.go @@ -683,3 +683,55 @@ func TestMapNestedAttributeValidateImplementation(t *testing.T) { }) } } + +func TestMapNestedAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapNestedAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.MapNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapNestedAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapNestedAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.MapNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/schema/number_attribute.go b/provider/schema/number_attribute.go index bb6ffc6d6..d8b467cc8 100644 --- a/provider/schema/number_attribute.go +++ b/provider/schema/number_attribute.go @@ -185,6 +185,18 @@ func (a NumberAttribute) IsWriteOnly() bool { return false } +// IsRequiredForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a NumberAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a NumberAttribute) IsOptionalForImport() bool { + return false +} + // NumberValidators returns the Validators field value. func (a NumberAttribute) NumberValidators() []validator.Number { return a.Validators diff --git a/provider/schema/number_attribute_test.go b/provider/schema/number_attribute_test.go index 80dba9dd1..daff176a5 100644 --- a/provider/schema/number_attribute_test.go +++ b/provider/schema/number_attribute_test.go @@ -422,3 +422,55 @@ func TestNumberAttributeNumberValidators(t *testing.T) { }) } } + +func TestNumberAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.NumberAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.NumberAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNumberAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.NumberAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.NumberAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/schema/object_attribute.go b/provider/schema/object_attribute.go index c5c81a1ba..ddd0e1b23 100644 --- a/provider/schema/object_attribute.go +++ b/provider/schema/object_attribute.go @@ -204,6 +204,18 @@ func (a ObjectAttribute) IsWriteOnly() bool { return false } +// IsRequiredForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a ObjectAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a ObjectAttribute) IsOptionalForImport() bool { + return false +} + // ObjectValidators returns the Validators field value. func (a ObjectAttribute) ObjectValidators() []validator.Object { return a.Validators diff --git a/provider/schema/object_attribute_test.go b/provider/schema/object_attribute_test.go index a53bc6bf5..2ae9d0eec 100644 --- a/provider/schema/object_attribute_test.go +++ b/provider/schema/object_attribute_test.go @@ -551,3 +551,55 @@ func TestObjectAttributeValidateImplementation(t *testing.T) { }) } } + +func TestObjectAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ObjectAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.ObjectAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestObjectAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ObjectAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.ObjectAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/schema/set_attribute.go b/provider/schema/set_attribute.go index eaf73344e..87ee73636 100644 --- a/provider/schema/set_attribute.go +++ b/provider/schema/set_attribute.go @@ -200,6 +200,18 @@ func (a SetAttribute) IsWriteOnly() bool { return false } +// IsRequiredForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a SetAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a SetAttribute) IsOptionalForImport() bool { + return false +} + // SetValidators returns the Validators field value. func (a SetAttribute) SetValidators() []validator.Set { return a.Validators diff --git a/provider/schema/set_attribute_test.go b/provider/schema/set_attribute_test.go index bfa58193b..efa665df6 100644 --- a/provider/schema/set_attribute_test.go +++ b/provider/schema/set_attribute_test.go @@ -518,3 +518,55 @@ func TestSetAttributeValidateImplementation(t *testing.T) { }) } } + +func TestSetAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.SetAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.SetAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/schema/set_nested_attribute.go b/provider/schema/set_nested_attribute.go index a9dad64a7..b879b40c1 100644 --- a/provider/schema/set_nested_attribute.go +++ b/provider/schema/set_nested_attribute.go @@ -225,6 +225,18 @@ func (a SetNestedAttribute) IsWriteOnly() bool { return false } +// IsRequiredForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a SetNestedAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a SetNestedAttribute) IsOptionalForImport() bool { + return false +} + // SetValidators returns the Validators field value. func (a SetNestedAttribute) SetValidators() []validator.Set { return a.Validators diff --git a/provider/schema/set_nested_attribute_test.go b/provider/schema/set_nested_attribute_test.go index 3471f42e3..6dd173469 100644 --- a/provider/schema/set_nested_attribute_test.go +++ b/provider/schema/set_nested_attribute_test.go @@ -683,3 +683,55 @@ func TestSetNestedAttributeValidateImplementation(t *testing.T) { }) } } + +func TestSetNestedAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetNestedAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.SetNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetNestedAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetNestedAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.SetNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/schema/single_nested_attribute.go b/provider/schema/single_nested_attribute.go index aded0988e..65f35310b 100644 --- a/provider/schema/single_nested_attribute.go +++ b/provider/schema/single_nested_attribute.go @@ -239,6 +239,18 @@ func (a SingleNestedAttribute) IsWriteOnly() bool { return false } +// IsRequiredForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a SingleNestedAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a SingleNestedAttribute) IsOptionalForImport() bool { + return false +} + // ObjectValidators returns the Validators field value. func (a SingleNestedAttribute) ObjectValidators() []validator.Object { return a.Validators diff --git a/provider/schema/single_nested_attribute_test.go b/provider/schema/single_nested_attribute_test.go index cbb2f3aa6..a674a82a9 100644 --- a/provider/schema/single_nested_attribute_test.go +++ b/provider/schema/single_nested_attribute_test.go @@ -564,3 +564,55 @@ func TestSingleNestedAttributeObjectValidators(t *testing.T) { }) } } + +func TestSingleNestedAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SingleNestedAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.SingleNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSingleNestedAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SingleNestedAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.SingleNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/provider/schema/string_attribute.go b/provider/schema/string_attribute.go index eda7a02c4..db6667f03 100644 --- a/provider/schema/string_attribute.go +++ b/provider/schema/string_attribute.go @@ -181,6 +181,18 @@ func (a StringAttribute) IsWriteOnly() bool { return false } +// IsRequiredForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a StringAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a StringAttribute) IsOptionalForImport() bool { + return false +} + // StringValidators returns the Validators field value. func (a StringAttribute) StringValidators() []validator.String { return a.Validators diff --git a/provider/schema/string_attribute_test.go b/provider/schema/string_attribute_test.go index 35e9f03a8..366f84116 100644 --- a/provider/schema/string_attribute_test.go +++ b/provider/schema/string_attribute_test.go @@ -422,3 +422,55 @@ func TestStringAttributeStringValidators(t *testing.T) { }) } } + +func TestStringAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.StringAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.StringAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestStringAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.StringAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.StringAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} From 3c759ec580f1faef61752dbdc1c9bdc1559adc0b Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Mon, 24 Feb 2025 10:33:46 -0500 Subject: [PATCH 07/20] implement in resource/schema --- resource/schema/bool_attribute.go | 12 +++++ resource/schema/bool_attribute_test.go | 52 +++++++++++++++++++ resource/schema/dynamic_attribute.go | 12 +++++ resource/schema/dynamic_attribute_test.go | 52 +++++++++++++++++++ resource/schema/float32_attribute.go | 12 +++++ resource/schema/float32_attribute_test.go | 52 +++++++++++++++++++ resource/schema/float64_attribute.go | 12 +++++ resource/schema/float64_attribute_test.go | 52 +++++++++++++++++++ resource/schema/int32_attribute.go | 12 +++++ resource/schema/int32_attribute_test.go | 52 +++++++++++++++++++ resource/schema/int64_attribute.go | 12 +++++ resource/schema/int64_attribute_test.go | 52 +++++++++++++++++++ resource/schema/list_attribute.go | 12 +++++ resource/schema/list_attribute_test.go | 52 +++++++++++++++++++ resource/schema/list_nested_attribute.go | 12 +++++ resource/schema/list_nested_attribute_test.go | 52 +++++++++++++++++++ resource/schema/map_attribute.go | 12 +++++ resource/schema/map_attribute_test.go | 52 +++++++++++++++++++ resource/schema/map_nested_attribute.go | 12 +++++ resource/schema/map_nested_attribute_test.go | 52 +++++++++++++++++++ resource/schema/number_attribute.go | 12 +++++ resource/schema/number_attribute_test.go | 52 +++++++++++++++++++ resource/schema/object_attribute.go | 12 +++++ resource/schema/object_attribute_test.go | 52 +++++++++++++++++++ resource/schema/set_attribute.go | 12 +++++ resource/schema/set_attribute_test.go | 52 +++++++++++++++++++ resource/schema/set_nested_attribute.go | 12 +++++ resource/schema/set_nested_attribute_test.go | 52 +++++++++++++++++++ resource/schema/single_nested_attribute.go | 12 +++++ .../schema/single_nested_attribute_test.go | 52 +++++++++++++++++++ resource/schema/string_attribute.go | 12 +++++ resource/schema/string_attribute_test.go | 52 +++++++++++++++++++ 32 files changed, 1024 insertions(+) diff --git a/resource/schema/bool_attribute.go b/resource/schema/bool_attribute.go index fa80f565c..b3d57ee7c 100644 --- a/resource/schema/bool_attribute.go +++ b/resource/schema/bool_attribute.go @@ -244,6 +244,18 @@ func (a BoolAttribute) IsWriteOnly() bool { return a.WriteOnly } +// IsRequiredForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a BoolAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a BoolAttribute) IsOptionalForImport() bool { + return false +} + // ValidateImplementation contains logic for validating the // provider-defined implementation of the attribute to prevent unexpected // errors or panics. This logic runs during the GetProviderSchema RPC and diff --git a/resource/schema/bool_attribute_test.go b/resource/schema/bool_attribute_test.go index 9f2d2e863..e260c76d6 100644 --- a/resource/schema/bool_attribute_test.go +++ b/resource/schema/bool_attribute_test.go @@ -580,3 +580,55 @@ func TestBoolAttributeValidateImplementation(t *testing.T) { }) } } + +func TestBoolAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.BoolAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.BoolAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestBoolAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.BoolAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.BoolAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/resource/schema/dynamic_attribute.go b/resource/schema/dynamic_attribute.go index e06600ab4..72b746379 100644 --- a/resource/schema/dynamic_attribute.go +++ b/resource/schema/dynamic_attribute.go @@ -230,6 +230,18 @@ func (a DynamicAttribute) IsWriteOnly() bool { return a.WriteOnly } +// IsRequiredForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a DynamicAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a DynamicAttribute) IsOptionalForImport() bool { + return false +} + // DynamicDefaultValue returns the Default field value. func (a DynamicAttribute) DynamicDefaultValue() defaults.Dynamic { return a.Default diff --git a/resource/schema/dynamic_attribute_test.go b/resource/schema/dynamic_attribute_test.go index c1340d493..4bacf972d 100644 --- a/resource/schema/dynamic_attribute_test.go +++ b/resource/schema/dynamic_attribute_test.go @@ -580,3 +580,55 @@ func TestDynamicAttributeValidateImplementation(t *testing.T) { }) } } + +func TestDynamicAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.DynamicAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.DynamicAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestDynamicAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.DynamicAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.DynamicAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/resource/schema/float32_attribute.go b/resource/schema/float32_attribute.go index 3064b4ed9..ed7e5fb9b 100644 --- a/resource/schema/float32_attribute.go +++ b/resource/schema/float32_attribute.go @@ -247,6 +247,18 @@ func (a Float32Attribute) IsWriteOnly() bool { return a.WriteOnly } +// IsRequiredForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a Float32Attribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a Float32Attribute) IsOptionalForImport() bool { + return false +} + // ValidateImplementation contains logic for validating the // provider-defined implementation of the attribute to prevent unexpected // errors or panics. This logic runs during the GetProviderSchema RPC and diff --git a/resource/schema/float32_attribute_test.go b/resource/schema/float32_attribute_test.go index 4671b082f..18b8d1541 100644 --- a/resource/schema/float32_attribute_test.go +++ b/resource/schema/float32_attribute_test.go @@ -580,3 +580,55 @@ func TestFloat32AttributeValidateImplementation(t *testing.T) { }) } } + +func TestFloat32AttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.Float32Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float32Attribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.Float32Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/resource/schema/float64_attribute.go b/resource/schema/float64_attribute.go index 205af3f98..947c16954 100644 --- a/resource/schema/float64_attribute.go +++ b/resource/schema/float64_attribute.go @@ -247,6 +247,18 @@ func (a Float64Attribute) IsWriteOnly() bool { return a.WriteOnly } +// IsRequiredForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a Float64Attribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a Float64Attribute) IsOptionalForImport() bool { + return false +} + // ValidateImplementation contains logic for validating the // provider-defined implementation of the attribute to prevent unexpected // errors or panics. This logic runs during the GetProviderSchema RPC and diff --git a/resource/schema/float64_attribute_test.go b/resource/schema/float64_attribute_test.go index 0a42d4770..81d2ea52b 100644 --- a/resource/schema/float64_attribute_test.go +++ b/resource/schema/float64_attribute_test.go @@ -580,3 +580,55 @@ func TestFloat64AttributeValidateImplementation(t *testing.T) { }) } } + +func TestFloat64AttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float64Attribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.Float64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat64AttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Float64Attribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.Float64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/resource/schema/int32_attribute.go b/resource/schema/int32_attribute.go index d3f97d60b..102be0377 100644 --- a/resource/schema/int32_attribute.go +++ b/resource/schema/int32_attribute.go @@ -247,6 +247,18 @@ func (a Int32Attribute) IsWriteOnly() bool { return a.WriteOnly } +// IsRequiredForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a Int32Attribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a Int32Attribute) IsOptionalForImport() bool { + return false +} + // ValidateImplementation contains logic for validating the // provider-defined implementation of the attribute to prevent unexpected // errors or panics. This logic runs during the GetProviderSchema RPC and diff --git a/resource/schema/int32_attribute_test.go b/resource/schema/int32_attribute_test.go index a151c9406..2cd74f5d1 100644 --- a/resource/schema/int32_attribute_test.go +++ b/resource/schema/int32_attribute_test.go @@ -580,3 +580,55 @@ func TestInt32AttributeValidateImplementation(t *testing.T) { }) } } + +func TestInt32AttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int32Attribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.Int32Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt32AttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int32Attribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.Int32Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/resource/schema/int64_attribute.go b/resource/schema/int64_attribute.go index c65eb41fa..3455b7a52 100644 --- a/resource/schema/int64_attribute.go +++ b/resource/schema/int64_attribute.go @@ -247,6 +247,18 @@ func (a Int64Attribute) IsWriteOnly() bool { return a.WriteOnly } +// IsRequiredForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a Int64Attribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a Int64Attribute) IsOptionalForImport() bool { + return false +} + // ValidateImplementation contains logic for validating the // provider-defined implementation of the attribute to prevent unexpected // errors or panics. This logic runs during the GetProviderSchema RPC and diff --git a/resource/schema/int64_attribute_test.go b/resource/schema/int64_attribute_test.go index 1ce11e722..36b29096e 100644 --- a/resource/schema/int64_attribute_test.go +++ b/resource/schema/int64_attribute_test.go @@ -580,3 +580,55 @@ func TestInt64AttributeValidateImplementation(t *testing.T) { }) } } + +func TestInt64AttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int64Attribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.Int64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt64AttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.Int64Attribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.Int64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/resource/schema/list_attribute.go b/resource/schema/list_attribute.go index 9c1536dbe..85dbc9a34 100644 --- a/resource/schema/list_attribute.go +++ b/resource/schema/list_attribute.go @@ -247,6 +247,18 @@ func (a ListAttribute) IsWriteOnly() bool { return a.WriteOnly } +// IsRequiredForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a ListAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a ListAttribute) IsOptionalForImport() bool { + return false +} + // ListDefaultValue returns the Default field value. func (a ListAttribute) ListDefaultValue() defaults.List { return a.Default diff --git a/resource/schema/list_attribute_test.go b/resource/schema/list_attribute_test.go index 1be15bbb7..788ee84fe 100644 --- a/resource/schema/list_attribute_test.go +++ b/resource/schema/list_attribute_test.go @@ -732,3 +732,55 @@ func TestListAttributeValidateImplementation(t *testing.T) { }) } } + +func TestListAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.ListAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.ListAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/resource/schema/list_nested_attribute.go b/resource/schema/list_nested_attribute.go index ee1845bb5..74c739e90 100644 --- a/resource/schema/list_nested_attribute.go +++ b/resource/schema/list_nested_attribute.go @@ -278,6 +278,18 @@ func (a ListNestedAttribute) IsWriteOnly() bool { return a.WriteOnly } +// IsRequiredForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a ListNestedAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a ListNestedAttribute) IsOptionalForImport() bool { + return false +} + // ListDefaultValue returns the Default field value. func (a ListNestedAttribute) ListDefaultValue() defaults.List { return a.Default diff --git a/resource/schema/list_nested_attribute_test.go b/resource/schema/list_nested_attribute_test.go index 258d7cf44..e1eaa3683 100644 --- a/resource/schema/list_nested_attribute_test.go +++ b/resource/schema/list_nested_attribute_test.go @@ -1016,3 +1016,55 @@ func TestListNestedAttributeValidateImplementation(t *testing.T) { }) } } + +func TestListNestedAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListNestedAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.ListNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListNestedAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ListNestedAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.ListNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/resource/schema/map_attribute.go b/resource/schema/map_attribute.go index f76a295c3..f045a6c8a 100644 --- a/resource/schema/map_attribute.go +++ b/resource/schema/map_attribute.go @@ -250,6 +250,18 @@ func (a MapAttribute) IsWriteOnly() bool { return a.WriteOnly } +// IsRequiredForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a MapAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a MapAttribute) IsOptionalForImport() bool { + return false +} + // MapDefaultValue returns the Default field value. func (a MapAttribute) MapDefaultValue() defaults.Map { return a.Default diff --git a/resource/schema/map_attribute_test.go b/resource/schema/map_attribute_test.go index b62293350..ce5bfb7da 100644 --- a/resource/schema/map_attribute_test.go +++ b/resource/schema/map_attribute_test.go @@ -731,3 +731,55 @@ func TestMapAttributeValidateImplementation(t *testing.T) { }) } } + +func TestMapAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.MapAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.MapAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/resource/schema/map_nested_attribute.go b/resource/schema/map_nested_attribute.go index db868f726..717140e64 100644 --- a/resource/schema/map_nested_attribute.go +++ b/resource/schema/map_nested_attribute.go @@ -278,6 +278,18 @@ func (a MapNestedAttribute) IsWriteOnly() bool { return a.WriteOnly } +// IsRequiredForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a MapNestedAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a MapNestedAttribute) IsOptionalForImport() bool { + return false +} + // MapDefaultValue returns the Default field value. func (a MapNestedAttribute) MapDefaultValue() defaults.Map { return a.Default diff --git a/resource/schema/map_nested_attribute_test.go b/resource/schema/map_nested_attribute_test.go index 6f360d85b..94d5ff593 100644 --- a/resource/schema/map_nested_attribute_test.go +++ b/resource/schema/map_nested_attribute_test.go @@ -1016,3 +1016,55 @@ func TestMapNestedAttributeValidateImplementation(t *testing.T) { }) } } + +func TestMapNestedAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapNestedAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.MapNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestMapNestedAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.MapNestedAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.MapNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/resource/schema/number_attribute.go b/resource/schema/number_attribute.go index 8f367592e..910b2f5a8 100644 --- a/resource/schema/number_attribute.go +++ b/resource/schema/number_attribute.go @@ -233,6 +233,18 @@ func (a NumberAttribute) IsWriteOnly() bool { return a.WriteOnly } +// IsRequiredForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a NumberAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a NumberAttribute) IsOptionalForImport() bool { + return false +} + // NumberDefaultValue returns the Default field value. func (a NumberAttribute) NumberDefaultValue() defaults.Number { return a.Default diff --git a/resource/schema/number_attribute_test.go b/resource/schema/number_attribute_test.go index 537bd1bfc..027324224 100644 --- a/resource/schema/number_attribute_test.go +++ b/resource/schema/number_attribute_test.go @@ -585,3 +585,55 @@ func TestNumberAttributeValidateImplementation(t *testing.T) { }) } } + +func TestNumberAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.NumberAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.NumberAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNumberAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.NumberAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.NumberAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/resource/schema/object_attribute.go b/resource/schema/object_attribute.go index 03f35aa00..896b2921d 100644 --- a/resource/schema/object_attribute.go +++ b/resource/schema/object_attribute.go @@ -249,6 +249,18 @@ func (a ObjectAttribute) IsWriteOnly() bool { return a.WriteOnly } +// IsRequiredForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a ObjectAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a ObjectAttribute) IsOptionalForImport() bool { + return false +} + // ObjectDefaultValue returns the Default field value. func (a ObjectAttribute) ObjectDefaultValue() defaults.Object { return a.Default diff --git a/resource/schema/object_attribute_test.go b/resource/schema/object_attribute_test.go index 908c48395..fd6df413b 100644 --- a/resource/schema/object_attribute_test.go +++ b/resource/schema/object_attribute_test.go @@ -785,3 +785,55 @@ func TestObjectAttributeValidateImplementation(t *testing.T) { }) } } + +func TestObjectAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ObjectAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.ObjectAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestObjectAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.ObjectAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.ObjectAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/resource/schema/set_attribute.go b/resource/schema/set_attribute.go index b65656dac..f33fd3df4 100644 --- a/resource/schema/set_attribute.go +++ b/resource/schema/set_attribute.go @@ -235,6 +235,18 @@ func (a SetAttribute) IsWriteOnly() bool { return false } +// IsRequiredForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a SetAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a SetAttribute) IsOptionalForImport() bool { + return false +} + // SetDefaultValue returns the Default field value. func (a SetAttribute) SetDefaultValue() defaults.Set { return a.Default diff --git a/resource/schema/set_attribute_test.go b/resource/schema/set_attribute_test.go index 2795fbde8..268cb20b3 100644 --- a/resource/schema/set_attribute_test.go +++ b/resource/schema/set_attribute_test.go @@ -715,3 +715,55 @@ func TestSetAttributeValidateImplementation(t *testing.T) { }) } } + +func TestSetAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.SetAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.SetAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/resource/schema/set_nested_attribute.go b/resource/schema/set_nested_attribute.go index 5d408cf1a..0d6eef595 100644 --- a/resource/schema/set_nested_attribute.go +++ b/resource/schema/set_nested_attribute.go @@ -260,6 +260,18 @@ func (a SetNestedAttribute) IsWriteOnly() bool { return false } +// IsRequiredForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a SetNestedAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a SetNestedAttribute) IsOptionalForImport() bool { + return false +} + // SetDefaultValue returns the Default field value. func (a SetNestedAttribute) SetDefaultValue() defaults.Set { return a.Default diff --git a/resource/schema/set_nested_attribute_test.go b/resource/schema/set_nested_attribute_test.go index a0210f410..406297a92 100644 --- a/resource/schema/set_nested_attribute_test.go +++ b/resource/schema/set_nested_attribute_test.go @@ -983,3 +983,55 @@ func TestSetNestedAttributeValidateImplementation(t *testing.T) { }) } } + +func TestSetNestedAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetNestedAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.SetNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSetNestedAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SetNestedAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.SetNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/resource/schema/single_nested_attribute.go b/resource/schema/single_nested_attribute.go index 12cab9800..4ad79ffe1 100644 --- a/resource/schema/single_nested_attribute.go +++ b/resource/schema/single_nested_attribute.go @@ -286,6 +286,18 @@ func (a SingleNestedAttribute) IsWriteOnly() bool { return a.WriteOnly } +// IsRequiredForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a SingleNestedAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a SingleNestedAttribute) IsOptionalForImport() bool { + return false +} + // ObjectDefaultValue returns the Default field value. func (a SingleNestedAttribute) ObjectDefaultValue() defaults.Object { return a.Default diff --git a/resource/schema/single_nested_attribute_test.go b/resource/schema/single_nested_attribute_test.go index b03725b76..259102bfc 100644 --- a/resource/schema/single_nested_attribute_test.go +++ b/resource/schema/single_nested_attribute_test.go @@ -918,3 +918,55 @@ func TestSingleNestedAttributeValidateImplementation(t *testing.T) { }) } } + +func TestSingleNestedAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SingleNestedAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.SingleNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSingleNestedAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.SingleNestedAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.SingleNestedAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/resource/schema/string_attribute.go b/resource/schema/string_attribute.go index 693327016..50aad8a0c 100644 --- a/resource/schema/string_attribute.go +++ b/resource/schema/string_attribute.go @@ -229,6 +229,18 @@ func (a StringAttribute) IsWriteOnly() bool { return a.WriteOnly } +// IsRequiredForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a StringAttribute) IsRequiredForImport() bool { + return false +} + +// IsOptionalForImport returns false as this behavior is only revelant +// for managed resource identity schema attributes. +func (a StringAttribute) IsOptionalForImport() bool { + return false +} + // StringDefaultValue returns the Default field value. func (a StringAttribute) StringDefaultValue() defaults.String { return a.Default diff --git a/resource/schema/string_attribute_test.go b/resource/schema/string_attribute_test.go index 6bf70eda4..c1dffa2f2 100644 --- a/resource/schema/string_attribute_test.go +++ b/resource/schema/string_attribute_test.go @@ -580,3 +580,55 @@ func TestStringAttributeValidateImplementation(t *testing.T) { }) } } + +func TestStringAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.StringAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: schema.StringAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestStringAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute schema.StringAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: schema.StringAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} From 167b8244f34c643151298c50ad7a2be845d237d6 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Mon, 24 Feb 2025 11:39:51 -0500 Subject: [PATCH 08/20] add to final internal/testing/testschema --- internal/testing/testschema/attribute.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/internal/testing/testschema/attribute.go b/internal/testing/testschema/attribute.go index 979db68eb..3f4e7470a 100644 --- a/internal/testing/testschema/attribute.go +++ b/internal/testing/testschema/attribute.go @@ -21,6 +21,8 @@ type Attribute struct { Required bool Sensitive bool WriteOnly bool + RequiredForImport bool + OptionalForImport bool Type attr.Type } @@ -84,3 +86,13 @@ func (a Attribute) IsSensitive() bool { func (a Attribute) IsWriteOnly() bool { return a.WriteOnly } + +// IsRequiredForImport satisfies the fwschema.Attribute interface. +func (a Attribute) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport satisfies the fwschema.Attribute interface. +func (a Attribute) IsOptionalForImport() bool { + return a.OptionalForImport +} From 77ef5798e5ba9d31a7d1d4b2c43fbc4bdbf677be Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Thu, 6 Mar 2025 17:26:28 -0500 Subject: [PATCH 09/20] get resource identity schema implementation --- .../fromproto5/getresourceidentityschemas.go | 23 + .../getresourceidentityschemas_test.go | 44 + internal/fwserver/server.go | 45 + .../server_getresourceidentityschemas.go | 34 + .../server_getresourceidentityschemas_test.go | 229 +++++ .../server_getresourceidentityschemas.go | 27 + .../server_getresourceidentityschemas_test.go | 218 +++++ .../server_upgraderesourceidentity.go | 15 + .../testprovider/resourcewithidentity.go | 30 + .../toproto5/getresourceidentityschemas.go | 42 + .../getresourceidentityschemas_test.go | 367 +++++++ internal/toproto5/identity_schema.go | 52 + .../toproto5/identity_schema_attribute.go | 43 + .../identity_schema_attribute_test.go | 257 +++++ internal/toproto5/identity_schema_test.go | 138 +++ resource/identity_schema.go | 27 + resource/identityschema/attribute.go | 33 + resource/identityschema/bool_attribute.go | 127 +++ .../identityschema/bool_attribute_test.go | 431 +++++++++ resource/identityschema/doc.go | 11 + resource/identityschema/float32_attribute.go | 130 +++ .../identityschema/float32_attribute_test.go | 431 +++++++++ resource/identityschema/float64_attribute.go | 130 +++ .../identityschema/float64_attribute_test.go | 431 +++++++++ resource/identityschema/int32_attribute.go | 130 +++ .../identityschema/int32_attribute_test.go | 431 +++++++++ resource/identityschema/int64_attribute.go | 130 +++ .../identityschema/int64_attribute_test.go | 431 +++++++++ resource/identityschema/list_attribute.go | 173 ++++ .../identityschema/list_attribute_test.go | 520 ++++++++++ resource/identityschema/number_attribute.go | 131 +++ .../identityschema/number_attribute_test.go | 431 +++++++++ resource/identityschema/schema.go | 147 +++ resource/identityschema/schema_test.go | 915 ++++++++++++++++++ resource/identityschema/string_attribute.go | 127 +++ .../identityschema/string_attribute_test.go | 431 +++++++++ resource/resource.go | 10 + 37 files changed, 7322 insertions(+) create mode 100644 internal/fromproto5/getresourceidentityschemas.go create mode 100644 internal/fromproto5/getresourceidentityschemas_test.go create mode 100644 internal/fwserver/server_getresourceidentityschemas.go create mode 100644 internal/fwserver/server_getresourceidentityschemas_test.go create mode 100644 internal/proto5server/server_getresourceidentityschemas.go create mode 100644 internal/proto5server/server_getresourceidentityschemas_test.go create mode 100644 internal/proto5server/server_upgraderesourceidentity.go create mode 100644 internal/testing/testprovider/resourcewithidentity.go create mode 100644 internal/toproto5/getresourceidentityschemas.go create mode 100644 internal/toproto5/getresourceidentityschemas_test.go create mode 100644 internal/toproto5/identity_schema.go create mode 100644 internal/toproto5/identity_schema_attribute.go create mode 100644 internal/toproto5/identity_schema_attribute_test.go create mode 100644 internal/toproto5/identity_schema_test.go create mode 100644 resource/identity_schema.go create mode 100644 resource/identityschema/attribute.go create mode 100644 resource/identityschema/bool_attribute.go create mode 100644 resource/identityschema/bool_attribute_test.go create mode 100644 resource/identityschema/doc.go create mode 100644 resource/identityschema/float32_attribute.go create mode 100644 resource/identityschema/float32_attribute_test.go create mode 100644 resource/identityschema/float64_attribute.go create mode 100644 resource/identityschema/float64_attribute_test.go create mode 100644 resource/identityschema/int32_attribute.go create mode 100644 resource/identityschema/int32_attribute_test.go create mode 100644 resource/identityschema/int64_attribute.go create mode 100644 resource/identityschema/int64_attribute_test.go create mode 100644 resource/identityschema/list_attribute.go create mode 100644 resource/identityschema/list_attribute_test.go create mode 100644 resource/identityschema/number_attribute.go create mode 100644 resource/identityschema/number_attribute_test.go create mode 100644 resource/identityschema/schema.go create mode 100644 resource/identityschema/schema_test.go create mode 100644 resource/identityschema/string_attribute.go create mode 100644 resource/identityschema/string_attribute_test.go diff --git a/internal/fromproto5/getresourceidentityschemas.go b/internal/fromproto5/getresourceidentityschemas.go new file mode 100644 index 000000000..a5ca3b16c --- /dev/null +++ b/internal/fromproto5/getresourceidentityschemas.go @@ -0,0 +1,23 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fromproto5 + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" +) + +// GetResourceIdentitySchemasRequest returns the *fwserver.GetResourceIdentitySchemasRequest +// equivalent of a *tfprotov5.GetResourceIdentitySchemasRequest. +func GetResourceIdentitySchemasRequest(ctx context.Context, proto5 *tfprotov5.GetResourceIdentitySchemasRequest) *fwserver.GetResourceIdentitySchemasRequest { + if proto5 == nil { + return nil + } + + fw := &fwserver.GetResourceIdentitySchemasRequest{} + + return fw +} diff --git a/internal/fromproto5/getresourceidentityschemas_test.go b/internal/fromproto5/getresourceidentityschemas_test.go new file mode 100644 index 000000000..e3143c24d --- /dev/null +++ b/internal/fromproto5/getresourceidentityschemas_test.go @@ -0,0 +1,44 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fromproto5_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/internal/fromproto5" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" +) + +func TestGetResourceIdentitySchemasRequest(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + input *tfprotov5.GetResourceIdentitySchemasRequest + expected *fwserver.GetResourceIdentitySchemasRequest + }{ + "nil": { + input: nil, + expected: nil, + }, + "empty": { + input: &tfprotov5.GetResourceIdentitySchemasRequest{}, + expected: &fwserver.GetResourceIdentitySchemasRequest{}, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := fromproto5.GetResourceIdentitySchemasRequest(context.Background(), testCase.input) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/internal/fwserver/server.go b/internal/fwserver/server.go index b6f2bc85e..22723f9ff 100644 --- a/internal/fwserver/server.go +++ b/internal/fwserver/server.go @@ -689,3 +689,48 @@ func (s *Server) ResourceSchemas(ctx context.Context) (map[string]fwschema.Schem return resourceSchemas, diags } + +// ResourceIdentitySchemas returns a map of Resource Identity Schemas for the +// GetResourceIdentitySchemas RPC without caching since not all schemas are guaranteed to +// be necessary for later provider operations. The schema implementations are +// also validated. +func (s *Server) ResourceIdentitySchemas(ctx context.Context) (map[string]fwschema.Schema, diag.Diagnostics) { + resourceIdentitySchemas := make(map[string]fwschema.Schema) + + resourceFuncs, diags := s.ResourceFuncs(ctx) + + for typeName, resourceFunc := range resourceFuncs { + r := resourceFunc() + + rWithIdentity, ok := r.(resource.ResourceWithIdentity) + if !ok { + // Resource identity support is optional, so we can skip resources that don't implement it. + continue + } + + identitySchemaReq := resource.IdentitySchemaRequest{} + identitySchemaResp := resource.IdentitySchemaResponse{} + + logging.FrameworkTrace(ctx, "Calling provider defined Resource IdentitySchema method", map[string]interface{}{logging.KeyResourceType: typeName}) + rWithIdentity.IdentitySchema(ctx, identitySchemaReq, &identitySchemaResp) + logging.FrameworkTrace(ctx, "Called provider defined Resource IdentitySchema method", map[string]interface{}{logging.KeyResourceType: typeName}) + + diags.Append(identitySchemaResp.Diagnostics...) + + if identitySchemaResp.Diagnostics.HasError() { + continue + } + + validateDiags := identitySchemaResp.IdentitySchema.ValidateImplementation(ctx) + + diags.Append(validateDiags...) + + if validateDiags.HasError() { + continue + } + + resourceIdentitySchemas[typeName] = identitySchemaResp.IdentitySchema + } + + return resourceIdentitySchemas, diags +} diff --git a/internal/fwserver/server_getresourceidentityschemas.go b/internal/fwserver/server_getresourceidentityschemas.go new file mode 100644 index 000000000..511c6bb05 --- /dev/null +++ b/internal/fwserver/server_getresourceidentityschemas.go @@ -0,0 +1,34 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fwserver + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" +) + +// GetResourceIdentitySchemasRequest is the framework server request for the +// GetResourceIdentitySchemas RPC. +type GetResourceIdentitySchemasRequest struct{} + +// GetResourceIdentitySchemasResponse is the framework server response for the +// GetResourceIdentitySchemas RPC. +type GetResourceIdentitySchemasResponse struct { + IdentitySchemas map[string]fwschema.Schema + Diagnostics diag.Diagnostics +} + +// GetResourceIdentitySchemas implements the framework server GetResourceIdentitySchemas RPC. +func (s *Server) GetResourceIdentitySchemas(ctx context.Context, req *GetResourceIdentitySchemasRequest, resp *GetResourceIdentitySchemasResponse) { + identitySchemas, diags := s.ResourceIdentitySchemas(ctx) + resp.Diagnostics.Append(diags...) + + if resp.Diagnostics.HasError() { + return + } + + resp.IdentitySchemas = identitySchemas +} diff --git a/internal/fwserver/server_getresourceidentityschemas_test.go b/internal/fwserver/server_getresourceidentityschemas_test.go new file mode 100644 index 000000000..c780a7ff1 --- /dev/null +++ b/internal/fwserver/server_getresourceidentityschemas_test.go @@ -0,0 +1,229 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fwserver_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" +) + +func TestServerGetResourceIdentitySchemas(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + server *fwserver.Server + request *fwserver.GetResourceIdentitySchemasRequest + expectedResponse *fwserver.GetResourceIdentitySchemasResponse + }{ + "empty-provider": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + expectedResponse: &fwserver.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]fwschema.Schema{}, + }, + }, + "resource-no-identity-schemas": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{ + ResourcesMethod: func(_ context.Context) []func() resource.Resource { + return []func() resource.Resource{ + func() resource.Resource { + return &testprovider.Resource{ + MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "test_resource1" + }, + } + }, + func() resource.Resource { + return &testprovider.Resource{ + MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "test_resource2" + }, + } + }, + } + }, + }, + }, + request: &fwserver.GetResourceIdentitySchemasRequest{}, + expectedResponse: &fwserver.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]fwschema.Schema{}, + }, + }, + "resource-identity-schemas": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{ + ResourcesMethod: func(_ context.Context) []func() resource.Resource { + return []func() resource.Resource{ + func() resource.Resource { + return &testprovider.Resource{ + MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "test_resource1" + }, + } + }, + func() resource.Resource { + return &testprovider.ResourceWithIdentity{ + Resource: &testprovider.Resource{ + MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "test_resource2" + }, + }, + IdentitySchemaMethod: func(ctx context.Context, req resource.IdentitySchemaRequest, resp *resource.IdentitySchemaResponse) { + resp.IdentitySchema = identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test2": identityschema.StringAttribute{ + RequiredForImport: true, + }, + }, + } + }, + } + }, + func() resource.Resource { + return &testprovider.Resource{ + MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "test_resource3" + }, + } + }, + func() resource.Resource { + return &testprovider.ResourceWithIdentity{ + Resource: &testprovider.Resource{ + MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "test_resource4" + }, + }, + IdentitySchemaMethod: func(ctx context.Context, req resource.IdentitySchemaRequest, resp *resource.IdentitySchemaResponse) { + resp.IdentitySchema = identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test4": identityschema.BoolAttribute{ + RequiredForImport: true, + }, + }, + } + }, + } + }, + } + }, + }, + }, + request: &fwserver.GetResourceIdentitySchemasRequest{}, + expectedResponse: &fwserver.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]fwschema.Schema{ + "test_resource2": identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test2": identityschema.StringAttribute{ + RequiredForImport: true, + }, + }, + }, + "test_resource4": identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test4": identityschema.BoolAttribute{ + RequiredForImport: true, + }, + }, + }, + }, + }, + }, + "resource-identity-schemas-invalid-attribute-name": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{ + ResourcesMethod: func(_ context.Context) []func() resource.Resource { + return []func() resource.Resource{ + func() resource.Resource { + return &testprovider.Resource{ + MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "test_resource1" + }, + } + }, + func() resource.Resource { + return &testprovider.ResourceWithIdentity{ + Resource: &testprovider.Resource{ + MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "test_resource2" + }, + }, + IdentitySchemaMethod: func(ctx context.Context, req resource.IdentitySchemaRequest, resp *resource.IdentitySchemaResponse) { + resp.IdentitySchema = identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "$": identityschema.StringAttribute{ + RequiredForImport: true, + }, + }, + } + }, + } + }, + func() resource.Resource { + return &testprovider.Resource{ + MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "test_resource3" + }, + } + }, + func() resource.Resource { + return &testprovider.ResourceWithIdentity{ + Resource: &testprovider.Resource{ + MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "test_resource4" + }, + }, + IdentitySchemaMethod: func(ctx context.Context, req resource.IdentitySchemaRequest, resp *resource.IdentitySchemaResponse) { + resp.IdentitySchema = identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test4": identityschema.BoolAttribute{ + RequiredForImport: true, + }, + }, + } + }, + } + }, + } + }, + }, + }, + request: &fwserver.GetResourceIdentitySchemasRequest{}, + expectedResponse: &fwserver.GetResourceIdentitySchemasResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Attribute/Block Name", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"$\" at schema path \"$\" is an invalid attribute/block name. "+ + "Names must only contain lowercase alphanumeric characters (a-z, 0-9) and underscores (_).", + ), + }, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + response := &fwserver.GetResourceIdentitySchemasResponse{} + testCase.server.GetResourceIdentitySchemas(context.Background(), testCase.request, response) + + if diff := cmp.Diff(response, testCase.expectedResponse); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/internal/proto5server/server_getresourceidentityschemas.go b/internal/proto5server/server_getresourceidentityschemas.go new file mode 100644 index 000000000..97ecb47ea --- /dev/null +++ b/internal/proto5server/server_getresourceidentityschemas.go @@ -0,0 +1,27 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package proto5server + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/internal/fromproto5" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/logging" + "github.com/hashicorp/terraform-plugin-framework/internal/toproto5" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" +) + +// GetResourceIdentitySchemas satisfies the tfprotov5.ProviderServer interface. +func (s *Server) GetResourceIdentitySchemas(ctx context.Context, proto5Req *tfprotov5.GetResourceIdentitySchemasRequest) (*tfprotov5.GetResourceIdentitySchemasResponse, error) { + ctx = s.registerContext(ctx) + ctx = logging.InitContext(ctx) + + fwReq := fromproto5.GetResourceIdentitySchemasRequest(ctx, proto5Req) + fwResp := &fwserver.GetResourceIdentitySchemasResponse{} + + s.FrameworkServer.GetResourceIdentitySchemas(ctx, fwReq, fwResp) + + return toproto5.GetResourceIdentitySchemasResponse(ctx, fwResp), nil +} diff --git a/internal/proto5server/server_getresourceidentityschemas_test.go b/internal/proto5server/server_getresourceidentityschemas_test.go new file mode 100644 index 000000000..e054e2e9b --- /dev/null +++ b/internal/proto5server/server_getresourceidentityschemas_test.go @@ -0,0 +1,218 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package proto5server + +import ( + "bytes" + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/logging" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-log/tfsdklogtest" +) + +func TestServerGetResourceIdentitySchemas(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + server *Server + request *tfprotov5.GetResourceIdentitySchemasRequest + expectedError error + expectedResponse *tfprotov5.GetResourceIdentitySchemasResponse + }{ + "resource-identity-schemas": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + ResourcesMethod: func(_ context.Context) []func() resource.Resource { + return []func() resource.Resource{ + func() resource.Resource { + return &testprovider.ResourceWithIdentity{ + Resource: &testprovider.Resource{ + MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "test_resource1" + }, + }, + IdentitySchemaMethod: func(_ context.Context, _ resource.IdentitySchemaRequest, resp *resource.IdentitySchemaResponse) { + resp.IdentitySchema = identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test1": identityschema.StringAttribute{ + RequiredForImport: true, + }, + }, + } + }, + } + }, + func() resource.Resource { + return &testprovider.ResourceWithIdentity{ + Resource: &testprovider.Resource{ + MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "test_resource2" + }, + }, + IdentitySchemaMethod: func(_ context.Context, _ resource.IdentitySchemaRequest, resp *resource.IdentitySchemaResponse) { + resp.IdentitySchema = identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test2": identityschema.BoolAttribute{ + RequiredForImport: true, + }, + }, + } + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.GetResourceIdentitySchemasRequest{}, + expectedResponse: &tfprotov5.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]*tfprotov5.ResourceIdentitySchema{ + "test_resource1": { + IdentityAttributes: []*tfprotov5.ResourceIdentitySchemaAttribute{ + { + Name: "test1", + RequiredForImport: true, + Type: tftypes.String, + }, + }, + }, + "test_resource2": { + IdentityAttributes: []*tfprotov5.ResourceIdentitySchemaAttribute{ + { + Name: "test2", + RequiredForImport: true, + Type: tftypes.Bool, + }, + }, + }, + }, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.server.GetResourceIdentitySchemas(context.Background(), new(tfprotov5.GetResourceIdentitySchemasRequest)) + + if diff := cmp.Diff(testCase.expectedError, err); diff != "" { + t.Errorf("unexpected error difference: %s", diff) + } + + if diff := cmp.Diff(testCase.expectedResponse, got); diff != "" { + t.Errorf("unexpected response difference: %s", diff) + } + }) + } +} + +func TestServerGetResourceIdentitySchemas_logging(t *testing.T) { + t.Parallel() + + var output bytes.Buffer + + ctx := tfsdklogtest.RootLogger(context.Background(), &output) + ctx = logging.InitContext(ctx) + + testServer := &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + ResourcesMethod: func(ctx context.Context) []func() resource.Resource { + return []func() resource.Resource{ + func() resource.Resource { + return &testprovider.ResourceWithIdentity{ + Resource: &testprovider.Resource{ + MetadataMethod: func(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "examplecloud_thing" + }, + }, + IdentitySchemaMethod: func(ctx context.Context, req resource.IdentitySchemaRequest, resp *resource.IdentitySchemaResponse) { + resp.IdentitySchema = identityschema.Schema{} + }, + } + }, + } + }, + }, + }, + } + + _, err := testServer.GetResourceIdentitySchemas(ctx, new(tfprotov5.GetResourceIdentitySchemasRequest)) + + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + entries, err := tfsdklogtest.MultilineJSONDecode(&output) + + if err != nil { + t.Fatalf("unable to read multiple line JSON: %s", err) + } + + expectedEntries := []map[string]interface{}{ + { + "@level": "trace", + "@message": "Checking ResourceTypes lock", + "@module": "sdk.framework", + }, + { + "@level": "trace", + "@message": "Checking ProviderTypeName lock", + "@module": "sdk.framework", + }, + { + "@level": "trace", + "@message": "Calling provider defined Provider Metadata", + "@module": "sdk.framework", + }, + { + "@level": "trace", + "@message": "Called provider defined Provider Metadata", + "@module": "sdk.framework", + }, + { + "@level": "trace", + "@message": "Calling provider defined Provider Resources", + "@module": "sdk.framework", + }, + { + "@level": "trace", + "@message": "Called provider defined Provider Resources", + "@module": "sdk.framework", + }, + { + "@level": "trace", + "@message": "Found resource type", + "@module": "sdk.framework", + "tf_resource_type": "examplecloud_thing", + }, + { + "@level": "trace", + "@message": "Calling provider defined Resource IdentitySchema method", + "@module": "sdk.framework", + "tf_resource_type": "examplecloud_thing", + }, + { + "@level": "trace", + "@message": "Called provider defined Resource IdentitySchema method", + "@module": "sdk.framework", + "tf_resource_type": "examplecloud_thing", + }, + } + + if diff := cmp.Diff(entries, expectedEntries); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } +} diff --git a/internal/proto5server/server_upgraderesourceidentity.go b/internal/proto5server/server_upgraderesourceidentity.go new file mode 100644 index 000000000..bbd9a0a11 --- /dev/null +++ b/internal/proto5server/server_upgraderesourceidentity.go @@ -0,0 +1,15 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package proto5server + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-go/tfprotov5" +) + +// UpgradeResourceIdentity satisfies the tfprotov5.ProviderServer interface. +func (s *Server) UpgradeResourceIdentity(ctx context.Context, proto5Req *tfprotov5.UpgradeResourceIdentityRequest) (*tfprotov5.UpgradeResourceIdentityResponse, error) { + panic("unimplemented") // TODO: implement +} diff --git a/internal/testing/testprovider/resourcewithidentity.go b/internal/testing/testprovider/resourcewithidentity.go new file mode 100644 index 000000000..514b979d0 --- /dev/null +++ b/internal/testing/testprovider/resourcewithidentity.go @@ -0,0 +1,30 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package testprovider + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/resource" +) + +var _ resource.Resource = &ResourceWithIdentity{} +var _ resource.ResourceWithIdentity = &ResourceWithIdentity{} + +// Declarative resource.ResourceWithIdentity for unit testing. +type ResourceWithIdentity struct { + *Resource + + // ResourceWithIdentity interface methods + IdentitySchemaMethod func(context.Context, resource.IdentitySchemaRequest, *resource.IdentitySchemaResponse) +} + +// IdentitySchema implements resource.ResourceWithIdentity. +func (p *ResourceWithIdentity) IdentitySchema(ctx context.Context, req resource.IdentitySchemaRequest, resp *resource.IdentitySchemaResponse) { + if p.IdentitySchemaMethod == nil { + return + } + + p.IdentitySchemaMethod(ctx, req, resp) +} diff --git a/internal/toproto5/getresourceidentityschemas.go b/internal/toproto5/getresourceidentityschemas.go new file mode 100644 index 000000000..4080a1710 --- /dev/null +++ b/internal/toproto5/getresourceidentityschemas.go @@ -0,0 +1,42 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto5 + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" +) + +// GetResourceIdentitySchemasResponse returns the *tfprotov5.GetResourceIdentitySchemasResponse +// equivalent of a *fwserver.GetResourceIdentitySchemasResponse. +func GetResourceIdentitySchemasResponse(ctx context.Context, fw *fwserver.GetResourceIdentitySchemasResponse) *tfprotov5.GetResourceIdentitySchemasResponse { + if fw == nil { + return nil + } + + protov5 := &tfprotov5.GetResourceIdentitySchemasResponse{ + Diagnostics: Diagnostics(ctx, fw.Diagnostics), + IdentitySchemas: make(map[string]*tfprotov5.ResourceIdentitySchema, len(fw.IdentitySchemas)), + } + + var err error + + for resourceType, identitySchema := range fw.IdentitySchemas { + protov5.IdentitySchemas[resourceType], err = IdentitySchema(ctx, identitySchema) + + if err != nil { + protov5.Diagnostics = append(protov5.Diagnostics, &tfprotov5.Diagnostic{ + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "Error converting resource identity schema", + Detail: "The identity schema for the resource \"" + resourceType + "\" couldn't be converted into a usable type. This is always a problem with the provider. Please report the following to the provider developer:\n\n" + err.Error(), + }) + + return protov5 + } + } + + return protov5 +} diff --git a/internal/toproto5/getresourceidentityschemas_test.go b/internal/toproto5/getresourceidentityschemas_test.go new file mode 100644 index 000000000..0dff87452 --- /dev/null +++ b/internal/toproto5/getresourceidentityschemas_test.go @@ -0,0 +1,367 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto5_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/toproto5" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestGetResourceIdentitySchemasResponse(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + input *fwserver.GetResourceIdentitySchemasResponse + expected *tfprotov5.GetResourceIdentitySchemasResponse + }{ + "nil": { + input: nil, + expected: nil, + }, + "resource-identity-identity-multiple-resources": { + input: &fwserver.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]fwschema.Schema{ + "test_resource_1": identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_attribute": identityschema.BoolAttribute{ + RequiredForImport: true, + }, + }, + }, + "test_resource_2": identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_attribute": identityschema.BoolAttribute{ + RequiredForImport: true, + }, + }, + }, + }, + }, + expected: &tfprotov5.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]*tfprotov5.ResourceIdentitySchema{ + "test_resource_1": { + IdentityAttributes: []*tfprotov5.ResourceIdentitySchemaAttribute{ + { + RequiredForImport: true, + Name: "test_attribute", + Type: tftypes.Bool, + }, + }, + }, + "test_resource_2": { + IdentityAttributes: []*tfprotov5.ResourceIdentitySchemaAttribute{ + { + RequiredForImport: true, + Name: "test_attribute", + Type: tftypes.Bool, + }, + }, + }, + }, + }, + }, + "resource-identity-attribute-optionalforimport": { + input: &fwserver.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]fwschema.Schema{ + "test_resource": identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_attribute": identityschema.BoolAttribute{ + OptionalForImport: true, + }, + }, + }, + }, + }, + expected: &tfprotov5.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]*tfprotov5.ResourceIdentitySchema{ + "test_resource": { + IdentityAttributes: []*tfprotov5.ResourceIdentitySchemaAttribute{ + { + Name: "test_attribute", + OptionalForImport: true, + Type: tftypes.Bool, + }, + }, + }, + }, + }, + }, + "resource-identity-attribute-requiredforimport": { + input: &fwserver.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]fwschema.Schema{ + "test_resource": identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_attribute": identityschema.BoolAttribute{ + RequiredForImport: true, + }, + }, + }, + }, + }, + expected: &tfprotov5.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]*tfprotov5.ResourceIdentitySchema{ + "test_resource": { + IdentityAttributes: []*tfprotov5.ResourceIdentitySchemaAttribute{ + { + Name: "test_attribute", + Type: tftypes.Bool, + RequiredForImport: true, + }, + }, + }, + }, + }, + }, + "resource-identity-attribute-type-bool": { + input: &fwserver.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]fwschema.Schema{ + "test_resource": identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_attribute": identityschema.BoolAttribute{ + RequiredForImport: true, + }, + }, + }, + }, + }, + expected: &tfprotov5.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]*tfprotov5.ResourceIdentitySchema{ + "test_resource": { + IdentityAttributes: []*tfprotov5.ResourceIdentitySchemaAttribute{ + { + Name: "test_attribute", + RequiredForImport: true, + Type: tftypes.Bool, + }, + }, + }, + }, + }, + }, + "resource-identity-attribute-type-float32": { + input: &fwserver.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]fwschema.Schema{ + "test_resource": identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_attribute": identityschema.Float32Attribute{ + RequiredForImport: true, + }, + }, + }, + }, + }, + expected: &tfprotov5.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]*tfprotov5.ResourceIdentitySchema{ + "test_resource": { + IdentityAttributes: []*tfprotov5.ResourceIdentitySchemaAttribute{ + { + Name: "test_attribute", + RequiredForImport: true, + Type: tftypes.Number, + }, + }, + }, + }, + }, + }, + "resource-identity-attribute-type-float64": { + input: &fwserver.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]fwschema.Schema{ + "test_resource": identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_attribute": identityschema.Float64Attribute{ + RequiredForImport: true, + }, + }, + }, + }, + }, + expected: &tfprotov5.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]*tfprotov5.ResourceIdentitySchema{ + "test_resource": { + IdentityAttributes: []*tfprotov5.ResourceIdentitySchemaAttribute{ + { + Name: "test_attribute", + RequiredForImport: true, + Type: tftypes.Number, + }, + }, + }, + }, + }, + }, + "resource-identity-attribute-type-int32": { + input: &fwserver.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]fwschema.Schema{ + "test_resource": identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_attribute": identityschema.Int32Attribute{ + RequiredForImport: true, + }, + }, + }, + }, + }, + expected: &tfprotov5.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]*tfprotov5.ResourceIdentitySchema{ + "test_resource": { + IdentityAttributes: []*tfprotov5.ResourceIdentitySchemaAttribute{ + { + Name: "test_attribute", + RequiredForImport: true, + Type: tftypes.Number, + }, + }, + }, + }, + }, + }, + "resource-identity-attribute-type-int64": { + input: &fwserver.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]fwschema.Schema{ + "test_resource": identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_attribute": identityschema.Int64Attribute{ + RequiredForImport: true, + }, + }, + }, + }, + }, + expected: &tfprotov5.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]*tfprotov5.ResourceIdentitySchema{ + "test_resource": { + IdentityAttributes: []*tfprotov5.ResourceIdentitySchemaAttribute{ + { + Name: "test_attribute", + RequiredForImport: true, + Type: tftypes.Number, + }, + }, + }, + }, + }, + }, + "resource-identity-attribute-type-list-string": { + input: &fwserver.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]fwschema.Schema{ + "test_resource": identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_attribute": identityschema.ListAttribute{ + RequiredForImport: true, + ElementType: types.StringType, + }, + }, + }, + }, + }, + expected: &tfprotov5.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]*tfprotov5.ResourceIdentitySchema{ + "test_resource": { + IdentityAttributes: []*tfprotov5.ResourceIdentitySchemaAttribute{ + { + Name: "test_attribute", + RequiredForImport: true, + Type: tftypes.List{ + ElementType: tftypes.String, + }, + }, + }, + }, + }, + }, + }, + "resource-identity-attribute-type-number": { + input: &fwserver.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]fwschema.Schema{ + "test_resource": identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_attribute": identityschema.NumberAttribute{ + RequiredForImport: true, + }, + }, + }, + }, + }, + expected: &tfprotov5.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]*tfprotov5.ResourceIdentitySchema{ + "test_resource": { + IdentityAttributes: []*tfprotov5.ResourceIdentitySchemaAttribute{ + { + Name: "test_attribute", + RequiredForImport: true, + Type: tftypes.Number, + }, + }, + }, + }, + }, + }, + "resource-identity-attribute-type-string": { + input: &fwserver.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]fwschema.Schema{ + "test_resource": identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_attribute": identityschema.StringAttribute{ + RequiredForImport: true, + }, + }, + }, + }, + }, + expected: &tfprotov5.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]*tfprotov5.ResourceIdentitySchema{ + "test_resource": { + IdentityAttributes: []*tfprotov5.ResourceIdentitySchemaAttribute{ + { + Name: "test_attribute", + RequiredForImport: true, + Type: tftypes.String, + }, + }, + }, + }, + }, + }, + "resource-identity-version": { + input: &fwserver.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]fwschema.Schema{ + "test_resource": identityschema.Schema{ + Version: 123, + }, + }, + }, + expected: &tfprotov5.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]*tfprotov5.ResourceIdentitySchema{ + "test_resource": { + IdentityAttributes: []*tfprotov5.ResourceIdentitySchemaAttribute{}, + Version: 123, + }, + }, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := toproto5.GetResourceIdentitySchemasResponse(context.Background(), testCase.input) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/internal/toproto5/identity_schema.go b/internal/toproto5/identity_schema.go new file mode 100644 index 000000000..003dbf155 --- /dev/null +++ b/internal/toproto5/identity_schema.go @@ -0,0 +1,52 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto5 + +import ( + "context" + "sort" + + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +// IdentitySchema returns the *tfprotov5.ResourceIdentitySchema equivalent of a Schema. +func IdentitySchema(ctx context.Context, s fwschema.Schema) (*tfprotov5.ResourceIdentitySchema, error) { + if s == nil { + return nil, nil + } + + result := &tfprotov5.ResourceIdentitySchema{ + Version: s.GetVersion(), + } + + attrs := make([]*tfprotov5.ResourceIdentitySchemaAttribute, 0) + + for name, attr := range s.GetAttributes() { + a, err := IdentitySchemaAttribute(ctx, name, tftypes.NewAttributePath().WithAttributeName(name), attr) + + if err != nil { + return nil, err + } + + attrs = append(attrs, a) + } + + sort.Slice(attrs, func(i, j int) bool { + if attrs[i] == nil { + return true + } + + if attrs[j] == nil { + return false + } + + return attrs[i].Name < attrs[j].Name + }) + + result.IdentityAttributes = attrs + + return result, nil +} diff --git a/internal/toproto5/identity_schema_attribute.go b/internal/toproto5/identity_schema_attribute.go new file mode 100644 index 000000000..01714f7a3 --- /dev/null +++ b/internal/toproto5/identity_schema_attribute.go @@ -0,0 +1,43 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto5 + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" +) + +// IdentitySchemaAttribute returns the *tfprotov5.ResourceIdentitySchemaAttribute equivalent of an +// Attribute. Errors will be tftypes.AttributePathErrors based on `path`. `name` is the name of the attribute. +func IdentitySchemaAttribute(ctx context.Context, name string, path *tftypes.AttributePath, a fwschema.Attribute) (*tfprotov5.ResourceIdentitySchemaAttribute, error) { + if _, ok := a.(fwschema.NestedAttribute); ok { + return nil, path.NewErrorf("identity schemas and protocol version 5 don't support NestedAttribute") + } + + if a.GetType() == nil { + return nil, path.NewErrorf("must have Type set") + } + + if !a.IsRequiredForImport() && !a.IsOptionalForImport() { + return nil, path.NewErrorf("must have RequiredForImport or OptionalForImport set") + } + + identitySchemaAttribute := &tfprotov5.ResourceIdentitySchemaAttribute{ + Name: name, + RequiredForImport: a.IsRequiredForImport(), + OptionalForImport: a.IsOptionalForImport(), + Type: a.GetType().TerraformType(ctx), + + // Unlike other schema attributes, identity attributes only have a single description field which + // is assumed to be markdown. Both a.GetDescription() and a.GetMarkdownDescription() will return + // the same string, so we just chose one here. + Description: a.GetDescription(), + } + + return identitySchemaAttribute, nil +} diff --git a/internal/toproto5/identity_schema_attribute_test.go b/internal/toproto5/identity_schema_attribute_test.go new file mode 100644 index 000000000..28704ba5d --- /dev/null +++ b/internal/toproto5/identity_schema_attribute_test.go @@ -0,0 +1,257 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto5_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/internal/toproto5" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestIdentitySchemaAttribute(t *testing.T) { + t.Parallel() + + type testCase struct { + name string + attr fwschema.Attribute + path *tftypes.AttributePath + expected *tfprotov5.ResourceIdentitySchemaAttribute + expectedErr string + } + + tests := map[string]testCase{ + "description": { + name: "string", + attr: testschema.Attribute{ + Type: types.StringType, + RequiredForImport: true, + Description: "A string attribute", + }, + path: tftypes.NewAttributePath(), + expected: &tfprotov5.ResourceIdentitySchemaAttribute{ + Name: "string", + Type: tftypes.String, + RequiredForImport: true, + Description: "A string attribute", + }, + }, + "attr-string": { + name: "string", + attr: testschema.Attribute{ + Type: types.StringType, + RequiredForImport: true, + }, + path: tftypes.NewAttributePath(), + expected: &tfprotov5.ResourceIdentitySchemaAttribute{ + Name: "string", + Type: tftypes.String, + RequiredForImport: true, + }, + }, + "attr-bool": { + name: "bool", + attr: testschema.Attribute{ + Type: types.BoolType, + RequiredForImport: true, + }, + path: tftypes.NewAttributePath(), + expected: &tfprotov5.ResourceIdentitySchemaAttribute{ + Name: "bool", + Type: tftypes.Bool, + RequiredForImport: true, + }, + }, + "attr-number": { + name: "number", + attr: testschema.Attribute{ + Type: types.NumberType, + RequiredForImport: true, + }, + path: tftypes.NewAttributePath(), + expected: &tfprotov5.ResourceIdentitySchemaAttribute{ + Name: "number", + Type: tftypes.Number, + RequiredForImport: true, + }, + }, + "attr-list": { + name: "list", + attr: testschema.Attribute{ + Type: types.ListType{ElemType: types.NumberType}, + RequiredForImport: true, + }, + path: tftypes.NewAttributePath(), + expected: &tfprotov5.ResourceIdentitySchemaAttribute{ + Name: "list", + Type: tftypes.List{ElementType: tftypes.Number}, + RequiredForImport: true, + }, + }, + "requiredforimport": { + name: "string", + attr: testschema.Attribute{ + Type: types.StringType, + RequiredForImport: true, + }, + path: tftypes.NewAttributePath(), + expected: &tfprotov5.ResourceIdentitySchemaAttribute{ + Name: "string", + Type: tftypes.String, + RequiredForImport: true, + }, + }, + "optionalforimport": { + name: "string", + attr: testschema.Attribute{ + Type: types.StringType, + OptionalForImport: true, + }, + path: tftypes.NewAttributePath(), + expected: &tfprotov5.ResourceIdentitySchemaAttribute{ + Name: "string", + Type: tftypes.String, + OptionalForImport: true, + }, + }, + "nested-attr-single-error": { + name: "single_nested", + attr: testschema.NestedAttribute{ + NestedObject: testschema.NestedAttributeObject{ + Attributes: map[string]fwschema.Attribute{ + "string": testschema.Attribute{ + Type: types.StringType, + Optional: true, + }, + "computed": testschema.Attribute{ + Type: types.NumberType, + Computed: true, + Sensitive: true, + }, + }, + }, + NestingMode: fwschema.NestingModeSingle, + Optional: true, + }, + path: tftypes.NewAttributePath(), + expectedErr: "identity schemas and protocol version 5 don't support NestedAttribute", + }, + "nested-attr-list-error": { + name: "list_nested", + attr: testschema.NestedAttribute{ + NestedObject: testschema.NestedAttributeObject{ + Attributes: map[string]fwschema.Attribute{ + "string": testschema.Attribute{ + Type: types.StringType, + Optional: true, + }, + "computed": testschema.Attribute{ + Type: types.NumberType, + Computed: true, + Sensitive: true, + }, + }, + }, + NestingMode: fwschema.NestingModeList, + Optional: true, + }, + path: tftypes.NewAttributePath(), + expectedErr: "identity schemas and protocol version 5 don't support NestedAttribute", + }, + "nested-attr-map-error": { + name: "map_nested", + attr: testschema.NestedAttribute{ + NestedObject: testschema.NestedAttributeObject{ + Attributes: map[string]fwschema.Attribute{ + "string": testschema.Attribute{ + Type: types.StringType, + Optional: true, + }, + "computed": testschema.Attribute{ + Type: types.NumberType, + Computed: true, + Sensitive: true, + }, + }, + }, + NestingMode: fwschema.NestingModeMap, + Optional: true, + }, + path: tftypes.NewAttributePath(), + expectedErr: "identity schemas and protocol version 5 don't support NestedAttribute", + }, + "nested-attr-set-error": { + name: "set_nested", + attr: testschema.NestedAttribute{ + NestedObject: testschema.NestedAttributeObject{ + Attributes: map[string]fwschema.Attribute{ + "string": testschema.Attribute{ + Type: types.StringType, + Optional: true, + }, + "computed": testschema.Attribute{ + Type: types.NumberType, + Computed: true, + Sensitive: true, + }, + }, + }, + NestingMode: fwschema.NestingModeSet, + Optional: true, + }, + path: tftypes.NewAttributePath(), + expectedErr: "identity schemas and protocol version 5 don't support NestedAttribute", + }, + "attr-unset": { + name: "whoops", + attr: testschema.Attribute{ + Optional: true, + }, + path: tftypes.NewAttributePath(), + expectedErr: "must have Type set", + }, + "missing-requiredforimport-and-optionalforimport": { + name: "whoops", + attr: testschema.Attribute{ + Type: types.StringType, + }, + path: tftypes.NewAttributePath(), + expectedErr: "must have RequiredForImport or OptionalForImport set", + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := toproto5.IdentitySchemaAttribute(context.Background(), tc.name, tc.path, tc.attr) + if err != nil { + if tc.expectedErr == "" { + t.Errorf("Unexpected error: %s", err) + return + } + if err.Error() != tc.expectedErr { + t.Errorf("Expected error to be %q, got %q", tc.expectedErr, err.Error()) + return + } + // got expected error + return + } + if err == nil && tc.expectedErr != "" { + t.Errorf("Expected error to be %q, got nil", tc.expectedErr) + return + } + if diff := cmp.Diff(got, tc.expected); diff != "" { + t.Errorf("Unexpected diff (+wanted, -got): %s", diff) + return + } + }) + } +} diff --git a/internal/toproto5/identity_schema_test.go b/internal/toproto5/identity_schema_test.go new file mode 100644 index 000000000..8388c4c16 --- /dev/null +++ b/internal/toproto5/identity_schema_test.go @@ -0,0 +1,138 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto5_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/internal/toproto5" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestIdentitySchema(t *testing.T) { + t.Parallel() + + type testCase struct { + input fwschema.Schema + expected *tfprotov5.ResourceIdentitySchema + expectedErr string + } + + tests := map[string]testCase{ + "nil": { + input: nil, + expected: nil, + }, + "empty-val": { + input: testschema.Schema{}, + expected: &tfprotov5.ResourceIdentitySchema{ + IdentityAttributes: []*tfprotov5.ResourceIdentitySchemaAttribute{}, + Version: 0, + }, + }, + "basic-attrs": { + input: testschema.Schema{ + Version: 1, + Attributes: map[string]fwschema.Attribute{ + "string": testschema.Attribute{ + Type: types.StringType, + RequiredForImport: true, + }, + "number": testschema.Attribute{ + Type: types.NumberType, + OptionalForImport: true, + }, + "bool": testschema.Attribute{ + Type: types.BoolType, + OptionalForImport: true, + }, + }, + }, + expected: &tfprotov5.ResourceIdentitySchema{ + Version: 1, + IdentityAttributes: []*tfprotov5.ResourceIdentitySchemaAttribute{ + { + Name: "bool", + Type: tftypes.Bool, + OptionalForImport: true, + }, + { + Name: "number", + Type: tftypes.Number, + OptionalForImport: true, + }, + { + Name: "string", + Type: tftypes.String, + RequiredForImport: true, + }, + }, + }, + }, + "complex-attrs": { + input: testschema.Schema{ + Version: 2, + Attributes: map[string]fwschema.Attribute{ + "list_of_string": testschema.Attribute{ + Type: types.ListType{ElemType: types.StringType}, + RequiredForImport: true, + }, + "list_of_bool": testschema.Attribute{ + Type: types.ListType{ElemType: types.BoolType}, + RequiredForImport: true, + }, + }, + }, + expected: &tfprotov5.ResourceIdentitySchema{ + Version: 2, + IdentityAttributes: []*tfprotov5.ResourceIdentitySchemaAttribute{ + { + Name: "list_of_bool", + Type: tftypes.List{ElementType: tftypes.Bool}, + RequiredForImport: true, + }, + { + Name: "list_of_string", + Type: tftypes.List{ElementType: tftypes.String}, + RequiredForImport: true, + }, + }, + }, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := toproto5.IdentitySchema(context.Background(), tc.input) + if err != nil { + if tc.expectedErr == "" { + t.Errorf("Unexpected error: %s", err) + return + } + if err.Error() != tc.expectedErr { + t.Errorf("Expected error to be %q, got %q", tc.expectedErr, err.Error()) + return + } + // got expected error + return + } + if err == nil && tc.expectedErr != "" { + t.Errorf("Expected error to be %q, got nil", tc.expectedErr) + return + } + if diff := cmp.Diff(got, tc.expected); diff != "" { + t.Errorf("Unexpected diff (+wanted, -got): %s", diff) + return + } + }) + } +} diff --git a/resource/identity_schema.go b/resource/identity_schema.go new file mode 100644 index 000000000..812173d20 --- /dev/null +++ b/resource/identity_schema.go @@ -0,0 +1,27 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package resource + +import ( + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" +) + +// IdentitySchemaRequest represents a request for the Resource to return its identity schema. +// An instance of this request struct is supplied as an argument to the +// Resource type IdentitySchema method. +type IdentitySchemaRequest struct{} + +// IdentitySchemaResponse represents a response to a SchemaRequest. An instance of this +// response struct is supplied as an argument to the Resource type IdentitySchema +// method. +type IdentitySchemaResponse struct { + // IdentitySchema is the schema of the resource identity. + IdentitySchema identityschema.Schema + + // Diagnostics report errors or warnings related to retrieving the resource + // identity schema. An empty slice indicates success, with no warnings + // or errors generated. + Diagnostics diag.Diagnostics +} diff --git a/resource/identityschema/attribute.go b/resource/identityschema/attribute.go new file mode 100644 index 000000000..06ef93150 --- /dev/null +++ b/resource/identityschema/attribute.go @@ -0,0 +1,33 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package identityschema + +import ( + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" +) + +// Attribute define a value field inside the Schema. Implementations in this +// package include: +// - BoolAttribute +// - Float32Attribute +// - Float64Attribute +// - Int32Attribute +// - Int64Attribute +// - ListAttribute +// - NumberAttribute +// - StringAttribute +// +// The available attribute types for a resource identity schema are intentionally +// limited. Nested attributes and blocks are not supported in identity schemas, +// as well as ListAttribute definitions can only have primitive element types of: +// - types.BoolType +// - types.Float32Type +// - types.Float64Type +// - types.Int32Type +// - types.Int64Type +// - types.NumberType +// - types.StringType +type Attribute interface { + fwschema.Attribute +} diff --git a/resource/identityschema/bool_attribute.go b/resource/identityschema/bool_attribute.go new file mode 100644 index 000000000..c17ae9549 --- /dev/null +++ b/resource/identityschema/bool_attribute.go @@ -0,0 +1,127 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package identityschema + +import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +// Ensure the implementation satisfies the desired interfaces. +var ( + _ Attribute = BoolAttribute{} +) + +// BoolAttribute represents a schema attribute that is a boolean. When +// retrieving the value for this attribute, use types.Bool as the value type +// unless the CustomType field is set. +// +// Terraform configurations configure this attribute using expressions that +// return a boolean or directly via the true/false keywords. +// +// example_attribute = true +type BoolAttribute struct { + // CustomType enables the use of a custom attribute type in place of the + // default basetypes.BoolType. When retrieving data, the basetypes.BoolValuable + // associated with this custom type must be used in place of types.Bool. + CustomType basetypes.BoolTypable + + // RequiredForImport indicates whether the practitioner must enter a value for + // this attribute when importing a managed resource by this identity. + // RequiredForImport and OptionalForImport cannot both be true. + RequiredForImport bool + + // OptionalForImport indicates whether the practitioner can choose to enter a value + // for this attribute when importing a managed resource by this identity. + // OptionalForImport and RequiredForImport cannot both be true. + OptionalForImport bool + + // Description is used in various tooling, like the language server or the documentation + // generator, to give practitioners more information about what this attribute is, + // what it's for, and how it should be used. It can be written as plain text with no + // special formatting, or formatted as Markdown. + Description string +} + +// ApplyTerraform5AttributePathStep always returns an error as it is not +// possible to step further into a BoolAttribute. +func (a BoolAttribute) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + return a.GetType().ApplyTerraform5AttributePathStep(step) +} + +// Equal returns true if the given Attribute is a BoolAttribute +// and all fields are equal. +func (a BoolAttribute) Equal(o fwschema.Attribute) bool { + if _, ok := o.(BoolAttribute); !ok { + return false + } + + return fwschema.AttributesEqual(a, o) +} + +// GetDeprecationMessage returns an empty string as identity attributes cannot +// surface deprecation messages. +func (a BoolAttribute) GetDeprecationMessage() string { + return "" +} + +// GetDescription returns the Description field value. For identity attributes, +// there is only a single description field that is permitted to contain plaintext or Markdown. +func (a BoolAttribute) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription returns the Description field value. For identity attributes, +// there is only a single description field that is permitted to contain Markdown or plaintext. +func (a BoolAttribute) GetMarkdownDescription() string { + return a.Description +} + +// GetType returns types.StringType or the CustomType field value if defined. +func (a BoolAttribute) GetType() attr.Type { + if a.CustomType != nil { + return a.CustomType + } + + return types.BoolType +} + +// IsComputed returns false as it's not relevant for identity schemas. +func (a BoolAttribute) IsComputed() bool { + return false +} + +// IsOptional returns false as it's not relevant for identity schemas. +func (a BoolAttribute) IsOptional() bool { + return false +} + +// IsRequired returns false as it's not relevant for identity schemas. +func (a BoolAttribute) IsRequired() bool { + return false +} + +// IsSensitive returns false as it's not relevant for identity schemas. +func (a BoolAttribute) IsSensitive() bool { + return false +} + +// IsWriteOnly returns false as it's not relevant for identity schemas. +func (a BoolAttribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns the RequiredForImport field value. +func (a BoolAttribute) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport returns the OptionalForImport field value. +func (a BoolAttribute) IsOptionalForImport() bool { + return a.OptionalForImport +} diff --git a/resource/identityschema/bool_attribute_test.go b/resource/identityschema/bool_attribute_test.go new file mode 100644 index 000000000..057cde795 --- /dev/null +++ b/resource/identityschema/bool_attribute_test.go @@ -0,0 +1,431 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package identityschema_test + +import ( + "fmt" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestBoolAttributeApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.BoolAttribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: identityschema.BoolAttribute{}, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.AttributeName to basetypes.BoolType"), + }, + "ElementKeyInt": { + attribute: identityschema.BoolAttribute{}, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyInt to basetypes.BoolType"), + }, + "ElementKeyString": { + attribute: identityschema.BoolAttribute{}, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyString to basetypes.BoolType"), + }, + "ElementKeyValue": { + attribute: identityschema.BoolAttribute{}, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyValue to basetypes.BoolType"), + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.attribute.ApplyTerraform5AttributePathStep(testCase.step) + + if err != nil { + if testCase.expectedError == nil { + t.Fatalf("expected no error, got: %s", err) + } + + if !strings.Contains(err.Error(), testCase.expectedError.Error()) { + t.Fatalf("expected error %q, got: %s", testCase.expectedError, err) + } + } + + if err == nil && testCase.expectedError != nil { + t.Fatalf("got no error, expected: %s", testCase.expectedError) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestBoolAttributeGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.BoolAttribute + expected string + }{ + "no-deprecation-message": { + attribute: identityschema.BoolAttribute{}, + expected: "", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDeprecationMessage() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestBoolAttributeEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.BoolAttribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: identityschema.BoolAttribute{}, + other: testschema.AttributeWithBoolValidators{}, + expected: false, + }, + "equal": { + attribute: identityschema.BoolAttribute{}, + other: identityschema.BoolAttribute{}, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.Equal(testCase.other) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestBoolAttributeGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.BoolAttribute + expected string + }{ + "no-description": { + attribute: identityschema.BoolAttribute{}, + expected: "", + }, + "description": { + attribute: identityschema.BoolAttribute{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestBoolAttributeGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.BoolAttribute + expected string + }{ + "no-markdown-description": { + attribute: identityschema.BoolAttribute{}, + expected: "", + }, + "markdown-description-from-description": { + attribute: identityschema.BoolAttribute{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetMarkdownDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestBoolAttributeGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.BoolAttribute + expected attr.Type + }{ + "base": { + attribute: identityschema.BoolAttribute{}, + expected: types.BoolType, + }, + "custom-type": { + attribute: identityschema.BoolAttribute{ + CustomType: testtypes.BoolType{}, + }, + expected: testtypes.BoolType{}, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetType() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestBoolAttributeIsComputed(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.BoolAttribute + expected bool + }{ + "not-computed": { + attribute: identityschema.BoolAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsComputed() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestBoolAttributeIsOptional(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.BoolAttribute + expected bool + }{ + "not-optional": { + attribute: identityschema.BoolAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptional() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestBoolAttributeIsRequired(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.BoolAttribute + expected bool + }{ + "not-required": { + attribute: identityschema.BoolAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequired() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestBoolAttributeIsSensitive(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.BoolAttribute + expected bool + }{ + "not-sensitive": { + attribute: identityschema.BoolAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsSensitive() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestBoolAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.BoolAttribute + expected bool + }{ + "not-writeOnly": { + attribute: identityschema.BoolAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestBoolAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.BoolAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: identityschema.BoolAttribute{}, + expected: false, + }, + "requiredForImport": { + attribute: identityschema.BoolAttribute{ + RequiredForImport: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestBoolAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.BoolAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: identityschema.BoolAttribute{}, + expected: false, + }, + "optionalForImport": { + attribute: identityschema.BoolAttribute{ + OptionalForImport: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/resource/identityschema/doc.go b/resource/identityschema/doc.go new file mode 100644 index 000000000..14efe3ecc --- /dev/null +++ b/resource/identityschema/doc.go @@ -0,0 +1,11 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +// Package identityschema contains all available schema functionality for managed +// resource identity. +// +// TODO: Verify these type names are what we ended with. +// +// Resource identity schemas define the structure and value types for identity state data. +// Schemas are implemented via the resource.ResourceWithIdentity type IdentitySchema method. +package identityschema diff --git a/resource/identityschema/float32_attribute.go b/resource/identityschema/float32_attribute.go new file mode 100644 index 000000000..ce741d32c --- /dev/null +++ b/resource/identityschema/float32_attribute.go @@ -0,0 +1,130 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package identityschema + +import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +// Ensure the implementation satisfies the desired interfaces. +var ( + _ Attribute = Float32Attribute{} +) + +// Float32Attribute represents a schema attribute that is a 32-bit floating +// point number. When retrieving the value for this attribute, use +// types.Float32 as the value type unless the CustomType field is set. +// +// Use Int32Attribute for 32-bit integer attributes or NumberAttribute for +// 512-bit generic number attributes. +// +// Terraform configurations configure this attribute using expressions that +// return a number or directly via a floating point value. +// +// example_attribute = 123.45 +type Float32Attribute struct { + // CustomType enables the use of a custom attribute type in place of the + // default basetypes.Float32Type. When retrieving data, the basetypes.Float32Valuable + // associated with this custom type must be used in place of types.Float32. + CustomType basetypes.Float32Typable + + // RequiredForImport indicates whether the practitioner must enter a value for + // this attribute when importing a managed resource by this identity. + // RequiredForImport and OptionalForImport cannot both be true. + RequiredForImport bool + + // OptionalForImport indicates whether the practitioner can choose to enter a value + // for this attribute when importing a managed resource by this identity. + // OptionalForImport and RequiredForImport cannot both be true. + OptionalForImport bool + + // Description is used in various tooling, like the language server or the documentation + // generator, to give practitioners more information about what this attribute is, + // what it's for, and how it should be used. It can be written as plain text with no + // special formatting, or formatted as Markdown. + Description string +} + +// ApplyTerraform5AttributePathStep always returns an error as it is not +// possible to step further into a Float32Attribute. +func (a Float32Attribute) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + return a.GetType().ApplyTerraform5AttributePathStep(step) +} + +// Equal returns true if the given Attribute is a Float32Attribute +// and all fields are equal. +func (a Float32Attribute) Equal(o fwschema.Attribute) bool { + if _, ok := o.(Float32Attribute); !ok { + return false + } + + return fwschema.AttributesEqual(a, o) +} + +// GetDeprecationMessage returns an empty string as identity attributes cannot +// surface deprecation messages. +func (a Float32Attribute) GetDeprecationMessage() string { + return "" +} + +// GetDescription returns the Description field value. For identity attributes, +// there is only a single description field that is permitted to contain plaintext or Markdown. +func (a Float32Attribute) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription returns the Description field value. For identity attributes, +// there is only a single description field that is permitted to contain Markdown or plaintext. +func (a Float32Attribute) GetMarkdownDescription() string { + return a.Description +} + +// GetType returns types.Float32Type or the CustomType field value if defined. +func (a Float32Attribute) GetType() attr.Type { + if a.CustomType != nil { + return a.CustomType + } + + return types.Float32Type +} + +// IsComputed returns false as it's not relevant for identity schemas. +func (a Float32Attribute) IsComputed() bool { + return false +} + +// IsOptional returns false as it's not relevant for identity schemas. +func (a Float32Attribute) IsOptional() bool { + return false +} + +// IsRequired returns false as it's not relevant for identity schemas. +func (a Float32Attribute) IsRequired() bool { + return false +} + +// IsSensitive returns false as it's not relevant for identity schemas. +func (a Float32Attribute) IsSensitive() bool { + return false +} + +// IsWriteOnly returns false as it's not relevant for identity schemas. +func (a Float32Attribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns the RequiredForImport field value. +func (a Float32Attribute) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport returns the OptionalForImport field value. +func (a Float32Attribute) IsOptionalForImport() bool { + return a.OptionalForImport +} diff --git a/resource/identityschema/float32_attribute_test.go b/resource/identityschema/float32_attribute_test.go new file mode 100644 index 000000000..0f906f88d --- /dev/null +++ b/resource/identityschema/float32_attribute_test.go @@ -0,0 +1,431 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package identityschema_test + +import ( + "fmt" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestFloat32AttributeApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Float32Attribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: identityschema.Float32Attribute{}, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.AttributeName to basetypes.Float32Type"), + }, + "ElementKeyInt": { + attribute: identityschema.Float32Attribute{}, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyInt to basetypes.Float32Type"), + }, + "ElementKeyString": { + attribute: identityschema.Float32Attribute{}, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyString to basetypes.Float32Type"), + }, + "ElementKeyValue": { + attribute: identityschema.Float32Attribute{}, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyValue to basetypes.Float32Type"), + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.attribute.ApplyTerraform5AttributePathStep(testCase.step) + + if err != nil { + if testCase.expectedError == nil { + t.Fatalf("expected no error, got: %s", err) + } + + if !strings.Contains(err.Error(), testCase.expectedError.Error()) { + t.Fatalf("expected error %q, got: %s", testCase.expectedError, err) + } + } + + if err == nil && testCase.expectedError != nil { + t.Fatalf("got no error, expected: %s", testCase.expectedError) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Float32Attribute + expected string + }{ + "no-deprecation-message": { + attribute: identityschema.Float32Attribute{}, + expected: "", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDeprecationMessage() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Float32Attribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: identityschema.Float32Attribute{}, + other: testschema.AttributeWithFloat32Validators{}, + expected: false, + }, + "equal": { + attribute: identityschema.Float32Attribute{}, + other: identityschema.Float32Attribute{}, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.Equal(testCase.other) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Float32Attribute + expected string + }{ + "no-description": { + attribute: identityschema.Float32Attribute{}, + expected: "", + }, + "description": { + attribute: identityschema.Float32Attribute{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Float32Attribute + expected string + }{ + "no-markdown-description": { + attribute: identityschema.Float32Attribute{}, + expected: "", + }, + "markdown-description-from-description": { + attribute: identityschema.Float32Attribute{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetMarkdownDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Float32Attribute + expected attr.Type + }{ + "base": { + attribute: identityschema.Float32Attribute{}, + expected: types.Float32Type, + }, + "custom-type": { + attribute: identityschema.Float32Attribute{ + CustomType: testtypes.Float32Type{}, + }, + expected: testtypes.Float32Type{}, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetType() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeIsComputed(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Float32Attribute + expected bool + }{ + "not-computed": { + attribute: identityschema.Float32Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsComputed() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeIsOptional(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Float32Attribute + expected bool + }{ + "not-optional": { + attribute: identityschema.Float32Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptional() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeIsRequired(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Float32Attribute + expected bool + }{ + "not-required": { + attribute: identityschema.Float32Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequired() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeIsSensitive(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Float32Attribute + expected bool + }{ + "not-sensitive": { + attribute: identityschema.Float32Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsSensitive() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Float32Attribute + expected bool + }{ + "not-writeOnly": { + attribute: identityschema.Float32Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Float32Attribute + expected bool + }{ + "not-requiredForImport": { + attribute: identityschema.Float32Attribute{}, + expected: false, + }, + "requiredForImport": { + attribute: identityschema.Float32Attribute{ + RequiredForImport: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat32AttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Float32Attribute + expected bool + }{ + "not-optionalForImport": { + attribute: identityschema.Float32Attribute{}, + expected: false, + }, + "optionalForImport": { + attribute: identityschema.Float32Attribute{ + OptionalForImport: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/resource/identityschema/float64_attribute.go b/resource/identityschema/float64_attribute.go new file mode 100644 index 000000000..a6e33fb3c --- /dev/null +++ b/resource/identityschema/float64_attribute.go @@ -0,0 +1,130 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package identityschema + +import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +// Ensure the implementation satisfies the desired interfaces. +var ( + _ Attribute = Float64Attribute{} +) + +// Float64Attribute represents a schema attribute that is a 64-bit floating +// point number. When retrieving the value for this attribute, use +// types.Float64 as the value type unless the CustomType field is set. +// +// Use Int64Attribute for 64-bit integer attributes or NumberAttribute for +// 512-bit generic number attributes. +// +// Terraform configurations configure this attribute using expressions that +// return a number or directly via a floating point value. +// +// example_attribute = 123.45 +type Float64Attribute struct { + // CustomType enables the use of a custom attribute type in place of the + // default basetypes.Float64Type. When retrieving data, the basetypes.Float64Valuable + // associated with this custom type must be used in place of types.Float64. + CustomType basetypes.Float64Typable + + // RequiredForImport indicates whether the practitioner must enter a value for + // this attribute when importing a managed resource by this identity. + // RequiredForImport and OptionalForImport cannot both be true. + RequiredForImport bool + + // OptionalForImport indicates whether the practitioner can choose to enter a value + // for this attribute when importing a managed resource by this identity. + // OptionalForImport and RequiredForImport cannot both be true. + OptionalForImport bool + + // Description is used in various tooling, like the language server or the documentation + // generator, to give practitioners more information about what this attribute is, + // what it's for, and how it should be used. It can be written as plain text with no + // special formatting, or formatted as Markdown. + Description string +} + +// ApplyTerraform5AttributePathStep always returns an error as it is not +// possible to step further into a Float64Attribute. +func (a Float64Attribute) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + return a.GetType().ApplyTerraform5AttributePathStep(step) +} + +// Equal returns true if the given Attribute is a Float64Attribute +// and all fields are equal. +func (a Float64Attribute) Equal(o fwschema.Attribute) bool { + if _, ok := o.(Float64Attribute); !ok { + return false + } + + return fwschema.AttributesEqual(a, o) +} + +// GetDeprecationMessage returns an empty string as identity attributes cannot +// surface deprecation messages. +func (a Float64Attribute) GetDeprecationMessage() string { + return "" +} + +// GetDescription returns the Description field value. For identity attributes, +// there is only a single description field that is permitted to contain plaintext or Markdown. +func (a Float64Attribute) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription returns the Description field value. For identity attributes, +// there is only a single description field that is permitted to contain Markdown or plaintext. +func (a Float64Attribute) GetMarkdownDescription() string { + return a.Description +} + +// GetType returns types.Float64Type or the CustomType field value if defined. +func (a Float64Attribute) GetType() attr.Type { + if a.CustomType != nil { + return a.CustomType + } + + return types.Float64Type +} + +// IsComputed returns false as it's not relevant for identity schemas. +func (a Float64Attribute) IsComputed() bool { + return false +} + +// IsOptional returns false as it's not relevant for identity schemas. +func (a Float64Attribute) IsOptional() bool { + return false +} + +// IsRequired returns false as it's not relevant for identity schemas. +func (a Float64Attribute) IsRequired() bool { + return false +} + +// IsSensitive returns false as it's not relevant for identity schemas. +func (a Float64Attribute) IsSensitive() bool { + return false +} + +// IsWriteOnly returns false as it's not relevant for identity schemas. +func (a Float64Attribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns the RequiredForImport field value. +func (a Float64Attribute) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport returns the OptionalForImport field value. +func (a Float64Attribute) IsOptionalForImport() bool { + return a.OptionalForImport +} diff --git a/resource/identityschema/float64_attribute_test.go b/resource/identityschema/float64_attribute_test.go new file mode 100644 index 000000000..e5473e7b3 --- /dev/null +++ b/resource/identityschema/float64_attribute_test.go @@ -0,0 +1,431 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package identityschema_test + +import ( + "fmt" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestFloat64AttributeApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Float64Attribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: identityschema.Float64Attribute{}, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.AttributeName to basetypes.Float64Type"), + }, + "ElementKeyInt": { + attribute: identityschema.Float64Attribute{}, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyInt to basetypes.Float64Type"), + }, + "ElementKeyString": { + attribute: identityschema.Float64Attribute{}, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyString to basetypes.Float64Type"), + }, + "ElementKeyValue": { + attribute: identityschema.Float64Attribute{}, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyValue to basetypes.Float64Type"), + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.attribute.ApplyTerraform5AttributePathStep(testCase.step) + + if err != nil { + if testCase.expectedError == nil { + t.Fatalf("expected no error, got: %s", err) + } + + if !strings.Contains(err.Error(), testCase.expectedError.Error()) { + t.Fatalf("expected error %q, got: %s", testCase.expectedError, err) + } + } + + if err == nil && testCase.expectedError != nil { + t.Fatalf("got no error, expected: %s", testCase.expectedError) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat64AttributeGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Float64Attribute + expected string + }{ + "no-deprecation-message": { + attribute: identityschema.Float64Attribute{}, + expected: "", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDeprecationMessage() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat64AttributeEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Float64Attribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: identityschema.Float64Attribute{}, + other: testschema.AttributeWithFloat64Validators{}, + expected: false, + }, + "equal": { + attribute: identityschema.Float64Attribute{}, + other: identityschema.Float64Attribute{}, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.Equal(testCase.other) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat64AttributeGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Float64Attribute + expected string + }{ + "no-description": { + attribute: identityschema.Float64Attribute{}, + expected: "", + }, + "description": { + attribute: identityschema.Float64Attribute{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat64AttributeGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Float64Attribute + expected string + }{ + "no-markdown-description": { + attribute: identityschema.Float64Attribute{}, + expected: "", + }, + "markdown-description-from-description": { + attribute: identityschema.Float64Attribute{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetMarkdownDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat64AttributeGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Float64Attribute + expected attr.Type + }{ + "base": { + attribute: identityschema.Float64Attribute{}, + expected: types.Float64Type, + }, + "custom-type": { + attribute: identityschema.Float64Attribute{ + CustomType: testtypes.Float64Type{}, + }, + expected: testtypes.Float64Type{}, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetType() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat64AttributeIsComputed(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Float64Attribute + expected bool + }{ + "not-computed": { + attribute: identityschema.Float64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsComputed() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat64AttributeIsOptional(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Float64Attribute + expected bool + }{ + "not-optional": { + attribute: identityschema.Float64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptional() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat64AttributeIsRequired(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Float64Attribute + expected bool + }{ + "not-required": { + attribute: identityschema.Float64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequired() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat64AttributeIsSensitive(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Float64Attribute + expected bool + }{ + "not-sensitive": { + attribute: identityschema.Float64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsSensitive() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat64AttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Float64Attribute + expected bool + }{ + "not-writeOnly": { + attribute: identityschema.Float64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat64AttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Float64Attribute + expected bool + }{ + "not-requiredForImport": { + attribute: identityschema.Float64Attribute{}, + expected: false, + }, + "requiredForImport": { + attribute: identityschema.Float64Attribute{ + RequiredForImport: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestFloat64AttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Float64Attribute + expected bool + }{ + "not-optionalForImport": { + attribute: identityschema.Float64Attribute{}, + expected: false, + }, + "optionalForImport": { + attribute: identityschema.Float64Attribute{ + OptionalForImport: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/resource/identityschema/int32_attribute.go b/resource/identityschema/int32_attribute.go new file mode 100644 index 000000000..a5ef7e819 --- /dev/null +++ b/resource/identityschema/int32_attribute.go @@ -0,0 +1,130 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package identityschema + +import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +// Ensure the implementation satisfies the desired interfaces. +var ( + _ Attribute = Int32Attribute{} +) + +// Int32Attribute represents a schema attribute that is a 32-bit integer. +// When retrieving the value for this attribute, use types.Int32 as the value +// type unless the CustomType field is set. +// +// Use Float32Attribute for 32-bit floating point number attributes or +// NumberAttribute for 512-bit generic number attributes. +// +// Terraform configurations configure this attribute using expressions that +// return a number or directly via an integer value. +// +// example_attribute = 123 +type Int32Attribute struct { + // CustomType enables the use of a custom attribute type in place of the + // default basetypes.Int32Type. When retrieving data, the basetypes.Int32Valuable + // associated with this custom type must be used in place of types.Int32. + CustomType basetypes.Int32Typable + + // RequiredForImport indicates whether the practitioner must enter a value for + // this attribute when importing a managed resource by this identity. + // RequiredForImport and OptionalForImport cannot both be true. + RequiredForImport bool + + // OptionalForImport indicates whether the practitioner can choose to enter a value + // for this attribute when importing a managed resource by this identity. + // OptionalForImport and RequiredForImport cannot both be true. + OptionalForImport bool + + // Description is used in various tooling, like the language server or the documentation + // generator, to give practitioners more information about what this attribute is, + // what it's for, and how it should be used. It can be written as plain text with no + // special formatting, or formatted as Markdown. + Description string +} + +// ApplyTerraform5AttributePathStep always returns an error as it is not +// possible to step further into a Int32Attribute. +func (a Int32Attribute) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + return a.GetType().ApplyTerraform5AttributePathStep(step) +} + +// Equal returns true if the given Attribute is a Int32Attribute +// and all fields are equal. +func (a Int32Attribute) Equal(o fwschema.Attribute) bool { + if _, ok := o.(Int32Attribute); !ok { + return false + } + + return fwschema.AttributesEqual(a, o) +} + +// GetDeprecationMessage returns an empty string as identity attributes cannot +// surface deprecation messages. +func (a Int32Attribute) GetDeprecationMessage() string { + return "" +} + +// GetDescription returns the Description field value. For identity attributes, +// there is only a single description field that is permitted to contain plaintext or Markdown. +func (a Int32Attribute) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription returns the Description field value. For identity attributes, +// there is only a single description field that is permitted to contain Markdown or plaintext. +func (a Int32Attribute) GetMarkdownDescription() string { + return a.Description +} + +// GetType returns types.Int32Type or the CustomType field value if defined. +func (a Int32Attribute) GetType() attr.Type { + if a.CustomType != nil { + return a.CustomType + } + + return types.Int32Type +} + +// IsComputed returns false as it's not relevant for identity schemas. +func (a Int32Attribute) IsComputed() bool { + return false +} + +// IsOptional returns false as it's not relevant for identity schemas. +func (a Int32Attribute) IsOptional() bool { + return false +} + +// IsRequired returns false as it's not relevant for identity schemas. +func (a Int32Attribute) IsRequired() bool { + return false +} + +// IsSensitive returns false as it's not relevant for identity schemas. +func (a Int32Attribute) IsSensitive() bool { + return false +} + +// IsWriteOnly returns false as it's not relevant for identity schemas. +func (a Int32Attribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns the RequiredForImport field value. +func (a Int32Attribute) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport returns the OptionalForImport field value. +func (a Int32Attribute) IsOptionalForImport() bool { + return a.OptionalForImport +} diff --git a/resource/identityschema/int32_attribute_test.go b/resource/identityschema/int32_attribute_test.go new file mode 100644 index 000000000..7b68255c2 --- /dev/null +++ b/resource/identityschema/int32_attribute_test.go @@ -0,0 +1,431 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package identityschema_test + +import ( + "fmt" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestInt32AttributeApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Int32Attribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: identityschema.Int32Attribute{}, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.AttributeName to basetypes.Int32Type"), + }, + "ElementKeyInt": { + attribute: identityschema.Int32Attribute{}, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyInt to basetypes.Int32Type"), + }, + "ElementKeyString": { + attribute: identityschema.Int32Attribute{}, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyString to basetypes.Int32Type"), + }, + "ElementKeyValue": { + attribute: identityschema.Int32Attribute{}, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyValue to basetypes.Int32Type"), + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.attribute.ApplyTerraform5AttributePathStep(testCase.step) + + if err != nil { + if testCase.expectedError == nil { + t.Fatalf("expected no error, got: %s", err) + } + + if !strings.Contains(err.Error(), testCase.expectedError.Error()) { + t.Fatalf("expected error %q, got: %s", testCase.expectedError, err) + } + } + + if err == nil && testCase.expectedError != nil { + t.Fatalf("got no error, expected: %s", testCase.expectedError) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt32AttributeGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Int32Attribute + expected string + }{ + "no-deprecation-message": { + attribute: identityschema.Int32Attribute{}, + expected: "", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDeprecationMessage() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt32AttributeEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Int32Attribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: identityschema.Int32Attribute{}, + other: testschema.AttributeWithInt32Validators{}, + expected: false, + }, + "equal": { + attribute: identityschema.Int32Attribute{}, + other: identityschema.Int32Attribute{}, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.Equal(testCase.other) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt32AttributeGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Int32Attribute + expected string + }{ + "no-description": { + attribute: identityschema.Int32Attribute{}, + expected: "", + }, + "description": { + attribute: identityschema.Int32Attribute{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt32AttributeGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Int32Attribute + expected string + }{ + "no-markdown-description": { + attribute: identityschema.Int32Attribute{}, + expected: "", + }, + "markdown-description-from-description": { + attribute: identityschema.Int32Attribute{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetMarkdownDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt32AttributeGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Int32Attribute + expected attr.Type + }{ + "base": { + attribute: identityschema.Int32Attribute{}, + expected: types.Int32Type, + }, + "custom-type": { + attribute: identityschema.Int32Attribute{ + CustomType: testtypes.Int32Type{}, + }, + expected: testtypes.Int32Type{}, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetType() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt32AttributeIsComputed(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Int32Attribute + expected bool + }{ + "not-computed": { + attribute: identityschema.Int32Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsComputed() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt32AttributeIsOptional(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Int32Attribute + expected bool + }{ + "not-optional": { + attribute: identityschema.Int32Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptional() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt32AttributeIsRequired(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Int32Attribute + expected bool + }{ + "not-required": { + attribute: identityschema.Int32Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequired() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt32AttributeIsSensitive(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Int32Attribute + expected bool + }{ + "not-sensitive": { + attribute: identityschema.Int32Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsSensitive() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt32AttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Int32Attribute + expected bool + }{ + "not-writeOnly": { + attribute: identityschema.Int32Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt32AttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Int32Attribute + expected bool + }{ + "not-requiredForImport": { + attribute: identityschema.Int32Attribute{}, + expected: false, + }, + "requiredForImport": { + attribute: identityschema.Int32Attribute{ + RequiredForImport: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt32AttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Int32Attribute + expected bool + }{ + "not-optionalForImport": { + attribute: identityschema.Int32Attribute{}, + expected: false, + }, + "optionalForImport": { + attribute: identityschema.Int32Attribute{ + OptionalForImport: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/resource/identityschema/int64_attribute.go b/resource/identityschema/int64_attribute.go new file mode 100644 index 000000000..332a1d1c1 --- /dev/null +++ b/resource/identityschema/int64_attribute.go @@ -0,0 +1,130 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package identityschema + +import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +// Ensure the implementation satisfies the desired interfaces. +var ( + _ Attribute = Int64Attribute{} +) + +// Int64Attribute represents a schema attribute that is a 64-bit integer. +// When retrieving the value for this attribute, use types.Int64 as the value +// type unless the CustomType field is set. +// +// Use Float64Attribute for 64-bit floating point number attributes or +// NumberAttribute for 512-bit generic number attributes. +// +// Terraform configurations configure this attribute using expressions that +// return a number or directly via an integer value. +// +// example_attribute = 123 +type Int64Attribute struct { + // CustomType enables the use of a custom attribute type in place of the + // default basetypes.Int64Type. When retrieving data, the basetypes.Int64Valuable + // associated with this custom type must be used in place of types.Int64. + CustomType basetypes.Int64Typable + + // RequiredForImport indicates whether the practitioner must enter a value for + // this attribute when importing a managed resource by this identity. + // RequiredForImport and OptionalForImport cannot both be true. + RequiredForImport bool + + // OptionalForImport indicates whether the practitioner can choose to enter a value + // for this attribute when importing a managed resource by this identity. + // OptionalForImport and RequiredForImport cannot both be true. + OptionalForImport bool + + // Description is used in various tooling, like the language server or the documentation + // generator, to give practitioners more information about what this attribute is, + // what it's for, and how it should be used. It can be written as plain text with no + // special formatting, or formatted as Markdown. + Description string +} + +// ApplyTerraform5AttributePathStep always returns an error as it is not +// possible to step further into a Int64Attribute. +func (a Int64Attribute) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + return a.GetType().ApplyTerraform5AttributePathStep(step) +} + +// Equal returns true if the given Attribute is a Int64Attribute +// and all fields are equal. +func (a Int64Attribute) Equal(o fwschema.Attribute) bool { + if _, ok := o.(Int64Attribute); !ok { + return false + } + + return fwschema.AttributesEqual(a, o) +} + +// GetDeprecationMessage returns an empty string as identity attributes cannot +// surface deprecation messages. +func (a Int64Attribute) GetDeprecationMessage() string { + return "" +} + +// GetDescription returns the Description field value. For identity attributes, +// there is only a single description field that is permitted to contain plaintext or Markdown. +func (a Int64Attribute) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription returns the Description field value. For identity attributes, +// there is only a single description field that is permitted to contain Markdown or plaintext. +func (a Int64Attribute) GetMarkdownDescription() string { + return a.Description +} + +// GetType returns types.Int64Type or the CustomType field value if defined. +func (a Int64Attribute) GetType() attr.Type { + if a.CustomType != nil { + return a.CustomType + } + + return types.Int64Type +} + +// IsComputed returns false as it's not relevant for identity schemas. +func (a Int64Attribute) IsComputed() bool { + return false +} + +// IsOptional returns false as it's not relevant for identity schemas. +func (a Int64Attribute) IsOptional() bool { + return false +} + +// IsRequired returns false as it's not relevant for identity schemas. +func (a Int64Attribute) IsRequired() bool { + return false +} + +// IsSensitive returns false as it's not relevant for identity schemas. +func (a Int64Attribute) IsSensitive() bool { + return false +} + +// IsWriteOnly returns false as it's not relevant for identity schemas. +func (a Int64Attribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns the RequiredForImport field value. +func (a Int64Attribute) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport returns the OptionalForImport field value. +func (a Int64Attribute) IsOptionalForImport() bool { + return a.OptionalForImport +} diff --git a/resource/identityschema/int64_attribute_test.go b/resource/identityschema/int64_attribute_test.go new file mode 100644 index 000000000..2bba9c500 --- /dev/null +++ b/resource/identityschema/int64_attribute_test.go @@ -0,0 +1,431 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package identityschema_test + +import ( + "fmt" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestInt64AttributeApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Int64Attribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: identityschema.Int64Attribute{}, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.AttributeName to basetypes.Int64Type"), + }, + "ElementKeyInt": { + attribute: identityschema.Int64Attribute{}, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyInt to basetypes.Int64Type"), + }, + "ElementKeyString": { + attribute: identityschema.Int64Attribute{}, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyString to basetypes.Int64Type"), + }, + "ElementKeyValue": { + attribute: identityschema.Int64Attribute{}, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyValue to basetypes.Int64Type"), + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.attribute.ApplyTerraform5AttributePathStep(testCase.step) + + if err != nil { + if testCase.expectedError == nil { + t.Fatalf("expected no error, got: %s", err) + } + + if !strings.Contains(err.Error(), testCase.expectedError.Error()) { + t.Fatalf("expected error %q, got: %s", testCase.expectedError, err) + } + } + + if err == nil && testCase.expectedError != nil { + t.Fatalf("got no error, expected: %s", testCase.expectedError) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt64AttributeGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Int64Attribute + expected string + }{ + "no-deprecation-message": { + attribute: identityschema.Int64Attribute{}, + expected: "", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDeprecationMessage() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt64AttributeEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Int64Attribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: identityschema.Int64Attribute{}, + other: testschema.AttributeWithInt64Validators{}, + expected: false, + }, + "equal": { + attribute: identityschema.Int64Attribute{}, + other: identityschema.Int64Attribute{}, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.Equal(testCase.other) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt64AttributeGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Int64Attribute + expected string + }{ + "no-description": { + attribute: identityschema.Int64Attribute{}, + expected: "", + }, + "description": { + attribute: identityschema.Int64Attribute{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt64AttributeGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Int64Attribute + expected string + }{ + "no-markdown-description": { + attribute: identityschema.Int64Attribute{}, + expected: "", + }, + "markdown-description-from-description": { + attribute: identityschema.Int64Attribute{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetMarkdownDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt64AttributeGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Int64Attribute + expected attr.Type + }{ + "base": { + attribute: identityschema.Int64Attribute{}, + expected: types.Int64Type, + }, + "custom-type": { + attribute: identityschema.Int64Attribute{ + CustomType: testtypes.Int64Type{}, + }, + expected: testtypes.Int64Type{}, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetType() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt64AttributeIsComputed(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Int64Attribute + expected bool + }{ + "not-computed": { + attribute: identityschema.Int64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsComputed() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt64AttributeIsOptional(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Int64Attribute + expected bool + }{ + "not-optional": { + attribute: identityschema.Int64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptional() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt64AttributeIsRequired(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Int64Attribute + expected bool + }{ + "not-required": { + attribute: identityschema.Int64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequired() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt64AttributeIsSensitive(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Int64Attribute + expected bool + }{ + "not-sensitive": { + attribute: identityschema.Int64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsSensitive() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt64AttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Int64Attribute + expected bool + }{ + "not-writeOnly": { + attribute: identityschema.Int64Attribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt64AttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Int64Attribute + expected bool + }{ + "not-requiredForImport": { + attribute: identityschema.Int64Attribute{}, + expected: false, + }, + "requiredForImport": { + attribute: identityschema.Int64Attribute{ + RequiredForImport: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestInt64AttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.Int64Attribute + expected bool + }{ + "not-optionalForImport": { + attribute: identityschema.Int64Attribute{}, + expected: false, + }, + "optionalForImport": { + attribute: identityschema.Int64Attribute{ + OptionalForImport: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/resource/identityschema/list_attribute.go b/resource/identityschema/list_attribute.go new file mode 100644 index 000000000..86fe79730 --- /dev/null +++ b/resource/identityschema/list_attribute.go @@ -0,0 +1,173 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package identityschema + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +// Ensure the implementation satisfies the desired interfaces. +var ( + _ Attribute = ListAttribute{} + _ fwschema.AttributeWithValidateImplementation = ListAttribute{} +) + +// ListAttribute represents a schema attribute that is a list with a single +// element type. When retrieving the value for this attribute, use types.List +// as the value type unless the CustomType field is set. The ElementType field +// must be set. +// +// In identity schemas, ListAttribute is only permitted to have a primitive ElementType, +// which are: +// - types.BoolType +// - types.Float32Type +// - types.Float64Type +// - types.Int32Type +// - types.Int64Type +// - types.NumberType +// - types.StringType +// +// Terraform configurations configure this attribute using expressions that +// return a list or directly via square brace syntax. +// +// # list of strings +// example_attribute = ["first", "second"] +type ListAttribute struct { + // ElementType is the type for all elements of the list. This field must be + // set. + // + // ElementType must be a primitive, which are: + // - types.BoolType + // - types.Float32Type + // - types.Float64Type + // - types.Int32Type + // - types.Int64Type + // - types.NumberType + // - types.StringType + ElementType attr.Type + + // CustomType enables the use of a custom attribute type in place of the + // default basetypes.ListType. When retrieving data, the basetypes.ListValuable + // associated with this custom type must be used in place of types.List. + CustomType basetypes.ListTypable + + // RequiredForImport indicates whether the practitioner must enter a value for + // this attribute when importing a managed resource by this identity. + // RequiredForImport and OptionalForImport cannot both be true. + RequiredForImport bool + + // OptionalForImport indicates whether the practitioner can choose to enter a value + // for this attribute when importing a managed resource by this identity. + // OptionalForImport and RequiredForImport cannot both be true. + OptionalForImport bool + + // Description is used in various tooling, like the language server or the documentation + // generator, to give practitioners more information about what this attribute is, + // what it's for, and how it should be used. It can be written as plain text with no + // special formatting, or formatted as Markdown. + Description string +} + +// ApplyTerraform5AttributePathStep returns the result of stepping into a list +// index or an error. +func (a ListAttribute) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + return a.GetType().ApplyTerraform5AttributePathStep(step) +} + +// Equal returns true if the given Attribute is a ListAttribute +// and all fields are equal. +func (a ListAttribute) Equal(o fwschema.Attribute) bool { + if _, ok := o.(ListAttribute); !ok { + return false + } + + return fwschema.AttributesEqual(a, o) +} + +// GetDeprecationMessage returns an empty string as identity attributes cannot +// surface deprecation messages. +func (a ListAttribute) GetDeprecationMessage() string { + return "" +} + +// GetDescription returns the Description field value. For identity attributes, +// there is only a single description field that is permitted to contain plaintext or Markdown. +func (a ListAttribute) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription returns the Description field value. For identity attributes, +// there is only a single description field that is permitted to contain Markdown or plaintext. +func (a ListAttribute) GetMarkdownDescription() string { + return a.Description +} + +// GetType returns types.ListType or the CustomType field value if defined. +func (a ListAttribute) GetType() attr.Type { + if a.CustomType != nil { + return a.CustomType + } + + return types.ListType{ + ElemType: a.ElementType, + } +} + +// IsComputed returns false as it's not relevant for identity schemas. +func (a ListAttribute) IsComputed() bool { + return false +} + +// IsOptional returns false as it's not relevant for identity schemas. +func (a ListAttribute) IsOptional() bool { + return false +} + +// IsRequired returns false as it's not relevant for identity schemas. +func (a ListAttribute) IsRequired() bool { + return false +} + +// IsSensitive returns false as it's not relevant for identity schemas. +func (a ListAttribute) IsSensitive() bool { + return false +} + +// IsWriteOnly returns false as it's not relevant for identity schemas. +func (a ListAttribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns the RequiredForImport field value. +func (a ListAttribute) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport returns the OptionalForImport field value. +func (a ListAttribute) IsOptionalForImport() bool { + return a.OptionalForImport +} + +// ValidateImplementation contains logic for validating the +// provider-defined implementation of the attribute to prevent unexpected +// errors or panics. This logic runs during the GetResourceIdentitySchemas RPC and +// should never include false positives. +func (a ListAttribute) ValidateImplementation(ctx context.Context, req fwschema.ValidateImplementationRequest, resp *fwschema.ValidateImplementationResponse) { + if a.CustomType == nil && a.ElementType == nil { + resp.Diagnostics.Append(fwschema.AttributeMissingElementTypeDiag(req.Path)) + } + + // TODO: Write validation similar to the dynamic type validation/diagnostic for detecting + // non-primitive element types + // if a.CustomType == nil && fwtype.ContainsCollectionWithDynamic(a.GetType()) { + // resp.Diagnostics.Append(fwtype.AttributeCollectionWithDynamicTypeDiag(req.Path)) + // } +} diff --git a/resource/identityschema/list_attribute_test.go b/resource/identityschema/list_attribute_test.go new file mode 100644 index 000000000..48c90f006 --- /dev/null +++ b/resource/identityschema/list_attribute_test.go @@ -0,0 +1,520 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package identityschema_test + +import ( + "context" + "fmt" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestListAttributeApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.ListAttribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: identityschema.ListAttribute{ElementType: types.StringType}, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.AttributeName to ListType"), + }, + "ElementKeyInt": { + attribute: identityschema.ListAttribute{ElementType: types.StringType}, + step: tftypes.ElementKeyInt(1), + expected: types.StringType, + expectedError: nil, + }, + "ElementKeyString": { + attribute: identityschema.ListAttribute{ElementType: types.StringType}, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyString to ListType"), + }, + "ElementKeyValue": { + attribute: identityschema.ListAttribute{ElementType: types.StringType}, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply step tftypes.ElementKeyValue to ListType"), + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.attribute.ApplyTerraform5AttributePathStep(testCase.step) + + if err != nil { + if testCase.expectedError == nil { + t.Fatalf("expected no error, got: %s", err) + } + + if !strings.Contains(err.Error(), testCase.expectedError.Error()) { + t.Fatalf("expected error %q, got: %s", testCase.expectedError, err) + } + } + + if err == nil && testCase.expectedError != nil { + t.Fatalf("got no error, expected: %s", testCase.expectedError) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListAttributeGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.ListAttribute + expected string + }{ + "no-deprecation-message": { + attribute: identityschema.ListAttribute{ElementType: types.StringType}, + expected: "", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDeprecationMessage() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListAttributeEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.ListAttribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: identityschema.ListAttribute{ElementType: types.StringType}, + other: testschema.AttributeWithListValidators{}, + expected: false, + }, + "different-element-type": { + attribute: identityschema.ListAttribute{ElementType: types.StringType}, + other: identityschema.ListAttribute{ElementType: types.BoolType}, + expected: false, + }, + "equal": { + attribute: identityschema.ListAttribute{ElementType: types.StringType}, + other: identityschema.ListAttribute{ElementType: types.StringType}, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.Equal(testCase.other) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListAttributeGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.ListAttribute + expected string + }{ + "no-description": { + attribute: identityschema.ListAttribute{ElementType: types.StringType}, + expected: "", + }, + "description": { + attribute: identityschema.ListAttribute{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListAttributeGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.ListAttribute + expected string + }{ + "no-markdown-description": { + attribute: identityschema.ListAttribute{ElementType: types.StringType}, + expected: "", + }, + "markdown-description-from-description": { + attribute: identityschema.ListAttribute{ + ElementType: types.StringType, + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetMarkdownDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListAttributeGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.ListAttribute + expected attr.Type + }{ + "base": { + attribute: identityschema.ListAttribute{ElementType: types.StringType}, + expected: types.ListType{ElemType: types.StringType}, + }, + "custom-type": { + attribute: identityschema.ListAttribute{ + CustomType: testtypes.ListType{ListType: types.ListType{ElemType: types.StringType}}, + }, + expected: testtypes.ListType{ListType: types.ListType{ElemType: types.StringType}}, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetType() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListAttributeIsComputed(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.ListAttribute + expected bool + }{ + "not-computed": { + attribute: identityschema.ListAttribute{ElementType: types.StringType}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsComputed() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListAttributeIsOptional(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.ListAttribute + expected bool + }{ + "not-optional": { + attribute: identityschema.ListAttribute{ElementType: types.StringType}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptional() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListAttributeIsRequired(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.ListAttribute + expected bool + }{ + "not-required": { + attribute: identityschema.ListAttribute{ElementType: types.StringType}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequired() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListAttributeIsSensitive(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.ListAttribute + expected bool + }{ + "not-sensitive": { + attribute: identityschema.ListAttribute{ElementType: types.StringType}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsSensitive() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.ListAttribute + expected bool + }{ + "not-writeOnly": { + attribute: identityschema.ListAttribute{ElementType: types.StringType}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.ListAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: identityschema.ListAttribute{ElementType: types.StringType}, + expected: false, + }, + "requiredForImport": { + attribute: identityschema.ListAttribute{ + ElementType: types.StringType, + RequiredForImport: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.ListAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: identityschema.ListAttribute{ElementType: types.StringType}, + expected: false, + }, + "optionalForImport": { + attribute: identityschema.ListAttribute{ + ElementType: types.StringType, + OptionalForImport: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestListAttributeValidateImplementation(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.ListAttribute + request fwschema.ValidateImplementationRequest + expected *fwschema.ValidateImplementationResponse + }{ + "elementtype": { + attribute: identityschema.ListAttribute{ + RequiredForImport: true, + ElementType: types.StringType, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{}, + }, + // TODO: Update this test once the validation logic is in place + // "elementtype-invalid": { + // attribute: identityschema.ListAttribute{ + // RequiredForImport: true, + // ElementType: types.DynamicType, + // }, + // request: fwschema.ValidateImplementationRequest{ + // Name: "test", + // Path: path.Root("test"), + // }, + // expected: &fwschema.ValidateImplementationResponse{ + // Diagnostics: diag.Diagnostics{ + // diag.NewErrorDiagnostic( + // "Invalid Schema Implementation", + // "When validating the schema, an implementation issue was found. "+ + // "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + // "\"test\" is an attribute that contains a collection type with a nested dynamic type.\n\n"+ + // "Dynamic types inside of collections are not currently supported in terraform-plugin-framework. "+ + // "If underlying dynamic values are required, replace the \"test\" attribute definition with DynamicAttribute instead.", + // ), + // }, + // }, + // }, + "elementtype-missing": { + attribute: identityschema.ListAttribute{ + RequiredForImport: true, + }, + request: fwschema.ValidateImplementationRequest{ + Name: "test", + Path: path.Root("test"), + }, + expected: &fwschema.ValidateImplementationResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Attribute Implementation", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"test\" is missing the CustomType or ElementType field on a collection Attribute. "+ + "One of these fields is required to prevent other unexpected errors or panics.", + ), + }, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := &fwschema.ValidateImplementationResponse{} + testCase.attribute.ValidateImplementation(context.Background(), testCase.request, got) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/resource/identityschema/number_attribute.go b/resource/identityschema/number_attribute.go new file mode 100644 index 000000000..d1533477a --- /dev/null +++ b/resource/identityschema/number_attribute.go @@ -0,0 +1,131 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package identityschema + +import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +// Ensure the implementation satisfies the desired interfaces. +var ( + _ Attribute = NumberAttribute{} +) + +// NumberAttribute represents a schema attribute that is a generic number with +// up to 512 bits of floating point or integer precision. When retrieving the +// value for this attribute, use types.Number as the value type unless the +// CustomType field is set. +// +// Use Float64Attribute for 64-bit floating point number attributes or +// Int64Attribute for 64-bit integer number attributes. +// +// Terraform configurations configure this attribute using expressions that +// return a number or directly via a floating point or integer value. +// +// example_attribute = 123 +type NumberAttribute struct { + // CustomType enables the use of a custom attribute type in place of the + // default basetypes.NumberType. When retrieving data, the basetypes.NumberValuable + // associated with this custom type must be used in place of types.Number. + CustomType basetypes.NumberTypable + + // RequiredForImport indicates whether the practitioner must enter a value for + // this attribute when importing a managed resource by this identity. + // RequiredForImport and OptionalForImport cannot both be true. + RequiredForImport bool + + // OptionalForImport indicates whether the practitioner can choose to enter a value + // for this attribute when importing a managed resource by this identity. + // OptionalForImport and RequiredForImport cannot both be true. + OptionalForImport bool + + // Description is used in various tooling, like the language server or the documentation + // generator, to give practitioners more information about what this attribute is, + // what it's for, and how it should be used. It can be written as plain text with no + // special formatting, or formatted as Markdown. + Description string +} + +// ApplyTerraform5AttributePathStep always returns an error as it is not +// possible to step further into a NumberAttribute. +func (a NumberAttribute) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + return a.GetType().ApplyTerraform5AttributePathStep(step) +} + +// Equal returns true if the given Attribute is a NumberAttribute +// and all fields are equal. +func (a NumberAttribute) Equal(o fwschema.Attribute) bool { + if _, ok := o.(NumberAttribute); !ok { + return false + } + + return fwschema.AttributesEqual(a, o) +} + +// GetDeprecationMessage returns an empty string as identity attributes cannot +// surface deprecation messages. +func (a NumberAttribute) GetDeprecationMessage() string { + return "" +} + +// GetDescription returns the Description field value. For identity attributes, +// there is only a single description field that is permitted to contain plaintext or Markdown. +func (a NumberAttribute) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription returns the Description field value. For identity attributes, +// there is only a single description field that is permitted to contain Markdown or plaintext. +func (a NumberAttribute) GetMarkdownDescription() string { + return a.Description +} + +// GetType returns types.NumberType or the CustomType field value if defined. +func (a NumberAttribute) GetType() attr.Type { + if a.CustomType != nil { + return a.CustomType + } + + return types.NumberType +} + +// IsComputed returns false as it's not relevant for identity schemas. +func (a NumberAttribute) IsComputed() bool { + return false +} + +// IsOptional returns false as it's not relevant for identity schemas. +func (a NumberAttribute) IsOptional() bool { + return false +} + +// IsRequired returns false as it's not relevant for identity schemas. +func (a NumberAttribute) IsRequired() bool { + return false +} + +// IsSensitive returns false as it's not relevant for identity schemas. +func (a NumberAttribute) IsSensitive() bool { + return false +} + +// IsWriteOnly returns false as it's not relevant for identity schemas. +func (a NumberAttribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns the RequiredForImport field value. +func (a NumberAttribute) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport returns the OptionalForImport field value. +func (a NumberAttribute) IsOptionalForImport() bool { + return a.OptionalForImport +} diff --git a/resource/identityschema/number_attribute_test.go b/resource/identityschema/number_attribute_test.go new file mode 100644 index 000000000..9597643cd --- /dev/null +++ b/resource/identityschema/number_attribute_test.go @@ -0,0 +1,431 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package identityschema_test + +import ( + "fmt" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestNumberAttributeApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.NumberAttribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: identityschema.NumberAttribute{}, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.AttributeName to basetypes.NumberType"), + }, + "ElementKeyInt": { + attribute: identityschema.NumberAttribute{}, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyInt to basetypes.NumberType"), + }, + "ElementKeyString": { + attribute: identityschema.NumberAttribute{}, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyString to basetypes.NumberType"), + }, + "ElementKeyValue": { + attribute: identityschema.NumberAttribute{}, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyValue to basetypes.NumberType"), + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.attribute.ApplyTerraform5AttributePathStep(testCase.step) + + if err != nil { + if testCase.expectedError == nil { + t.Fatalf("expected no error, got: %s", err) + } + + if !strings.Contains(err.Error(), testCase.expectedError.Error()) { + t.Fatalf("expected error %q, got: %s", testCase.expectedError, err) + } + } + + if err == nil && testCase.expectedError != nil { + t.Fatalf("got no error, expected: %s", testCase.expectedError) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNumberAttributeGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.NumberAttribute + expected string + }{ + "no-deprecation-message": { + attribute: identityschema.NumberAttribute{}, + expected: "", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDeprecationMessage() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNumberAttributeEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.NumberAttribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: identityschema.NumberAttribute{}, + other: testschema.AttributeWithNumberValidators{}, + expected: false, + }, + "equal": { + attribute: identityschema.NumberAttribute{}, + other: identityschema.NumberAttribute{}, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.Equal(testCase.other) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNumberAttributeGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.NumberAttribute + expected string + }{ + "no-description": { + attribute: identityschema.NumberAttribute{}, + expected: "", + }, + "description": { + attribute: identityschema.NumberAttribute{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNumberAttributeGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.NumberAttribute + expected string + }{ + "no-markdown-description": { + attribute: identityschema.NumberAttribute{}, + expected: "", + }, + "markdown-description-from-description": { + attribute: identityschema.NumberAttribute{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetMarkdownDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNumberAttributeGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.NumberAttribute + expected attr.Type + }{ + "base": { + attribute: identityschema.NumberAttribute{}, + expected: types.NumberType, + }, + "custom-type": { + attribute: identityschema.NumberAttribute{ + CustomType: testtypes.NumberType{}, + }, + expected: testtypes.NumberType{}, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetType() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNumberAttributeIsComputed(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.NumberAttribute + expected bool + }{ + "not-computed": { + attribute: identityschema.NumberAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsComputed() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNumberAttributeIsOptional(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.NumberAttribute + expected bool + }{ + "not-optional": { + attribute: identityschema.NumberAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptional() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNumberAttributeIsRequired(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.NumberAttribute + expected bool + }{ + "not-required": { + attribute: identityschema.NumberAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequired() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNumberAttributeIsSensitive(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.NumberAttribute + expected bool + }{ + "not-sensitive": { + attribute: identityschema.NumberAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsSensitive() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNumberAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.NumberAttribute + expected bool + }{ + "not-writeOnly": { + attribute: identityschema.NumberAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNumberAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.NumberAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: identityschema.NumberAttribute{}, + expected: false, + }, + "requiredForImport": { + attribute: identityschema.NumberAttribute{ + RequiredForImport: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestNumberAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.NumberAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: identityschema.NumberAttribute{}, + expected: false, + }, + "optionalForImport": { + attribute: identityschema.NumberAttribute{ + OptionalForImport: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/resource/identityschema/schema.go b/resource/identityschema/schema.go new file mode 100644 index 000000000..1dfd0983f --- /dev/null +++ b/resource/identityschema/schema.go @@ -0,0 +1,147 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package identityschema + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/path" +) + +// Schema must satify the fwschema.Schema interface. +var _ fwschema.Schema = Schema{} + +// TODO: Verify these type names are what we ended with. +// +// Schema defines the structure and value types of resource identity data. This type +// is used as the resource.IdentitySchemaResponse type Schema field, which is +// implemented by the resource.ResourceWithIdentity type IdentitySchema method. +type Schema struct { + // Attributes is the mapping of underlying attribute names to attribute + // definitions. + // + // Names must only contain lowercase letters, numbers, and underscores. + Attributes map[string]Attribute + + // Version indicates the current version of the resource identity schema. Resource + // identity schema versioning enables identity state upgrades in conjunction with the + // [resource.ResourceWithIdentityStateUpgrades] interface. Versioning is only + // required if there is a breaking change involving existing identity state data, + // such as changing an attribute type in a manner that is incompatible with the Terraform type. + // + // Versions are conventionally only incremented by one each release. + Version int64 +} + +// ApplyTerraform5AttributePathStep applies the given AttributePathStep to the +// schema. +func (s Schema) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (any, error) { + return fwschema.SchemaApplyTerraform5AttributePathStep(s, step) +} + +// AttributeAtPath returns the Attribute at the passed path. If the path points +// to an element or attribute of a complex type, rather than to an Attribute, +// it will return an ErrPathInsideAtomicAttribute error. +func (s Schema) AttributeAtPath(ctx context.Context, p path.Path) (fwschema.Attribute, diag.Diagnostics) { + return fwschema.SchemaAttributeAtPath(ctx, s, p) +} + +// AttributeAtPath returns the Attribute at the passed path. If the path points +// to an element or attribute of a complex type, rather than to an Attribute, +// it will return an ErrPathInsideAtomicAttribute error. +func (s Schema) AttributeAtTerraformPath(ctx context.Context, p *tftypes.AttributePath) (fwschema.Attribute, error) { + return fwschema.SchemaAttributeAtTerraformPath(ctx, s, p) +} + +// GetAttributes returns the Attributes field value. +func (s Schema) GetAttributes() map[string]fwschema.Attribute { + return schemaAttributes(s.Attributes) +} + +// GetBlocks returns an empty map as it's not relevant for identity schemas. +func (s Schema) GetBlocks() map[string]fwschema.Block { + return map[string]fwschema.Block{} +} + +// GetDeprecationMessage returns an empty string as identity schemas cannot +// surface deprecation messages. +func (s Schema) GetDeprecationMessage() string { + return "" +} + +// GetDescription returns an empty string as identity schemas cannot +// surface descriptions. +func (s Schema) GetDescription() string { + return "" +} + +// GetMarkdownDescription returns an empty string as identity schemas cannot +// surface descriptions. +func (s Schema) GetMarkdownDescription() string { + return "" +} + +// GetVersion returns the Version field value. +func (s Schema) GetVersion() int64 { + return s.Version +} + +// Type returns the framework type of the schema. +func (s Schema) Type() attr.Type { + return fwschema.SchemaType(s) +} + +// TypeAtPath returns the framework type at the given schema path. +func (s Schema) TypeAtPath(ctx context.Context, p path.Path) (attr.Type, diag.Diagnostics) { + return fwschema.SchemaTypeAtPath(ctx, s, p) +} + +// TypeAtTerraformPath returns the framework type at the given tftypes path. +func (s Schema) TypeAtTerraformPath(ctx context.Context, p *tftypes.AttributePath) (attr.Type, error) { + return fwschema.SchemaTypeAtTerraformPath(ctx, s, p) +} + +// Validate verifies that the schema is not using a reserved field name for a top-level attribute. +// +// Deprecated: Use the ValidateImplementation method instead. +func (s Schema) Validate() diag.Diagnostics { + return s.ValidateImplementation(context.Background()) +} + +// ValidateImplementation contains logic for validating the provider-defined +// implementation of the schema and underlying attributes and blocks to prevent +// unexpected errors or panics. This logic runs during the +// GetResourceIdentitySchemas RPC, or via provider-defined unit testing, and should +// never include false positives. +func (s Schema) ValidateImplementation(ctx context.Context) diag.Diagnostics { + var diags diag.Diagnostics + + for attributeName, attribute := range s.GetAttributes() { + req := fwschema.ValidateImplementationRequest{ + Name: attributeName, + Path: path.Root(attributeName), + } + + diags.Append(fwschema.IsReservedResourceAttributeName(req.Name, req.Path)...) + diags.Append(fwschema.ValidateAttributeImplementation(ctx, attribute, req)...) + } + + return diags +} + +// schemaAttributes is a identity to fwschema type conversion function. +func schemaAttributes(attributes map[string]Attribute) map[string]fwschema.Attribute { + result := make(map[string]fwschema.Attribute, len(attributes)) + + for name, attribute := range attributes { + result[name] = attribute + } + + return result +} diff --git a/resource/identityschema/schema_test.go b/resource/identityschema/schema_test.go new file mode 100644 index 000000000..394cdd426 --- /dev/null +++ b/resource/identityschema/schema_test.go @@ -0,0 +1,915 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package identityschema_test + +import ( + "context" + "fmt" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestSchemaApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schema identityschema.Schema + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName-attribute": { + schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "testattr": identityschema.StringAttribute{}, + }, + }, + step: tftypes.AttributeName("testattr"), + expected: identityschema.StringAttribute{}, + expectedError: nil, + }, + "AttributeName-missing": { + schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "testattr": identityschema.StringAttribute{}, + }, + }, + step: tftypes.AttributeName("other"), + expected: nil, + expectedError: fmt.Errorf("could not find attribute or block \"other\" in schema"), + }, + "ElementKeyInt": { + schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "testattr": identityschema.StringAttribute{}, + }, + }, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyInt to schema"), + }, + "ElementKeyString": { + schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "testattr": identityschema.StringAttribute{}, + }, + }, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyString to schema"), + }, + "ElementKeyValue": { + schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "testattr": identityschema.StringAttribute{}, + }, + }, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyValue to schema"), + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.schema.ApplyTerraform5AttributePathStep(testCase.step) + + if err != nil { + if testCase.expectedError == nil { + t.Fatalf("expected no error, got: %s", err) + } + + if !strings.Contains(err.Error(), testCase.expectedError.Error()) { + t.Fatalf("expected error %q, got: %s", testCase.expectedError, err) + } + } + + if err == nil && testCase.expectedError != nil { + t.Fatalf("got no error, expected: %s", testCase.expectedError) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSchemaAttributeAtPath(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schema identityschema.Schema + path path.Path + expected fwschema.Attribute + expectedDiags diag.Diagnostics + }{ + "empty-root": { + schema: identityschema.Schema{}, + path: path.Empty(), + expected: nil, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Empty(), + "Invalid Schema Path", + "When attempting to get the framework attribute associated with a schema path, an unexpected error was returned. "+ + "This is always an issue with the provider. Please report this to the provider developers.\n\n"+ + "Path: \n"+ + "Original Error: got unexpected type identityschema.Schema", + ), + }, + }, + "root": { + schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test": identityschema.StringAttribute{}, + }, + }, + path: path.Empty(), + expected: nil, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Empty(), + "Invalid Schema Path", + "When attempting to get the framework attribute associated with a schema path, an unexpected error was returned. "+ + "This is always an issue with the provider. Please report this to the provider developers.\n\n"+ + "Path: \n"+ + "Original Error: got unexpected type identityschema.Schema", + ), + }, + }, + "WithAttributeName-attribute": { + schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "other": identityschema.BoolAttribute{}, + "test": identityschema.StringAttribute{}, + }, + }, + path: path.Root("test"), + expected: identityschema.StringAttribute{}, + }, + "WithElementKeyInt": { + schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test": identityschema.StringAttribute{}, + }, + }, + path: path.Empty().AtListIndex(0), + expected: nil, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Empty().AtListIndex(0), + "Invalid Schema Path", + "When attempting to get the framework attribute associated with a schema path, an unexpected error was returned. "+ + "This is always an issue with the provider. Please report this to the provider developers.\n\n"+ + "Path: [0]\n"+ + "Original Error: ElementKeyInt(0) still remains in the path: cannot apply AttributePathStep tftypes.ElementKeyInt to schema", + ), + }, + }, + "WithElementKeyString": { + schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test": identityschema.StringAttribute{}, + }, + }, + path: path.Empty().AtMapKey("test"), + expected: nil, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Empty().AtMapKey("test"), + "Invalid Schema Path", + "When attempting to get the framework attribute associated with a schema path, an unexpected error was returned. "+ + "This is always an issue with the provider. Please report this to the provider developers.\n\n"+ + "Path: [\"test\"]\n"+ + "Original Error: ElementKeyString(\"test\") still remains in the path: cannot apply AttributePathStep tftypes.ElementKeyString to schema", + ), + }, + }, + "WithElementKeyValue": { + schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test": identityschema.StringAttribute{}, + }, + }, + path: path.Empty().AtSetValue(types.StringValue("test")), + expected: nil, + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Empty().AtSetValue(types.StringValue("test")), + "Invalid Schema Path", + "When attempting to get the framework attribute associated with a schema path, an unexpected error was returned. "+ + "This is always an issue with the provider. Please report this to the provider developers.\n\n"+ + "Path: [Value(\"test\")]\n"+ + "Original Error: ElementKeyValue(tftypes.String<\"test\">) still remains in the path: cannot apply AttributePathStep tftypes.ElementKeyValue to schema", + ), + }, + }, + } + + for name, tc := range testCases { + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, diags := tc.schema.AttributeAtPath(context.Background(), tc.path) + + if diff := cmp.Diff(diags, tc.expectedDiags); diff != "" { + t.Errorf("Unexpected diagnostics (+wanted, -got): %s", diff) + } + + if diff := cmp.Diff(got, tc.expected); diff != "" { + t.Errorf("Unexpected result (+wanted, -got): %s", diff) + } + }) + } +} + +func TestSchemaAttributeAtTerraformPath(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schema identityschema.Schema + path *tftypes.AttributePath + expected fwschema.Attribute + expectedErr string + }{ + "empty-root": { + schema: identityschema.Schema{}, + path: tftypes.NewAttributePath(), + expected: nil, + expectedErr: "got unexpected type identityschema.Schema", + }, + "empty-nil": { + schema: identityschema.Schema{}, + path: nil, + expected: nil, + expectedErr: "got unexpected type identityschema.Schema", + }, + "root": { + schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test": identityschema.StringAttribute{}, + }, + }, + path: tftypes.NewAttributePath(), + expected: nil, + expectedErr: "got unexpected type identityschema.Schema", + }, + "nil": { + schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test": identityschema.StringAttribute{}, + }, + }, + path: nil, + expected: nil, + expectedErr: "got unexpected type identityschema.Schema", + }, + "WithAttributeName-attribute": { + schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "other": identityschema.BoolAttribute{}, + "test": identityschema.StringAttribute{}, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("test"), + expected: identityschema.StringAttribute{}, + }, + "WithElementKeyInt": { + schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test": identityschema.StringAttribute{}, + }, + }, + path: tftypes.NewAttributePath().WithElementKeyInt(0), + expected: nil, + expectedErr: "ElementKeyInt(0) still remains in the path: cannot apply AttributePathStep tftypes.ElementKeyInt to schema", + }, + "WithElementKeyString": { + schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test": identityschema.StringAttribute{}, + }, + }, + path: tftypes.NewAttributePath().WithElementKeyString("test"), + expected: nil, + expectedErr: "ElementKeyString(\"test\") still remains in the path: cannot apply AttributePathStep tftypes.ElementKeyString to schema", + }, + "WithElementKeyValue": { + schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test": identityschema.StringAttribute{}, + }, + }, + path: tftypes.NewAttributePath().WithElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedErr: "ElementKeyValue(tftypes.String<\"test\">) still remains in the path: cannot apply AttributePathStep tftypes.ElementKeyValue to schema", + }, + } + + for name, tc := range testCases { + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := tc.schema.AttributeAtTerraformPath(context.Background(), tc.path) + + if err != nil { + if tc.expectedErr == "" { + t.Errorf("Unexpected error: %s", err) + return + } + if err.Error() != tc.expectedErr { + t.Errorf("Expected error to be %q, got %q", tc.expectedErr, err.Error()) + return + } + // got expected error + return + } + + if err == nil && tc.expectedErr != "" { + t.Errorf("Expected error to be %q, got nil", tc.expectedErr) + return + } + + if diff := cmp.Diff(got, tc.expected); diff != "" { + t.Errorf("Unexpected result (+wanted, -got): %s", diff) + } + }) + } +} + +func TestSchemaGetAttributes(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schema identityschema.Schema + expected map[string]fwschema.Attribute + }{ + "no-attributes": { + schema: identityschema.Schema{}, + expected: map[string]fwschema.Attribute{}, + }, + "attributes": { + schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "testattr1": identityschema.StringAttribute{}, + "testattr2": identityschema.StringAttribute{}, + }, + }, + expected: map[string]fwschema.Attribute{ + "testattr1": identityschema.StringAttribute{}, + "testattr2": identityschema.StringAttribute{}, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.schema.GetAttributes() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSchemaGetBlocks(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schema identityschema.Schema + expected map[string]fwschema.Block + }{ + "no-blocks": { + schema: identityschema.Schema{}, + expected: map[string]fwschema.Block{}, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.schema.GetBlocks() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSchemaGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schema identityschema.Schema + expected string + }{ + "no-deprecation-message": { + schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "testattr": identityschema.StringAttribute{}, + }, + }, + expected: "", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.schema.GetDeprecationMessage() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSchemaGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schema identityschema.Schema + expected string + }{ + "no-description": { + schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "testattr": identityschema.StringAttribute{}, + }, + }, + expected: "", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.schema.GetDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSchemaGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schema identityschema.Schema + expected string + }{ + "no-markdown-description": { + schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "testattr": identityschema.StringAttribute{}, + }, + }, + expected: "", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.schema.GetMarkdownDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSchemaGetVersion(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schema identityschema.Schema + expected int64 + }{ + "no-version": { + schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "testattr": identityschema.StringAttribute{}, + }, + }, + expected: 0, + }, + "version": { + schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "testattr": identityschema.StringAttribute{}, + }, + Version: 1, + }, + expected: 1, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.schema.GetVersion() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSchemaType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schema identityschema.Schema + expected attr.Type + }{ + "base": { + schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "testattr": identityschema.StringAttribute{}, + }, + }, + expected: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "testattr": types.StringType, + }, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.schema.Type() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSchemaTypeAtPath(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schema identityschema.Schema + path path.Path + expected attr.Type + expectedDiags diag.Diagnostics + }{ + "empty-schema-empty-path": { + schema: identityschema.Schema{}, + path: path.Empty(), + expected: types.ObjectType{}, + }, + "empty-path": { + schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "bool": identityschema.BoolAttribute{}, + "string": identityschema.StringAttribute{}, + }, + }, + path: path.Empty(), + expected: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "bool": types.BoolType, + "string": types.StringType, + }, + }, + }, + "AttributeName-Attribute": { + schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "bool": identityschema.BoolAttribute{}, + "string": identityschema.StringAttribute{}, + }, + }, + path: path.Root("string"), + expected: types.StringType, + }, + "AttributeName-non-existent": { + schema: identityschema.Schema{}, + path: path.Root("non-existent"), + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("non-existent"), + "Invalid Schema Path", + "When attempting to get the framework type associated with a schema path, an unexpected error was returned. This is always an issue with the provider. Please report this to the provider developers.\n\n"+ + "Path: non-existent\n"+ + "Original Error: AttributeName(\"non-existent\") still remains in the path: could not find attribute or block \"non-existent\" in schema", + ), + }, + }, + "ElementKeyInt": { + schema: identityschema.Schema{}, + path: path.Empty().AtListIndex(0), + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Empty().AtListIndex(0), + "Invalid Schema Path", + "When attempting to get the framework type associated with a schema path, an unexpected error was returned. This is always an issue with the provider. Please report this to the provider developers.\n\n"+ + "Path: [0]\n"+ + "Original Error: ElementKeyInt(0) still remains in the path: cannot apply AttributePathStep tftypes.ElementKeyInt to schema", + ), + }, + }, + "ElementKeyString": { + schema: identityschema.Schema{}, + path: path.Empty().AtMapKey("invalid"), + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Empty().AtMapKey("invalid"), + "Invalid Schema Path", + "When attempting to get the framework type associated with a schema path, an unexpected error was returned. This is always an issue with the provider. Please report this to the provider developers.\n\n"+ + "Path: [\"invalid\"]\n"+ + "Original Error: ElementKeyString(\"invalid\") still remains in the path: cannot apply AttributePathStep tftypes.ElementKeyString to schema", + ), + }, + }, + "ElementKeyValue": { + schema: identityschema.Schema{}, + path: path.Empty().AtSetValue(types.StringNull()), + expectedDiags: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Empty().AtSetValue(types.StringNull()), + "Invalid Schema Path", + "When attempting to get the framework type associated with a schema path, an unexpected error was returned. This is always an issue with the provider. Please report this to the provider developers.\n\n"+ + "Path: [Value()]\n"+ + "Original Error: ElementKeyValue(tftypes.String) still remains in the path: cannot apply AttributePathStep tftypes.ElementKeyValue to schema", + ), + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, diags := testCase.schema.TypeAtPath(context.Background(), testCase.path) + + if diff := cmp.Diff(diags, testCase.expectedDiags); diff != "" { + t.Errorf("unexpected diagnostics difference: %s", diff) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSchemaTypeAtTerraformPath(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schema identityschema.Schema + path *tftypes.AttributePath + expected attr.Type + expectedError error + }{ + "empty-schema-nil-path": { + schema: identityschema.Schema{}, + path: nil, + expected: types.ObjectType{}, + }, + "empty-schema-empty-path": { + schema: identityschema.Schema{}, + path: tftypes.NewAttributePath(), + expected: types.ObjectType{}, + }, + "nil-path": { + schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "bool": identityschema.BoolAttribute{}, + "string": identityschema.StringAttribute{}, + }, + }, + path: nil, + expected: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "bool": types.BoolType, + "string": types.StringType, + }, + }, + }, + "empty-path": { + schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "bool": identityschema.BoolAttribute{}, + "string": identityschema.StringAttribute{}, + }, + }, + path: tftypes.NewAttributePath(), + expected: types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "bool": types.BoolType, + "string": types.StringType, + }, + }, + }, + "AttributeName-Attribute": { + schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "bool": identityschema.BoolAttribute{}, + "string": identityschema.StringAttribute{}, + }, + }, + path: tftypes.NewAttributePath().WithAttributeName("string"), + expected: types.StringType, + }, + "AttributeName-non-existent": { + schema: identityschema.Schema{}, + path: tftypes.NewAttributePath().WithAttributeName("non-existent"), + expectedError: fmt.Errorf("AttributeName(\"non-existent\") still remains in the path: could not find attribute or block \"non-existent\" in schema"), + }, + "ElementKeyInt": { + schema: identityschema.Schema{}, + path: tftypes.NewAttributePath().WithElementKeyInt(0), + expectedError: fmt.Errorf("ElementKeyInt(0) still remains in the path: cannot apply AttributePathStep tftypes.ElementKeyInt to schema"), + }, + "ElementKeyString": { + schema: identityschema.Schema{}, + path: tftypes.NewAttributePath().WithElementKeyString("invalid"), + expectedError: fmt.Errorf("ElementKeyString(\"invalid\") still remains in the path: cannot apply AttributePathStep tftypes.ElementKeyString to schema"), + }, + "ElementKeyValue": { + schema: identityschema.Schema{}, + path: tftypes.NewAttributePath().WithElementKeyValue(tftypes.NewValue(tftypes.String, nil)), + expectedError: fmt.Errorf("ElementKeyValue(tftypes.String) still remains in the path: cannot apply AttributePathStep tftypes.ElementKeyValue to schema"), + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.schema.TypeAtTerraformPath(context.Background(), testCase.path) + + if err != nil { + if testCase.expectedError == nil { + t.Fatalf("expected no error, got: %s", err) + } + + if !strings.Contains(err.Error(), testCase.expectedError.Error()) { + t.Fatalf("expected error %q, got: %s", testCase.expectedError, err) + } + } + + if err == nil && testCase.expectedError != nil { + t.Fatalf("got no error, expected: %s", testCase.expectedError) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSchemaValidate(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schema identityschema.Schema + expectedDiags diag.Diagnostics + }{ + "empty-schema": { + schema: identityschema.Schema{}, + }, + "validate-implementation-error": { + schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "depends_on": identityschema.StringAttribute{}, + }, + }, + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Reserved Root Attribute/Block Name", + "When validating the resource or data source schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"depends_on\" is a reserved root attribute/block name. "+ + "This is to prevent practitioners from needing special Terraform configuration syntax.", + ), + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + diags := testCase.schema.Validate() + + if diff := cmp.Diff(diags, testCase.expectedDiags); diff != "" { + t.Errorf("Unexpected diagnostics (+wanted, -got): %s", diff) + } + }) + } +} + +func TestSchemaValidateImplementation(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + schema identityschema.Schema + expectedDiags diag.Diagnostics + }{ + "empty-schema": { + schema: identityschema.Schema{}, + }, + "attribute-using-reserved-field-name": { + schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "depends_on": identityschema.StringAttribute{}, + }, + }, + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Reserved Root Attribute/Block Name", + "When validating the resource or data source schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"depends_on\" is a reserved root attribute/block name. "+ + "This is to prevent practitioners from needing special Terraform configuration syntax.", + ), + }, + }, + "attribute-using-invalid-field-name": { + schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "^": identityschema.StringAttribute{}, + }, + }, + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Attribute/Block Name", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"^\" at schema path \"^\" is an invalid attribute/block name. "+ + "Names must only contain lowercase alphanumeric characters (a-z, 0-9) and underscores (_).", + ), + }, + }, + "attribute-with-validate-attribute-implementation-error": { + schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test": identityschema.ListAttribute{ + RequiredForImport: true, + }, + }, + }, + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Attribute Implementation", + "When validating the schema, an implementation issue was found. "+ + "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ + "\"test\" is missing the CustomType or ElementType field on a collection Attribute. "+ + "One of these fields is required to prevent other unexpected errors or panics.", + ), + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + diags := testCase.schema.ValidateImplementation(context.Background()) + + if diff := cmp.Diff(diags, testCase.expectedDiags); diff != "" { + t.Errorf("Unexpected diagnostics (+wanted, -got): %s", diff) + } + }) + } +} diff --git a/resource/identityschema/string_attribute.go b/resource/identityschema/string_attribute.go new file mode 100644 index 000000000..3061b89cf --- /dev/null +++ b/resource/identityschema/string_attribute.go @@ -0,0 +1,127 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package identityschema + +import ( + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +// Ensure the implementation satisfies the desired interfaces. +var ( + _ Attribute = StringAttribute{} +) + +// StringAttribute represents a schema attribute that is a string. When +// retrieving the value for this attribute, use types.String as the value type +// unless the CustomType field is set. +// +// Terraform configurations configure this attribute using expressions that +// return a string or directly via double quote syntax. +// +// example_attribute = "value" +type StringAttribute struct { + // CustomType enables the use of a custom attribute type in place of the + // default basetypes.StringType. When retrieving data, the basetypes.StringValuable + // associated with this custom type must be used in place of types.String. + CustomType basetypes.StringTypable + + // RequiredForImport indicates whether the practitioner must enter a value for + // this attribute when importing a managed resource by this identity. + // RequiredForImport and OptionalForImport cannot both be true. + RequiredForImport bool + + // OptionalForImport indicates whether the practitioner can choose to enter a value + // for this attribute when importing a managed resource by this identity. + // OptionalForImport and RequiredForImport cannot both be true. + OptionalForImport bool + + // Description is used in various tooling, like the language server or the documentation + // generator, to give practitioners more information about what this attribute is, + // what it's for, and how it should be used. It can be written as plain text with no + // special formatting, or formatted as Markdown. + Description string +} + +// ApplyTerraform5AttributePathStep always returns an error as it is not +// possible to step further into a StringAttribute. +func (a StringAttribute) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) { + return a.GetType().ApplyTerraform5AttributePathStep(step) +} + +// Equal returns true if the given Attribute is a StringAttribute +// and all fields are equal. +func (a StringAttribute) Equal(o fwschema.Attribute) bool { + if _, ok := o.(StringAttribute); !ok { + return false + } + + return fwschema.AttributesEqual(a, o) +} + +// GetDeprecationMessage returns an empty string as identity attributes cannot +// surface deprecation messages. +func (a StringAttribute) GetDeprecationMessage() string { + return "" +} + +// GetDescription returns the Description field value. For identity attributes, +// there is only a single description field that is permitted to contain plaintext or Markdown. +func (a StringAttribute) GetDescription() string { + return a.Description +} + +// GetMarkdownDescription returns the Description field value. For identity attributes, +// there is only a single description field that is permitted to contain Markdown or plaintext. +func (a StringAttribute) GetMarkdownDescription() string { + return a.Description +} + +// GetType returns types.StringType or the CustomType field value if defined. +func (a StringAttribute) GetType() attr.Type { + if a.CustomType != nil { + return a.CustomType + } + + return types.StringType +} + +// IsComputed returns false as it's not relevant for identity schemas. +func (a StringAttribute) IsComputed() bool { + return false +} + +// IsOptional returns false as it's not relevant for identity schemas. +func (a StringAttribute) IsOptional() bool { + return false +} + +// IsRequired returns false as it's not relevant for identity schemas. +func (a StringAttribute) IsRequired() bool { + return false +} + +// IsSensitive returns false as it's not relevant for identity schemas. +func (a StringAttribute) IsSensitive() bool { + return false +} + +// IsWriteOnly returns false as it's not relevant for identity schemas. +func (a StringAttribute) IsWriteOnly() bool { + return false +} + +// IsRequiredForImport returns the RequiredForImport field value. +func (a StringAttribute) IsRequiredForImport() bool { + return a.RequiredForImport +} + +// IsOptionalForImport returns the OptionalForImport field value. +func (a StringAttribute) IsOptionalForImport() bool { + return a.OptionalForImport +} diff --git a/resource/identityschema/string_attribute_test.go b/resource/identityschema/string_attribute_test.go new file mode 100644 index 000000000..18f6666e6 --- /dev/null +++ b/resource/identityschema/string_attribute_test.go @@ -0,0 +1,431 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package identityschema_test + +import ( + "fmt" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestStringAttributeApplyTerraform5AttributePathStep(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.StringAttribute + step tftypes.AttributePathStep + expected any + expectedError error + }{ + "AttributeName": { + attribute: identityschema.StringAttribute{}, + step: tftypes.AttributeName("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.AttributeName to basetypes.StringType"), + }, + "ElementKeyInt": { + attribute: identityschema.StringAttribute{}, + step: tftypes.ElementKeyInt(1), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyInt to basetypes.StringType"), + }, + "ElementKeyString": { + attribute: identityschema.StringAttribute{}, + step: tftypes.ElementKeyString("test"), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyString to basetypes.StringType"), + }, + "ElementKeyValue": { + attribute: identityschema.StringAttribute{}, + step: tftypes.ElementKeyValue(tftypes.NewValue(tftypes.String, "test")), + expected: nil, + expectedError: fmt.Errorf("cannot apply AttributePathStep tftypes.ElementKeyValue to basetypes.StringType"), + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.attribute.ApplyTerraform5AttributePathStep(testCase.step) + + if err != nil { + if testCase.expectedError == nil { + t.Fatalf("expected no error, got: %s", err) + } + + if !strings.Contains(err.Error(), testCase.expectedError.Error()) { + t.Fatalf("expected error %q, got: %s", testCase.expectedError, err) + } + } + + if err == nil && testCase.expectedError != nil { + t.Fatalf("got no error, expected: %s", testCase.expectedError) + } + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestStringAttributeGetDeprecationMessage(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.StringAttribute + expected string + }{ + "no-deprecation-message": { + attribute: identityschema.StringAttribute{}, + expected: "", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDeprecationMessage() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestStringAttributeEqual(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.StringAttribute + other fwschema.Attribute + expected bool + }{ + "different-type": { + attribute: identityschema.StringAttribute{}, + other: testschema.AttributeWithStringValidators{}, + expected: false, + }, + "equal": { + attribute: identityschema.StringAttribute{}, + other: identityschema.StringAttribute{}, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.Equal(testCase.other) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestStringAttributeGetDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.StringAttribute + expected string + }{ + "no-description": { + attribute: identityschema.StringAttribute{}, + expected: "", + }, + "description": { + attribute: identityschema.StringAttribute{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestStringAttributeGetMarkdownDescription(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.StringAttribute + expected string + }{ + "no-markdown-description": { + attribute: identityschema.StringAttribute{}, + expected: "", + }, + "markdown-description-from-description": { + attribute: identityschema.StringAttribute{ + Description: "test description", + }, + expected: "test description", + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetMarkdownDescription() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestStringAttributeGetType(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.StringAttribute + expected attr.Type + }{ + "base": { + attribute: identityschema.StringAttribute{}, + expected: types.StringType, + }, + "custom-type": { + attribute: identityschema.StringAttribute{ + CustomType: testtypes.StringType{}, + }, + expected: testtypes.StringType{}, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.GetType() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestStringAttributeIsComputed(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.StringAttribute + expected bool + }{ + "not-computed": { + attribute: identityschema.StringAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsComputed() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestStringAttributeIsOptional(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.StringAttribute + expected bool + }{ + "not-optional": { + attribute: identityschema.StringAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptional() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestStringAttributeIsRequired(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.StringAttribute + expected bool + }{ + "not-required": { + attribute: identityschema.StringAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequired() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestStringAttributeIsSensitive(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.StringAttribute + expected bool + }{ + "not-sensitive": { + attribute: identityschema.StringAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsSensitive() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestStringAttributeIsWriteOnly(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.StringAttribute + expected bool + }{ + "not-writeOnly": { + attribute: identityschema.StringAttribute{}, + expected: false, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsWriteOnly() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestStringAttributeIsRequiredForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.StringAttribute + expected bool + }{ + "not-requiredForImport": { + attribute: identityschema.StringAttribute{}, + expected: false, + }, + "requiredForImport": { + attribute: identityschema.StringAttribute{ + RequiredForImport: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsRequiredForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestStringAttributeIsOptionalForImport(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + attribute identityschema.StringAttribute + expected bool + }{ + "not-optionalForImport": { + attribute: identityschema.StringAttribute{}, + expected: false, + }, + "optionalForImport": { + attribute: identityschema.StringAttribute{ + OptionalForImport: true, + }, + expected: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.attribute.IsOptionalForImport() + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/resource/resource.go b/resource/resource.go index 7113d4bf5..ce2694a28 100644 --- a/resource/resource.go +++ b/resource/resource.go @@ -196,3 +196,13 @@ type ResourceWithValidateConfig interface { // ValidateConfig performs the validation. ValidateConfig(context.Context, ValidateConfigRequest, *ValidateConfigResponse) } + +// ResourceWithIdentity is an interface type that extends Resource to implement managed resource identity. +// +// TODO: Describe identity concept in more detail, reference the upgrade identity interface +type ResourceWithIdentity interface { + Resource + + // IdentitySchema should return the identity schema for this resource. + IdentitySchema(context.Context, IdentitySchemaRequest, *IdentitySchemaResponse) +} From 63dc8256674896f1155d28953ae879c3d851fd39 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Thu, 6 Mar 2025 17:42:14 -0500 Subject: [PATCH 10/20] protov6 + tests --- .../fromproto6/getresourceidentityschemas.go | 23 ++ .../getresourceidentityschemas_test.go | 44 +++ .../server_getresourceidentityschemas.go | 27 ++ .../server_getresourceidentityschemas_test.go | 218 +++++++++++ .../server_upgraderesourceidentity.go | 15 + .../toproto6/getresourceidentityschemas.go | 42 ++ .../getresourceidentityschemas_test.go | 367 ++++++++++++++++++ internal/toproto6/identity_schema.go | 52 +++ .../toproto6/identity_schema_attribute.go | 43 ++ .../identity_schema_attribute_test.go | 257 ++++++++++++ internal/toproto6/identity_schema_test.go | 138 +++++++ 11 files changed, 1226 insertions(+) create mode 100644 internal/fromproto6/getresourceidentityschemas.go create mode 100644 internal/fromproto6/getresourceidentityschemas_test.go create mode 100644 internal/proto6server/server_getresourceidentityschemas.go create mode 100644 internal/proto6server/server_getresourceidentityschemas_test.go create mode 100644 internal/proto6server/server_upgraderesourceidentity.go create mode 100644 internal/toproto6/getresourceidentityschemas.go create mode 100644 internal/toproto6/getresourceidentityschemas_test.go create mode 100644 internal/toproto6/identity_schema.go create mode 100644 internal/toproto6/identity_schema_attribute.go create mode 100644 internal/toproto6/identity_schema_attribute_test.go create mode 100644 internal/toproto6/identity_schema_test.go diff --git a/internal/fromproto6/getresourceidentityschemas.go b/internal/fromproto6/getresourceidentityschemas.go new file mode 100644 index 000000000..448c5b56a --- /dev/null +++ b/internal/fromproto6/getresourceidentityschemas.go @@ -0,0 +1,23 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fromproto6 + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" +) + +// GetResourceIdentitySchemasRequest returns the *fwserver.GetResourceIdentitySchemasRequest +// equivalent of a *tfprotov6.GetResourceIdentitySchemasRequest. +func GetResourceIdentitySchemasRequest(ctx context.Context, proto6 *tfprotov6.GetResourceIdentitySchemasRequest) *fwserver.GetResourceIdentitySchemasRequest { + if proto6 == nil { + return nil + } + + fw := &fwserver.GetResourceIdentitySchemasRequest{} + + return fw +} diff --git a/internal/fromproto6/getresourceidentityschemas_test.go b/internal/fromproto6/getresourceidentityschemas_test.go new file mode 100644 index 000000000..22ab51200 --- /dev/null +++ b/internal/fromproto6/getresourceidentityschemas_test.go @@ -0,0 +1,44 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fromproto6_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/internal/fromproto6" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" +) + +func TestGetResourceIdentitySchemasRequest(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + input *tfprotov6.GetResourceIdentitySchemasRequest + expected *fwserver.GetResourceIdentitySchemasRequest + }{ + "nil": { + input: nil, + expected: nil, + }, + "empty": { + input: &tfprotov6.GetResourceIdentitySchemasRequest{}, + expected: &fwserver.GetResourceIdentitySchemasRequest{}, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := fromproto6.GetResourceIdentitySchemasRequest(context.Background(), testCase.input) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/internal/proto6server/server_getresourceidentityschemas.go b/internal/proto6server/server_getresourceidentityschemas.go new file mode 100644 index 000000000..5258d024c --- /dev/null +++ b/internal/proto6server/server_getresourceidentityschemas.go @@ -0,0 +1,27 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package proto6server + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/internal/fromproto6" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/logging" + "github.com/hashicorp/terraform-plugin-framework/internal/toproto6" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" +) + +// GetResourceIdentitySchemas satisfies the tfprotov6.ProviderServer interface. +func (s *Server) GetResourceIdentitySchemas(ctx context.Context, proto6Req *tfprotov6.GetResourceIdentitySchemasRequest) (*tfprotov6.GetResourceIdentitySchemasResponse, error) { + ctx = s.registerContext(ctx) + ctx = logging.InitContext(ctx) + + fwReq := fromproto6.GetResourceIdentitySchemasRequest(ctx, proto6Req) + fwResp := &fwserver.GetResourceIdentitySchemasResponse{} + + s.FrameworkServer.GetResourceIdentitySchemas(ctx, fwReq, fwResp) + + return toproto6.GetResourceIdentitySchemasResponse(ctx, fwResp), nil +} diff --git a/internal/proto6server/server_getresourceidentityschemas_test.go b/internal/proto6server/server_getresourceidentityschemas_test.go new file mode 100644 index 000000000..d5d7ddfca --- /dev/null +++ b/internal/proto6server/server_getresourceidentityschemas_test.go @@ -0,0 +1,218 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package proto6server + +import ( + "bytes" + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/logging" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/hashicorp/terraform-plugin-log/tfsdklogtest" +) + +func TestServerGetResourceIdentitySchemas(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + server *Server + request *tfprotov6.GetResourceIdentitySchemasRequest + expectedError error + expectedResponse *tfprotov6.GetResourceIdentitySchemasResponse + }{ + "resource-identity-schemas": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + ResourcesMethod: func(_ context.Context) []func() resource.Resource { + return []func() resource.Resource{ + func() resource.Resource { + return &testprovider.ResourceWithIdentity{ + Resource: &testprovider.Resource{ + MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "test_resource1" + }, + }, + IdentitySchemaMethod: func(_ context.Context, _ resource.IdentitySchemaRequest, resp *resource.IdentitySchemaResponse) { + resp.IdentitySchema = identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test1": identityschema.StringAttribute{ + RequiredForImport: true, + }, + }, + } + }, + } + }, + func() resource.Resource { + return &testprovider.ResourceWithIdentity{ + Resource: &testprovider.Resource{ + MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "test_resource2" + }, + }, + IdentitySchemaMethod: func(_ context.Context, _ resource.IdentitySchemaRequest, resp *resource.IdentitySchemaResponse) { + resp.IdentitySchema = identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test2": identityschema.BoolAttribute{ + RequiredForImport: true, + }, + }, + } + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov6.GetResourceIdentitySchemasRequest{}, + expectedResponse: &tfprotov6.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]*tfprotov6.ResourceIdentitySchema{ + "test_resource1": { + IdentityAttributes: []*tfprotov6.ResourceIdentitySchemaAttribute{ + { + Name: "test1", + RequiredForImport: true, + Type: tftypes.String, + }, + }, + }, + "test_resource2": { + IdentityAttributes: []*tfprotov6.ResourceIdentitySchemaAttribute{ + { + Name: "test2", + RequiredForImport: true, + Type: tftypes.Bool, + }, + }, + }, + }, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := testCase.server.GetResourceIdentitySchemas(context.Background(), new(tfprotov6.GetResourceIdentitySchemasRequest)) + + if diff := cmp.Diff(testCase.expectedError, err); diff != "" { + t.Errorf("unexpected error difference: %s", diff) + } + + if diff := cmp.Diff(testCase.expectedResponse, got); diff != "" { + t.Errorf("unexpected response difference: %s", diff) + } + }) + } +} + +func TestServerGetResourceIdentitySchemas_logging(t *testing.T) { + t.Parallel() + + var output bytes.Buffer + + ctx := tfsdklogtest.RootLogger(context.Background(), &output) + ctx = logging.InitContext(ctx) + + testServer := &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + ResourcesMethod: func(ctx context.Context) []func() resource.Resource { + return []func() resource.Resource{ + func() resource.Resource { + return &testprovider.ResourceWithIdentity{ + Resource: &testprovider.Resource{ + MetadataMethod: func(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "examplecloud_thing" + }, + }, + IdentitySchemaMethod: func(ctx context.Context, req resource.IdentitySchemaRequest, resp *resource.IdentitySchemaResponse) { + resp.IdentitySchema = identityschema.Schema{} + }, + } + }, + } + }, + }, + }, + } + + _, err := testServer.GetResourceIdentitySchemas(ctx, new(tfprotov6.GetResourceIdentitySchemasRequest)) + + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + entries, err := tfsdklogtest.MultilineJSONDecode(&output) + + if err != nil { + t.Fatalf("unable to read multiple line JSON: %s", err) + } + + expectedEntries := []map[string]interface{}{ + { + "@level": "trace", + "@message": "Checking ResourceTypes lock", + "@module": "sdk.framework", + }, + { + "@level": "trace", + "@message": "Checking ProviderTypeName lock", + "@module": "sdk.framework", + }, + { + "@level": "trace", + "@message": "Calling provider defined Provider Metadata", + "@module": "sdk.framework", + }, + { + "@level": "trace", + "@message": "Called provider defined Provider Metadata", + "@module": "sdk.framework", + }, + { + "@level": "trace", + "@message": "Calling provider defined Provider Resources", + "@module": "sdk.framework", + }, + { + "@level": "trace", + "@message": "Called provider defined Provider Resources", + "@module": "sdk.framework", + }, + { + "@level": "trace", + "@message": "Found resource type", + "@module": "sdk.framework", + "tf_resource_type": "examplecloud_thing", + }, + { + "@level": "trace", + "@message": "Calling provider defined Resource IdentitySchema method", + "@module": "sdk.framework", + "tf_resource_type": "examplecloud_thing", + }, + { + "@level": "trace", + "@message": "Called provider defined Resource IdentitySchema method", + "@module": "sdk.framework", + "tf_resource_type": "examplecloud_thing", + }, + } + + if diff := cmp.Diff(entries, expectedEntries); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } +} diff --git a/internal/proto6server/server_upgraderesourceidentity.go b/internal/proto6server/server_upgraderesourceidentity.go new file mode 100644 index 000000000..d5f55ab23 --- /dev/null +++ b/internal/proto6server/server_upgraderesourceidentity.go @@ -0,0 +1,15 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package proto6server + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-go/tfprotov6" +) + +// UpgradeResourceIdentity satisfies the tfprotov6.ProviderServer interface. +func (s *Server) UpgradeResourceIdentity(ctx context.Context, proto6Req *tfprotov6.UpgradeResourceIdentityRequest) (*tfprotov6.UpgradeResourceIdentityResponse, error) { + panic("unimplemented") // TODO: implement +} diff --git a/internal/toproto6/getresourceidentityschemas.go b/internal/toproto6/getresourceidentityschemas.go new file mode 100644 index 000000000..eb5d406e2 --- /dev/null +++ b/internal/toproto6/getresourceidentityschemas.go @@ -0,0 +1,42 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto6 + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" +) + +// GetResourceIdentitySchemasResponse returns the *tfprotov6.GetResourceIdentitySchemasResponse +// equivalent of a *fwserver.GetResourceIdentitySchemasResponse. +func GetResourceIdentitySchemasResponse(ctx context.Context, fw *fwserver.GetResourceIdentitySchemasResponse) *tfprotov6.GetResourceIdentitySchemasResponse { + if fw == nil { + return nil + } + + protov6 := &tfprotov6.GetResourceIdentitySchemasResponse{ + Diagnostics: Diagnostics(ctx, fw.Diagnostics), + IdentitySchemas: make(map[string]*tfprotov6.ResourceIdentitySchema, len(fw.IdentitySchemas)), + } + + var err error + + for resourceType, identitySchema := range fw.IdentitySchemas { + protov6.IdentitySchemas[resourceType], err = IdentitySchema(ctx, identitySchema) + + if err != nil { + protov6.Diagnostics = append(protov6.Diagnostics, &tfprotov6.Diagnostic{ + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Error converting resource identity schema", + Detail: "The identity schema for the resource \"" + resourceType + "\" couldn't be converted into a usable type. This is always a problem with the provider. Please report the following to the provider developer:\n\n" + err.Error(), + }) + + return protov6 + } + } + + return protov6 +} diff --git a/internal/toproto6/getresourceidentityschemas_test.go b/internal/toproto6/getresourceidentityschemas_test.go new file mode 100644 index 000000000..86670bdf4 --- /dev/null +++ b/internal/toproto6/getresourceidentityschemas_test.go @@ -0,0 +1,367 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto6_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" + "github.com/hashicorp/terraform-plugin-framework/internal/toproto6" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func TestGetResourceIdentitySchemasResponse(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + input *fwserver.GetResourceIdentitySchemasResponse + expected *tfprotov6.GetResourceIdentitySchemasResponse + }{ + "nil": { + input: nil, + expected: nil, + }, + "resource-identity-identity-multiple-resources": { + input: &fwserver.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]fwschema.Schema{ + "test_resource_1": identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_attribute": identityschema.BoolAttribute{ + RequiredForImport: true, + }, + }, + }, + "test_resource_2": identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_attribute": identityschema.BoolAttribute{ + RequiredForImport: true, + }, + }, + }, + }, + }, + expected: &tfprotov6.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]*tfprotov6.ResourceIdentitySchema{ + "test_resource_1": { + IdentityAttributes: []*tfprotov6.ResourceIdentitySchemaAttribute{ + { + RequiredForImport: true, + Name: "test_attribute", + Type: tftypes.Bool, + }, + }, + }, + "test_resource_2": { + IdentityAttributes: []*tfprotov6.ResourceIdentitySchemaAttribute{ + { + RequiredForImport: true, + Name: "test_attribute", + Type: tftypes.Bool, + }, + }, + }, + }, + }, + }, + "resource-identity-attribute-optionalforimport": { + input: &fwserver.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]fwschema.Schema{ + "test_resource": identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_attribute": identityschema.BoolAttribute{ + OptionalForImport: true, + }, + }, + }, + }, + }, + expected: &tfprotov6.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]*tfprotov6.ResourceIdentitySchema{ + "test_resource": { + IdentityAttributes: []*tfprotov6.ResourceIdentitySchemaAttribute{ + { + Name: "test_attribute", + OptionalForImport: true, + Type: tftypes.Bool, + }, + }, + }, + }, + }, + }, + "resource-identity-attribute-requiredforimport": { + input: &fwserver.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]fwschema.Schema{ + "test_resource": identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_attribute": identityschema.BoolAttribute{ + RequiredForImport: true, + }, + }, + }, + }, + }, + expected: &tfprotov6.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]*tfprotov6.ResourceIdentitySchema{ + "test_resource": { + IdentityAttributes: []*tfprotov6.ResourceIdentitySchemaAttribute{ + { + Name: "test_attribute", + Type: tftypes.Bool, + RequiredForImport: true, + }, + }, + }, + }, + }, + }, + "resource-identity-attribute-type-bool": { + input: &fwserver.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]fwschema.Schema{ + "test_resource": identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_attribute": identityschema.BoolAttribute{ + RequiredForImport: true, + }, + }, + }, + }, + }, + expected: &tfprotov6.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]*tfprotov6.ResourceIdentitySchema{ + "test_resource": { + IdentityAttributes: []*tfprotov6.ResourceIdentitySchemaAttribute{ + { + Name: "test_attribute", + RequiredForImport: true, + Type: tftypes.Bool, + }, + }, + }, + }, + }, + }, + "resource-identity-attribute-type-float32": { + input: &fwserver.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]fwschema.Schema{ + "test_resource": identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_attribute": identityschema.Float32Attribute{ + RequiredForImport: true, + }, + }, + }, + }, + }, + expected: &tfprotov6.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]*tfprotov6.ResourceIdentitySchema{ + "test_resource": { + IdentityAttributes: []*tfprotov6.ResourceIdentitySchemaAttribute{ + { + Name: "test_attribute", + RequiredForImport: true, + Type: tftypes.Number, + }, + }, + }, + }, + }, + }, + "resource-identity-attribute-type-float64": { + input: &fwserver.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]fwschema.Schema{ + "test_resource": identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_attribute": identityschema.Float64Attribute{ + RequiredForImport: true, + }, + }, + }, + }, + }, + expected: &tfprotov6.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]*tfprotov6.ResourceIdentitySchema{ + "test_resource": { + IdentityAttributes: []*tfprotov6.ResourceIdentitySchemaAttribute{ + { + Name: "test_attribute", + RequiredForImport: true, + Type: tftypes.Number, + }, + }, + }, + }, + }, + }, + "resource-identity-attribute-type-int32": { + input: &fwserver.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]fwschema.Schema{ + "test_resource": identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_attribute": identityschema.Int32Attribute{ + RequiredForImport: true, + }, + }, + }, + }, + }, + expected: &tfprotov6.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]*tfprotov6.ResourceIdentitySchema{ + "test_resource": { + IdentityAttributes: []*tfprotov6.ResourceIdentitySchemaAttribute{ + { + Name: "test_attribute", + RequiredForImport: true, + Type: tftypes.Number, + }, + }, + }, + }, + }, + }, + "resource-identity-attribute-type-int64": { + input: &fwserver.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]fwschema.Schema{ + "test_resource": identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_attribute": identityschema.Int64Attribute{ + RequiredForImport: true, + }, + }, + }, + }, + }, + expected: &tfprotov6.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]*tfprotov6.ResourceIdentitySchema{ + "test_resource": { + IdentityAttributes: []*tfprotov6.ResourceIdentitySchemaAttribute{ + { + Name: "test_attribute", + RequiredForImport: true, + Type: tftypes.Number, + }, + }, + }, + }, + }, + }, + "resource-identity-attribute-type-list-string": { + input: &fwserver.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]fwschema.Schema{ + "test_resource": identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_attribute": identityschema.ListAttribute{ + RequiredForImport: true, + ElementType: types.StringType, + }, + }, + }, + }, + }, + expected: &tfprotov6.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]*tfprotov6.ResourceIdentitySchema{ + "test_resource": { + IdentityAttributes: []*tfprotov6.ResourceIdentitySchemaAttribute{ + { + Name: "test_attribute", + RequiredForImport: true, + Type: tftypes.List{ + ElementType: tftypes.String, + }, + }, + }, + }, + }, + }, + }, + "resource-identity-attribute-type-number": { + input: &fwserver.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]fwschema.Schema{ + "test_resource": identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_attribute": identityschema.NumberAttribute{ + RequiredForImport: true, + }, + }, + }, + }, + }, + expected: &tfprotov6.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]*tfprotov6.ResourceIdentitySchema{ + "test_resource": { + IdentityAttributes: []*tfprotov6.ResourceIdentitySchemaAttribute{ + { + Name: "test_attribute", + RequiredForImport: true, + Type: tftypes.Number, + }, + }, + }, + }, + }, + }, + "resource-identity-attribute-type-string": { + input: &fwserver.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]fwschema.Schema{ + "test_resource": identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_attribute": identityschema.StringAttribute{ + RequiredForImport: true, + }, + }, + }, + }, + }, + expected: &tfprotov6.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]*tfprotov6.ResourceIdentitySchema{ + "test_resource": { + IdentityAttributes: []*tfprotov6.ResourceIdentitySchemaAttribute{ + { + Name: "test_attribute", + RequiredForImport: true, + Type: tftypes.String, + }, + }, + }, + }, + }, + }, + "resource-identity-version": { + input: &fwserver.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]fwschema.Schema{ + "test_resource": identityschema.Schema{ + Version: 123, + }, + }, + }, + expected: &tfprotov6.GetResourceIdentitySchemasResponse{ + IdentitySchemas: map[string]*tfprotov6.ResourceIdentitySchema{ + "test_resource": { + IdentityAttributes: []*tfprotov6.ResourceIdentitySchemaAttribute{}, + Version: 123, + }, + }, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := toproto6.GetResourceIdentitySchemasResponse(context.Background(), testCase.input) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/internal/toproto6/identity_schema.go b/internal/toproto6/identity_schema.go new file mode 100644 index 000000000..3c6455800 --- /dev/null +++ b/internal/toproto6/identity_schema.go @@ -0,0 +1,52 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto6 + +import ( + "context" + "sort" + + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +// IdentitySchema returns the *tfprotov6.ResourceIdentitySchema equivalent of a Schema. +func IdentitySchema(ctx context.Context, s fwschema.Schema) (*tfprotov6.ResourceIdentitySchema, error) { + if s == nil { + return nil, nil + } + + result := &tfprotov6.ResourceIdentitySchema{ + Version: s.GetVersion(), + } + + attrs := make([]*tfprotov6.ResourceIdentitySchemaAttribute, 0) + + for name, attr := range s.GetAttributes() { + a, err := IdentitySchemaAttribute(ctx, name, tftypes.NewAttributePath().WithAttributeName(name), attr) + + if err != nil { + return nil, err + } + + attrs = append(attrs, a) + } + + sort.Slice(attrs, func(i, j int) bool { + if attrs[i] == nil { + return true + } + + if attrs[j] == nil { + return false + } + + return attrs[i].Name < attrs[j].Name + }) + + result.IdentityAttributes = attrs + + return result, nil +} diff --git a/internal/toproto6/identity_schema_attribute.go b/internal/toproto6/identity_schema_attribute.go new file mode 100644 index 000000000..5fe815de1 --- /dev/null +++ b/internal/toproto6/identity_schema_attribute.go @@ -0,0 +1,43 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto6 + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" + + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" +) + +// IdentitySchemaAttribute returns the *tfprotov6.ResourceIdentitySchemaAttribute equivalent of an +// Attribute. Errors will be tftypes.AttributePathErrors based on `path`. `name` is the name of the attribute. +func IdentitySchemaAttribute(ctx context.Context, name string, path *tftypes.AttributePath, a fwschema.Attribute) (*tfprotov6.ResourceIdentitySchemaAttribute, error) { + if _, ok := a.(fwschema.NestedAttribute); ok { + return nil, path.NewErrorf("identity schemas don't support NestedAttribute") + } + + if a.GetType() == nil { + return nil, path.NewErrorf("must have Type set") + } + + if !a.IsRequiredForImport() && !a.IsOptionalForImport() { + return nil, path.NewErrorf("must have RequiredForImport or OptionalForImport set") + } + + identitySchemaAttribute := &tfprotov6.ResourceIdentitySchemaAttribute{ + Name: name, + RequiredForImport: a.IsRequiredForImport(), + OptionalForImport: a.IsOptionalForImport(), + Type: a.GetType().TerraformType(ctx), + + // Unlike other schema attributes, identity attributes only have a single description field which + // is assumed to be markdown. Both a.GetDescription() and a.GetMarkdownDescription() will return + // the same string, so we just chose one here. + Description: a.GetDescription(), + } + + return identitySchemaAttribute, nil +} diff --git a/internal/toproto6/identity_schema_attribute_test.go b/internal/toproto6/identity_schema_attribute_test.go new file mode 100644 index 000000000..45fa0d1a1 --- /dev/null +++ b/internal/toproto6/identity_schema_attribute_test.go @@ -0,0 +1,257 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto6_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/internal/toproto6" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestIdentitySchemaAttribute(t *testing.T) { + t.Parallel() + + type testCase struct { + name string + attr fwschema.Attribute + path *tftypes.AttributePath + expected *tfprotov6.ResourceIdentitySchemaAttribute + expectedErr string + } + + tests := map[string]testCase{ + "description": { + name: "string", + attr: testschema.Attribute{ + Type: types.StringType, + RequiredForImport: true, + Description: "A string attribute", + }, + path: tftypes.NewAttributePath(), + expected: &tfprotov6.ResourceIdentitySchemaAttribute{ + Name: "string", + Type: tftypes.String, + RequiredForImport: true, + Description: "A string attribute", + }, + }, + "attr-string": { + name: "string", + attr: testschema.Attribute{ + Type: types.StringType, + RequiredForImport: true, + }, + path: tftypes.NewAttributePath(), + expected: &tfprotov6.ResourceIdentitySchemaAttribute{ + Name: "string", + Type: tftypes.String, + RequiredForImport: true, + }, + }, + "attr-bool": { + name: "bool", + attr: testschema.Attribute{ + Type: types.BoolType, + RequiredForImport: true, + }, + path: tftypes.NewAttributePath(), + expected: &tfprotov6.ResourceIdentitySchemaAttribute{ + Name: "bool", + Type: tftypes.Bool, + RequiredForImport: true, + }, + }, + "attr-number": { + name: "number", + attr: testschema.Attribute{ + Type: types.NumberType, + RequiredForImport: true, + }, + path: tftypes.NewAttributePath(), + expected: &tfprotov6.ResourceIdentitySchemaAttribute{ + Name: "number", + Type: tftypes.Number, + RequiredForImport: true, + }, + }, + "attr-list": { + name: "list", + attr: testschema.Attribute{ + Type: types.ListType{ElemType: types.NumberType}, + RequiredForImport: true, + }, + path: tftypes.NewAttributePath(), + expected: &tfprotov6.ResourceIdentitySchemaAttribute{ + Name: "list", + Type: tftypes.List{ElementType: tftypes.Number}, + RequiredForImport: true, + }, + }, + "requiredforimport": { + name: "string", + attr: testschema.Attribute{ + Type: types.StringType, + RequiredForImport: true, + }, + path: tftypes.NewAttributePath(), + expected: &tfprotov6.ResourceIdentitySchemaAttribute{ + Name: "string", + Type: tftypes.String, + RequiredForImport: true, + }, + }, + "optionalforimport": { + name: "string", + attr: testschema.Attribute{ + Type: types.StringType, + OptionalForImport: true, + }, + path: tftypes.NewAttributePath(), + expected: &tfprotov6.ResourceIdentitySchemaAttribute{ + Name: "string", + Type: tftypes.String, + OptionalForImport: true, + }, + }, + "nested-attr-single-error": { + name: "single_nested", + attr: testschema.NestedAttribute{ + NestedObject: testschema.NestedAttributeObject{ + Attributes: map[string]fwschema.Attribute{ + "string": testschema.Attribute{ + Type: types.StringType, + Optional: true, + }, + "computed": testschema.Attribute{ + Type: types.NumberType, + Computed: true, + Sensitive: true, + }, + }, + }, + NestingMode: fwschema.NestingModeSingle, + Optional: true, + }, + path: tftypes.NewAttributePath(), + expectedErr: "identity schemas don't support NestedAttribute", + }, + "nested-attr-list-error": { + name: "list_nested", + attr: testschema.NestedAttribute{ + NestedObject: testschema.NestedAttributeObject{ + Attributes: map[string]fwschema.Attribute{ + "string": testschema.Attribute{ + Type: types.StringType, + Optional: true, + }, + "computed": testschema.Attribute{ + Type: types.NumberType, + Computed: true, + Sensitive: true, + }, + }, + }, + NestingMode: fwschema.NestingModeList, + Optional: true, + }, + path: tftypes.NewAttributePath(), + expectedErr: "identity schemas don't support NestedAttribute", + }, + "nested-attr-map-error": { + name: "map_nested", + attr: testschema.NestedAttribute{ + NestedObject: testschema.NestedAttributeObject{ + Attributes: map[string]fwschema.Attribute{ + "string": testschema.Attribute{ + Type: types.StringType, + Optional: true, + }, + "computed": testschema.Attribute{ + Type: types.NumberType, + Computed: true, + Sensitive: true, + }, + }, + }, + NestingMode: fwschema.NestingModeMap, + Optional: true, + }, + path: tftypes.NewAttributePath(), + expectedErr: "identity schemas don't support NestedAttribute", + }, + "nested-attr-set-error": { + name: "set_nested", + attr: testschema.NestedAttribute{ + NestedObject: testschema.NestedAttributeObject{ + Attributes: map[string]fwschema.Attribute{ + "string": testschema.Attribute{ + Type: types.StringType, + Optional: true, + }, + "computed": testschema.Attribute{ + Type: types.NumberType, + Computed: true, + Sensitive: true, + }, + }, + }, + NestingMode: fwschema.NestingModeSet, + Optional: true, + }, + path: tftypes.NewAttributePath(), + expectedErr: "identity schemas don't support NestedAttribute", + }, + "attr-unset": { + name: "whoops", + attr: testschema.Attribute{ + Optional: true, + }, + path: tftypes.NewAttributePath(), + expectedErr: "must have Type set", + }, + "missing-requiredforimport-and-optionalforimport": { + name: "whoops", + attr: testschema.Attribute{ + Type: types.StringType, + }, + path: tftypes.NewAttributePath(), + expectedErr: "must have RequiredForImport or OptionalForImport set", + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := toproto6.IdentitySchemaAttribute(context.Background(), tc.name, tc.path, tc.attr) + if err != nil { + if tc.expectedErr == "" { + t.Errorf("Unexpected error: %s", err) + return + } + if err.Error() != tc.expectedErr { + t.Errorf("Expected error to be %q, got %q", tc.expectedErr, err.Error()) + return + } + // got expected error + return + } + if err == nil && tc.expectedErr != "" { + t.Errorf("Expected error to be %q, got nil", tc.expectedErr) + return + } + if diff := cmp.Diff(got, tc.expected); diff != "" { + t.Errorf("Unexpected diff (+wanted, -got): %s", diff) + return + } + }) + } +} diff --git a/internal/toproto6/identity_schema_test.go b/internal/toproto6/identity_schema_test.go new file mode 100644 index 000000000..f8ebd1728 --- /dev/null +++ b/internal/toproto6/identity_schema_test.go @@ -0,0 +1,138 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto6_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/internal/toproto6" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestIdentitySchema(t *testing.T) { + t.Parallel() + + type testCase struct { + input fwschema.Schema + expected *tfprotov6.ResourceIdentitySchema + expectedErr string + } + + tests := map[string]testCase{ + "nil": { + input: nil, + expected: nil, + }, + "empty-val": { + input: testschema.Schema{}, + expected: &tfprotov6.ResourceIdentitySchema{ + IdentityAttributes: []*tfprotov6.ResourceIdentitySchemaAttribute{}, + Version: 0, + }, + }, + "basic-attrs": { + input: testschema.Schema{ + Version: 1, + Attributes: map[string]fwschema.Attribute{ + "string": testschema.Attribute{ + Type: types.StringType, + RequiredForImport: true, + }, + "number": testschema.Attribute{ + Type: types.NumberType, + OptionalForImport: true, + }, + "bool": testschema.Attribute{ + Type: types.BoolType, + OptionalForImport: true, + }, + }, + }, + expected: &tfprotov6.ResourceIdentitySchema{ + Version: 1, + IdentityAttributes: []*tfprotov6.ResourceIdentitySchemaAttribute{ + { + Name: "bool", + Type: tftypes.Bool, + OptionalForImport: true, + }, + { + Name: "number", + Type: tftypes.Number, + OptionalForImport: true, + }, + { + Name: "string", + Type: tftypes.String, + RequiredForImport: true, + }, + }, + }, + }, + "complex-attrs": { + input: testschema.Schema{ + Version: 2, + Attributes: map[string]fwschema.Attribute{ + "list_of_string": testschema.Attribute{ + Type: types.ListType{ElemType: types.StringType}, + RequiredForImport: true, + }, + "list_of_bool": testschema.Attribute{ + Type: types.ListType{ElemType: types.BoolType}, + RequiredForImport: true, + }, + }, + }, + expected: &tfprotov6.ResourceIdentitySchema{ + Version: 2, + IdentityAttributes: []*tfprotov6.ResourceIdentitySchemaAttribute{ + { + Name: "list_of_bool", + Type: tftypes.List{ElementType: tftypes.Bool}, + RequiredForImport: true, + }, + { + Name: "list_of_string", + Type: tftypes.List{ElementType: tftypes.String}, + RequiredForImport: true, + }, + }, + }, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, err := toproto6.IdentitySchema(context.Background(), tc.input) + if err != nil { + if tc.expectedErr == "" { + t.Errorf("Unexpected error: %s", err) + return + } + if err.Error() != tc.expectedErr { + t.Errorf("Expected error to be %q, got %q", tc.expectedErr, err.Error()) + return + } + // got expected error + return + } + if err == nil && tc.expectedErr != "" { + t.Errorf("Expected error to be %q, got nil", tc.expectedErr) + return + } + if diff := cmp.Diff(got, tc.expected); diff != "" { + t.Errorf("Unexpected diff (+wanted, -got): %s", diff) + return + } + }) + } +} From f97d278827b800746c4bd5554092d62741ae822b Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Thu, 6 Mar 2025 17:57:38 -0500 Subject: [PATCH 11/20] fix up package docs + build specific TODO comments --- .../server_upgraderesourceidentity.go | 2 +- .../server_upgraderesourceidentity.go | 2 +- resource/identityschema/doc.go | 2 -- resource/identityschema/list_attribute.go | 6 +---- .../identityschema/list_attribute_test.go | 23 ------------------- resource/identityschema/schema.go | 6 ++--- resource/resource.go | 2 +- 7 files changed, 6 insertions(+), 37 deletions(-) diff --git a/internal/proto5server/server_upgraderesourceidentity.go b/internal/proto5server/server_upgraderesourceidentity.go index bbd9a0a11..f5b1979f1 100644 --- a/internal/proto5server/server_upgraderesourceidentity.go +++ b/internal/proto5server/server_upgraderesourceidentity.go @@ -11,5 +11,5 @@ import ( // UpgradeResourceIdentity satisfies the tfprotov5.ProviderServer interface. func (s *Server) UpgradeResourceIdentity(ctx context.Context, proto5Req *tfprotov5.UpgradeResourceIdentityRequest) (*tfprotov5.UpgradeResourceIdentityResponse, error) { - panic("unimplemented") // TODO: implement + panic("unimplemented") // TODO:ResourceIdentity: implement } diff --git a/internal/proto6server/server_upgraderesourceidentity.go b/internal/proto6server/server_upgraderesourceidentity.go index d5f55ab23..042efce8e 100644 --- a/internal/proto6server/server_upgraderesourceidentity.go +++ b/internal/proto6server/server_upgraderesourceidentity.go @@ -11,5 +11,5 @@ import ( // UpgradeResourceIdentity satisfies the tfprotov6.ProviderServer interface. func (s *Server) UpgradeResourceIdentity(ctx context.Context, proto6Req *tfprotov6.UpgradeResourceIdentityRequest) (*tfprotov6.UpgradeResourceIdentityResponse, error) { - panic("unimplemented") // TODO: implement + panic("unimplemented") // TODO:ResourceIdentity: implement } diff --git a/resource/identityschema/doc.go b/resource/identityschema/doc.go index 14efe3ecc..5e468a5cf 100644 --- a/resource/identityschema/doc.go +++ b/resource/identityschema/doc.go @@ -4,8 +4,6 @@ // Package identityschema contains all available schema functionality for managed // resource identity. // -// TODO: Verify these type names are what we ended with. -// // Resource identity schemas define the structure and value types for identity state data. // Schemas are implemented via the resource.ResourceWithIdentity type IdentitySchema method. package identityschema diff --git a/resource/identityschema/list_attribute.go b/resource/identityschema/list_attribute.go index 86fe79730..ef396c248 100644 --- a/resource/identityschema/list_attribute.go +++ b/resource/identityschema/list_attribute.go @@ -165,9 +165,5 @@ func (a ListAttribute) ValidateImplementation(ctx context.Context, req fwschema. resp.Diagnostics.Append(fwschema.AttributeMissingElementTypeDiag(req.Path)) } - // TODO: Write validation similar to the dynamic type validation/diagnostic for detecting - // non-primitive element types - // if a.CustomType == nil && fwtype.ContainsCollectionWithDynamic(a.GetType()) { - // resp.Diagnostics.Append(fwtype.AttributeCollectionWithDynamicTypeDiag(req.Path)) - // } + // TODO:ResourceIdentity: Write validation + tests that ensure the element type only contains primitive elements (bool, string, number) } diff --git a/resource/identityschema/list_attribute_test.go b/resource/identityschema/list_attribute_test.go index 48c90f006..45fc40c17 100644 --- a/resource/identityschema/list_attribute_test.go +++ b/resource/identityschema/list_attribute_test.go @@ -460,29 +460,6 @@ func TestListAttributeValidateImplementation(t *testing.T) { }, expected: &fwschema.ValidateImplementationResponse{}, }, - // TODO: Update this test once the validation logic is in place - // "elementtype-invalid": { - // attribute: identityschema.ListAttribute{ - // RequiredForImport: true, - // ElementType: types.DynamicType, - // }, - // request: fwschema.ValidateImplementationRequest{ - // Name: "test", - // Path: path.Root("test"), - // }, - // expected: &fwschema.ValidateImplementationResponse{ - // Diagnostics: diag.Diagnostics{ - // diag.NewErrorDiagnostic( - // "Invalid Schema Implementation", - // "When validating the schema, an implementation issue was found. "+ - // "This is always an issue with the provider and should be reported to the provider developers.\n\n"+ - // "\"test\" is an attribute that contains a collection type with a nested dynamic type.\n\n"+ - // "Dynamic types inside of collections are not currently supported in terraform-plugin-framework. "+ - // "If underlying dynamic values are required, replace the \"test\" attribute definition with DynamicAttribute instead.", - // ), - // }, - // }, - // }, "elementtype-missing": { attribute: identityschema.ListAttribute{ RequiredForImport: true, diff --git a/resource/identityschema/schema.go b/resource/identityschema/schema.go index 1dfd0983f..6d68aff9a 100644 --- a/resource/identityschema/schema.go +++ b/resource/identityschema/schema.go @@ -17,10 +17,8 @@ import ( // Schema must satify the fwschema.Schema interface. var _ fwschema.Schema = Schema{} -// TODO: Verify these type names are what we ended with. -// // Schema defines the structure and value types of resource identity data. This type -// is used as the resource.IdentitySchemaResponse type Schema field, which is +// is used as the resource.IdentitySchemaResponse type IdentitySchema field, which is // implemented by the resource.ResourceWithIdentity type IdentitySchema method. type Schema struct { // Attributes is the mapping of underlying attribute names to attribute @@ -31,7 +29,7 @@ type Schema struct { // Version indicates the current version of the resource identity schema. Resource // identity schema versioning enables identity state upgrades in conjunction with the - // [resource.ResourceWithIdentityStateUpgrades] interface. Versioning is only + // [resource.ResourceWithUpgradeIdentity] interface. Versioning is only // required if there is a breaking change involving existing identity state data, // such as changing an attribute type in a manner that is incompatible with the Terraform type. // diff --git a/resource/resource.go b/resource/resource.go index ce2694a28..f99824023 100644 --- a/resource/resource.go +++ b/resource/resource.go @@ -199,7 +199,7 @@ type ResourceWithValidateConfig interface { // ResourceWithIdentity is an interface type that extends Resource to implement managed resource identity. // -// TODO: Describe identity concept in more detail, reference the upgrade identity interface +// TODO:ResourceIdentity: Add more documentation here to describe what identity is used for. type ResourceWithIdentity interface { Resource From 5578a7ddf5f17765f55c0ceae5a7c590c4143191 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Thu, 6 Mar 2025 18:18:39 -0500 Subject: [PATCH 12/20] spellcheck! --- datasource/schema/bool_attribute.go | 4 ++-- datasource/schema/dynamic_attribute.go | 4 ++-- datasource/schema/float32_attribute.go | 4 ++-- datasource/schema/float64_attribute.go | 4 ++-- datasource/schema/int32_attribute.go | 4 ++-- datasource/schema/int64_attribute.go | 4 ++-- datasource/schema/list_attribute.go | 4 ++-- datasource/schema/list_nested_attribute.go | 4 ++-- datasource/schema/map_attribute.go | 4 ++-- datasource/schema/map_nested_attribute.go | 4 ++-- datasource/schema/number_attribute.go | 4 ++-- datasource/schema/object_attribute.go | 4 ++-- datasource/schema/set_attribute.go | 4 ++-- datasource/schema/set_nested_attribute.go | 4 ++-- datasource/schema/single_nested_attribute.go | 4 ++-- datasource/schema/string_attribute.go | 4 ++-- ephemeral/schema/bool_attribute.go | 4 ++-- ephemeral/schema/dynamic_attribute.go | 4 ++-- ephemeral/schema/float32_attribute.go | 4 ++-- ephemeral/schema/float64_attribute.go | 4 ++-- ephemeral/schema/int32_attribute.go | 4 ++-- ephemeral/schema/int64_attribute.go | 4 ++-- ephemeral/schema/list_attribute.go | 4 ++-- ephemeral/schema/list_nested_attribute.go | 4 ++-- ephemeral/schema/map_attribute.go | 4 ++-- ephemeral/schema/map_nested_attribute.go | 4 ++-- ephemeral/schema/number_attribute.go | 4 ++-- ephemeral/schema/object_attribute.go | 4 ++-- ephemeral/schema/set_attribute.go | 4 ++-- ephemeral/schema/set_nested_attribute.go | 4 ++-- ephemeral/schema/single_nested_attribute.go | 4 ++-- ephemeral/schema/string_attribute.go | 4 ++-- provider/metaschema/bool_attribute.go | 4 ++-- provider/metaschema/float64_attribute.go | 4 ++-- provider/metaschema/int64_attribute.go | 4 ++-- provider/metaschema/list_attribute.go | 4 ++-- provider/metaschema/list_nested_attribute.go | 4 ++-- provider/metaschema/map_attribute.go | 4 ++-- provider/metaschema/map_nested_attribute.go | 4 ++-- provider/metaschema/number_attribute.go | 4 ++-- provider/metaschema/object_attribute.go | 4 ++-- provider/metaschema/set_attribute.go | 4 ++-- provider/metaschema/set_nested_attribute.go | 4 ++-- provider/metaschema/single_nested_attribute.go | 4 ++-- provider/metaschema/string_attribute.go | 4 ++-- provider/schema/bool_attribute.go | 4 ++-- provider/schema/dynamic_attribute.go | 4 ++-- provider/schema/float32_attribute.go | 4 ++-- provider/schema/float64_attribute.go | 4 ++-- provider/schema/int32_attribute.go | 4 ++-- provider/schema/int64_attribute.go | 4 ++-- provider/schema/list_attribute.go | 4 ++-- provider/schema/list_nested_attribute.go | 4 ++-- provider/schema/map_attribute.go | 4 ++-- provider/schema/map_nested_attribute.go | 4 ++-- provider/schema/number_attribute.go | 4 ++-- provider/schema/object_attribute.go | 4 ++-- provider/schema/set_attribute.go | 4 ++-- provider/schema/set_nested_attribute.go | 4 ++-- provider/schema/single_nested_attribute.go | 4 ++-- provider/schema/string_attribute.go | 4 ++-- resource/schema/bool_attribute.go | 4 ++-- resource/schema/dynamic_attribute.go | 4 ++-- resource/schema/float32_attribute.go | 4 ++-- resource/schema/float64_attribute.go | 4 ++-- resource/schema/int32_attribute.go | 4 ++-- resource/schema/int64_attribute.go | 4 ++-- resource/schema/list_attribute.go | 4 ++-- resource/schema/list_nested_attribute.go | 4 ++-- resource/schema/map_attribute.go | 4 ++-- resource/schema/map_nested_attribute.go | 4 ++-- resource/schema/number_attribute.go | 4 ++-- resource/schema/object_attribute.go | 4 ++-- resource/schema/set_attribute.go | 4 ++-- resource/schema/set_nested_attribute.go | 4 ++-- resource/schema/single_nested_attribute.go | 4 ++-- resource/schema/string_attribute.go | 4 ++-- 77 files changed, 154 insertions(+), 154 deletions(-) diff --git a/datasource/schema/bool_attribute.go b/datasource/schema/bool_attribute.go index 55a982573..5830d6c29 100644 --- a/datasource/schema/bool_attribute.go +++ b/datasource/schema/bool_attribute.go @@ -192,13 +192,13 @@ func (a BoolAttribute) IsSensitive() bool { return a.Sensitive } -// IsRequiredForImport returns false as this behavior is only revelant +// IsRequiredForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a BoolAttribute) IsRequiredForImport() bool { return false } -// IsOptionalForImport returns false as this behavior is only revelant +// IsOptionalForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a BoolAttribute) IsOptionalForImport() bool { return false diff --git a/datasource/schema/dynamic_attribute.go b/datasource/schema/dynamic_attribute.go index 526490383..2d489fe83 100644 --- a/datasource/schema/dynamic_attribute.go +++ b/datasource/schema/dynamic_attribute.go @@ -188,13 +188,13 @@ func (a DynamicAttribute) IsWriteOnly() bool { return false } -// IsRequiredForImport returns false as this behavior is only revelant +// IsRequiredForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a DynamicAttribute) IsRequiredForImport() bool { return false } -// IsOptionalForImport returns false as this behavior is only revelant +// IsOptionalForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a DynamicAttribute) IsOptionalForImport() bool { return false diff --git a/datasource/schema/float32_attribute.go b/datasource/schema/float32_attribute.go index 4ad4203ca..c7c09f6c2 100644 --- a/datasource/schema/float32_attribute.go +++ b/datasource/schema/float32_attribute.go @@ -195,13 +195,13 @@ func (a Float32Attribute) IsWriteOnly() bool { return false } -// IsRequiredForImport returns false as this behavior is only revelant +// IsRequiredForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a Float32Attribute) IsRequiredForImport() bool { return false } -// IsOptionalForImport returns false as this behavior is only revelant +// IsOptionalForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a Float32Attribute) IsOptionalForImport() bool { return false diff --git a/datasource/schema/float64_attribute.go b/datasource/schema/float64_attribute.go index 6e965edfb..b688653e3 100644 --- a/datasource/schema/float64_attribute.go +++ b/datasource/schema/float64_attribute.go @@ -195,13 +195,13 @@ func (a Float64Attribute) IsWriteOnly() bool { return false } -// IsRequiredForImport returns false as this behavior is only revelant +// IsRequiredForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a Float64Attribute) IsRequiredForImport() bool { return false } -// IsOptionalForImport returns false as this behavior is only revelant +// IsOptionalForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a Float64Attribute) IsOptionalForImport() bool { return false diff --git a/datasource/schema/int32_attribute.go b/datasource/schema/int32_attribute.go index 67b355d29..44c2b631f 100644 --- a/datasource/schema/int32_attribute.go +++ b/datasource/schema/int32_attribute.go @@ -195,13 +195,13 @@ func (a Int32Attribute) IsWriteOnly() bool { return false } -// IsRequiredForImport returns false as this behavior is only revelant +// IsRequiredForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a Int32Attribute) IsRequiredForImport() bool { return false } -// IsOptionalForImport returns false as this behavior is only revelant +// IsOptionalForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a Int32Attribute) IsOptionalForImport() bool { return false diff --git a/datasource/schema/int64_attribute.go b/datasource/schema/int64_attribute.go index 535f1f8a1..c5dc49dc7 100644 --- a/datasource/schema/int64_attribute.go +++ b/datasource/schema/int64_attribute.go @@ -195,13 +195,13 @@ func (a Int64Attribute) IsWriteOnly() bool { return false } -// IsRequiredForImport returns false as this behavior is only revelant +// IsRequiredForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a Int64Attribute) IsRequiredForImport() bool { return false } -// IsOptionalForImport returns false as this behavior is only revelant +// IsOptionalForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a Int64Attribute) IsOptionalForImport() bool { return false diff --git a/datasource/schema/list_attribute.go b/datasource/schema/list_attribute.go index de211ed62..d581d6638 100644 --- a/datasource/schema/list_attribute.go +++ b/datasource/schema/list_attribute.go @@ -213,13 +213,13 @@ func (a ListAttribute) ListValidators() []validator.List { return a.Validators } -// IsRequiredForImport returns false as this behavior is only revelant +// IsRequiredForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a ListAttribute) IsRequiredForImport() bool { return false } -// IsOptionalForImport returns false as this behavior is only revelant +// IsOptionalForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a ListAttribute) IsOptionalForImport() bool { return false diff --git a/datasource/schema/list_nested_attribute.go b/datasource/schema/list_nested_attribute.go index 4e844574d..f2b88a87c 100644 --- a/datasource/schema/list_nested_attribute.go +++ b/datasource/schema/list_nested_attribute.go @@ -236,13 +236,13 @@ func (a ListNestedAttribute) IsWriteOnly() bool { return false } -// IsRequiredForImport returns false as this behavior is only revelant +// IsRequiredForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a ListNestedAttribute) IsRequiredForImport() bool { return false } -// IsOptionalForImport returns false as this behavior is only revelant +// IsOptionalForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a ListNestedAttribute) IsOptionalForImport() bool { return false diff --git a/datasource/schema/map_attribute.go b/datasource/schema/map_attribute.go index 8e7030888..164133370 100644 --- a/datasource/schema/map_attribute.go +++ b/datasource/schema/map_attribute.go @@ -211,13 +211,13 @@ func (a MapAttribute) IsWriteOnly() bool { return false } -// IsRequiredForImport returns false as this behavior is only revelant +// IsRequiredForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a MapAttribute) IsRequiredForImport() bool { return false } -// IsOptionalForImport returns false as this behavior is only revelant +// IsOptionalForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a MapAttribute) IsOptionalForImport() bool { return false diff --git a/datasource/schema/map_nested_attribute.go b/datasource/schema/map_nested_attribute.go index b9a8bb905..30c0c2240 100644 --- a/datasource/schema/map_nested_attribute.go +++ b/datasource/schema/map_nested_attribute.go @@ -236,13 +236,13 @@ func (a MapNestedAttribute) IsWriteOnly() bool { return false } -// IsRequiredForImport returns false as this behavior is only revelant +// IsRequiredForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a MapNestedAttribute) IsRequiredForImport() bool { return false } -// IsOptionalForImport returns false as this behavior is only revelant +// IsOptionalForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a MapNestedAttribute) IsOptionalForImport() bool { return false diff --git a/datasource/schema/number_attribute.go b/datasource/schema/number_attribute.go index 61e9184ea..fc12b09dd 100644 --- a/datasource/schema/number_attribute.go +++ b/datasource/schema/number_attribute.go @@ -191,13 +191,13 @@ func (a NumberAttribute) IsWriteOnly() bool { return false } -// IsRequiredForImport returns false as this behavior is only revelant +// IsRequiredForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a NumberAttribute) IsRequiredForImport() bool { return false } -// IsOptionalForImport returns false as this behavior is only revelant +// IsOptionalForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a NumberAttribute) IsOptionalForImport() bool { return false diff --git a/datasource/schema/object_attribute.go b/datasource/schema/object_attribute.go index 981285181..21586932a 100644 --- a/datasource/schema/object_attribute.go +++ b/datasource/schema/object_attribute.go @@ -210,13 +210,13 @@ func (a ObjectAttribute) IsWriteOnly() bool { return false } -// IsRequiredForImport returns false as this behavior is only revelant +// IsRequiredForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a ObjectAttribute) IsRequiredForImport() bool { return false } -// IsOptionalForImport returns false as this behavior is only revelant +// IsOptionalForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a ObjectAttribute) IsOptionalForImport() bool { return false diff --git a/datasource/schema/set_attribute.go b/datasource/schema/set_attribute.go index 714a96022..638c21345 100644 --- a/datasource/schema/set_attribute.go +++ b/datasource/schema/set_attribute.go @@ -206,13 +206,13 @@ func (a SetAttribute) IsWriteOnly() bool { return false } -// IsRequiredForImport returns false as this behavior is only revelant +// IsRequiredForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a SetAttribute) IsRequiredForImport() bool { return false } -// IsOptionalForImport returns false as this behavior is only revelant +// IsOptionalForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a SetAttribute) IsOptionalForImport() bool { return false diff --git a/datasource/schema/set_nested_attribute.go b/datasource/schema/set_nested_attribute.go index 30ed7573d..4247933f2 100644 --- a/datasource/schema/set_nested_attribute.go +++ b/datasource/schema/set_nested_attribute.go @@ -231,13 +231,13 @@ func (a SetNestedAttribute) IsWriteOnly() bool { return false } -// IsRequiredForImport returns false as this behavior is only revelant +// IsRequiredForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a SetNestedAttribute) IsRequiredForImport() bool { return false } -// IsOptionalForImport returns false as this behavior is only revelant +// IsOptionalForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a SetNestedAttribute) IsOptionalForImport() bool { return false diff --git a/datasource/schema/single_nested_attribute.go b/datasource/schema/single_nested_attribute.go index 290d21075..838c9e333 100644 --- a/datasource/schema/single_nested_attribute.go +++ b/datasource/schema/single_nested_attribute.go @@ -245,13 +245,13 @@ func (a SingleNestedAttribute) IsWriteOnly() bool { return false } -// IsRequiredForImport returns false as this behavior is only revelant +// IsRequiredForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a SingleNestedAttribute) IsRequiredForImport() bool { return false } -// IsOptionalForImport returns false as this behavior is only revelant +// IsOptionalForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a SingleNestedAttribute) IsOptionalForImport() bool { return false diff --git a/datasource/schema/string_attribute.go b/datasource/schema/string_attribute.go index 250fb5ff2..9a61de9b2 100644 --- a/datasource/schema/string_attribute.go +++ b/datasource/schema/string_attribute.go @@ -187,13 +187,13 @@ func (a StringAttribute) IsWriteOnly() bool { return false } -// IsRequiredForImport returns false as this behavior is only revelant +// IsRequiredForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a StringAttribute) IsRequiredForImport() bool { return false } -// IsOptionalForImport returns false as this behavior is only revelant +// IsOptionalForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a StringAttribute) IsOptionalForImport() bool { return false diff --git a/ephemeral/schema/bool_attribute.go b/ephemeral/schema/bool_attribute.go index 4ff3b43bc..f8ebd5ef7 100644 --- a/ephemeral/schema/bool_attribute.go +++ b/ephemeral/schema/bool_attribute.go @@ -191,13 +191,13 @@ func (a BoolAttribute) IsWriteOnly() bool { return false } -// IsRequiredForImport returns false as this behavior is only revelant +// IsRequiredForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a BoolAttribute) IsRequiredForImport() bool { return false } -// IsOptionalForImport returns false as this behavior is only revelant +// IsOptionalForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a BoolAttribute) IsOptionalForImport() bool { return false diff --git a/ephemeral/schema/dynamic_attribute.go b/ephemeral/schema/dynamic_attribute.go index 205c73ea2..339775fe6 100644 --- a/ephemeral/schema/dynamic_attribute.go +++ b/ephemeral/schema/dynamic_attribute.go @@ -192,13 +192,13 @@ func (a DynamicAttribute) DynamicValidators() []validator.Dynamic { return a.Validators } -// IsRequiredForImport returns false as this behavior is only revelant +// IsRequiredForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a DynamicAttribute) IsRequiredForImport() bool { return false } -// IsOptionalForImport returns false as this behavior is only revelant +// IsOptionalForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a DynamicAttribute) IsOptionalForImport() bool { return false diff --git a/ephemeral/schema/float32_attribute.go b/ephemeral/schema/float32_attribute.go index cb5de0fb7..733502fef 100644 --- a/ephemeral/schema/float32_attribute.go +++ b/ephemeral/schema/float32_attribute.go @@ -194,13 +194,13 @@ func (a Float32Attribute) IsWriteOnly() bool { return false } -// IsRequiredForImport returns false as this behavior is only revelant +// IsRequiredForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a Float32Attribute) IsRequiredForImport() bool { return false } -// IsOptionalForImport returns false as this behavior is only revelant +// IsOptionalForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a Float32Attribute) IsOptionalForImport() bool { return false diff --git a/ephemeral/schema/float64_attribute.go b/ephemeral/schema/float64_attribute.go index a295ace36..9c72720f1 100644 --- a/ephemeral/schema/float64_attribute.go +++ b/ephemeral/schema/float64_attribute.go @@ -194,13 +194,13 @@ func (a Float64Attribute) IsWriteOnly() bool { return false } -// IsRequiredForImport returns false as this behavior is only revelant +// IsRequiredForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a Float64Attribute) IsRequiredForImport() bool { return false } -// IsOptionalForImport returns false as this behavior is only revelant +// IsOptionalForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a Float64Attribute) IsOptionalForImport() bool { return false diff --git a/ephemeral/schema/int32_attribute.go b/ephemeral/schema/int32_attribute.go index 1f948eaf1..c493b08c3 100644 --- a/ephemeral/schema/int32_attribute.go +++ b/ephemeral/schema/int32_attribute.go @@ -194,13 +194,13 @@ func (a Int32Attribute) IsWriteOnly() bool { return false } -// IsRequiredForImport returns false as this behavior is only revelant +// IsRequiredForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a Int32Attribute) IsRequiredForImport() bool { return false } -// IsOptionalForImport returns false as this behavior is only revelant +// IsOptionalForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a Int32Attribute) IsOptionalForImport() bool { return false diff --git a/ephemeral/schema/int64_attribute.go b/ephemeral/schema/int64_attribute.go index 95b96fe2b..c356ac65b 100644 --- a/ephemeral/schema/int64_attribute.go +++ b/ephemeral/schema/int64_attribute.go @@ -194,13 +194,13 @@ func (a Int64Attribute) IsWriteOnly() bool { return false } -// IsRequiredForImport returns false as this behavior is only revelant +// IsRequiredForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a Int64Attribute) IsRequiredForImport() bool { return false } -// IsOptionalForImport returns false as this behavior is only revelant +// IsOptionalForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a Int64Attribute) IsOptionalForImport() bool { return false diff --git a/ephemeral/schema/list_attribute.go b/ephemeral/schema/list_attribute.go index 05cc68955..246e77a78 100644 --- a/ephemeral/schema/list_attribute.go +++ b/ephemeral/schema/list_attribute.go @@ -206,13 +206,13 @@ func (a ListAttribute) IsWriteOnly() bool { return false } -// IsRequiredForImport returns false as this behavior is only revelant +// IsRequiredForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a ListAttribute) IsRequiredForImport() bool { return false } -// IsOptionalForImport returns false as this behavior is only revelant +// IsOptionalForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a ListAttribute) IsOptionalForImport() bool { return false diff --git a/ephemeral/schema/list_nested_attribute.go b/ephemeral/schema/list_nested_attribute.go index ea4954da9..a70b6835f 100644 --- a/ephemeral/schema/list_nested_attribute.go +++ b/ephemeral/schema/list_nested_attribute.go @@ -235,13 +235,13 @@ func (a ListNestedAttribute) IsWriteOnly() bool { return false } -// IsRequiredForImport returns false as this behavior is only revelant +// IsRequiredForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a ListNestedAttribute) IsRequiredForImport() bool { return false } -// IsOptionalForImport returns false as this behavior is only revelant +// IsOptionalForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a ListNestedAttribute) IsOptionalForImport() bool { return false diff --git a/ephemeral/schema/map_attribute.go b/ephemeral/schema/map_attribute.go index 018e83c40..86480e01f 100644 --- a/ephemeral/schema/map_attribute.go +++ b/ephemeral/schema/map_attribute.go @@ -210,13 +210,13 @@ func (a MapAttribute) IsWriteOnly() bool { return false } -// IsRequiredForImport returns false as this behavior is only revelant +// IsRequiredForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a MapAttribute) IsRequiredForImport() bool { return false } -// IsOptionalForImport returns false as this behavior is only revelant +// IsOptionalForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a MapAttribute) IsOptionalForImport() bool { return false diff --git a/ephemeral/schema/map_nested_attribute.go b/ephemeral/schema/map_nested_attribute.go index f935c12c0..6e8df3dde 100644 --- a/ephemeral/schema/map_nested_attribute.go +++ b/ephemeral/schema/map_nested_attribute.go @@ -235,13 +235,13 @@ func (a MapNestedAttribute) IsWriteOnly() bool { return false } -// IsRequiredForImport returns false as this behavior is only revelant +// IsRequiredForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a MapNestedAttribute) IsRequiredForImport() bool { return false } -// IsOptionalForImport returns false as this behavior is only revelant +// IsOptionalForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a MapNestedAttribute) IsOptionalForImport() bool { return false diff --git a/ephemeral/schema/number_attribute.go b/ephemeral/schema/number_attribute.go index 6b5de920e..d56a4ddb1 100644 --- a/ephemeral/schema/number_attribute.go +++ b/ephemeral/schema/number_attribute.go @@ -190,13 +190,13 @@ func (a NumberAttribute) IsWriteOnly() bool { return false } -// IsRequiredForImport returns false as this behavior is only revelant +// IsRequiredForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a NumberAttribute) IsRequiredForImport() bool { return false } -// IsOptionalForImport returns false as this behavior is only revelant +// IsOptionalForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a NumberAttribute) IsOptionalForImport() bool { return false diff --git a/ephemeral/schema/object_attribute.go b/ephemeral/schema/object_attribute.go index 327769075..cab38b7e8 100644 --- a/ephemeral/schema/object_attribute.go +++ b/ephemeral/schema/object_attribute.go @@ -208,13 +208,13 @@ func (a ObjectAttribute) IsWriteOnly() bool { return false } -// IsRequiredForImport returns false as this behavior is only revelant +// IsRequiredForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a ObjectAttribute) IsRequiredForImport() bool { return false } -// IsOptionalForImport returns false as this behavior is only revelant +// IsOptionalForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a ObjectAttribute) IsOptionalForImport() bool { return false diff --git a/ephemeral/schema/set_attribute.go b/ephemeral/schema/set_attribute.go index a05235023..963253f91 100644 --- a/ephemeral/schema/set_attribute.go +++ b/ephemeral/schema/set_attribute.go @@ -205,13 +205,13 @@ func (a SetAttribute) IsWriteOnly() bool { return false } -// IsRequiredForImport returns false as this behavior is only revelant +// IsRequiredForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a SetAttribute) IsRequiredForImport() bool { return false } -// IsOptionalForImport returns false as this behavior is only revelant +// IsOptionalForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a SetAttribute) IsOptionalForImport() bool { return false diff --git a/ephemeral/schema/set_nested_attribute.go b/ephemeral/schema/set_nested_attribute.go index f5740cd57..0e1dc40af 100644 --- a/ephemeral/schema/set_nested_attribute.go +++ b/ephemeral/schema/set_nested_attribute.go @@ -230,13 +230,13 @@ func (a SetNestedAttribute) IsWriteOnly() bool { return false } -// IsRequiredForImport returns false as this behavior is only revelant +// IsRequiredForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a SetNestedAttribute) IsRequiredForImport() bool { return false } -// IsOptionalForImport returns false as this behavior is only revelant +// IsOptionalForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a SetNestedAttribute) IsOptionalForImport() bool { return false diff --git a/ephemeral/schema/single_nested_attribute.go b/ephemeral/schema/single_nested_attribute.go index 16cc31fc9..23bc8cf11 100644 --- a/ephemeral/schema/single_nested_attribute.go +++ b/ephemeral/schema/single_nested_attribute.go @@ -244,13 +244,13 @@ func (a SingleNestedAttribute) IsWriteOnly() bool { return false } -// IsRequiredForImport returns false as this behavior is only revelant +// IsRequiredForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a SingleNestedAttribute) IsRequiredForImport() bool { return false } -// IsOptionalForImport returns false as this behavior is only revelant +// IsOptionalForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a SingleNestedAttribute) IsOptionalForImport() bool { return false diff --git a/ephemeral/schema/string_attribute.go b/ephemeral/schema/string_attribute.go index c751c9872..ec8f8692f 100644 --- a/ephemeral/schema/string_attribute.go +++ b/ephemeral/schema/string_attribute.go @@ -186,13 +186,13 @@ func (a StringAttribute) IsWriteOnly() bool { return false } -// IsRequiredForImport returns false as this behavior is only revelant +// IsRequiredForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a StringAttribute) IsRequiredForImport() bool { return false } -// IsOptionalForImport returns false as this behavior is only revelant +// IsOptionalForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a StringAttribute) IsOptionalForImport() bool { return false diff --git a/provider/metaschema/bool_attribute.go b/provider/metaschema/bool_attribute.go index 811a94e34..d02fcc801 100644 --- a/provider/metaschema/bool_attribute.go +++ b/provider/metaschema/bool_attribute.go @@ -125,13 +125,13 @@ func (a BoolAttribute) IsWriteOnly() bool { return false } -// IsRequiredForImport returns false as this behavior is only revelant +// IsRequiredForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a BoolAttribute) IsRequiredForImport() bool { return false } -// IsOptionalForImport returns false as this behavior is only revelant +// IsOptionalForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a BoolAttribute) IsOptionalForImport() bool { return false diff --git a/provider/metaschema/float64_attribute.go b/provider/metaschema/float64_attribute.go index a4eb9e9d0..96c634fbf 100644 --- a/provider/metaschema/float64_attribute.go +++ b/provider/metaschema/float64_attribute.go @@ -128,13 +128,13 @@ func (a Float64Attribute) IsWriteOnly() bool { return false } -// IsRequiredForImport returns false as this behavior is only revelant +// IsRequiredForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a Float64Attribute) IsRequiredForImport() bool { return false } -// IsOptionalForImport returns false as this behavior is only revelant +// IsOptionalForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a Float64Attribute) IsOptionalForImport() bool { return false diff --git a/provider/metaschema/int64_attribute.go b/provider/metaschema/int64_attribute.go index 25a214934..7490bd7b5 100644 --- a/provider/metaschema/int64_attribute.go +++ b/provider/metaschema/int64_attribute.go @@ -128,13 +128,13 @@ func (a Int64Attribute) IsWriteOnly() bool { return false } -// IsRequiredForImport returns false as this behavior is only revelant +// IsRequiredForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a Int64Attribute) IsRequiredForImport() bool { return false } -// IsOptionalForImport returns false as this behavior is only revelant +// IsOptionalForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a Int64Attribute) IsOptionalForImport() bool { return false diff --git a/provider/metaschema/list_attribute.go b/provider/metaschema/list_attribute.go index 2149c212c..ea663a8a9 100644 --- a/provider/metaschema/list_attribute.go +++ b/provider/metaschema/list_attribute.go @@ -141,13 +141,13 @@ func (a ListAttribute) IsWriteOnly() bool { return false } -// IsRequiredForImport returns false as this behavior is only revelant +// IsRequiredForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a ListAttribute) IsRequiredForImport() bool { return false } -// IsOptionalForImport returns false as this behavior is only revelant +// IsOptionalForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a ListAttribute) IsOptionalForImport() bool { return false diff --git a/provider/metaschema/list_nested_attribute.go b/provider/metaschema/list_nested_attribute.go index bf9b2f948..ccc3293d6 100644 --- a/provider/metaschema/list_nested_attribute.go +++ b/provider/metaschema/list_nested_attribute.go @@ -167,13 +167,13 @@ func (a ListNestedAttribute) IsWriteOnly() bool { return false } -// IsRequiredForImport returns false as this behavior is only revelant +// IsRequiredForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a ListNestedAttribute) IsRequiredForImport() bool { return false } -// IsOptionalForImport returns false as this behavior is only revelant +// IsOptionalForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a ListNestedAttribute) IsOptionalForImport() bool { return false diff --git a/provider/metaschema/map_attribute.go b/provider/metaschema/map_attribute.go index 18a3e9866..99d90e767 100644 --- a/provider/metaschema/map_attribute.go +++ b/provider/metaschema/map_attribute.go @@ -144,13 +144,13 @@ func (a MapAttribute) IsWriteOnly() bool { return false } -// IsRequiredForImport returns false as this behavior is only revelant +// IsRequiredForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a MapAttribute) IsRequiredForImport() bool { return false } -// IsOptionalForImport returns false as this behavior is only revelant +// IsOptionalForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a MapAttribute) IsOptionalForImport() bool { return false diff --git a/provider/metaschema/map_nested_attribute.go b/provider/metaschema/map_nested_attribute.go index 5962d38a4..79029c833 100644 --- a/provider/metaschema/map_nested_attribute.go +++ b/provider/metaschema/map_nested_attribute.go @@ -167,13 +167,13 @@ func (a MapNestedAttribute) IsWriteOnly() bool { return false } -// IsRequiredForImport returns false as this behavior is only revelant +// IsRequiredForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a MapNestedAttribute) IsRequiredForImport() bool { return false } -// IsOptionalForImport returns false as this behavior is only revelant +// IsOptionalForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a MapNestedAttribute) IsOptionalForImport() bool { return false diff --git a/provider/metaschema/number_attribute.go b/provider/metaschema/number_attribute.go index 829c0a504..3e345a7fb 100644 --- a/provider/metaschema/number_attribute.go +++ b/provider/metaschema/number_attribute.go @@ -129,13 +129,13 @@ func (a NumberAttribute) IsWriteOnly() bool { return false } -// IsRequiredForImport returns false as this behavior is only revelant +// IsRequiredForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a NumberAttribute) IsRequiredForImport() bool { return false } -// IsOptionalForImport returns false as this behavior is only revelant +// IsOptionalForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a NumberAttribute) IsOptionalForImport() bool { return false diff --git a/provider/metaschema/object_attribute.go b/provider/metaschema/object_attribute.go index 091fff810..7c325a238 100644 --- a/provider/metaschema/object_attribute.go +++ b/provider/metaschema/object_attribute.go @@ -143,13 +143,13 @@ func (a ObjectAttribute) IsWriteOnly() bool { return false } -// IsRequiredForImport returns false as this behavior is only revelant +// IsRequiredForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a ObjectAttribute) IsRequiredForImport() bool { return false } -// IsOptionalForImport returns false as this behavior is only revelant +// IsOptionalForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a ObjectAttribute) IsOptionalForImport() bool { return false diff --git a/provider/metaschema/set_attribute.go b/provider/metaschema/set_attribute.go index a4b8365c0..5878c6ec8 100644 --- a/provider/metaschema/set_attribute.go +++ b/provider/metaschema/set_attribute.go @@ -139,13 +139,13 @@ func (a SetAttribute) IsWriteOnly() bool { return false } -// IsRequiredForImport returns false as this behavior is only revelant +// IsRequiredForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a SetAttribute) IsRequiredForImport() bool { return false } -// IsOptionalForImport returns false as this behavior is only revelant +// IsOptionalForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a SetAttribute) IsOptionalForImport() bool { return false diff --git a/provider/metaschema/set_nested_attribute.go b/provider/metaschema/set_nested_attribute.go index a137dedac..102ede616 100644 --- a/provider/metaschema/set_nested_attribute.go +++ b/provider/metaschema/set_nested_attribute.go @@ -162,13 +162,13 @@ func (a SetNestedAttribute) IsWriteOnly() bool { return false } -// IsRequiredForImport returns false as this behavior is only revelant +// IsRequiredForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a SetNestedAttribute) IsRequiredForImport() bool { return false } -// IsOptionalForImport returns false as this behavior is only revelant +// IsOptionalForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a SetNestedAttribute) IsOptionalForImport() bool { return false diff --git a/provider/metaschema/single_nested_attribute.go b/provider/metaschema/single_nested_attribute.go index a10d23685..51b7c73b6 100644 --- a/provider/metaschema/single_nested_attribute.go +++ b/provider/metaschema/single_nested_attribute.go @@ -182,13 +182,13 @@ func (a SingleNestedAttribute) IsWriteOnly() bool { return false } -// IsRequiredForImport returns false as this behavior is only revelant +// IsRequiredForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a SingleNestedAttribute) IsRequiredForImport() bool { return false } -// IsOptionalForImport returns false as this behavior is only revelant +// IsOptionalForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a SingleNestedAttribute) IsOptionalForImport() bool { return false diff --git a/provider/metaschema/string_attribute.go b/provider/metaschema/string_attribute.go index 5efcba288..eb056e2d1 100644 --- a/provider/metaschema/string_attribute.go +++ b/provider/metaschema/string_attribute.go @@ -125,13 +125,13 @@ func (a StringAttribute) IsWriteOnly() bool { return false } -// IsRequiredForImport returns false as this behavior is only revelant +// IsRequiredForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a StringAttribute) IsRequiredForImport() bool { return false } -// IsOptionalForImport returns false as this behavior is only revelant +// IsOptionalForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a StringAttribute) IsOptionalForImport() bool { return false diff --git a/provider/schema/bool_attribute.go b/provider/schema/bool_attribute.go index ea1eb8f9c..9f4f37138 100644 --- a/provider/schema/bool_attribute.go +++ b/provider/schema/bool_attribute.go @@ -186,13 +186,13 @@ func (a BoolAttribute) IsWriteOnly() bool { return false } -// IsRequiredForImport returns false as this behavior is only revelant +// IsRequiredForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a BoolAttribute) IsRequiredForImport() bool { return false } -// IsOptionalForImport returns false as this behavior is only revelant +// IsOptionalForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a BoolAttribute) IsOptionalForImport() bool { return false diff --git a/provider/schema/dynamic_attribute.go b/provider/schema/dynamic_attribute.go index 5e5c07edc..86da9aef2 100644 --- a/provider/schema/dynamic_attribute.go +++ b/provider/schema/dynamic_attribute.go @@ -183,13 +183,13 @@ func (a DynamicAttribute) IsWriteOnly() bool { return false } -// IsRequiredForImport returns false as this behavior is only revelant +// IsRequiredForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a DynamicAttribute) IsRequiredForImport() bool { return false } -// IsOptionalForImport returns false as this behavior is only revelant +// IsOptionalForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a DynamicAttribute) IsOptionalForImport() bool { return false diff --git a/provider/schema/float32_attribute.go b/provider/schema/float32_attribute.go index f272539b9..bd124a33f 100644 --- a/provider/schema/float32_attribute.go +++ b/provider/schema/float32_attribute.go @@ -189,13 +189,13 @@ func (a Float32Attribute) IsWriteOnly() bool { return false } -// IsRequiredForImport returns false as this behavior is only revelant +// IsRequiredForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a Float32Attribute) IsRequiredForImport() bool { return false } -// IsOptionalForImport returns false as this behavior is only revelant +// IsOptionalForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a Float32Attribute) IsOptionalForImport() bool { return false diff --git a/provider/schema/float64_attribute.go b/provider/schema/float64_attribute.go index 336154468..d7c328329 100644 --- a/provider/schema/float64_attribute.go +++ b/provider/schema/float64_attribute.go @@ -189,13 +189,13 @@ func (a Float64Attribute) IsWriteOnly() bool { return false } -// IsRequiredForImport returns false as this behavior is only revelant +// IsRequiredForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a Float64Attribute) IsRequiredForImport() bool { return false } -// IsOptionalForImport returns false as this behavior is only revelant +// IsOptionalForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a Float64Attribute) IsOptionalForImport() bool { return false diff --git a/provider/schema/int32_attribute.go b/provider/schema/int32_attribute.go index 3b97a0f37..1c5bde95f 100644 --- a/provider/schema/int32_attribute.go +++ b/provider/schema/int32_attribute.go @@ -189,13 +189,13 @@ func (a Int32Attribute) IsWriteOnly() bool { return false } -// IsRequiredForImport returns false as this behavior is only revelant +// IsRequiredForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a Int32Attribute) IsRequiredForImport() bool { return false } -// IsOptionalForImport returns false as this behavior is only revelant +// IsOptionalForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a Int32Attribute) IsOptionalForImport() bool { return false diff --git a/provider/schema/int64_attribute.go b/provider/schema/int64_attribute.go index 1dc1f85eb..bf334a6d3 100644 --- a/provider/schema/int64_attribute.go +++ b/provider/schema/int64_attribute.go @@ -189,13 +189,13 @@ func (a Int64Attribute) IsWriteOnly() bool { return false } -// IsRequiredForImport returns false as this behavior is only revelant +// IsRequiredForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a Int64Attribute) IsRequiredForImport() bool { return false } -// IsOptionalForImport returns false as this behavior is only revelant +// IsOptionalForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a Int64Attribute) IsOptionalForImport() bool { return false diff --git a/provider/schema/list_attribute.go b/provider/schema/list_attribute.go index e94825973..55f6f233c 100644 --- a/provider/schema/list_attribute.go +++ b/provider/schema/list_attribute.go @@ -202,13 +202,13 @@ func (a ListAttribute) IsWriteOnly() bool { return false } -// IsRequiredForImport returns false as this behavior is only revelant +// IsRequiredForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a ListAttribute) IsRequiredForImport() bool { return false } -// IsOptionalForImport returns false as this behavior is only revelant +// IsOptionalForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a ListAttribute) IsOptionalForImport() bool { return false diff --git a/provider/schema/list_nested_attribute.go b/provider/schema/list_nested_attribute.go index 07d6ebd45..e00e940b0 100644 --- a/provider/schema/list_nested_attribute.go +++ b/provider/schema/list_nested_attribute.go @@ -230,13 +230,13 @@ func (a ListNestedAttribute) IsWriteOnly() bool { return false } -// IsRequiredForImport returns false as this behavior is only revelant +// IsRequiredForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a ListNestedAttribute) IsRequiredForImport() bool { return false } -// IsOptionalForImport returns false as this behavior is only revelant +// IsOptionalForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a ListNestedAttribute) IsOptionalForImport() bool { return false diff --git a/provider/schema/map_attribute.go b/provider/schema/map_attribute.go index a43108d13..28cc5f5b8 100644 --- a/provider/schema/map_attribute.go +++ b/provider/schema/map_attribute.go @@ -205,13 +205,13 @@ func (a MapAttribute) IsWriteOnly() bool { return false } -// IsRequiredForImport returns false as this behavior is only revelant +// IsRequiredForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a MapAttribute) IsRequiredForImport() bool { return false } -// IsOptionalForImport returns false as this behavior is only revelant +// IsOptionalForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a MapAttribute) IsOptionalForImport() bool { return false diff --git a/provider/schema/map_nested_attribute.go b/provider/schema/map_nested_attribute.go index d2ee6ece2..5b8246491 100644 --- a/provider/schema/map_nested_attribute.go +++ b/provider/schema/map_nested_attribute.go @@ -229,13 +229,13 @@ func (a MapNestedAttribute) IsWriteOnly() bool { return false } -// IsRequiredForImport returns false as this behavior is only revelant +// IsRequiredForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a MapNestedAttribute) IsRequiredForImport() bool { return false } -// IsOptionalForImport returns false as this behavior is only revelant +// IsOptionalForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a MapNestedAttribute) IsOptionalForImport() bool { return false diff --git a/provider/schema/number_attribute.go b/provider/schema/number_attribute.go index d8b467cc8..add021b1c 100644 --- a/provider/schema/number_attribute.go +++ b/provider/schema/number_attribute.go @@ -185,13 +185,13 @@ func (a NumberAttribute) IsWriteOnly() bool { return false } -// IsRequiredForImport returns false as this behavior is only revelant +// IsRequiredForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a NumberAttribute) IsRequiredForImport() bool { return false } -// IsOptionalForImport returns false as this behavior is only revelant +// IsOptionalForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a NumberAttribute) IsOptionalForImport() bool { return false diff --git a/provider/schema/object_attribute.go b/provider/schema/object_attribute.go index ddd0e1b23..0cb33f1fd 100644 --- a/provider/schema/object_attribute.go +++ b/provider/schema/object_attribute.go @@ -204,13 +204,13 @@ func (a ObjectAttribute) IsWriteOnly() bool { return false } -// IsRequiredForImport returns false as this behavior is only revelant +// IsRequiredForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a ObjectAttribute) IsRequiredForImport() bool { return false } -// IsOptionalForImport returns false as this behavior is only revelant +// IsOptionalForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a ObjectAttribute) IsOptionalForImport() bool { return false diff --git a/provider/schema/set_attribute.go b/provider/schema/set_attribute.go index 87ee73636..9d1a8f0c0 100644 --- a/provider/schema/set_attribute.go +++ b/provider/schema/set_attribute.go @@ -200,13 +200,13 @@ func (a SetAttribute) IsWriteOnly() bool { return false } -// IsRequiredForImport returns false as this behavior is only revelant +// IsRequiredForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a SetAttribute) IsRequiredForImport() bool { return false } -// IsOptionalForImport returns false as this behavior is only revelant +// IsOptionalForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a SetAttribute) IsOptionalForImport() bool { return false diff --git a/provider/schema/set_nested_attribute.go b/provider/schema/set_nested_attribute.go index b879b40c1..f5d788e6f 100644 --- a/provider/schema/set_nested_attribute.go +++ b/provider/schema/set_nested_attribute.go @@ -225,13 +225,13 @@ func (a SetNestedAttribute) IsWriteOnly() bool { return false } -// IsRequiredForImport returns false as this behavior is only revelant +// IsRequiredForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a SetNestedAttribute) IsRequiredForImport() bool { return false } -// IsOptionalForImport returns false as this behavior is only revelant +// IsOptionalForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a SetNestedAttribute) IsOptionalForImport() bool { return false diff --git a/provider/schema/single_nested_attribute.go b/provider/schema/single_nested_attribute.go index 65f35310b..6c306d167 100644 --- a/provider/schema/single_nested_attribute.go +++ b/provider/schema/single_nested_attribute.go @@ -239,13 +239,13 @@ func (a SingleNestedAttribute) IsWriteOnly() bool { return false } -// IsRequiredForImport returns false as this behavior is only revelant +// IsRequiredForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a SingleNestedAttribute) IsRequiredForImport() bool { return false } -// IsOptionalForImport returns false as this behavior is only revelant +// IsOptionalForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a SingleNestedAttribute) IsOptionalForImport() bool { return false diff --git a/provider/schema/string_attribute.go b/provider/schema/string_attribute.go index db6667f03..feb9e8039 100644 --- a/provider/schema/string_attribute.go +++ b/provider/schema/string_attribute.go @@ -181,13 +181,13 @@ func (a StringAttribute) IsWriteOnly() bool { return false } -// IsRequiredForImport returns false as this behavior is only revelant +// IsRequiredForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a StringAttribute) IsRequiredForImport() bool { return false } -// IsOptionalForImport returns false as this behavior is only revelant +// IsOptionalForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a StringAttribute) IsOptionalForImport() bool { return false diff --git a/resource/schema/bool_attribute.go b/resource/schema/bool_attribute.go index b3d57ee7c..3cdbc0e9f 100644 --- a/resource/schema/bool_attribute.go +++ b/resource/schema/bool_attribute.go @@ -244,13 +244,13 @@ func (a BoolAttribute) IsWriteOnly() bool { return a.WriteOnly } -// IsRequiredForImport returns false as this behavior is only revelant +// IsRequiredForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a BoolAttribute) IsRequiredForImport() bool { return false } -// IsOptionalForImport returns false as this behavior is only revelant +// IsOptionalForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a BoolAttribute) IsOptionalForImport() bool { return false diff --git a/resource/schema/dynamic_attribute.go b/resource/schema/dynamic_attribute.go index 72b746379..b7b4f38b5 100644 --- a/resource/schema/dynamic_attribute.go +++ b/resource/schema/dynamic_attribute.go @@ -230,13 +230,13 @@ func (a DynamicAttribute) IsWriteOnly() bool { return a.WriteOnly } -// IsRequiredForImport returns false as this behavior is only revelant +// IsRequiredForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a DynamicAttribute) IsRequiredForImport() bool { return false } -// IsOptionalForImport returns false as this behavior is only revelant +// IsOptionalForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a DynamicAttribute) IsOptionalForImport() bool { return false diff --git a/resource/schema/float32_attribute.go b/resource/schema/float32_attribute.go index ed7e5fb9b..acd37a157 100644 --- a/resource/schema/float32_attribute.go +++ b/resource/schema/float32_attribute.go @@ -247,13 +247,13 @@ func (a Float32Attribute) IsWriteOnly() bool { return a.WriteOnly } -// IsRequiredForImport returns false as this behavior is only revelant +// IsRequiredForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a Float32Attribute) IsRequiredForImport() bool { return false } -// IsOptionalForImport returns false as this behavior is only revelant +// IsOptionalForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a Float32Attribute) IsOptionalForImport() bool { return false diff --git a/resource/schema/float64_attribute.go b/resource/schema/float64_attribute.go index 947c16954..8db8fb0be 100644 --- a/resource/schema/float64_attribute.go +++ b/resource/schema/float64_attribute.go @@ -247,13 +247,13 @@ func (a Float64Attribute) IsWriteOnly() bool { return a.WriteOnly } -// IsRequiredForImport returns false as this behavior is only revelant +// IsRequiredForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a Float64Attribute) IsRequiredForImport() bool { return false } -// IsOptionalForImport returns false as this behavior is only revelant +// IsOptionalForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a Float64Attribute) IsOptionalForImport() bool { return false diff --git a/resource/schema/int32_attribute.go b/resource/schema/int32_attribute.go index 102be0377..5426a5743 100644 --- a/resource/schema/int32_attribute.go +++ b/resource/schema/int32_attribute.go @@ -247,13 +247,13 @@ func (a Int32Attribute) IsWriteOnly() bool { return a.WriteOnly } -// IsRequiredForImport returns false as this behavior is only revelant +// IsRequiredForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a Int32Attribute) IsRequiredForImport() bool { return false } -// IsOptionalForImport returns false as this behavior is only revelant +// IsOptionalForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a Int32Attribute) IsOptionalForImport() bool { return false diff --git a/resource/schema/int64_attribute.go b/resource/schema/int64_attribute.go index 3455b7a52..b700f2cca 100644 --- a/resource/schema/int64_attribute.go +++ b/resource/schema/int64_attribute.go @@ -247,13 +247,13 @@ func (a Int64Attribute) IsWriteOnly() bool { return a.WriteOnly } -// IsRequiredForImport returns false as this behavior is only revelant +// IsRequiredForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a Int64Attribute) IsRequiredForImport() bool { return false } -// IsOptionalForImport returns false as this behavior is only revelant +// IsOptionalForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a Int64Attribute) IsOptionalForImport() bool { return false diff --git a/resource/schema/list_attribute.go b/resource/schema/list_attribute.go index 85dbc9a34..29419ebd1 100644 --- a/resource/schema/list_attribute.go +++ b/resource/schema/list_attribute.go @@ -247,13 +247,13 @@ func (a ListAttribute) IsWriteOnly() bool { return a.WriteOnly } -// IsRequiredForImport returns false as this behavior is only revelant +// IsRequiredForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a ListAttribute) IsRequiredForImport() bool { return false } -// IsOptionalForImport returns false as this behavior is only revelant +// IsOptionalForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a ListAttribute) IsOptionalForImport() bool { return false diff --git a/resource/schema/list_nested_attribute.go b/resource/schema/list_nested_attribute.go index 74c739e90..76a4e3cdb 100644 --- a/resource/schema/list_nested_attribute.go +++ b/resource/schema/list_nested_attribute.go @@ -278,13 +278,13 @@ func (a ListNestedAttribute) IsWriteOnly() bool { return a.WriteOnly } -// IsRequiredForImport returns false as this behavior is only revelant +// IsRequiredForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a ListNestedAttribute) IsRequiredForImport() bool { return false } -// IsOptionalForImport returns false as this behavior is only revelant +// IsOptionalForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a ListNestedAttribute) IsOptionalForImport() bool { return false diff --git a/resource/schema/map_attribute.go b/resource/schema/map_attribute.go index f045a6c8a..837b31100 100644 --- a/resource/schema/map_attribute.go +++ b/resource/schema/map_attribute.go @@ -250,13 +250,13 @@ func (a MapAttribute) IsWriteOnly() bool { return a.WriteOnly } -// IsRequiredForImport returns false as this behavior is only revelant +// IsRequiredForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a MapAttribute) IsRequiredForImport() bool { return false } -// IsOptionalForImport returns false as this behavior is only revelant +// IsOptionalForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a MapAttribute) IsOptionalForImport() bool { return false diff --git a/resource/schema/map_nested_attribute.go b/resource/schema/map_nested_attribute.go index 717140e64..cdea0f5b0 100644 --- a/resource/schema/map_nested_attribute.go +++ b/resource/schema/map_nested_attribute.go @@ -278,13 +278,13 @@ func (a MapNestedAttribute) IsWriteOnly() bool { return a.WriteOnly } -// IsRequiredForImport returns false as this behavior is only revelant +// IsRequiredForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a MapNestedAttribute) IsRequiredForImport() bool { return false } -// IsOptionalForImport returns false as this behavior is only revelant +// IsOptionalForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a MapNestedAttribute) IsOptionalForImport() bool { return false diff --git a/resource/schema/number_attribute.go b/resource/schema/number_attribute.go index 910b2f5a8..a443945e8 100644 --- a/resource/schema/number_attribute.go +++ b/resource/schema/number_attribute.go @@ -233,13 +233,13 @@ func (a NumberAttribute) IsWriteOnly() bool { return a.WriteOnly } -// IsRequiredForImport returns false as this behavior is only revelant +// IsRequiredForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a NumberAttribute) IsRequiredForImport() bool { return false } -// IsOptionalForImport returns false as this behavior is only revelant +// IsOptionalForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a NumberAttribute) IsOptionalForImport() bool { return false diff --git a/resource/schema/object_attribute.go b/resource/schema/object_attribute.go index 896b2921d..1de48e873 100644 --- a/resource/schema/object_attribute.go +++ b/resource/schema/object_attribute.go @@ -249,13 +249,13 @@ func (a ObjectAttribute) IsWriteOnly() bool { return a.WriteOnly } -// IsRequiredForImport returns false as this behavior is only revelant +// IsRequiredForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a ObjectAttribute) IsRequiredForImport() bool { return false } -// IsOptionalForImport returns false as this behavior is only revelant +// IsOptionalForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a ObjectAttribute) IsOptionalForImport() bool { return false diff --git a/resource/schema/set_attribute.go b/resource/schema/set_attribute.go index f33fd3df4..2c9f08286 100644 --- a/resource/schema/set_attribute.go +++ b/resource/schema/set_attribute.go @@ -235,13 +235,13 @@ func (a SetAttribute) IsWriteOnly() bool { return false } -// IsRequiredForImport returns false as this behavior is only revelant +// IsRequiredForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a SetAttribute) IsRequiredForImport() bool { return false } -// IsOptionalForImport returns false as this behavior is only revelant +// IsOptionalForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a SetAttribute) IsOptionalForImport() bool { return false diff --git a/resource/schema/set_nested_attribute.go b/resource/schema/set_nested_attribute.go index 0d6eef595..c3b26780e 100644 --- a/resource/schema/set_nested_attribute.go +++ b/resource/schema/set_nested_attribute.go @@ -260,13 +260,13 @@ func (a SetNestedAttribute) IsWriteOnly() bool { return false } -// IsRequiredForImport returns false as this behavior is only revelant +// IsRequiredForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a SetNestedAttribute) IsRequiredForImport() bool { return false } -// IsOptionalForImport returns false as this behavior is only revelant +// IsOptionalForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a SetNestedAttribute) IsOptionalForImport() bool { return false diff --git a/resource/schema/single_nested_attribute.go b/resource/schema/single_nested_attribute.go index 4ad79ffe1..0d4e00e24 100644 --- a/resource/schema/single_nested_attribute.go +++ b/resource/schema/single_nested_attribute.go @@ -286,13 +286,13 @@ func (a SingleNestedAttribute) IsWriteOnly() bool { return a.WriteOnly } -// IsRequiredForImport returns false as this behavior is only revelant +// IsRequiredForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a SingleNestedAttribute) IsRequiredForImport() bool { return false } -// IsOptionalForImport returns false as this behavior is only revelant +// IsOptionalForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a SingleNestedAttribute) IsOptionalForImport() bool { return false diff --git a/resource/schema/string_attribute.go b/resource/schema/string_attribute.go index 50aad8a0c..12340ee6e 100644 --- a/resource/schema/string_attribute.go +++ b/resource/schema/string_attribute.go @@ -229,13 +229,13 @@ func (a StringAttribute) IsWriteOnly() bool { return a.WriteOnly } -// IsRequiredForImport returns false as this behavior is only revelant +// IsRequiredForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a StringAttribute) IsRequiredForImport() bool { return false } -// IsOptionalForImport returns false as this behavior is only revelant +// IsOptionalForImport returns false as this behavior is only relevant // for managed resource identity schema attributes. func (a StringAttribute) IsOptionalForImport() bool { return false From 7eba2e787ac9e600005c3efdf6da28c8506b4c6e Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Mon, 10 Mar 2025 08:36:03 -0400 Subject: [PATCH 13/20] add tfsdk object --- internal/fwschemadata/data_description.go | 6 + tfsdk/resource_identity.go | 91 ++++ tfsdk/resource_identity_test.go | 482 ++++++++++++++++++++++ 3 files changed, 579 insertions(+) create mode 100644 tfsdk/resource_identity.go create mode 100644 tfsdk/resource_identity_test.go diff --git a/internal/fwschemadata/data_description.go b/internal/fwschemadata/data_description.go index 70ae62c76..282a53321 100644 --- a/internal/fwschemadata/data_description.go +++ b/internal/fwschemadata/data_description.go @@ -19,6 +19,10 @@ const ( // DataDescriptionEphemeralResultData is used for Data that represents // the result of an ephemeral operation. DataDescriptionEphemeralResultData DataDescription = "ephemeral result data" + + // DataDescriptionResourceIdentity is used for Data that represents + // a managed resource identity. + DataDescriptionResourceIdentity DataDescription = "resource identity" ) // DataDescription is a human friendly type for Data. Used in error @@ -46,6 +50,8 @@ func (d DataDescription) Title() string { return "State" case DataDescriptionEphemeralResultData: return "Ephemeral Result Data" + case DataDescriptionResourceIdentity: + return "Resource Identity" default: return "Data" } diff --git a/tfsdk/resource_identity.go b/tfsdk/resource_identity.go new file mode 100644 index 000000000..d02f1e8e3 --- /dev/null +++ b/tfsdk/resource_identity.go @@ -0,0 +1,91 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package tfsdk + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschemadata" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +// ResourceIdentity represents the identity data for a managed resource. +type ResourceIdentity struct { + Raw tftypes.Value + Schema fwschema.Schema +} + +// Get populates the struct passed as `target` with the entire identity. +func (s ResourceIdentity) Get(ctx context.Context, target interface{}) diag.Diagnostics { + return s.data().Get(ctx, target) +} + +// GetAttribute retrieves the attribute found at `path` and populates +// the `target` with the value. +// +// Elements under null or unknown collections return null values, however this +// behavior is not protected by compatibility promises. +func (s ResourceIdentity) GetAttribute(ctx context.Context, path path.Path, target interface{}) diag.Diagnostics { + return s.data().GetAtPath(ctx, path, target) +} + +// PathMatches returns all matching path.Paths from the given path.Expression. +// +// If a parent path is null or unknown, which would prevent a full expression +// from matching, the parent path is returned rather than no match to prevent +// false positives. +func (s ResourceIdentity) PathMatches(ctx context.Context, pathExpr path.Expression) (path.Paths, diag.Diagnostics) { + return s.data().PathMatches(ctx, pathExpr) +} + +// Set populates the entire identity using the supplied Go value. The value `val` +// should be a struct whose values have one of the attr.Value types. Each field +// must be tagged with the corresponding schema field. +func (s *ResourceIdentity) Set(ctx context.Context, val interface{}) diag.Diagnostics { + data := s.data() + diags := data.Set(ctx, val) + + if diags.HasError() { + return diags + } + + s.Raw = data.TerraformValue + + return diags +} + +// SetAttribute sets the attribute at `path` using the supplied Go value. +// +// The attribute path and value must be valid with the current schema. If the +// attribute path already has a value, it will be overwritten. If the attribute +// path does not have a value, it will be added. +// +// The value must not be an untyped nil. Use a typed nil or types package null +// value function instead. For example with a types.StringType attribute, +// use (*string)(nil) or types.StringNull(). +// +// Lists can only have the next element added according to the current length. +func (s *ResourceIdentity) SetAttribute(ctx context.Context, path path.Path, val interface{}) diag.Diagnostics { + data := s.data() + diags := data.SetAtPath(ctx, path, val) + + if diags.HasError() { + return diags + } + + s.Raw = data.TerraformValue + + return diags +} + +func (s ResourceIdentity) data() *fwschemadata.Data { + return &fwschemadata.Data{ + Description: fwschemadata.DataDescriptionResourceIdentity, + Schema: s.Schema, + TerraformValue: s.Raw, + } +} diff --git a/tfsdk/resource_identity_test.go b/tfsdk/resource_identity_test.go new file mode 100644 index 000000000..e73d07872 --- /dev/null +++ b/tfsdk/resource_identity_test.go @@ -0,0 +1,482 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package tfsdk_test + +import ( + "context" + "reflect" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + intreflect "github.com/hashicorp/terraform-plugin-framework/internal/reflect" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestResourceIdentityGet(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + resourceIdentity tfsdk.ResourceIdentity + target any + expected any + expectedDiags diag.Diagnostics + }{ + // Refer to fwschemadata.TestDataGet for more exhaustive unit testing. + // These test cases are to ensure ResourceIdentity schema and data values are + // passed appropriately to the shared implementation. + "valid": { + resourceIdentity: tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "string": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "string": tftypes.NewValue(tftypes.String, "test"), + }, + ), + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "string": testschema.Attribute{ + RequiredForImport: true, + Type: types.StringType, + }, + }, + }, + }, + target: new(struct { + String types.String `tfsdk:"string"` + }), + expected: &struct { + String types.String `tfsdk:"string"` + }{ + String: types.StringValue("test"), + }, + }, + "diagnostic": { + resourceIdentity: tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "bool": tftypes.Bool, + }, + }, + map[string]tftypes.Value{ + "bool": tftypes.NewValue(tftypes.Bool, nil), + }, + ), + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "bool": testschema.Attribute{ + RequiredForImport: true, + Type: types.BoolType, + }, + }, + }, + }, + target: new(struct { + String types.String `tfsdk:"bool"` + }), + expected: &struct { + String types.String `tfsdk:"bool"` + }{ + String: types.String{}, + }, + expectedDiags: diag.Diagnostics{ + diag.WithPath( + path.Root("bool"), + intreflect.DiagNewAttributeValueIntoWrongType{ + ValType: reflect.TypeOf(types.Bool{}), + TargetType: reflect.TypeOf(types.String{}), + SchemaType: types.BoolType, + }, + ), + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + diags := testCase.resourceIdentity.Get(context.Background(), testCase.target) + + if diff := cmp.Diff(diags, testCase.expectedDiags); diff != "" { + t.Errorf("unexpected diagnostics (+wanted, -got): %s", diff) + } + + if diff := cmp.Diff(testCase.target, testCase.expected); diff != "" { + t.Errorf("unexpected value (+wanted, -got): %s", diff) + } + }) + } +} + +func TestResourceIdentityGetAttribute(t *testing.T) { + t.Parallel() + + type testCase struct { + resourceIdentity tfsdk.ResourceIdentity + target interface{} + expected interface{} + expectedDiags diag.Diagnostics + } + + testCases := map[string]testCase{ + // Refer to fwschemadata.TestDataGetAtPath for more exhaustive unit + // testing. These test cases are to ensure ResourceIdentity schema and data values + // are passed appropriately to the shared implementation. + "valid": { + resourceIdentity: tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "name": tftypes.String, + }, + }, map[string]tftypes.Value{ + "name": tftypes.NewValue(tftypes.String, "namevalue"), + }), + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "name": testschema.Attribute{ + Type: types.StringType, + RequiredForImport: true, + }, + }, + }, + }, + target: new(string), + expected: pointer("namevalue"), + }, + "diagnostics": { + resourceIdentity: tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "name": tftypes.String, + }, + }, map[string]tftypes.Value{ + "name": tftypes.NewValue(tftypes.String, "namevalue"), + }), + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "name": testschema.Attribute{ + Type: testtypes.StringTypeWithValidateWarning{}, + RequiredForImport: true, + }, + }, + }, + }, + target: new(testtypes.String), + expected: &testtypes.String{InternalString: types.StringValue("namevalue"), CreatedBy: testtypes.StringTypeWithValidateWarning{}}, + expectedDiags: diag.Diagnostics{testtypes.TestWarningDiagnostic(path.Root("name"))}, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + diags := tc.resourceIdentity.GetAttribute(context.Background(), path.Root("name"), tc.target) + + if diff := cmp.Diff(diags, tc.expectedDiags); diff != "" { + t.Errorf("unexpected diagnostics (+wanted, -got): %s", diff) + } + + if diff := cmp.Diff(tc.target, tc.expected, cmp.Transformer("testtypes", func(in *testtypes.String) testtypes.String { return *in }), cmp.Transformer("types", func(in *types.String) types.String { return *in })); diff != "" { + t.Errorf("unexpected value (+wanted, -got): %s", diff) + } + }) + } +} + +func TestResourceIdentityPathMatches(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + resourceIdentity tfsdk.ResourceIdentity + expression path.Expression + expected path.Paths + expectedDiags diag.Diagnostics + }{ + // Refer to fwschemadata.TestDataPathMatches for more exhaustive unit testing. + // These test cases are to ensure ResourceIdentity schema and data values are + // passed appropriately to the shared implementation. + "AttributeNameExact-match": { + resourceIdentity: tfsdk.ResourceIdentity{ + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "test": testschema.Attribute{ + Type: types.StringType, + RequiredForImport: true, + }, + }, + }, + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "test-value"), + }, + ), + }, + expression: path.MatchRoot("test"), + expected: path.Paths{ + path.Root("test"), + }, + }, + "AttributeNameExact-mismatch": { + resourceIdentity: tfsdk.ResourceIdentity{ + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "test": testschema.Attribute{ + Type: types.StringType, + RequiredForImport: true, + }, + }, + }, + Raw: tftypes.NewValue( + tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + }, + }, + map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "test-value"), + }, + ), + }, + expression: path.MatchRoot("not-test"), + expected: nil, + expectedDiags: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Invalid Path Expression for Schema", + "The Terraform Provider unexpectedly provided a path expression that does not match the current schema. "+ + "This can happen if the path expression does not correctly follow the schema in structure or types. "+ + "Please report this to the provider developers.\n\n"+ + "Path Expression: not-test", + ), + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, diags := testCase.resourceIdentity.PathMatches(context.Background(), testCase.expression) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + + if diff := cmp.Diff(diags, testCase.expectedDiags); diff != "" { + t.Errorf("unexpected diagnostics difference: %s", diff) + } + }) + } +} + +func TestResourceIdentitySet(t *testing.T) { + t.Parallel() + + type testCase struct { + resourceIdentity tfsdk.ResourceIdentity + val interface{} + expected tftypes.Value + expectedDiags diag.Diagnostics + } + + testCases := map[string]testCase{ + // Refer to fwschemadata.TestDataSet for more exhaustive unit testing. + // These test cases are to ensure ResourceIdentity schema and data values are + // passed appropriately to the shared implementation. + "valid": { + resourceIdentity: tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "name": tftypes.String, + }, + }, map[string]tftypes.Value{ + "name": tftypes.NewValue(tftypes.String, "oldvalue"), + }), + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "name": testschema.Attribute{ + Type: types.StringType, + RequiredForImport: true, + }, + }, + }, + }, + val: struct { + Name string `tfsdk:"name"` + }{ + Name: "newvalue", + }, + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "name": tftypes.String, + }, + }, map[string]tftypes.Value{ + "name": tftypes.NewValue(tftypes.String, "newvalue"), + }), + }, + "diagnostics": { + resourceIdentity: tfsdk.ResourceIdentity{ + Raw: tftypes.Value{}, + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "name": testschema.Attribute{ + Type: testtypes.StringTypeWithValidateWarning{}, + RequiredForImport: true, + }, + }, + }, + }, + val: struct { + Name string `tfsdk:"name"` + }{ + Name: "newvalue", + }, + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "name": tftypes.String, + }, + }, map[string]tftypes.Value{ + "name": tftypes.NewValue(tftypes.String, "newvalue"), + }), + expectedDiags: diag.Diagnostics{testtypes.TestWarningDiagnostic(path.Root("name"))}, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + diags := tc.resourceIdentity.Set(context.Background(), tc.val) + + if diff := cmp.Diff(diags, tc.expectedDiags); diff != "" { + t.Errorf("unexpected diagnostics (+wanted, -got): %s", diff) + } + + if diff := cmp.Diff(tc.resourceIdentity.Raw, tc.expected); diff != "" { + t.Errorf("unexpected value (+wanted, -got): %s", diff) + } + }) + } +} + +func TestResourceIdentitySetAttribute(t *testing.T) { + t.Parallel() + + type testCase struct { + resourceIdentity tfsdk.ResourceIdentity + path path.Path + val interface{} + expected tftypes.Value + expectedDiags diag.Diagnostics + } + + testCases := map[string]testCase{ + // Refer to fwschemadata.TestDataSetAtPath for more exhaustive unit + // testing. These test cases are to ensure ResourceIdentity schema and data values + // are passed appropriately to the shared implementation. + "valid": { + resourceIdentity: tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "originalvalue"), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "test": testschema.Attribute{ + Type: types.StringType, + RequiredForImport: true, + }, + "other": testschema.Attribute{ + Type: types.StringType, + OptionalForImport: true, + }, + }, + }, + }, + path: path.Root("test"), + val: "newvalue", + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test": tftypes.String, + "other": tftypes.String, + }, + }, map[string]tftypes.Value{ + "test": tftypes.NewValue(tftypes.String, "newvalue"), + "other": tftypes.NewValue(tftypes.String, "should be untouched"), + }), + }, + "diagnostics": { + resourceIdentity: tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "name": tftypes.String, + }, + }, map[string]tftypes.Value{ + "name": tftypes.NewValue(tftypes.String, "originalname"), + }), + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "name": testschema.Attribute{ + Type: testtypes.StringTypeWithValidateWarning{}, + RequiredForImport: true, + }, + }, + }, + }, + path: path.Root("name"), + val: "newname", + expected: tftypes.NewValue(tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "name": tftypes.String, + }, + }, map[string]tftypes.Value{ + "name": tftypes.NewValue(tftypes.String, "newname"), + }), + expectedDiags: diag.Diagnostics{ + testtypes.TestWarningDiagnostic(path.Root("name")), + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + diags := tc.resourceIdentity.SetAttribute(context.Background(), tc.path, tc.val) + + if diff := cmp.Diff(diags, tc.expectedDiags); diff != "" { + for _, diagnostic := range diags { + t.Log(diagnostic) + } + t.Errorf("unexpected diagnostics (+wanted, -got): %s", diff) + } + + if diff := cmp.Diff(tc.resourceIdentity.Raw, tc.expected); diff != "" { + t.Errorf("unexpected value (+wanted, -got): %s", diff) + } + }) + } +} From d07f756b6e3e4e6c54d4be4b2d12e0fcf55a4a07 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Wed, 12 Mar 2025 14:38:45 -0400 Subject: [PATCH 14/20] ReadResource RPC implementation for v5 --- internal/fromproto5/readresource.go | 9 +- internal/fromproto5/readresource_test.go | 59 ++++++- internal/fromproto5/resource_identity.go | 62 +++++++ internal/fromproto5/resource_identity_test.go | 129 +++++++++++++++ internal/fwserver/server.go | 70 ++++++++ internal/fwserver/server_readresource.go | 43 +++++ internal/fwserver/server_readresource_test.go | 156 ++++++++++++++++++ internal/proto5server/server_readresource.go | 10 +- .../proto5server/server_readresource_test.go | 135 +++++++++++++++ internal/toproto5/readresource.go | 5 + internal/toproto5/readresource_test.go | 80 +++++++++ internal/toproto5/resource_identity.go | 35 ++++ internal/toproto5/resource_identity_test.go | 109 ++++++++++++ resource/read.go | 12 ++ 14 files changed, 911 insertions(+), 3 deletions(-) create mode 100644 internal/fromproto5/resource_identity.go create mode 100644 internal/fromproto5/resource_identity_test.go create mode 100644 internal/toproto5/resource_identity.go create mode 100644 internal/toproto5/resource_identity_test.go diff --git a/internal/fromproto5/readresource.go b/internal/fromproto5/readresource.go index 04d4b4d2e..003424579 100644 --- a/internal/fromproto5/readresource.go +++ b/internal/fromproto5/readresource.go @@ -17,7 +17,7 @@ import ( // ReadResourceRequest returns the *fwserver.ReadResourceRequest // equivalent of a *tfprotov5.ReadResourceRequest. -func ReadResourceRequest(ctx context.Context, proto5 *tfprotov5.ReadResourceRequest, reqResource resource.Resource, resourceSchema fwschema.Schema, providerMetaSchema fwschema.Schema) (*fwserver.ReadResourceRequest, diag.Diagnostics) { +func ReadResourceRequest(ctx context.Context, proto5 *tfprotov5.ReadResourceRequest, reqResource resource.Resource, resourceSchema fwschema.Schema, providerMetaSchema fwschema.Schema, identitySchema fwschema.Schema) (*fwserver.ReadResourceRequest, diag.Diagnostics) { if proto5 == nil { return nil, nil } @@ -26,6 +26,7 @@ func ReadResourceRequest(ctx context.Context, proto5 *tfprotov5.ReadResourceRequ fw := &fwserver.ReadResourceRequest{ Resource: reqResource, + IdentitySchema: identitySchema, ClientCapabilities: ReadResourceClientCapabilities(proto5.ClientCapabilities), } @@ -35,6 +36,12 @@ func ReadResourceRequest(ctx context.Context, proto5 *tfprotov5.ReadResourceRequ fw.CurrentState = currentState + currentIdentity, currentIdentityDiags := ResourceIdentity(ctx, proto5.CurrentIdentity, identitySchema) + + diags.Append(currentIdentityDiags...) + + fw.CurrentIdentity = currentIdentity + providerMeta, providerMetaDiags := ProviderMeta(ctx, proto5.ProviderMeta, providerMetaSchema) diags.Append(providerMetaDiags...) diff --git a/internal/fromproto5/readresource_test.go b/internal/fromproto5/readresource_test.go index c58fc01cc..1c8adb7f6 100644 --- a/internal/fromproto5/readresource_test.go +++ b/internal/fromproto5/readresource_test.go @@ -17,6 +17,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/tfsdk" ) @@ -48,6 +49,30 @@ func TestReadResourceRequest(t *testing.T) { }, } + testIdentityProto5Type := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_identity_attribute": tftypes.String, + }, + } + + testIdentityProto5Value := tftypes.NewValue(testIdentityProto5Type, map[string]tftypes.Value{ + "test_identity_attribute": tftypes.NewValue(tftypes.String, "id-123"), + }) + + testIdentityProto5DynamicValue, err := tfprotov5.NewDynamicValue(testIdentityProto5Type, testIdentityProto5Value) + + if err != nil { + t.Fatalf("unexpected error calling tfprotov5.NewDynamicValue(): %s", err) + } + + testIdentitySchema := identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_identity_attribute": identityschema.StringAttribute{ + RequiredForImport: true, + }, + }, + } + testProviderKeyValue := privatestate.MustMarshalToJson(map[string][]byte{ "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), }) @@ -59,6 +84,7 @@ func TestReadResourceRequest(t *testing.T) { testCases := map[string]struct { input *tfprotov5.ReadResourceRequest resourceSchema fwschema.Schema + identitySchema fwschema.Schema resource resource.Resource providerMetaSchema fwschema.Schema expected *fwserver.ReadResourceRequest @@ -99,6 +125,37 @@ func TestReadResourceRequest(t *testing.T) { }, }, }, + "currentidentity-missing-schema": { + input: &tfprotov5.ReadResourceRequest{ + CurrentIdentity: &tfprotov5.ResourceIdentityData{ + IdentityData: &testIdentityProto5DynamicValue, + }, + }, + expected: &fwserver.ReadResourceRequest{}, + expectedDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Unable to Convert Resource Identity", + "An unexpected error was encountered when converting the resource identity from the protocol type. "+ + "Identity data was sent in the protocol to a resource that doesn't support identity.\n\n"+ + "This is always a problem with Terraform or terraform-plugin-framework. Please report this to the provider developer.", + ), + }, + }, + "currentidentity": { + input: &tfprotov5.ReadResourceRequest{ + CurrentIdentity: &tfprotov5.ResourceIdentityData{ + IdentityData: &testIdentityProto5DynamicValue, + }, + }, + identitySchema: testIdentitySchema, + expected: &fwserver.ReadResourceRequest{ + IdentitySchema: testIdentitySchema, + CurrentIdentity: &tfsdk.ResourceIdentity{ + Raw: testIdentityProto5Value, + Schema: testIdentitySchema, + }, + }, + }, "private-malformed-json": { input: &tfprotov5.ReadResourceRequest{ Private: []byte(`{`), @@ -200,7 +257,7 @@ func TestReadResourceRequest(t *testing.T) { t.Run(name, func(t *testing.T) { t.Parallel() - got, diags := fromproto5.ReadResourceRequest(context.Background(), testCase.input, testCase.resource, testCase.resourceSchema, testCase.providerMetaSchema) + got, diags := fromproto5.ReadResourceRequest(context.Background(), testCase.input, testCase.resource, testCase.resourceSchema, testCase.providerMetaSchema, testCase.identitySchema) if diff := cmp.Diff(got, testCase.expected, cmp.AllowUnexported(privatestate.ProviderData{})); diff != "" { t.Errorf("unexpected difference: %s", diff) diff --git a/internal/fromproto5/resource_identity.go b/internal/fromproto5/resource_identity.go new file mode 100644 index 000000000..c6e1a8014 --- /dev/null +++ b/internal/fromproto5/resource_identity.go @@ -0,0 +1,62 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fromproto5 + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschemadata" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" +) + +// TODO:ResourceIdentity: Should we create a wrapping struct to contain the identity data? To match the protocol (in-case we want to introduce other identity things) +// - Need to think more on this (like what if we want to introduce display-only attributes) +// - If we introduce one, add a test as well. +func ResourceIdentity(ctx context.Context, in *tfprotov5.ResourceIdentityData, schema fwschema.Schema) (*tfsdk.ResourceIdentity, diag.Diagnostics) { + if in == nil { + return nil, nil + } + + return IdentityData(ctx, in.IdentityData, schema) +} + +// IdentityData returns the *tfsdk.ResourceIdentity for a *tfprotov5.DynamicValue and fwschema.Schema. +func IdentityData(ctx context.Context, proto5DynamicValue *tfprotov5.DynamicValue, schema fwschema.Schema) (*tfsdk.ResourceIdentity, diag.Diagnostics) { + if proto5DynamicValue == nil { + return nil, nil + } + + var diags diag.Diagnostics + + // Panic prevention here to simplify the calling implementations. + // This should not happen, but just in case. + if schema == nil { + diags.AddError( + "Unable to Convert Resource Identity", + "An unexpected error was encountered when converting the resource identity from the protocol type. "+ + "Identity data was sent in the protocol to a resource that doesn't support identity.\n\n"+ + "This is always a problem with Terraform or terraform-plugin-framework. Please report this to the provider developer.", + ) + + return nil, diags + } + + data, dynamicValueDiags := DynamicValue(ctx, proto5DynamicValue, schema, fwschemadata.DataDescriptionResourceIdentity) + + diags.Append(dynamicValueDiags...) + + if diags.HasError() { + return nil, diags + } + + fw := &tfsdk.ResourceIdentity{ + Raw: data.TerraformValue, + Schema: schema, + } + + return fw, diags +} diff --git a/internal/fromproto5/resource_identity_test.go b/internal/fromproto5/resource_identity_test.go new file mode 100644 index 000000000..8e7687140 --- /dev/null +++ b/internal/fromproto5/resource_identity_test.go @@ -0,0 +1,129 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fromproto5_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fromproto5" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestResourceIdentity(t *testing.T) { + t.Parallel() + + testProto5Type := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_attribute": tftypes.String, + }, + } + + testProto5Value := tftypes.NewValue(testProto5Type, map[string]tftypes.Value{ + "test_attribute": tftypes.NewValue(tftypes.String, "test-value"), + }) + + testProto5DynamicValue, err := tfprotov5.NewDynamicValue(testProto5Type, testProto5Value) + + if err != nil { + t.Fatalf("unexpected error calling tfprotov5.NewDynamicValue(): %s", err) + } + + testFwSchema := testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "test_attribute": testschema.Attribute{ + RequiredForImport: true, + Type: types.StringType, + }, + }, + } + + testFwSchemaInvalid := testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "test_attribute": testschema.Attribute{ + RequiredForImport: true, + Type: types.BoolType, + }, + }, + } + + testCases := map[string]struct { + input *tfprotov5.ResourceIdentityData + schema fwschema.Schema + expected *tfsdk.ResourceIdentity + expectedDiagnostics diag.Diagnostics + }{ + "nil": { + input: nil, + expected: nil, + }, + "empty": { + input: &tfprotov5.ResourceIdentityData{}, + expected: nil, + }, + "missing-schema": { + input: &tfprotov5.ResourceIdentityData{ + IdentityData: &testProto5DynamicValue, + }, + expected: nil, + expectedDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Unable to Convert Resource Identity", + "An unexpected error was encountered when converting the resource identity from the protocol type. "+ + "Identity data was sent in the protocol to a resource that doesn't support identity.\n\n"+ + "This is always a problem with Terraform or terraform-plugin-framework. Please report this to the provider developer.", + ), + }, + }, + "invalid-schema": { + input: &tfprotov5.ResourceIdentityData{ + IdentityData: &testProto5DynamicValue, + }, + schema: testFwSchemaInvalid, + expected: nil, + expectedDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Unable to Convert Resource Identity", + "An unexpected error was encountered when converting the resource identity from the protocol type. "+ + "This is always an issue in terraform-plugin-framework used to implement the provider and should be reported to the provider developers.\n\n"+ + "Please report this to the provider developer:\n\n"+ + "Unable to unmarshal DynamicValue: AttributeName(\"test_attribute\"): couldn't decode bool: msgpack: invalid code=aa decoding bool", + ), + }, + }, + "valid": { + input: &tfprotov5.ResourceIdentityData{ + IdentityData: &testProto5DynamicValue, + }, + schema: testFwSchema, + expected: &tfsdk.ResourceIdentity{ + Raw: testProto5Value, + Schema: testFwSchema, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, diags := fromproto5.ResourceIdentity(context.Background(), testCase.input, testCase.schema) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + + if diff := cmp.Diff(diags, testCase.expectedDiagnostics); diff != "" { + t.Errorf("unexpected diagnostics difference: %s", diff) + } + }) + } +} diff --git a/internal/fwserver/server.go b/internal/fwserver/server.go index 22723f9ff..b4b8a779b 100644 --- a/internal/fwserver/server.go +++ b/internal/fwserver/server.go @@ -158,6 +158,15 @@ type Server struct { // access from race conditions. resourceSchemasMutex sync.RWMutex + // resourceIdentitySchemas is the cached Resource Identity Schemas for RPCs that need to + // convert resource identity data from the protocol. If not found, it will be + // fetched from the Resource IdentitySchema method. + resourceIdentitySchemas map[string]fwschema.Schema + + // resourceIdentitySchemasMutex is a mutex to protect concurrent resourceIdentitySchemas + // access from race conditions. + resourceIdentitySchemasMutex sync.RWMutex + // resourceFuncs is the cached Resource functions for RPCs that need to // access resources. If not found, it will be fetched from the // Provider.Resources() method. @@ -690,6 +699,67 @@ func (s *Server) ResourceSchemas(ctx context.Context) (map[string]fwschema.Schem return resourceSchemas, diags } +// ResourceIdentitySchema returns the Resource Identity Schema for the given type name and +// caches the result for later Identity operations. Identity is an optional feature for resources, +// so this function will return a nil schema with no diagnostics if the resource type doesn't define +// an identity schema. +func (s *Server) ResourceIdentitySchema(ctx context.Context, typeName string) (fwschema.Schema, diag.Diagnostics) { + s.resourceIdentitySchemasMutex.RLock() + resourceIdentitySchema, ok := s.resourceIdentitySchemas[typeName] + s.resourceIdentitySchemasMutex.RUnlock() + + if ok { + return resourceIdentitySchema, nil + } + + var diags diag.Diagnostics + + r, resourceDiags := s.Resource(ctx, typeName) + + diags.Append(resourceDiags...) + + if diags.HasError() { + return nil, diags + } + + resourceWithIdentity, ok := r.(resource.ResourceWithIdentity) + if !ok { + // It's valid for a resource to not have an identity, so cache and return a nil schema + s.resourceIdentitySchemasMutex.Lock() + if s.resourceIdentitySchemas == nil { + s.resourceIdentitySchemas = make(map[string]fwschema.Schema) + } + + s.resourceIdentitySchemas[typeName] = nil + s.resourceIdentitySchemasMutex.Unlock() + + return nil, nil + } + + identitySchemaReq := resource.IdentitySchemaRequest{} + identitySchemaResp := resource.IdentitySchemaResponse{} + + logging.FrameworkTrace(ctx, "Calling provider defined Resource IdentitySchema method", map[string]interface{}{logging.KeyResourceType: typeName}) + resourceWithIdentity.IdentitySchema(ctx, identitySchemaReq, &identitySchemaResp) + logging.FrameworkTrace(ctx, "Called provider defined Resource IdentitySchema method", map[string]interface{}{logging.KeyResourceType: typeName}) + + diags.Append(identitySchemaResp.Diagnostics...) + + if diags.HasError() { + return identitySchemaResp.IdentitySchema, diags + } + + s.resourceIdentitySchemasMutex.Lock() + if s.resourceIdentitySchemas == nil { + s.resourceIdentitySchemas = make(map[string]fwschema.Schema) + } + + s.resourceIdentitySchemas[typeName] = identitySchemaResp.IdentitySchema + s.resourceIdentitySchemasMutex.Unlock() + + return identitySchemaResp.IdentitySchema, diags +} + // ResourceIdentitySchemas returns a map of Resource Identity Schemas for the // GetResourceIdentitySchemas RPC without caching since not all schemas are guaranteed to // be necessary for later provider operations. The schema implementations are diff --git a/internal/fwserver/server_readresource.go b/internal/fwserver/server_readresource.go index d260e2912..6ceeda853 100644 --- a/internal/fwserver/server_readresource.go +++ b/internal/fwserver/server_readresource.go @@ -9,6 +9,7 @@ import ( "github.com/hashicorp/terraform-plugin-go/tftypes" "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschemadata" "github.com/hashicorp/terraform-plugin-framework/internal/logging" "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" @@ -20,6 +21,8 @@ import ( // ReadResource RPC. type ReadResourceRequest struct { ClientCapabilities resource.ReadClientCapabilities + IdentitySchema fwschema.Schema + CurrentIdentity *tfsdk.ResourceIdentity CurrentState *tfsdk.State Resource resource.Resource Private *privatestate.Data @@ -32,6 +35,7 @@ type ReadResourceResponse struct { Deferred *resource.Deferred Diagnostics diag.Diagnostics NewState *tfsdk.State + NewIdentity *tfsdk.ResourceIdentity Private *privatestate.Data } @@ -115,12 +119,41 @@ func (s *Server) ReadResource(ctx context.Context, req *ReadResourceRequest, res resp.Private = req.Private } + if req.CurrentIdentity != nil { + readReq.Identity = &tfsdk.ResourceIdentity{ + Schema: req.CurrentIdentity.Schema, + Raw: req.CurrentIdentity.Raw.Copy(), + } + + readResp.Identity = &tfsdk.ResourceIdentity{ + Schema: req.CurrentIdentity.Schema, + Raw: req.CurrentIdentity.Raw.Copy(), + } + } + + // If the resource supports identity and there is no current identity data, pre-populate with a null value. + // TODO:ResourceIdentity: Is there any reason a provider WOULD NOT want to populate an identity when it supports one? + if req.CurrentIdentity == nil && req.IdentitySchema != nil { + nullTfValue := tftypes.NewValue(req.IdentitySchema.Type().TerraformType(ctx), nil) + + readReq.Identity = &tfsdk.ResourceIdentity{ + Schema: req.IdentitySchema, + Raw: nullTfValue.Copy(), + } + + readResp.Identity = &tfsdk.ResourceIdentity{ + Schema: req.IdentitySchema, + Raw: nullTfValue.Copy(), + } + } + logging.FrameworkTrace(ctx, "Calling provider defined Resource Read") req.Resource.Read(ctx, readReq, &readResp) logging.FrameworkTrace(ctx, "Called provider defined Resource Read") resp.Diagnostics = readResp.Diagnostics resp.NewState = &readResp.State + resp.NewIdentity = readResp.Identity resp.Deferred = readResp.Deferred if readResp.Private != nil { @@ -135,6 +168,16 @@ func (s *Server) ReadResource(ctx context.Context, req *ReadResourceRequest, res return } + if resp.NewIdentity != nil && req.IdentitySchema == nil { + resp.Diagnostics.AddError( + "Unexpected Read Response", + "An unexpected error was encountered when creating the read response. New identity data was returned by the provider read operation, but the resource does not indicate identity support.\n\n"+ + "This is always a problem with the provider and should be reported to the provider developer.", + ) + + return + } + semanticEqualityReq := SchemaSemanticEqualityRequest{ PriorData: fwschemadata.Data{ Description: fwschemadata.DataDescriptionState, diff --git a/internal/fwserver/server_readresource_test.go b/internal/fwserver/server_readresource_test.go index 9f3aa7065..a846d389c 100644 --- a/internal/fwserver/server_readresource_test.go +++ b/internal/fwserver/server_readresource_test.go @@ -19,6 +19,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" "github.com/hashicorp/terraform-plugin-framework/provider" "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" @@ -34,6 +35,12 @@ func TestServerReadResource(t *testing.T) { }, } + testIdentityType := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_id": tftypes.String, + }, + } + testTypeWriteOnly := tftypes.Object{ AttributeTypes: map[string]tftypes.Type{ "test_write_only": tftypes.String, @@ -46,6 +53,10 @@ func TestServerReadResource(t *testing.T) { "test_required": tftypes.NewValue(tftypes.String, "test-currentstate-value"), }) + testCurrentIdentityValue := tftypes.NewValue(testIdentityType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "id-123"), + }) + testCurrentStateValueWriteOnly := tftypes.NewValue(testTypeWriteOnly, map[string]tftypes.Value{ "test_write_only": tftypes.NewValue(tftypes.String, nil), "test_required": tftypes.NewValue(tftypes.String, "test-currentstate-value"), @@ -56,6 +67,10 @@ func TestServerReadResource(t *testing.T) { "test_required": tftypes.NewValue(tftypes.String, "test-currentstate-value"), }) + testNewIdentityValue := tftypes.NewValue(testIdentityType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "new-id-123"), + }) + testSchema := schema.Schema{ Attributes: map[string]schema.Attribute{ "test_computed": schema.StringAttribute{ @@ -67,6 +82,14 @@ func TestServerReadResource(t *testing.T) { }, } + testIdentitySchema := identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_id": identityschema.StringAttribute{ + RequiredForImport: true, + }, + }, + } + testSchemaWriteOnly := schema.Schema{ Attributes: map[string]schema.Attribute{ "test_write_only": schema.StringAttribute{ @@ -121,6 +144,11 @@ func TestServerReadResource(t *testing.T) { Schema: testSchema, } + testCurrentIdentity := &tfsdk.ResourceIdentity{ + Raw: testCurrentIdentityValue, + Schema: testIdentitySchema, + } + testCurrentStateWriteOnly := &tfsdk.State{ Raw: testCurrentStateValueWriteOnly, Schema: testSchemaWriteOnly, @@ -131,6 +159,11 @@ func TestServerReadResource(t *testing.T) { Schema: testSchema, } + testNewIdentity := &tfsdk.ResourceIdentity{ + Raw: testNewIdentityValue, + Schema: testIdentitySchema, + } + testNewStateRemoved := &tfsdk.State{ Raw: tftypes.NewValue(testType, nil), Schema: testSchema, @@ -249,6 +282,36 @@ func TestServerReadResource(t *testing.T) { Private: testEmptyPrivate, }, }, + "request-currentidentity": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ReadResourceRequest{ + CurrentState: testCurrentState, + IdentitySchema: testIdentitySchema, + CurrentIdentity: testCurrentIdentity, + Resource: &testprovider.ResourceWithIdentity{ + Resource: &testprovider.Resource{ + ReadMethod: func(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var identityData struct { + TestID types.String `tfsdk:"test_id"` + } + + resp.Diagnostics.Append(req.Identity.Get(ctx, &identityData)...) + + if identityData.TestID.ValueString() != "id-123" { + resp.Diagnostics.AddError("unexpected req.Identity value: %s", identityData.TestID.ValueString()) + } + }, + }, + }, + }, + expectedResponse: &fwserver.ReadResourceResponse{ + NewState: testCurrentState, + NewIdentity: testCurrentIdentity, + Private: testEmptyPrivate, + }, + }, "request-providermeta": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, @@ -532,6 +595,99 @@ func TestServerReadResource(t *testing.T) { Private: testEmptyPrivate, }, }, + "response-identity-new": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ReadResourceRequest{ + CurrentState: testCurrentState, + // Resource supports identity but there isn't one in state yet + CurrentIdentity: nil, + IdentitySchema: testIdentitySchema, + Resource: &testprovider.ResourceWithIdentity{ + Resource: &testprovider.Resource{ + ReadMethod: func(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + if !req.Identity.Raw.IsNull() { + resp.Diagnostics.AddError("Unexpected request", "expected req.Identity to be null") + return + } + + identityData := struct { + TestID types.String `tfsdk:"test_id"` + }{ + TestID: types.StringValue("new-id-123"), + } + + resp.Diagnostics.Append(resp.Identity.Set(ctx, &identityData)...) + }, + }, + }, + }, + expectedResponse: &fwserver.ReadResourceResponse{ + NewState: testCurrentState, + NewIdentity: testNewIdentity, + Private: testEmptyPrivate, + }, + }, + "response-identity-update": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ReadResourceRequest{ + CurrentState: testCurrentState, + CurrentIdentity: testCurrentIdentity, + IdentitySchema: testIdentitySchema, + Resource: &testprovider.ResourceWithIdentity{ + Resource: &testprovider.Resource{ + ReadMethod: func(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var identityData struct { + TestID types.String `tfsdk:"test_id"` + } + + resp.Diagnostics.Append(req.Identity.Get(ctx, &identityData)...) + + identityData.TestID = types.StringValue("new-id-123") + + resp.Diagnostics.Append(resp.Identity.Set(ctx, &identityData)...) + }, + }, + }, + }, + expectedResponse: &fwserver.ReadResourceResponse{ + NewState: testCurrentState, + NewIdentity: testNewIdentity, + Private: testEmptyPrivate, + }, + }, + "response-invalid-identity": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ReadResourceRequest{ + CurrentState: testCurrentState, + Resource: &testprovider.Resource{ + ReadMethod: func(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + // This resource doesn't indicate identity support (via a schema), so this should raise a diagnostic. + resp.Identity = &tfsdk.ResourceIdentity{ + Raw: testNewIdentityValue, + Schema: testIdentitySchema, + } + }, + }, + }, + expectedResponse: &fwserver.ReadResourceResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Unexpected Read Response", + "An unexpected error was encountered when creating the read response. New identity data was returned by the provider read operation, but the resource does not indicate identity support.\n\n"+ + "This is always a problem with the provider and should be reported to the provider developer.", + ), + }, + NewState: testCurrentState, + NewIdentity: testNewIdentity, + Private: testEmptyPrivate, + }, + }, "response-state-removeresource": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, diff --git a/internal/proto5server/server_readresource.go b/internal/proto5server/server_readresource.go index e9863e148..299b67835 100644 --- a/internal/proto5server/server_readresource.go +++ b/internal/proto5server/server_readresource.go @@ -37,6 +37,14 @@ func (s *Server) ReadResource(ctx context.Context, proto5Req *tfprotov5.ReadReso return toproto5.ReadResourceResponse(ctx, fwResp), nil } + identitySchema, diags := s.FrameworkServer.ResourceIdentitySchema(ctx, proto5Req.TypeName) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto5.ReadResourceResponse(ctx, fwResp), nil + } + providerMetaSchema, diags := s.FrameworkServer.ProviderMetaSchema(ctx) fwResp.Diagnostics.Append(diags...) @@ -45,7 +53,7 @@ func (s *Server) ReadResource(ctx context.Context, proto5Req *tfprotov5.ReadReso return toproto5.ReadResourceResponse(ctx, fwResp), nil } - fwReq, diags := fromproto5.ReadResourceRequest(ctx, proto5Req, resource, resourceSchema, providerMetaSchema) + fwReq, diags := fromproto5.ReadResourceRequest(ctx, proto5Req, resource, resourceSchema, providerMetaSchema, identitySchema) fwResp.Diagnostics.Append(diags...) diff --git a/internal/proto5server/server_readresource_test.go b/internal/proto5server/server_readresource_test.go index 8fbba4647..e23054fb6 100644 --- a/internal/proto5server/server_readresource_test.go +++ b/internal/proto5server/server_readresource_test.go @@ -18,6 +18,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/provider" "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/types" ) @@ -46,6 +47,20 @@ func TestServerReadResource(t *testing.T) { testNewStateRemovedDynamicValue, _ := tfprotov5.NewDynamicValue(testType, tftypes.NewValue(testType, nil)) + testIdentityType := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_id": tftypes.String, + }, + } + + testCurrentIdentityValue := testNewDynamicValue(t, testIdentityType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "id-123"), + }) + + testNewIdentityDynamicValue := testNewDynamicValue(t, testIdentityType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "new-id-123"), + }) + testProviderMetaDynamicValue := testNewDynamicValue(t, tftypes.Object{ AttributeTypes: map[string]tftypes.Type{ @@ -70,6 +85,14 @@ func TestServerReadResource(t *testing.T) { }, } + testIdentitySchema := identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_id": identityschema.StringAttribute{ + RequiredForImport: true, + }, + }, + } + testCases := map[string]struct { server *Server request *tfprotov5.ReadResourceRequest @@ -246,6 +269,69 @@ func TestServerReadResource(t *testing.T) { }), }, }, + "request-currentidentity": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.ProviderWithMetaSchema{ + Provider: &testprovider.Provider{ + ResourcesMethod: func(_ context.Context) []func() resource.Resource { + return []func() resource.Resource{ + func() resource.Resource { + return &testprovider.ResourceWithIdentity{ + Resource: &testprovider.Resource{ + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {}, + MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "test_resource" + }, + ReadMethod: func(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var identityData struct { + TestID types.String `tfsdk:"test_id"` + } + + resp.Diagnostics.Append(req.Identity.Get(ctx, &identityData)...) + + if identityData.TestID.ValueString() != "id-123" { + resp.Diagnostics.AddError("Unexpected req.Identity", identityData.TestID.ValueString()) + } + }, + }, + IdentitySchemaMethod: func(ctx context.Context, req resource.IdentitySchemaRequest, resp *resource.IdentitySchemaResponse) { + resp.IdentitySchema = testIdentitySchema + }, + } + }, + } + }, + }, + MetaSchemaMethod: func(_ context.Context, _ provider.MetaSchemaRequest, resp *provider.MetaSchemaResponse) { + resp.Schema = metaschema.Schema{ + Attributes: map[string]metaschema.Attribute{ + "test_optional": metaschema.StringAttribute{ + Optional: true, + }, + "test_required": metaschema.StringAttribute{ + Required: true, + }, + }, + } + }, + }, + }, + }, + request: &tfprotov5.ReadResourceRequest{ + CurrentState: testEmptyDynamicValue, + CurrentIdentity: &tfprotov5.ResourceIdentityData{ + IdentityData: testCurrentIdentityValue, + }, + TypeName: "test_resource", + }, + expectedResponse: &tfprotov5.ReadResourceResponse{ + NewState: testEmptyDynamicValue, + NewIdentity: &tfprotov5.ResourceIdentityData{ + IdentityData: testCurrentIdentityValue, + }, + }, + }, "response-diagnostics": { server: &Server{ FrameworkServer: fwserver.Server{ @@ -332,6 +418,55 @@ func TestServerReadResource(t *testing.T) { NewState: testNewStateDynamicValue, }, }, + "response-identity": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + ResourcesMethod: func(_ context.Context) []func() resource.Resource { + return []func() resource.Resource{ + func() resource.Resource { + return &testprovider.ResourceWithIdentity{ + Resource: &testprovider.Resource{ + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {}, + MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "test_resource" + }, + ReadMethod: func(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var identityData struct { + TestID types.String `tfsdk:"test_id"` + } + + resp.Diagnostics.Append(req.Identity.Get(ctx, &identityData)...) + + identityData.TestID = types.StringValue("new-id-123") + + resp.Diagnostics.Append(resp.Identity.Set(ctx, identityData)...) + }, + }, + IdentitySchemaMethod: func(ctx context.Context, req resource.IdentitySchemaRequest, resp *resource.IdentitySchemaResponse) { + resp.IdentitySchema = testIdentitySchema + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.ReadResourceRequest{ + CurrentState: testEmptyDynamicValue, + CurrentIdentity: &tfprotov5.ResourceIdentityData{ + IdentityData: testNewIdentityDynamicValue, + }, + TypeName: "test_resource", + }, + expectedResponse: &tfprotov5.ReadResourceResponse{ + NewState: testEmptyDynamicValue, + NewIdentity: &tfprotov5.ResourceIdentityData{ + IdentityData: testNewIdentityDynamicValue, + }, + }, + }, "response-state-removeresource": { server: &Server{ FrameworkServer: fwserver.Server{ diff --git a/internal/toproto5/readresource.go b/internal/toproto5/readresource.go index 9193a3950..56695adae 100644 --- a/internal/toproto5/readresource.go +++ b/internal/toproto5/readresource.go @@ -28,6 +28,11 @@ func ReadResourceResponse(ctx context.Context, fw *fwserver.ReadResourceResponse proto5.Diagnostics = append(proto5.Diagnostics, Diagnostics(ctx, diags)...) proto5.NewState = newState + newIdentity, diags := ResourceIdentity(ctx, fw.NewIdentity) + + proto5.Diagnostics = append(proto5.Diagnostics, Diagnostics(ctx, diags)...) + proto5.NewIdentity = newIdentity + newPrivate, diags := fw.Private.Bytes(ctx) proto5.Diagnostics = append(proto5.Diagnostics, Diagnostics(ctx, diags)...) diff --git a/internal/toproto5/readresource_test.go b/internal/toproto5/readresource_test.go index 2e9c549d3..8058f1c38 100644 --- a/internal/toproto5/readresource_test.go +++ b/internal/toproto5/readresource_test.go @@ -16,6 +16,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" "github.com/hashicorp/terraform-plugin-framework/internal/toproto5" "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/tfsdk" ) @@ -39,6 +40,22 @@ func TestReadResourceResponse(t *testing.T) { t.Fatalf("unexpected error calling tfprotov5.NewDynamicValue(): %s", err) } + testIdentityProto5Type := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_id": tftypes.String, + }, + } + + testIdentityProto5Value := tftypes.NewValue(testIdentityProto5Type, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "id-123"), + }) + + testIdentityProto5DynamicValue, err := tfprotov5.NewDynamicValue(testIdentityProto5Type, testIdentityProto5Value) + + if err != nil { + t.Fatalf("unexpected error calling tfprotov5.NewDynamicValue(): %s", err) + } + testProviderKeyValue := privatestate.MustMarshalToJson(map[string][]byte{ "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), }) @@ -69,6 +86,28 @@ func TestReadResourceResponse(t *testing.T) { }, } + testIdentity := &tfsdk.ResourceIdentity{ + Raw: testIdentityProto5Value, + Schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_id": identityschema.StringAttribute{ + RequiredForImport: true, + }, + }, + }, + } + + testIdentityInvalid := &tfsdk.ResourceIdentity{ + Raw: testIdentityProto5Value, + Schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_id": identityschema.BoolAttribute{ + RequiredForImport: true, + }, + }, + }, + } + testDeferral := &resource.Deferred{ Reason: resource.DeferredReasonAbsentPrereq, } @@ -142,6 +181,37 @@ func TestReadResourceResponse(t *testing.T) { }, }, }, + "diagnostics-invalid-newidentity": { + input: &fwserver.ReadResourceResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewWarningDiagnostic("test warning summary", "test warning details"), + diag.NewErrorDiagnostic("test error summary", "test error details"), + }, + NewIdentity: testIdentityInvalid, + }, + expected: &tfprotov5.ReadResourceResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityWarning, + Summary: "test warning summary", + Detail: "test warning details", + }, + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "test error summary", + Detail: "test error details", + }, + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "Unable to Convert Resource Identity", + Detail: "An unexpected error was encountered when converting the resource identity to the protocol type. " + + "This is always an issue in terraform-plugin-framework used to implement the provider and should be reported to the provider developers.\n\n" + + "Please report this to the provider developer:\n\n" + + "Unable to create DynamicValue: AttributeName(\"test_id\"): unexpected value type string, tftypes.Bool values must be of type bool", + }, + }, + }, + }, "newstate": { input: &fwserver.ReadResourceResponse{ NewState: testState, @@ -150,6 +220,16 @@ func TestReadResourceResponse(t *testing.T) { NewState: &testProto5DynamicValue, }, }, + "newidentity": { + input: &fwserver.ReadResourceResponse{ + NewIdentity: testIdentity, + }, + expected: &tfprotov5.ReadResourceResponse{ + NewIdentity: &tfprotov5.ResourceIdentityData{ + IdentityData: &testIdentityProto5DynamicValue, + }, + }, + }, "private-empty": { input: &fwserver.ReadResourceResponse{ Private: &privatestate.Data{ diff --git a/internal/toproto5/resource_identity.go b/internal/toproto5/resource_identity.go new file mode 100644 index 000000000..eea046ac9 --- /dev/null +++ b/internal/toproto5/resource_identity.go @@ -0,0 +1,35 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto5 + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschemadata" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" +) + +// ResourceIdentity returns the *tfprotov5.ResourceIdentityData for a *tfsdk.ResourceIdentity. +func ResourceIdentity(ctx context.Context, fw *tfsdk.ResourceIdentity) (*tfprotov5.ResourceIdentityData, diag.Diagnostics) { + if fw == nil { + return nil, nil + } + + identitySchemaData := &fwschemadata.Data{ + Description: fwschemadata.DataDescriptionResourceIdentity, + Schema: fw.Schema, + TerraformValue: fw.Raw, + } + + identityData, diags := DynamicValue(ctx, identitySchemaData) + if diags.HasError() { + return nil, diags + } + + return &tfprotov5.ResourceIdentityData{ + IdentityData: identityData, + }, nil +} diff --git a/internal/toproto5/resource_identity_test.go b/internal/toproto5/resource_identity_test.go new file mode 100644 index 000000000..ff62eff67 --- /dev/null +++ b/internal/toproto5/resource_identity_test.go @@ -0,0 +1,109 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto5_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/internal/toproto5" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestResourceIdentity(t *testing.T) { + t.Parallel() + + testProto5Type := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_attribute": tftypes.String, + }, + } + + testProto5Value := tftypes.NewValue(testProto5Type, map[string]tftypes.Value{ + "test_attribute": tftypes.NewValue(tftypes.String, "test-value"), + }) + + testProto5DynamicValue, err := tfprotov5.NewDynamicValue(testProto5Type, testProto5Value) + + if err != nil { + t.Fatalf("unexpected error calling tfprotov5.NewDynamicValue(): %s", err) + } + + testResourceIdentity := &tfsdk.ResourceIdentity{ + Raw: testProto5Value, + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "test_attribute": testschema.Attribute{ + Required: true, + Type: types.StringType, + }, + }, + }, + } + + testResourceIdentityInvalid := &tfsdk.ResourceIdentity{ + Raw: testProto5Value, + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "test_attribute": testschema.Attribute{ + Required: true, + Type: types.BoolType, + }, + }, + }, + } + + testCases := map[string]struct { + input *tfsdk.ResourceIdentity + expected *tfprotov5.ResourceIdentityData + expectedDiagnostics diag.Diagnostics + }{ + "nil": { + input: nil, + expected: nil, + }, + "invalid-schema": { + input: testResourceIdentityInvalid, + expected: nil, + expectedDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Unable to Convert Resource Identity", + "An unexpected error was encountered when converting the resource identity to the protocol type. "+ + "This is always an issue in terraform-plugin-framework used to implement the provider and should be reported to the provider developers.\n\n"+ + "Please report this to the provider developer:\n\n"+ + "Unable to create DynamicValue: AttributeName(\"test_attribute\"): unexpected value type string, tftypes.Bool values must be of type bool", + ), + }, + }, + "valid": { + input: testResourceIdentity, + expected: &tfprotov5.ResourceIdentityData{ + IdentityData: &testProto5DynamicValue, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, diags := toproto5.ResourceIdentity(context.Background(), testCase.input) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + + if diff := cmp.Diff(diags, testCase.expectedDiagnostics); diff != "" { + t.Errorf("unexpected diagnostics difference: %s", diff) + } + }) + } +} diff --git a/resource/read.go b/resource/read.go index 53c4cb832..74820e333 100644 --- a/resource/read.go +++ b/resource/read.go @@ -30,6 +30,10 @@ type ReadRequest struct { // operation. State tfsdk.State + // Identity is the current identity of the resource prior to the Read + // operation. If the resource does not support identity, this value will not be set. + Identity *tfsdk.ResourceIdentity + // Private is provider-defined resource private state data which was previously // stored with the resource state. This data is opaque to Terraform and does // not affect plan output. Any existing data is copied to @@ -57,6 +61,14 @@ type ReadResponse struct { // should be set during the resource's Read operation. State tfsdk.State + // Identity is the identity of the resource following the Read operation. + // This field is pre-populated from ReadRequest.Identity and + // should be set during the resource's Read operation. + // + // If the resource does not support identity, this value will not be set and will + // raise a diagnostic if set by the resource's Read operation. + Identity *tfsdk.ResourceIdentity + // Private is the private state resource data following the Read operation. // This field is pre-populated from ReadResourceRequest.Private and // can be modified during the resource's Read operation. From e3dd2b6e9919bcaa8d3b7e807a6b5b98db91e419 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Wed, 12 Mar 2025 14:54:46 -0400 Subject: [PATCH 15/20] ReadResource RPC implementation for v6 --- internal/fromproto6/readresource.go | 9 +- internal/fromproto6/readresource_test.go | 59 +++++++- internal/fromproto6/resource_identity.go | 62 ++++++++ internal/fromproto6/resource_identity_test.go | 129 +++++++++++++++++ internal/proto6server/server_readresource.go | 10 +- .../proto6server/server_readresource_test.go | 135 ++++++++++++++++++ internal/toproto6/readresource.go | 5 + internal/toproto6/readresource_test.go | 80 +++++++++++ internal/toproto6/resource_identity.go | 35 +++++ internal/toproto6/resource_identity_test.go | 109 ++++++++++++++ 10 files changed, 630 insertions(+), 3 deletions(-) create mode 100644 internal/fromproto6/resource_identity.go create mode 100644 internal/fromproto6/resource_identity_test.go create mode 100644 internal/toproto6/resource_identity.go create mode 100644 internal/toproto6/resource_identity_test.go diff --git a/internal/fromproto6/readresource.go b/internal/fromproto6/readresource.go index a42f669d6..11ea6d844 100644 --- a/internal/fromproto6/readresource.go +++ b/internal/fromproto6/readresource.go @@ -17,7 +17,7 @@ import ( // ReadResourceRequest returns the *fwserver.ReadResourceRequest // equivalent of a *tfprotov6.ReadResourceRequest. -func ReadResourceRequest(ctx context.Context, proto6 *tfprotov6.ReadResourceRequest, reqResource resource.Resource, resourceSchema fwschema.Schema, providerMetaSchema fwschema.Schema) (*fwserver.ReadResourceRequest, diag.Diagnostics) { +func ReadResourceRequest(ctx context.Context, proto6 *tfprotov6.ReadResourceRequest, reqResource resource.Resource, resourceSchema fwschema.Schema, providerMetaSchema fwschema.Schema, identitySchema fwschema.Schema) (*fwserver.ReadResourceRequest, diag.Diagnostics) { if proto6 == nil { return nil, nil } @@ -26,6 +26,7 @@ func ReadResourceRequest(ctx context.Context, proto6 *tfprotov6.ReadResourceRequ fw := &fwserver.ReadResourceRequest{ Resource: reqResource, + IdentitySchema: identitySchema, ClientCapabilities: ReadResourceClientCapabilities(proto6.ClientCapabilities), } @@ -35,6 +36,12 @@ func ReadResourceRequest(ctx context.Context, proto6 *tfprotov6.ReadResourceRequ fw.CurrentState = currentState + currentIdentity, currentIdentityDiags := ResourceIdentity(ctx, proto6.CurrentIdentity, identitySchema) + + diags.Append(currentIdentityDiags...) + + fw.CurrentIdentity = currentIdentity + providerMeta, providerMetaDiags := ProviderMeta(ctx, proto6.ProviderMeta, providerMetaSchema) diags.Append(providerMetaDiags...) diff --git a/internal/fromproto6/readresource_test.go b/internal/fromproto6/readresource_test.go index 5ae9e1688..e24e9f5bc 100644 --- a/internal/fromproto6/readresource_test.go +++ b/internal/fromproto6/readresource_test.go @@ -17,6 +17,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/tfsdk" ) @@ -48,6 +49,30 @@ func TestReadResourceRequest(t *testing.T) { }, } + testIdentityProto6Type := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_identity_attribute": tftypes.String, + }, + } + + testIdentityProto6Value := tftypes.NewValue(testIdentityProto6Type, map[string]tftypes.Value{ + "test_identity_attribute": tftypes.NewValue(tftypes.String, "id-123"), + }) + + testIdentityProto6DynamicValue, err := tfprotov6.NewDynamicValue(testIdentityProto6Type, testIdentityProto6Value) + + if err != nil { + t.Fatalf("unexpected error calling tfprotov6.NewDynamicValue(): %s", err) + } + + testIdentitySchema := identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_identity_attribute": identityschema.StringAttribute{ + RequiredForImport: true, + }, + }, + } + testProviderKeyValue := privatestate.MustMarshalToJson(map[string][]byte{ "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), }) @@ -59,6 +84,7 @@ func TestReadResourceRequest(t *testing.T) { testCases := map[string]struct { input *tfprotov6.ReadResourceRequest resourceSchema fwschema.Schema + identitySchema fwschema.Schema resource resource.Resource providerMetaSchema fwschema.Schema expected *fwserver.ReadResourceRequest @@ -99,6 +125,37 @@ func TestReadResourceRequest(t *testing.T) { }, }, }, + "currentidentity-missing-schema": { + input: &tfprotov6.ReadResourceRequest{ + CurrentIdentity: &tfprotov6.ResourceIdentityData{ + IdentityData: &testIdentityProto6DynamicValue, + }, + }, + expected: &fwserver.ReadResourceRequest{}, + expectedDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Unable to Convert Resource Identity", + "An unexpected error was encountered when converting the resource identity from the protocol type. "+ + "Identity data was sent in the protocol to a resource that doesn't support identity.\n\n"+ + "This is always a problem with Terraform or terraform-plugin-framework. Please report this to the provider developer.", + ), + }, + }, + "currentidentity": { + input: &tfprotov6.ReadResourceRequest{ + CurrentIdentity: &tfprotov6.ResourceIdentityData{ + IdentityData: &testIdentityProto6DynamicValue, + }, + }, + identitySchema: testIdentitySchema, + expected: &fwserver.ReadResourceRequest{ + IdentitySchema: testIdentitySchema, + CurrentIdentity: &tfsdk.ResourceIdentity{ + Raw: testIdentityProto6Value, + Schema: testIdentitySchema, + }, + }, + }, "private-malformed-json": { input: &tfprotov6.ReadResourceRequest{ Private: []byte(`{`), @@ -200,7 +257,7 @@ func TestReadResourceRequest(t *testing.T) { t.Run(name, func(t *testing.T) { t.Parallel() - got, diags := fromproto6.ReadResourceRequest(context.Background(), testCase.input, testCase.resource, testCase.resourceSchema, testCase.providerMetaSchema) + got, diags := fromproto6.ReadResourceRequest(context.Background(), testCase.input, testCase.resource, testCase.resourceSchema, testCase.providerMetaSchema, testCase.identitySchema) if diff := cmp.Diff(got, testCase.expected, cmp.AllowUnexported(privatestate.ProviderData{})); diff != "" { t.Errorf("unexpected difference: %s", diff) diff --git a/internal/fromproto6/resource_identity.go b/internal/fromproto6/resource_identity.go new file mode 100644 index 000000000..d9fc1b4e5 --- /dev/null +++ b/internal/fromproto6/resource_identity.go @@ -0,0 +1,62 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fromproto6 + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschemadata" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" +) + +// TODO:ResourceIdentity: Should we create a wrapping struct to contain the identity data? To match the protocol (in-case we want to introduce other identity things) +// - Need to think more on this (like what if we want to introduce display-only attributes) +// - If we introduce one, add a test as well. +func ResourceIdentity(ctx context.Context, in *tfprotov6.ResourceIdentityData, schema fwschema.Schema) (*tfsdk.ResourceIdentity, diag.Diagnostics) { + if in == nil { + return nil, nil + } + + return IdentityData(ctx, in.IdentityData, schema) +} + +// IdentityData returns the *tfsdk.ResourceIdentity for a *tfprotov6.DynamicValue and fwschema.Schema. +func IdentityData(ctx context.Context, proto6DynamicValue *tfprotov6.DynamicValue, schema fwschema.Schema) (*tfsdk.ResourceIdentity, diag.Diagnostics) { + if proto6DynamicValue == nil { + return nil, nil + } + + var diags diag.Diagnostics + + // Panic prevention here to simplify the calling implementations. + // This should not happen, but just in case. + if schema == nil { + diags.AddError( + "Unable to Convert Resource Identity", + "An unexpected error was encountered when converting the resource identity from the protocol type. "+ + "Identity data was sent in the protocol to a resource that doesn't support identity.\n\n"+ + "This is always a problem with Terraform or terraform-plugin-framework. Please report this to the provider developer.", + ) + + return nil, diags + } + + data, dynamicValueDiags := DynamicValue(ctx, proto6DynamicValue, schema, fwschemadata.DataDescriptionResourceIdentity) + + diags.Append(dynamicValueDiags...) + + if diags.HasError() { + return nil, diags + } + + fw := &tfsdk.ResourceIdentity{ + Raw: data.TerraformValue, + Schema: schema, + } + + return fw, diags +} diff --git a/internal/fromproto6/resource_identity_test.go b/internal/fromproto6/resource_identity_test.go new file mode 100644 index 000000000..51724170d --- /dev/null +++ b/internal/fromproto6/resource_identity_test.go @@ -0,0 +1,129 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fromproto6_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fromproto6" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestResourceIdentity(t *testing.T) { + t.Parallel() + + testProto6Type := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_attribute": tftypes.String, + }, + } + + testProto6Value := tftypes.NewValue(testProto6Type, map[string]tftypes.Value{ + "test_attribute": tftypes.NewValue(tftypes.String, "test-value"), + }) + + testProto6DynamicValue, err := tfprotov6.NewDynamicValue(testProto6Type, testProto6Value) + + if err != nil { + t.Fatalf("unexpected error calling tfprotov6.NewDynamicValue(): %s", err) + } + + testFwSchema := testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "test_attribute": testschema.Attribute{ + RequiredForImport: true, + Type: types.StringType, + }, + }, + } + + testFwSchemaInvalid := testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "test_attribute": testschema.Attribute{ + RequiredForImport: true, + Type: types.BoolType, + }, + }, + } + + testCases := map[string]struct { + input *tfprotov6.ResourceIdentityData + schema fwschema.Schema + expected *tfsdk.ResourceIdentity + expectedDiagnostics diag.Diagnostics + }{ + "nil": { + input: nil, + expected: nil, + }, + "empty": { + input: &tfprotov6.ResourceIdentityData{}, + expected: nil, + }, + "missing-schema": { + input: &tfprotov6.ResourceIdentityData{ + IdentityData: &testProto6DynamicValue, + }, + expected: nil, + expectedDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Unable to Convert Resource Identity", + "An unexpected error was encountered when converting the resource identity from the protocol type. "+ + "Identity data was sent in the protocol to a resource that doesn't support identity.\n\n"+ + "This is always a problem with Terraform or terraform-plugin-framework. Please report this to the provider developer.", + ), + }, + }, + "invalid-schema": { + input: &tfprotov6.ResourceIdentityData{ + IdentityData: &testProto6DynamicValue, + }, + schema: testFwSchemaInvalid, + expected: nil, + expectedDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Unable to Convert Resource Identity", + "An unexpected error was encountered when converting the resource identity from the protocol type. "+ + "This is always an issue in terraform-plugin-framework used to implement the provider and should be reported to the provider developers.\n\n"+ + "Please report this to the provider developer:\n\n"+ + "Unable to unmarshal DynamicValue: AttributeName(\"test_attribute\"): couldn't decode bool: msgpack: invalid code=aa decoding bool", + ), + }, + }, + "valid": { + input: &tfprotov6.ResourceIdentityData{ + IdentityData: &testProto6DynamicValue, + }, + schema: testFwSchema, + expected: &tfsdk.ResourceIdentity{ + Raw: testProto6Value, + Schema: testFwSchema, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, diags := fromproto6.ResourceIdentity(context.Background(), testCase.input, testCase.schema) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + + if diff := cmp.Diff(diags, testCase.expectedDiagnostics); diff != "" { + t.Errorf("unexpected diagnostics difference: %s", diff) + } + }) + } +} diff --git a/internal/proto6server/server_readresource.go b/internal/proto6server/server_readresource.go index d5b0cfab5..89cec5523 100644 --- a/internal/proto6server/server_readresource.go +++ b/internal/proto6server/server_readresource.go @@ -36,6 +36,14 @@ func (s *Server) ReadResource(ctx context.Context, proto6Req *tfprotov6.ReadReso return toproto6.ReadResourceResponse(ctx, fwResp), nil } + identitySchema, diags := s.FrameworkServer.ResourceIdentitySchema(ctx, proto6Req.TypeName) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto6.ReadResourceResponse(ctx, fwResp), nil + } + providerMetaSchema, diags := s.FrameworkServer.ProviderMetaSchema(ctx) fwResp.Diagnostics.Append(diags...) @@ -44,7 +52,7 @@ func (s *Server) ReadResource(ctx context.Context, proto6Req *tfprotov6.ReadReso return toproto6.ReadResourceResponse(ctx, fwResp), nil } - fwReq, diags := fromproto6.ReadResourceRequest(ctx, proto6Req, resource, resourceSchema, providerMetaSchema) + fwReq, diags := fromproto6.ReadResourceRequest(ctx, proto6Req, resource, resourceSchema, providerMetaSchema, identitySchema) fwResp.Diagnostics.Append(diags...) diff --git a/internal/proto6server/server_readresource_test.go b/internal/proto6server/server_readresource_test.go index 7e2180008..aaa9a5235 100644 --- a/internal/proto6server/server_readresource_test.go +++ b/internal/proto6server/server_readresource_test.go @@ -18,6 +18,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/provider" "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/types" ) @@ -46,6 +47,20 @@ func TestServerReadResource(t *testing.T) { testNewStateRemovedDynamicValue, _ := tfprotov6.NewDynamicValue(testType, tftypes.NewValue(testType, nil)) + testIdentityType := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_id": tftypes.String, + }, + } + + testCurrentIdentityValue := testNewDynamicValue(t, testIdentityType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "id-123"), + }) + + testNewIdentityDynamicValue := testNewDynamicValue(t, testIdentityType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "new-id-123"), + }) + testProviderMetaDynamicValue := testNewDynamicValue(t, tftypes.Object{ AttributeTypes: map[string]tftypes.Type{ @@ -70,6 +85,14 @@ func TestServerReadResource(t *testing.T) { }, } + testIdentitySchema := identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_id": identityschema.StringAttribute{ + RequiredForImport: true, + }, + }, + } + testCases := map[string]struct { server *Server request *tfprotov6.ReadResourceRequest @@ -246,6 +269,69 @@ func TestServerReadResource(t *testing.T) { }), }, }, + "request-currentidentity": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.ProviderWithMetaSchema{ + Provider: &testprovider.Provider{ + ResourcesMethod: func(_ context.Context) []func() resource.Resource { + return []func() resource.Resource{ + func() resource.Resource { + return &testprovider.ResourceWithIdentity{ + Resource: &testprovider.Resource{ + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {}, + MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "test_resource" + }, + ReadMethod: func(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var identityData struct { + TestID types.String `tfsdk:"test_id"` + } + + resp.Diagnostics.Append(req.Identity.Get(ctx, &identityData)...) + + if identityData.TestID.ValueString() != "id-123" { + resp.Diagnostics.AddError("Unexpected req.Identity", identityData.TestID.ValueString()) + } + }, + }, + IdentitySchemaMethod: func(ctx context.Context, req resource.IdentitySchemaRequest, resp *resource.IdentitySchemaResponse) { + resp.IdentitySchema = testIdentitySchema + }, + } + }, + } + }, + }, + MetaSchemaMethod: func(_ context.Context, _ provider.MetaSchemaRequest, resp *provider.MetaSchemaResponse) { + resp.Schema = metaschema.Schema{ + Attributes: map[string]metaschema.Attribute{ + "test_optional": metaschema.StringAttribute{ + Optional: true, + }, + "test_required": metaschema.StringAttribute{ + Required: true, + }, + }, + } + }, + }, + }, + }, + request: &tfprotov6.ReadResourceRequest{ + CurrentState: testEmptyDynamicValue, + CurrentIdentity: &tfprotov6.ResourceIdentityData{ + IdentityData: testCurrentIdentityValue, + }, + TypeName: "test_resource", + }, + expectedResponse: &tfprotov6.ReadResourceResponse{ + NewState: testEmptyDynamicValue, + NewIdentity: &tfprotov6.ResourceIdentityData{ + IdentityData: testCurrentIdentityValue, + }, + }, + }, "response-diagnostics": { server: &Server{ FrameworkServer: fwserver.Server{ @@ -332,6 +418,55 @@ func TestServerReadResource(t *testing.T) { NewState: testNewStateDynamicValue, }, }, + "response-identity": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + ResourcesMethod: func(_ context.Context) []func() resource.Resource { + return []func() resource.Resource{ + func() resource.Resource { + return &testprovider.ResourceWithIdentity{ + Resource: &testprovider.Resource{ + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {}, + MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "test_resource" + }, + ReadMethod: func(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var identityData struct { + TestID types.String `tfsdk:"test_id"` + } + + resp.Diagnostics.Append(req.Identity.Get(ctx, &identityData)...) + + identityData.TestID = types.StringValue("new-id-123") + + resp.Diagnostics.Append(resp.Identity.Set(ctx, identityData)...) + }, + }, + IdentitySchemaMethod: func(ctx context.Context, req resource.IdentitySchemaRequest, resp *resource.IdentitySchemaResponse) { + resp.IdentitySchema = testIdentitySchema + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov6.ReadResourceRequest{ + CurrentState: testEmptyDynamicValue, + CurrentIdentity: &tfprotov6.ResourceIdentityData{ + IdentityData: testNewIdentityDynamicValue, + }, + TypeName: "test_resource", + }, + expectedResponse: &tfprotov6.ReadResourceResponse{ + NewState: testEmptyDynamicValue, + NewIdentity: &tfprotov6.ResourceIdentityData{ + IdentityData: testNewIdentityDynamicValue, + }, + }, + }, "response-state-removeresource": { server: &Server{ FrameworkServer: fwserver.Server{ diff --git a/internal/toproto6/readresource.go b/internal/toproto6/readresource.go index 2de7adf84..ea60e5a0f 100644 --- a/internal/toproto6/readresource.go +++ b/internal/toproto6/readresource.go @@ -28,6 +28,11 @@ func ReadResourceResponse(ctx context.Context, fw *fwserver.ReadResourceResponse proto6.Diagnostics = append(proto6.Diagnostics, Diagnostics(ctx, diags)...) proto6.NewState = newState + newIdentity, diags := ResourceIdentity(ctx, fw.NewIdentity) + + proto6.Diagnostics = append(proto6.Diagnostics, Diagnostics(ctx, diags)...) + proto6.NewIdentity = newIdentity + newPrivate, diags := fw.Private.Bytes(ctx) proto6.Diagnostics = append(proto6.Diagnostics, Diagnostics(ctx, diags)...) diff --git a/internal/toproto6/readresource_test.go b/internal/toproto6/readresource_test.go index 18842699e..0596dc30c 100644 --- a/internal/toproto6/readresource_test.go +++ b/internal/toproto6/readresource_test.go @@ -16,6 +16,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" "github.com/hashicorp/terraform-plugin-framework/internal/toproto6" "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/tfsdk" ) @@ -39,6 +40,22 @@ func TestReadResourceResponse(t *testing.T) { t.Fatalf("unexpected error calling tfprotov6.NewDynamicValue(): %s", err) } + testIdentityProto6Type := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_id": tftypes.String, + }, + } + + testIdentityProto6Value := tftypes.NewValue(testIdentityProto6Type, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "id-123"), + }) + + testIdentityProto6DynamicValue, err := tfprotov6.NewDynamicValue(testIdentityProto6Type, testIdentityProto6Value) + + if err != nil { + t.Fatalf("unexpected error calling tfprotov6.NewDynamicValue(): %s", err) + } + testState := &tfsdk.State{ Raw: testProto6Value, Schema: schema.Schema{ @@ -61,6 +78,28 @@ func TestReadResourceResponse(t *testing.T) { }, } + testIdentity := &tfsdk.ResourceIdentity{ + Raw: testIdentityProto6Value, + Schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_id": identityschema.StringAttribute{ + RequiredForImport: true, + }, + }, + }, + } + + testIdentityInvalid := &tfsdk.ResourceIdentity{ + Raw: testIdentityProto6Value, + Schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_id": identityschema.BoolAttribute{ + RequiredForImport: true, + }, + }, + }, + } + testProviderKeyValue := privatestate.MustMarshalToJson(map[string][]byte{ "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), }) @@ -142,6 +181,37 @@ func TestReadResourceResponse(t *testing.T) { }, }, }, + "diagnostics-invalid-newidentity": { + input: &fwserver.ReadResourceResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewWarningDiagnostic("test warning summary", "test warning details"), + diag.NewErrorDiagnostic("test error summary", "test error details"), + }, + NewIdentity: testIdentityInvalid, + }, + expected: &tfprotov6.ReadResourceResponse{ + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityWarning, + Summary: "test warning summary", + Detail: "test warning details", + }, + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "test error summary", + Detail: "test error details", + }, + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Unable to Convert Resource Identity", + Detail: "An unexpected error was encountered when converting the resource identity to the protocol type. " + + "This is always an issue in terraform-plugin-framework used to implement the provider and should be reported to the provider developers.\n\n" + + "Please report this to the provider developer:\n\n" + + "Unable to create DynamicValue: AttributeName(\"test_id\"): unexpected value type string, tftypes.Bool values must be of type bool", + }, + }, + }, + }, "newstate": { input: &fwserver.ReadResourceResponse{ NewState: testState, @@ -150,6 +220,16 @@ func TestReadResourceResponse(t *testing.T) { NewState: &testProto6DynamicValue, }, }, + "newidentity": { + input: &fwserver.ReadResourceResponse{ + NewIdentity: testIdentity, + }, + expected: &tfprotov6.ReadResourceResponse{ + NewIdentity: &tfprotov6.ResourceIdentityData{ + IdentityData: &testIdentityProto6DynamicValue, + }, + }, + }, "private-empty": { input: &fwserver.ReadResourceResponse{ Private: &privatestate.Data{ diff --git a/internal/toproto6/resource_identity.go b/internal/toproto6/resource_identity.go new file mode 100644 index 000000000..3dac3de99 --- /dev/null +++ b/internal/toproto6/resource_identity.go @@ -0,0 +1,35 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto6 + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschemadata" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" +) + +// ResourceIdentity returns the *tfprotov6.ResourceIdentityData for a *tfsdk.ResourceIdentity. +func ResourceIdentity(ctx context.Context, fw *tfsdk.ResourceIdentity) (*tfprotov6.ResourceIdentityData, diag.Diagnostics) { + if fw == nil { + return nil, nil + } + + identitySchemaData := &fwschemadata.Data{ + Description: fwschemadata.DataDescriptionResourceIdentity, + Schema: fw.Schema, + TerraformValue: fw.Raw, + } + + identityData, diags := DynamicValue(ctx, identitySchemaData) + if diags.HasError() { + return nil, diags + } + + return &tfprotov6.ResourceIdentityData{ + IdentityData: identityData, + }, nil +} diff --git a/internal/toproto6/resource_identity_test.go b/internal/toproto6/resource_identity_test.go new file mode 100644 index 000000000..699e29b64 --- /dev/null +++ b/internal/toproto6/resource_identity_test.go @@ -0,0 +1,109 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package toproto6_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" + "github.com/hashicorp/terraform-plugin-framework/internal/testing/testschema" + "github.com/hashicorp/terraform-plugin-framework/internal/toproto6" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tftypes" +) + +func TestResourceIdentity(t *testing.T) { + t.Parallel() + + testProto6Type := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_attribute": tftypes.String, + }, + } + + testProto6Value := tftypes.NewValue(testProto6Type, map[string]tftypes.Value{ + "test_attribute": tftypes.NewValue(tftypes.String, "test-value"), + }) + + testProto6DynamicValue, err := tfprotov6.NewDynamicValue(testProto6Type, testProto6Value) + + if err != nil { + t.Fatalf("unexpected error calling tfprotov6.NewDynamicValue(): %s", err) + } + + testResourceIdentity := &tfsdk.ResourceIdentity{ + Raw: testProto6Value, + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "test_attribute": testschema.Attribute{ + Required: true, + Type: types.StringType, + }, + }, + }, + } + + testResourceIdentityInvalid := &tfsdk.ResourceIdentity{ + Raw: testProto6Value, + Schema: testschema.Schema{ + Attributes: map[string]fwschema.Attribute{ + "test_attribute": testschema.Attribute{ + Required: true, + Type: types.BoolType, + }, + }, + }, + } + + testCases := map[string]struct { + input *tfsdk.ResourceIdentity + expected *tfprotov6.ResourceIdentityData + expectedDiagnostics diag.Diagnostics + }{ + "nil": { + input: nil, + expected: nil, + }, + "invalid-schema": { + input: testResourceIdentityInvalid, + expected: nil, + expectedDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Unable to Convert Resource Identity", + "An unexpected error was encountered when converting the resource identity to the protocol type. "+ + "This is always an issue in terraform-plugin-framework used to implement the provider and should be reported to the provider developers.\n\n"+ + "Please report this to the provider developer:\n\n"+ + "Unable to create DynamicValue: AttributeName(\"test_attribute\"): unexpected value type string, tftypes.Bool values must be of type bool", + ), + }, + }, + "valid": { + input: testResourceIdentity, + expected: &tfprotov6.ResourceIdentityData{ + IdentityData: &testProto6DynamicValue, + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got, diags := toproto6.ResourceIdentity(context.Background(), testCase.input) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + + if diff := cmp.Diff(diags, testCase.expectedDiagnostics); diff != "" { + t.Errorf("unexpected diagnostics difference: %s", diff) + } + }) + } +} From 8873227dcbfd81fdaaf4bf93a19c8040c6f10576 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Wed, 12 Mar 2025 18:07:21 -0400 Subject: [PATCH 16/20] protov5 + fwserver implementation for PlanResourceChange --- internal/fromproto5/planresourcechange.go | 9 +- .../fromproto5/planresourcechange_test.go | 64 ++++- .../fwserver/server_planresourcechange.go | 42 +++ .../server_planresourcechange_test.go | 267 ++++++++++++++++++ .../proto5server/server_planresourcechange.go | 10 +- .../server_planresourcechange_test.go | 147 ++++++++++ .../proto5server/server_readresource_test.go | 2 +- .../resourcewithidentityandmodifyplan.go | 43 +++ internal/toproto5/planresourcechange.go | 5 + internal/toproto5/planresourcechange_test.go | 80 ++++++ resource/modify_plan.go | 11 + 11 files changed, 676 insertions(+), 4 deletions(-) create mode 100644 internal/testing/testprovider/resourcewithidentityandmodifyplan.go diff --git a/internal/fromproto5/planresourcechange.go b/internal/fromproto5/planresourcechange.go index 5bd24c1dd..2255dc35b 100644 --- a/internal/fromproto5/planresourcechange.go +++ b/internal/fromproto5/planresourcechange.go @@ -17,7 +17,7 @@ import ( // PlanResourceChangeRequest returns the *fwserver.PlanResourceChangeRequest // equivalent of a *tfprotov5.PlanResourceChangeRequest. -func PlanResourceChangeRequest(ctx context.Context, proto5 *tfprotov5.PlanResourceChangeRequest, reqResource resource.Resource, resourceSchema fwschema.Schema, providerMetaSchema fwschema.Schema, resourceBehavior resource.ResourceBehavior) (*fwserver.PlanResourceChangeRequest, diag.Diagnostics) { +func PlanResourceChangeRequest(ctx context.Context, proto5 *tfprotov5.PlanResourceChangeRequest, reqResource resource.Resource, resourceSchema fwschema.Schema, providerMetaSchema fwschema.Schema, resourceBehavior resource.ResourceBehavior, identitySchema fwschema.Schema) (*fwserver.PlanResourceChangeRequest, diag.Diagnostics) { if proto5 == nil { return nil, nil } @@ -41,6 +41,7 @@ func PlanResourceChangeRequest(ctx context.Context, proto5 *tfprotov5.PlanResour fw := &fwserver.PlanResourceChangeRequest{ ResourceBehavior: resourceBehavior, ResourceSchema: resourceSchema, + IdentitySchema: identitySchema, Resource: reqResource, ClientCapabilities: ModifyPlanClientCapabilities(proto5.ClientCapabilities), } @@ -57,6 +58,12 @@ func PlanResourceChangeRequest(ctx context.Context, proto5 *tfprotov5.PlanResour fw.PriorState = priorState + priorIdentity, priorIdentityDiags := ResourceIdentity(ctx, proto5.PriorIdentity, identitySchema) + + diags.Append(priorIdentityDiags...) + + fw.PriorIdentity = priorIdentity + proposedNewState, proposedNewStateDiags := Plan(ctx, proto5.ProposedNewState, resourceSchema) diags.Append(proposedNewStateDiags...) diff --git a/internal/fromproto5/planresourcechange_test.go b/internal/fromproto5/planresourcechange_test.go index b223e0e59..fd37a170f 100644 --- a/internal/fromproto5/planresourcechange_test.go +++ b/internal/fromproto5/planresourcechange_test.go @@ -17,6 +17,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/tfsdk" ) @@ -48,6 +49,30 @@ func TestPlanResourceChangeRequest(t *testing.T) { }, } + testIdentityProto5Type := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_identity_attribute": tftypes.String, + }, + } + + testIdentityProto5Value := tftypes.NewValue(testIdentityProto5Type, map[string]tftypes.Value{ + "test_identity_attribute": tftypes.NewValue(tftypes.String, "id-123"), + }) + + testIdentityProto5DynamicValue, err := tfprotov5.NewDynamicValue(testIdentityProto5Type, testIdentityProto5Value) + + if err != nil { + t.Fatalf("unexpected error calling tfprotov5.NewDynamicValue(): %s", err) + } + + testIdentitySchema := identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_identity_attribute": identityschema.StringAttribute{ + RequiredForImport: true, + }, + }, + } + testProviderKeyValue := privatestate.MustMarshalToJson(map[string][]byte{ "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), }) @@ -58,6 +83,7 @@ func TestPlanResourceChangeRequest(t *testing.T) { input *tfprotov5.PlanResourceChangeRequest resourceBehavior resource.ResourceBehavior resourceSchema fwschema.Schema + identitySchema fwschema.Schema resource resource.Resource providerMetaSchema fwschema.Schema expected *fwserver.PlanResourceChangeRequest @@ -182,6 +208,42 @@ func TestPlanResourceChangeRequest(t *testing.T) { ResourceSchema: testFwSchema, }, }, + "prioridentity-missing-schema": { + input: &tfprotov5.PlanResourceChangeRequest{ + PriorIdentity: &tfprotov5.ResourceIdentityData{ + IdentityData: &testIdentityProto5DynamicValue, + }, + }, + resourceSchema: testFwSchema, + expected: &fwserver.PlanResourceChangeRequest{ + ResourceSchema: testFwSchema, + }, + expectedDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Unable to Convert Resource Identity", + "An unexpected error was encountered when converting the resource identity from the protocol type. "+ + "Identity data was sent in the protocol to a resource that doesn't support identity.\n\n"+ + "This is always a problem with Terraform or terraform-plugin-framework. Please report this to the provider developer.", + ), + }, + }, + "prioridentity": { + input: &tfprotov5.PlanResourceChangeRequest{ + PriorIdentity: &tfprotov5.ResourceIdentityData{ + IdentityData: &testIdentityProto5DynamicValue, + }, + }, + identitySchema: testIdentitySchema, + resourceSchema: testFwSchema, + expected: &fwserver.PlanResourceChangeRequest{ + IdentitySchema: testIdentitySchema, + PriorIdentity: &tfsdk.ResourceIdentity{ + Raw: testIdentityProto5Value, + Schema: testIdentitySchema, + }, + ResourceSchema: testFwSchema, + }, + }, "providermeta-missing-data": { input: &tfprotov5.PlanResourceChangeRequest{}, resourceSchema: testFwSchema, @@ -265,7 +327,7 @@ func TestPlanResourceChangeRequest(t *testing.T) { t.Run(name, func(t *testing.T) { t.Parallel() - got, diags := fromproto5.PlanResourceChangeRequest(context.Background(), testCase.input, testCase.resource, testCase.resourceSchema, testCase.providerMetaSchema, testCase.resourceBehavior) + got, diags := fromproto5.PlanResourceChangeRequest(context.Background(), testCase.input, testCase.resource, testCase.resourceSchema, testCase.providerMetaSchema, testCase.resourceBehavior, testCase.identitySchema) if diff := cmp.Diff(got, testCase.expected, cmp.AllowUnexported(privatestate.ProviderData{})); diff != "" { t.Errorf("unexpected difference: %s", diff) diff --git a/internal/fwserver/server_planresourcechange.go b/internal/fwserver/server_planresourcechange.go index 52b06688b..7910bac84 100644 --- a/internal/fwserver/server_planresourcechange.go +++ b/internal/fwserver/server_planresourcechange.go @@ -30,9 +30,11 @@ type PlanResourceChangeRequest struct { Config *tfsdk.Config PriorPrivate *privatestate.Data PriorState *tfsdk.State + PriorIdentity *tfsdk.ResourceIdentity ProposedNewState *tfsdk.Plan ProviderMeta *tfsdk.Config ResourceSchema fwschema.Schema + IdentitySchema fwschema.Schema Resource resource.Resource ResourceBehavior resource.ResourceBehavior } @@ -44,6 +46,7 @@ type PlanResourceChangeResponse struct { Diagnostics diag.Diagnostics PlannedPrivate *privatestate.Data PlannedState *tfsdk.State + PlannedIdentity *tfsdk.ResourceIdentity RequiresReplace path.Paths } @@ -115,6 +118,26 @@ func (s *Server) PlanResourceChange(ctx context.Context, req *PlanResourceChange } } + // If the resource supports identity and there is no prior identity data, pre-populate with a null value. + // TODO:ResourceIdentity: Is there any reason a provider WOULD NOT want to populate an identity when it supports one? + // TODO:ResourceIdentity: Should this be set to all unknowns? + if req.PriorIdentity == nil && req.IdentitySchema != nil { + nullIdentityTfValue := tftypes.NewValue(req.IdentitySchema.Type().TerraformType(ctx), nil) + + req.PriorIdentity = &tfsdk.ResourceIdentity{ + Schema: req.IdentitySchema, + Raw: nullIdentityTfValue.Copy(), + } + } + + // Set the planned identity to the prior identity by default (can be modified later). + if req.PriorIdentity != nil { + resp.PlannedIdentity = &tfsdk.ResourceIdentity{ + Schema: req.PriorIdentity.Schema, + Raw: req.PriorIdentity.Raw.Copy(), + } + } + // Ensure that resp.PlannedPrivate is never nil. resp.PlannedPrivate = privatestate.EmptyData(ctx) @@ -304,9 +327,17 @@ func (s *Server) PlanResourceChange(ctx context.Context, req *PlanResourceChange modifyPlanReq.ProviderMeta = *req.ProviderMeta } + if resp.PlannedIdentity != nil { + modifyPlanReq.Identity = &tfsdk.ResourceIdentity{ + Schema: resp.PlannedIdentity.Schema, + Raw: resp.PlannedIdentity.Raw.Copy(), + } + } + modifyPlanResp := resource.ModifyPlanResponse{ Diagnostics: resp.Diagnostics, Plan: modifyPlanReq.Plan, + Identity: modifyPlanReq.Identity, RequiresReplace: path.Paths{}, Private: modifyPlanReq.Private, } @@ -317,6 +348,7 @@ func (s *Server) PlanResourceChange(ctx context.Context, req *PlanResourceChange resp.Diagnostics = modifyPlanResp.Diagnostics resp.PlannedState = planToState(modifyPlanResp.Plan) + resp.PlannedIdentity = modifyPlanResp.Identity resp.RequiresReplace = append(resp.RequiresReplace, modifyPlanResp.RequiresReplace...) resp.PlannedPrivate.Provider = modifyPlanResp.Private resp.Deferred = modifyPlanResp.Deferred @@ -338,6 +370,16 @@ func (s *Server) PlanResourceChange(ctx context.Context, req *PlanResourceChange } } + if resp.PlannedIdentity != nil && req.IdentitySchema == nil { + resp.Diagnostics.AddError( + "Unexpected Plan Response", + "An unexpected error was encountered when creating the plan response. New identity data was returned by the provider planning operation, but the resource does not indicate identity support.\n\n"+ + "This is always a problem with the provider and should be reported to the provider developer.", + ) + + return + } + // Ensure deterministic RequiresReplace by sorting and deduplicating resp.RequiresReplace = NormaliseRequiresReplace(ctx, resp.RequiresReplace) diff --git a/internal/fwserver/server_planresourcechange_test.go b/internal/fwserver/server_planresourcechange_test.go index f1607daeb..e8cbf3ad7 100644 --- a/internal/fwserver/server_planresourcechange_test.go +++ b/internal/fwserver/server_planresourcechange_test.go @@ -24,6 +24,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/provider" "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" "github.com/hashicorp/terraform-plugin-framework/resource/schema/dynamicdefault" @@ -437,6 +438,12 @@ func TestServerPlanResourceChange(t *testing.T) { }, } + testIdentitySchemaType := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_id": tftypes.String, + }, + } + testSchemaTypeWriteOnly := tftypes.Object{ AttributeTypes: map[string]tftypes.Type{ "test_computed": tftypes.String, @@ -574,6 +581,14 @@ func TestServerPlanResourceChange(t *testing.T) { }, } + testIdentitySchema := identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_id": identityschema.StringAttribute{ + RequiredForImport: true, + }, + }, + } + testSchemaWriteOnly := schema.Schema{ Attributes: map[string]schema.Attribute{ "test_computed": schema.StringAttribute{ @@ -1107,6 +1122,10 @@ func TestServerPlanResourceChange(t *testing.T) { TestRequired types.String `tfsdk:"test_required"` } + type testIdentitySchemaData struct { + TestID types.String `tfsdk:"test_id"` + } + type testSchemaDataBlock struct { TestRequired types.String `tfsdk:"test_required"` TestOptionalBlock types.Object `tfsdk:"test_optional_block"` @@ -3072,6 +3091,66 @@ func TestServerPlanResourceChange(t *testing.T) { PlannedPrivate: testEmptyPrivate, }, }, + "create-resourcewithmodifyplan-request-prioridentity": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.PlanResourceChangeRequest{ + Config: &tfsdk.Config{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + Schema: testSchema, + }, + ProposedNewState: &tfsdk.Plan{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + Schema: testSchema, + }, + PriorState: testEmptyState, + PriorIdentity: &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(testIdentitySchemaType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "id-123"), + }), + Schema: testIdentitySchema, + }, + IdentitySchema: testIdentitySchema, + ResourceSchema: testSchema, + Resource: &testprovider.ResourceWithIdentityAndModifyPlan{ + ModifyPlanMethod: func(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + var data testIdentitySchemaData + + resp.Diagnostics.Append(req.Identity.Get(ctx, &data)...) + + if data.TestID.ValueString() != "id-123" { + resp.Diagnostics.AddError("Unexpected req.Identity Value", "Got: "+data.TestID.ValueString()) + } + }, + IdentitySchemaMethod: func(ctx context.Context, req resource.IdentitySchemaRequest, resp *resource.IdentitySchemaResponse) { + resp.IdentitySchema = testIdentitySchema + }, + }, + }, + expectedResponse: &fwserver.PlanResourceChangeResponse{ + PlannedState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, tftypes.UnknownValue), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + Schema: testSchema, + }, + PlannedIdentity: &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(testIdentitySchemaType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "id-123"), + }), + Schema: testIdentitySchema, + }, + PlannedPrivate: testEmptyPrivate, + }, + }, "create-resourcewithmodifyplan-request-providermeta": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, @@ -3420,6 +3499,124 @@ func TestServerPlanResourceChange(t *testing.T) { PlannedPrivate: testEmptyPrivate, }, }, + "create-resourcewithmodifyplan-response-plannedidentity-new": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.PlanResourceChangeRequest{ + Config: &tfsdk.Config{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + Schema: testSchema, + }, + ProposedNewState: &tfsdk.Plan{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + Schema: testSchema, + }, + PriorState: testEmptyState, + // Resource supports identity but there isn't one in state yet + PriorIdentity: nil, + IdentitySchema: testIdentitySchema, + ResourceSchema: testSchema, + Resource: &testprovider.ResourceWithIdentityAndModifyPlan{ + ModifyPlanMethod: func(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if !req.Identity.Raw.IsNull() { + resp.Diagnostics.AddError("Unexpected request", "expected req.Identity to be null") + return + } + + data := testIdentitySchemaData{ + TestID: types.StringValue("new-id-123"), + } + + resp.Diagnostics.Append(req.Identity.Set(ctx, &data)...) + }, + IdentitySchemaMethod: func(ctx context.Context, req resource.IdentitySchemaRequest, resp *resource.IdentitySchemaResponse) { + resp.IdentitySchema = testIdentitySchema + }, + }, + }, + expectedResponse: &fwserver.PlanResourceChangeResponse{ + PlannedState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, tftypes.UnknownValue), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + Schema: testSchema, + }, + PlannedIdentity: &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(testIdentitySchemaType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "new-id-123"), + }), + Schema: testIdentitySchema, + }, + PlannedPrivate: testEmptyPrivate, + }, + }, + "create-resourcewithmodifyplan-response-plannedidentity-update": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.PlanResourceChangeRequest{ + Config: &tfsdk.Config{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + Schema: testSchema, + }, + ProposedNewState: &tfsdk.Plan{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + Schema: testSchema, + }, + PriorState: testEmptyState, + PriorIdentity: &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(testIdentitySchemaType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "id-123"), + }), + Schema: testIdentitySchema, + }, + IdentitySchema: testIdentitySchema, + ResourceSchema: testSchema, + Resource: &testprovider.ResourceWithIdentityAndModifyPlan{ + ModifyPlanMethod: func(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + var data testIdentitySchemaData + resp.Diagnostics.Append(req.Identity.Get(ctx, &data)...) + + data.TestID = types.StringValue("new-id-123") + + resp.Diagnostics.Append(req.Identity.Set(ctx, &data)...) + }, + IdentitySchemaMethod: func(ctx context.Context, req resource.IdentitySchemaRequest, resp *resource.IdentitySchemaResponse) { + resp.IdentitySchema = testIdentitySchema + }, + }, + }, + expectedResponse: &fwserver.PlanResourceChangeResponse{ + PlannedState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, tftypes.UnknownValue), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + Schema: testSchema, + }, + PlannedIdentity: &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(testIdentitySchemaType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "new-id-123"), + }), + Schema: testIdentitySchema, + }, + PlannedPrivate: testEmptyPrivate, + }, + }, "create-resourcewithmodifyplan-response-private": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, @@ -3556,6 +3753,63 @@ func TestServerPlanResourceChange(t *testing.T) { PlannedPrivate: testPrivateProvider, }, }, + "create-resourcewithmodifyplan-response-invalid-identity": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.PlanResourceChangeRequest{ + Config: &tfsdk.Config{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + Schema: testSchema, + }, + ProposedNewState: &tfsdk.Plan{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + Schema: testSchema, + }, + PriorState: testEmptyState, + ResourceSchema: testSchema, + Resource: &testprovider.ResourceWithModifyPlan{ + ModifyPlanMethod: func(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + // This resource doesn't indicate identity support (via a schema), so this should raise a diagnostic. + resp.Identity = &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(testIdentitySchemaType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "new-id-123"), + }), + Schema: testIdentitySchema, + } + }, + }, + }, + expectedResponse: &fwserver.PlanResourceChangeResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Unexpected Plan Response", + "An unexpected error was encountered when creating the plan response. New identity data was returned by the provider planning operation, but the resource does not indicate identity support.\n\n"+ + "This is always a problem with the provider and should be reported to the provider developer.", + ), + }, + PlannedState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, tftypes.UnknownValue), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + Schema: testSchema, + }, + PlannedIdentity: &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(testIdentitySchemaType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "new-id-123"), + }), + Schema: testIdentitySchema, + }, + PlannedPrivate: testEmptyPrivate, + }, + }, "delete-resourcewithmodifyplan-request-config": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, @@ -5704,6 +5958,13 @@ func TestServerPlanResourceChange(t *testing.T) { }), Schema: testSchemaAttributePlanModifierAttributePlan, }, + PriorIdentity: &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(testIdentitySchemaType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "id-123"), + }), + Schema: testIdentitySchema, + }, + IdentitySchema: testIdentitySchema, ResourceSchema: testSchemaAttributePlanModifierAttributePlan, Resource: &testprovider.Resource{}, }, @@ -5722,6 +5983,12 @@ func TestServerPlanResourceChange(t *testing.T) { }), Schema: testSchemaAttributePlanModifierAttributePlan, }, + PlannedIdentity: &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(testIdentitySchemaType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "id-123"), + }), + Schema: testIdentitySchema, + }, PlannedPrivate: testEmptyPrivate, }, }, diff --git a/internal/proto5server/server_planresourcechange.go b/internal/proto5server/server_planresourcechange.go index a5cec1987..68918c532 100644 --- a/internal/proto5server/server_planresourcechange.go +++ b/internal/proto5server/server_planresourcechange.go @@ -37,6 +37,14 @@ func (s *Server) PlanResourceChange(ctx context.Context, proto5Req *tfprotov5.Pl return toproto5.PlanResourceChangeResponse(ctx, fwResp), nil } + identitySchema, diags := s.FrameworkServer.ResourceIdentitySchema(ctx, proto5Req.TypeName) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto5.PlanResourceChangeResponse(ctx, fwResp), nil + } + providerMetaSchema, diags := s.FrameworkServer.ProviderMetaSchema(ctx) fwResp.Diagnostics.Append(diags...) @@ -53,7 +61,7 @@ func (s *Server) PlanResourceChange(ctx context.Context, proto5Req *tfprotov5.Pl return toproto5.PlanResourceChangeResponse(ctx, fwResp), nil } - fwReq, diags := fromproto5.PlanResourceChangeRequest(ctx, proto5Req, resource, resourceSchema, providerMetaSchema, resourceBehavior) + fwReq, diags := fromproto5.PlanResourceChangeRequest(ctx, proto5Req, resource, resourceSchema, providerMetaSchema, resourceBehavior, identitySchema) fwResp.Diagnostics.Append(diags...) diff --git a/internal/proto5server/server_planresourcechange_test.go b/internal/proto5server/server_planresourcechange_test.go index fc452f6af..dbc9e609d 100644 --- a/internal/proto5server/server_planresourcechange_test.go +++ b/internal/proto5server/server_planresourcechange_test.go @@ -17,6 +17,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/provider" "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/types" ) @@ -33,6 +34,12 @@ func TestServerPlanResourceChange(t *testing.T) { testEmptyDynamicValue, _ := tfprotov5.NewDynamicValue(testSchemaType, tftypes.NewValue(testSchemaType, nil)) + testIdentitySchemaType := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_id": tftypes.String, + }, + } + testSchema := schema.Schema{ Attributes: map[string]schema.Attribute{ "test_computed": schema.StringAttribute{ @@ -44,6 +51,18 @@ func TestServerPlanResourceChange(t *testing.T) { }, } + testIdentitySchema := identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_id": identityschema.StringAttribute{ + RequiredForImport: true, + }, + }, + } + + type testIdentitySchemaData struct { + TestID types.String `tfsdk:"test_id"` + } + type testSchemaData struct { TestComputed types.String `tfsdk:"test_computed"` TestRequired types.String `tfsdk:"test_required"` @@ -179,6 +198,70 @@ func TestServerPlanResourceChange(t *testing.T) { }), }, }, + "create-request-plannedidentity": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + ResourcesMethod: func(_ context.Context) []func() resource.Resource { + return []func() resource.Resource{ + func() resource.Resource { + return &testprovider.ResourceWithIdentityAndModifyPlan{ + Resource: &testprovider.Resource{ + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema + }, + MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "test_resource" + }, + }, + IdentitySchemaMethod: func(ctx context.Context, req resource.IdentitySchemaRequest, resp *resource.IdentitySchemaResponse) { + resp.IdentitySchema = testIdentitySchema + }, + ModifyPlanMethod: func(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + var data testIdentitySchemaData + + resp.Diagnostics.Append(req.Identity.Get(ctx, &data)...) + + if data.TestID.ValueString() != "id-123" { + resp.Diagnostics.AddError("Unexpected req.Identity", data.TestID.ValueString()) + } + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.PlanResourceChangeRequest{ + Config: testNewDynamicValue(t, testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + ProposedNewState: testNewDynamicValue(t, testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + PriorIdentity: &tfprotov5.ResourceIdentityData{ + IdentityData: testNewDynamicValue(t, testIdentitySchemaType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "id-123"), + }), + }, + PriorState: &testEmptyDynamicValue, + TypeName: "test_resource", + }, + expectedResponse: &tfprotov5.PlanResourceChangeResponse{ + PlannedState: testNewDynamicValue(t, testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, tftypes.UnknownValue), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + PlannedIdentity: &tfprotov5.ResourceIdentityData{ + IdentityData: testNewDynamicValue(t, testIdentitySchemaType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "id-123"), + }), + }, + }, + }, "create-request-providermeta": { server: &Server{ FrameworkServer: fwserver.Server{ @@ -345,6 +428,70 @@ func TestServerPlanResourceChange(t *testing.T) { }), }, }, + "create-response-plannedidentity": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + ResourcesMethod: func(_ context.Context) []func() resource.Resource { + return []func() resource.Resource{ + func() resource.Resource { + return &testprovider.ResourceWithIdentityAndModifyPlan{ + Resource: &testprovider.Resource{ + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema + }, + MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "test_resource" + }, + }, + ModifyPlanMethod: func(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + var data testIdentitySchemaData + + resp.Diagnostics.Append(req.Identity.Get(ctx, &data)...) + + data.TestID = types.StringValue("new-id-123") + + resp.Diagnostics.Append(resp.Identity.Set(ctx, &data)...) + }, + IdentitySchemaMethod: func(ctx context.Context, req resource.IdentitySchemaRequest, resp *resource.IdentitySchemaResponse) { + resp.IdentitySchema = testIdentitySchema + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.PlanResourceChangeRequest{ + Config: testNewDynamicValue(t, testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + ProposedNewState: testNewDynamicValue(t, testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + PriorState: &testEmptyDynamicValue, + PriorIdentity: &tfprotov5.ResourceIdentityData{ + IdentityData: testNewDynamicValue(t, testIdentitySchemaType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "id-123"), + }), + }, + TypeName: "test_resource", + }, + expectedResponse: &tfprotov5.PlanResourceChangeResponse{ + PlannedState: testNewDynamicValue(t, testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, tftypes.UnknownValue), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + PlannedIdentity: &tfprotov5.ResourceIdentityData{ + IdentityData: testNewDynamicValue(t, testIdentitySchemaType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "new-id-123"), + }), + }, + }, + }, "create-response-requiresreplace": { server: &Server{ FrameworkServer: fwserver.Server{ diff --git a/internal/proto5server/server_readresource_test.go b/internal/proto5server/server_readresource_test.go index e23054fb6..9617d7b7a 100644 --- a/internal/proto5server/server_readresource_test.go +++ b/internal/proto5server/server_readresource_test.go @@ -456,7 +456,7 @@ func TestServerReadResource(t *testing.T) { request: &tfprotov5.ReadResourceRequest{ CurrentState: testEmptyDynamicValue, CurrentIdentity: &tfprotov5.ResourceIdentityData{ - IdentityData: testNewIdentityDynamicValue, + IdentityData: testCurrentIdentityValue, }, TypeName: "test_resource", }, diff --git a/internal/testing/testprovider/resourcewithidentityandmodifyplan.go b/internal/testing/testprovider/resourcewithidentityandmodifyplan.go new file mode 100644 index 000000000..5a1dc8847 --- /dev/null +++ b/internal/testing/testprovider/resourcewithidentityandmodifyplan.go @@ -0,0 +1,43 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package testprovider + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/resource" +) + +var _ resource.Resource = &ResourceWithIdentityAndModifyPlan{} +var _ resource.ResourceWithIdentity = &ResourceWithIdentityAndModifyPlan{} +var _ resource.ResourceWithModifyPlan = &ResourceWithIdentityAndModifyPlan{} + +// Declarative resource.ResourceWithIdentityAndModifyPlan for unit testing. +type ResourceWithIdentityAndModifyPlan struct { + *Resource + + // ResourceWithIdentity interface methods + IdentitySchemaMethod func(context.Context, resource.IdentitySchemaRequest, *resource.IdentitySchemaResponse) + + // ResourceWithModifyPlan interface methods + ModifyPlanMethod func(context.Context, resource.ModifyPlanRequest, *resource.ModifyPlanResponse) +} + +// IdentitySchema implements resource.ResourceWithIdentity. +func (p *ResourceWithIdentityAndModifyPlan) IdentitySchema(ctx context.Context, req resource.IdentitySchemaRequest, resp *resource.IdentitySchemaResponse) { + if p.IdentitySchemaMethod == nil { + return + } + + p.IdentitySchemaMethod(ctx, req, resp) +} + +// ModifyPlan satisfies the resource.ResourceWithModifyPlan interface. +func (r *ResourceWithIdentityAndModifyPlan) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if r.ModifyPlanMethod == nil { + return + } + + r.ModifyPlanMethod(ctx, req, resp) +} diff --git a/internal/toproto5/planresourcechange.go b/internal/toproto5/planresourcechange.go index f3292a963..23938df27 100644 --- a/internal/toproto5/planresourcechange.go +++ b/internal/toproto5/planresourcechange.go @@ -29,6 +29,11 @@ func PlanResourceChangeResponse(ctx context.Context, fw *fwserver.PlanResourceCh proto5.Diagnostics = append(proto5.Diagnostics, Diagnostics(ctx, diags)...) proto5.PlannedState = plannedState + plannedIdentity, diags := ResourceIdentity(ctx, fw.PlannedIdentity) + + proto5.Diagnostics = append(proto5.Diagnostics, Diagnostics(ctx, diags)...) + proto5.PlannedIdentity = plannedIdentity + requiresReplace, diags := totftypes.AttributePaths(ctx, fw.RequiresReplace) proto5.Diagnostics = append(proto5.Diagnostics, Diagnostics(ctx, diags)...) diff --git a/internal/toproto5/planresourcechange_test.go b/internal/toproto5/planresourcechange_test.go index 05924b5b4..9506ba513 100644 --- a/internal/toproto5/planresourcechange_test.go +++ b/internal/toproto5/planresourcechange_test.go @@ -17,6 +17,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/toproto5" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/tfsdk" ) @@ -40,6 +41,22 @@ func TestPlanResourceChangeResponse(t *testing.T) { t.Fatalf("unexpected error calling tfprotov5.NewDynamicValue(): %s", err) } + testIdentityProto5Type := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_id": tftypes.String, + }, + } + + testIdentityProto5Value := tftypes.NewValue(testIdentityProto5Type, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "id-123"), + }) + + testIdentityProto5DynamicValue, err := tfprotov5.NewDynamicValue(testIdentityProto5Type, testIdentityProto5Value) + + if err != nil { + t.Fatalf("unexpected error calling tfprotov5.NewDynamicValue(): %s", err) + } + testState := &tfsdk.State{ Raw: testProto5Value, Schema: schema.Schema{ @@ -62,6 +79,28 @@ func TestPlanResourceChangeResponse(t *testing.T) { }, } + testIdentity := &tfsdk.ResourceIdentity{ + Raw: testIdentityProto5Value, + Schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_id": identityschema.StringAttribute{ + RequiredForImport: true, + }, + }, + }, + } + + testIdentityInvalid := &tfsdk.ResourceIdentity{ + Raw: testIdentityProto5Value, + Schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_id": identityschema.BoolAttribute{ + RequiredForImport: true, + }, + }, + }, + } + testProviderKeyValue := privatestate.MustMarshalToJson(map[string][]byte{ "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), }) @@ -143,6 +182,37 @@ func TestPlanResourceChangeResponse(t *testing.T) { }, }, }, + "diagnostics-invalid-plannedidentity": { + input: &fwserver.PlanResourceChangeResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewWarningDiagnostic("test warning summary", "test warning details"), + diag.NewErrorDiagnostic("test error summary", "test error details"), + }, + PlannedIdentity: testIdentityInvalid, + }, + expected: &tfprotov5.PlanResourceChangeResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityWarning, + Summary: "test warning summary", + Detail: "test warning details", + }, + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "test error summary", + Detail: "test error details", + }, + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "Unable to Convert Resource Identity", + Detail: "An unexpected error was encountered when converting the resource identity to the protocol type. " + + "This is always an issue in terraform-plugin-framework used to implement the provider and should be reported to the provider developers.\n\n" + + "Please report this to the provider developer:\n\n" + + "Unable to create DynamicValue: AttributeName(\"test_id\"): unexpected value type string, tftypes.Bool values must be of type bool", + }, + }, + }, + }, "plannedprivate-empty": { input: &fwserver.PlanResourceChangeResponse{ PlannedPrivate: &privatestate.Data{ @@ -177,6 +247,16 @@ func TestPlanResourceChangeResponse(t *testing.T) { PlannedState: &testProto5DynamicValue, }, }, + "plannedidentity": { + input: &fwserver.PlanResourceChangeResponse{ + PlannedIdentity: testIdentity, + }, + expected: &tfprotov5.PlanResourceChangeResponse{ + PlannedIdentity: &tfprotov5.ResourceIdentityData{ + IdentityData: &testIdentityProto5DynamicValue, + }, + }, + }, "requiresreplace": { input: &fwserver.PlanResourceChangeResponse{ RequiresReplace: path.Paths{ diff --git a/resource/modify_plan.go b/resource/modify_plan.go index 28843cec1..0cc71bea9 100644 --- a/resource/modify_plan.go +++ b/resource/modify_plan.go @@ -35,6 +35,10 @@ type ModifyPlanRequest struct { // State is the current state of the resource. State tfsdk.State + // Identity is the current identity of the resource. If the resource does not + // support identity, this value will not be set. + Identity *tfsdk.ResourceIdentity + // Plan is the planned new state for the resource. Terraform 1.3 and later // supports resource destroy planning, in which this will contain a null // value. @@ -65,6 +69,13 @@ type ModifyPlanResponse struct { // Plan is the planned new state for the resource. Plan tfsdk.Plan + // Identity is the planned new identity of the resource. + // This field is pre-populated from ModifyPlanRequest.Identity. + // + // If the resource does not support identity, this value will not be set and will + // raise a diagnostic if set. + Identity *tfsdk.ResourceIdentity + // RequiresReplace is a list of attribute paths that require the // resource to be replaced. They should point to the specific field // that changed that requires the resource to be destroyed and From 774af730590d1a21f4d76d6ef389519499080747 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Wed, 12 Mar 2025 18:19:43 -0400 Subject: [PATCH 17/20] protov6 implementation of PlanResourceChange --- internal/fromproto6/planresourcechange.go | 9 +- .../fromproto6/planresourcechange_test.go | 64 +++++++- .../proto6server/server_planresourcechange.go | 10 +- .../server_planresourcechange_test.go | 147 ++++++++++++++++++ .../proto6server/server_readresource_test.go | 2 +- internal/toproto6/planresourcechange.go | 5 + internal/toproto6/planresourcechange_test.go | 80 ++++++++++ 7 files changed, 313 insertions(+), 4 deletions(-) diff --git a/internal/fromproto6/planresourcechange.go b/internal/fromproto6/planresourcechange.go index 6b95f0789..4bffe04d1 100644 --- a/internal/fromproto6/planresourcechange.go +++ b/internal/fromproto6/planresourcechange.go @@ -17,7 +17,7 @@ import ( // PlanResourceChangeRequest returns the *fwserver.PlanResourceChangeRequest // equivalent of a *tfprotov6.PlanResourceChangeRequest. -func PlanResourceChangeRequest(ctx context.Context, proto6 *tfprotov6.PlanResourceChangeRequest, reqResource resource.Resource, resourceSchema fwschema.Schema, providerMetaSchema fwschema.Schema, resourceBehavior resource.ResourceBehavior) (*fwserver.PlanResourceChangeRequest, diag.Diagnostics) { +func PlanResourceChangeRequest(ctx context.Context, proto6 *tfprotov6.PlanResourceChangeRequest, reqResource resource.Resource, resourceSchema fwschema.Schema, providerMetaSchema fwschema.Schema, resourceBehavior resource.ResourceBehavior, identitySchema fwschema.Schema) (*fwserver.PlanResourceChangeRequest, diag.Diagnostics) { if proto6 == nil { return nil, nil } @@ -41,6 +41,7 @@ func PlanResourceChangeRequest(ctx context.Context, proto6 *tfprotov6.PlanResour fw := &fwserver.PlanResourceChangeRequest{ ResourceBehavior: resourceBehavior, ResourceSchema: resourceSchema, + IdentitySchema: identitySchema, Resource: reqResource, ClientCapabilities: ModifyPlanClientCapabilities(proto6.ClientCapabilities), } @@ -57,6 +58,12 @@ func PlanResourceChangeRequest(ctx context.Context, proto6 *tfprotov6.PlanResour fw.PriorState = priorState + priorIdentity, priorIdentityDiags := ResourceIdentity(ctx, proto6.PriorIdentity, identitySchema) + + diags.Append(priorIdentityDiags...) + + fw.PriorIdentity = priorIdentity + proposedNewState, proposedNewStateDiags := Plan(ctx, proto6.ProposedNewState, resourceSchema) diags.Append(proposedNewStateDiags...) diff --git a/internal/fromproto6/planresourcechange_test.go b/internal/fromproto6/planresourcechange_test.go index af8ee025c..3660535ae 100644 --- a/internal/fromproto6/planresourcechange_test.go +++ b/internal/fromproto6/planresourcechange_test.go @@ -17,6 +17,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/tfsdk" ) @@ -48,6 +49,30 @@ func TestPlanResourceChangeRequest(t *testing.T) { }, } + testIdentityProto6Type := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_identity_attribute": tftypes.String, + }, + } + + testIdentityProto6Value := tftypes.NewValue(testIdentityProto6Type, map[string]tftypes.Value{ + "test_identity_attribute": tftypes.NewValue(tftypes.String, "id-123"), + }) + + testIdentityProto6DynamicValue, err := tfprotov6.NewDynamicValue(testIdentityProto6Type, testIdentityProto6Value) + + if err != nil { + t.Fatalf("unexpected error calling tfprotov6.NewDynamicValue(): %s", err) + } + + testIdentitySchema := identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_identity_attribute": identityschema.StringAttribute{ + RequiredForImport: true, + }, + }, + } + testProviderKeyValue := privatestate.MustMarshalToJson(map[string][]byte{ "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), }) @@ -58,6 +83,7 @@ func TestPlanResourceChangeRequest(t *testing.T) { input *tfprotov6.PlanResourceChangeRequest resourceBehavior resource.ResourceBehavior resourceSchema fwschema.Schema + identitySchema fwschema.Schema resource resource.Resource providerMetaSchema fwschema.Schema expected *fwserver.PlanResourceChangeRequest @@ -182,6 +208,42 @@ func TestPlanResourceChangeRequest(t *testing.T) { ResourceSchema: testFwSchema, }, }, + "prioridentity-missing-schema": { + input: &tfprotov6.PlanResourceChangeRequest{ + PriorIdentity: &tfprotov6.ResourceIdentityData{ + IdentityData: &testIdentityProto6DynamicValue, + }, + }, + resourceSchema: testFwSchema, + expected: &fwserver.PlanResourceChangeRequest{ + ResourceSchema: testFwSchema, + }, + expectedDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Unable to Convert Resource Identity", + "An unexpected error was encountered when converting the resource identity from the protocol type. "+ + "Identity data was sent in the protocol to a resource that doesn't support identity.\n\n"+ + "This is always a problem with Terraform or terraform-plugin-framework. Please report this to the provider developer.", + ), + }, + }, + "prioridentity": { + input: &tfprotov6.PlanResourceChangeRequest{ + PriorIdentity: &tfprotov6.ResourceIdentityData{ + IdentityData: &testIdentityProto6DynamicValue, + }, + }, + identitySchema: testIdentitySchema, + resourceSchema: testFwSchema, + expected: &fwserver.PlanResourceChangeRequest{ + IdentitySchema: testIdentitySchema, + PriorIdentity: &tfsdk.ResourceIdentity{ + Raw: testIdentityProto6Value, + Schema: testIdentitySchema, + }, + ResourceSchema: testFwSchema, + }, + }, "providermeta-missing-data": { input: &tfprotov6.PlanResourceChangeRequest{}, resourceSchema: testFwSchema, @@ -265,7 +327,7 @@ func TestPlanResourceChangeRequest(t *testing.T) { t.Run(name, func(t *testing.T) { t.Parallel() - got, diags := fromproto6.PlanResourceChangeRequest(context.Background(), testCase.input, testCase.resource, testCase.resourceSchema, testCase.providerMetaSchema, testCase.resourceBehavior) + got, diags := fromproto6.PlanResourceChangeRequest(context.Background(), testCase.input, testCase.resource, testCase.resourceSchema, testCase.providerMetaSchema, testCase.resourceBehavior, testCase.identitySchema) if diff := cmp.Diff(got, testCase.expected, cmp.AllowUnexported(privatestate.ProviderData{})); diff != "" { t.Errorf("unexpected difference: %s", diff) diff --git a/internal/proto6server/server_planresourcechange.go b/internal/proto6server/server_planresourcechange.go index 32d13ddd6..cdd057e2f 100644 --- a/internal/proto6server/server_planresourcechange.go +++ b/internal/proto6server/server_planresourcechange.go @@ -37,6 +37,14 @@ func (s *Server) PlanResourceChange(ctx context.Context, proto6Req *tfprotov6.Pl return toproto6.PlanResourceChangeResponse(ctx, fwResp), nil } + identitySchema, diags := s.FrameworkServer.ResourceIdentitySchema(ctx, proto6Req.TypeName) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto6.PlanResourceChangeResponse(ctx, fwResp), nil + } + providerMetaSchema, diags := s.FrameworkServer.ProviderMetaSchema(ctx) fwResp.Diagnostics.Append(diags...) @@ -53,7 +61,7 @@ func (s *Server) PlanResourceChange(ctx context.Context, proto6Req *tfprotov6.Pl return toproto6.PlanResourceChangeResponse(ctx, fwResp), nil } - fwReq, diags := fromproto6.PlanResourceChangeRequest(ctx, proto6Req, resource, resourceSchema, providerMetaSchema, resourceBehavior) + fwReq, diags := fromproto6.PlanResourceChangeRequest(ctx, proto6Req, resource, resourceSchema, providerMetaSchema, resourceBehavior, identitySchema) fwResp.Diagnostics.Append(diags...) diff --git a/internal/proto6server/server_planresourcechange_test.go b/internal/proto6server/server_planresourcechange_test.go index 35b39c2e0..6cd50604f 100644 --- a/internal/proto6server/server_planresourcechange_test.go +++ b/internal/proto6server/server_planresourcechange_test.go @@ -14,6 +14,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/provider" "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-go/tfprotov6" @@ -32,6 +33,12 @@ func TestServerPlanResourceChange(t *testing.T) { testEmptyDynamicValue, _ := tfprotov6.NewDynamicValue(testSchemaType, tftypes.NewValue(testSchemaType, nil)) + testIdentitySchemaType := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_id": tftypes.String, + }, + } + testSchema := schema.Schema{ Attributes: map[string]schema.Attribute{ "test_computed": schema.StringAttribute{ @@ -43,6 +50,18 @@ func TestServerPlanResourceChange(t *testing.T) { }, } + testIdentitySchema := identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_id": identityschema.StringAttribute{ + RequiredForImport: true, + }, + }, + } + + type testIdentitySchemaData struct { + TestID types.String `tfsdk:"test_id"` + } + type testSchemaData struct { TestComputed types.String `tfsdk:"test_computed"` TestRequired types.String `tfsdk:"test_required"` @@ -178,6 +197,70 @@ func TestServerPlanResourceChange(t *testing.T) { }), }, }, + "create-request-plannedidentity": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + ResourcesMethod: func(_ context.Context) []func() resource.Resource { + return []func() resource.Resource{ + func() resource.Resource { + return &testprovider.ResourceWithIdentityAndModifyPlan{ + Resource: &testprovider.Resource{ + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema + }, + MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "test_resource" + }, + }, + IdentitySchemaMethod: func(ctx context.Context, req resource.IdentitySchemaRequest, resp *resource.IdentitySchemaResponse) { + resp.IdentitySchema = testIdentitySchema + }, + ModifyPlanMethod: func(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + var data testIdentitySchemaData + + resp.Diagnostics.Append(req.Identity.Get(ctx, &data)...) + + if data.TestID.ValueString() != "id-123" { + resp.Diagnostics.AddError("Unexpected req.Identity", data.TestID.ValueString()) + } + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov6.PlanResourceChangeRequest{ + Config: testNewDynamicValue(t, testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + ProposedNewState: testNewDynamicValue(t, testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + PriorIdentity: &tfprotov6.ResourceIdentityData{ + IdentityData: testNewDynamicValue(t, testIdentitySchemaType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "id-123"), + }), + }, + PriorState: &testEmptyDynamicValue, + TypeName: "test_resource", + }, + expectedResponse: &tfprotov6.PlanResourceChangeResponse{ + PlannedState: testNewDynamicValue(t, testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, tftypes.UnknownValue), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + PlannedIdentity: &tfprotov6.ResourceIdentityData{ + IdentityData: testNewDynamicValue(t, testIdentitySchemaType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "id-123"), + }), + }, + }, + }, "create-request-providermeta": { server: &Server{ FrameworkServer: fwserver.Server{ @@ -344,6 +427,70 @@ func TestServerPlanResourceChange(t *testing.T) { }), }, }, + "create-response-plannedidentity": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + ResourcesMethod: func(_ context.Context) []func() resource.Resource { + return []func() resource.Resource{ + func() resource.Resource { + return &testprovider.ResourceWithIdentityAndModifyPlan{ + Resource: &testprovider.Resource{ + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema + }, + MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "test_resource" + }, + }, + ModifyPlanMethod: func(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + var data testIdentitySchemaData + + resp.Diagnostics.Append(req.Identity.Get(ctx, &data)...) + + data.TestID = types.StringValue("new-id-123") + + resp.Diagnostics.Append(resp.Identity.Set(ctx, &data)...) + }, + IdentitySchemaMethod: func(ctx context.Context, req resource.IdentitySchemaRequest, resp *resource.IdentitySchemaResponse) { + resp.IdentitySchema = testIdentitySchema + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov6.PlanResourceChangeRequest{ + Config: testNewDynamicValue(t, testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + ProposedNewState: testNewDynamicValue(t, testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + PriorState: &testEmptyDynamicValue, + PriorIdentity: &tfprotov6.ResourceIdentityData{ + IdentityData: testNewDynamicValue(t, testIdentitySchemaType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "id-123"), + }), + }, + TypeName: "test_resource", + }, + expectedResponse: &tfprotov6.PlanResourceChangeResponse{ + PlannedState: testNewDynamicValue(t, testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, tftypes.UnknownValue), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + PlannedIdentity: &tfprotov6.ResourceIdentityData{ + IdentityData: testNewDynamicValue(t, testIdentitySchemaType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "new-id-123"), + }), + }, + }, + }, "create-response-requiresreplace": { server: &Server{ FrameworkServer: fwserver.Server{ diff --git a/internal/proto6server/server_readresource_test.go b/internal/proto6server/server_readresource_test.go index aaa9a5235..2f096bbef 100644 --- a/internal/proto6server/server_readresource_test.go +++ b/internal/proto6server/server_readresource_test.go @@ -456,7 +456,7 @@ func TestServerReadResource(t *testing.T) { request: &tfprotov6.ReadResourceRequest{ CurrentState: testEmptyDynamicValue, CurrentIdentity: &tfprotov6.ResourceIdentityData{ - IdentityData: testNewIdentityDynamicValue, + IdentityData: testCurrentIdentityValue, }, TypeName: "test_resource", }, diff --git a/internal/toproto6/planresourcechange.go b/internal/toproto6/planresourcechange.go index 486f7ab02..cf9b2642a 100644 --- a/internal/toproto6/planresourcechange.go +++ b/internal/toproto6/planresourcechange.go @@ -29,6 +29,11 @@ func PlanResourceChangeResponse(ctx context.Context, fw *fwserver.PlanResourceCh proto6.Diagnostics = append(proto6.Diagnostics, Diagnostics(ctx, diags)...) proto6.PlannedState = plannedState + plannedIdentity, diags := ResourceIdentity(ctx, fw.PlannedIdentity) + + proto6.Diagnostics = append(proto6.Diagnostics, Diagnostics(ctx, diags)...) + proto6.PlannedIdentity = plannedIdentity + requiresReplace, diags := totftypes.AttributePaths(ctx, fw.RequiresReplace) proto6.Diagnostics = append(proto6.Diagnostics, Diagnostics(ctx, diags)...) diff --git a/internal/toproto6/planresourcechange_test.go b/internal/toproto6/planresourcechange_test.go index 376ec1f9b..344ac0a21 100644 --- a/internal/toproto6/planresourcechange_test.go +++ b/internal/toproto6/planresourcechange_test.go @@ -17,6 +17,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/toproto6" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/tfsdk" ) @@ -40,6 +41,22 @@ func TestPlanResourceChangeResponse(t *testing.T) { t.Fatalf("unexpected error calling tfprotov6.NewDynamicValue(): %s", err) } + testIdentityProto6Type := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_id": tftypes.String, + }, + } + + testIdentityProto6Value := tftypes.NewValue(testIdentityProto6Type, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "id-123"), + }) + + testIdentityProto6DynamicValue, err := tfprotov6.NewDynamicValue(testIdentityProto6Type, testIdentityProto6Value) + + if err != nil { + t.Fatalf("unexpected error calling tfprotov6.NewDynamicValue(): %s", err) + } + testState := &tfsdk.State{ Raw: testProto6Value, Schema: schema.Schema{ @@ -62,6 +79,28 @@ func TestPlanResourceChangeResponse(t *testing.T) { }, } + testIdentity := &tfsdk.ResourceIdentity{ + Raw: testIdentityProto6Value, + Schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_id": identityschema.StringAttribute{ + RequiredForImport: true, + }, + }, + }, + } + + testIdentityInvalid := &tfsdk.ResourceIdentity{ + Raw: testIdentityProto6Value, + Schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_id": identityschema.BoolAttribute{ + RequiredForImport: true, + }, + }, + }, + } + testProviderKeyValue := privatestate.MustMarshalToJson(map[string][]byte{ "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), }) @@ -143,6 +182,37 @@ func TestPlanResourceChangeResponse(t *testing.T) { }, }, }, + "diagnostics-invalid-plannedidentity": { + input: &fwserver.PlanResourceChangeResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewWarningDiagnostic("test warning summary", "test warning details"), + diag.NewErrorDiagnostic("test error summary", "test error details"), + }, + PlannedIdentity: testIdentityInvalid, + }, + expected: &tfprotov6.PlanResourceChangeResponse{ + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityWarning, + Summary: "test warning summary", + Detail: "test warning details", + }, + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "test error summary", + Detail: "test error details", + }, + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Unable to Convert Resource Identity", + Detail: "An unexpected error was encountered when converting the resource identity to the protocol type. " + + "This is always an issue in terraform-plugin-framework used to implement the provider and should be reported to the provider developers.\n\n" + + "Please report this to the provider developer:\n\n" + + "Unable to create DynamicValue: AttributeName(\"test_id\"): unexpected value type string, tftypes.Bool values must be of type bool", + }, + }, + }, + }, "plannedprivate-empty": { input: &fwserver.PlanResourceChangeResponse{ PlannedPrivate: &privatestate.Data{ @@ -177,6 +247,16 @@ func TestPlanResourceChangeResponse(t *testing.T) { PlannedState: &testProto6DynamicValue, }, }, + "plannedidentity": { + input: &fwserver.PlanResourceChangeResponse{ + PlannedIdentity: testIdentity, + }, + expected: &tfprotov6.PlanResourceChangeResponse{ + PlannedIdentity: &tfprotov6.ResourceIdentityData{ + IdentityData: &testIdentityProto6DynamicValue, + }, + }, + }, "requiresreplace": { input: &fwserver.PlanResourceChangeResponse{ RequiresReplace: path.Paths{ From 8a90ecbe171aef38cf25c595c7e9a88b5c7136db Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Thu, 13 Mar 2025 17:55:19 -0400 Subject: [PATCH 18/20] implement ApplyResourceChange fwserver + protov5 --- go.mod | 6 +- go.sum | 34 +- internal/fromproto5/applyresourcechange.go | 9 +- .../fromproto5/applyresourcechange_test.go | 64 +++- .../fwserver/server_applyresourcechange.go | 51 +-- .../server_applyresourcechange_test.go | 317 ++++++++++++++++++ internal/fwserver/server_createresource.go | 50 ++- .../fwserver/server_createresource_test.go | 180 ++++++++++ internal/fwserver/server_deleteresource.go | 34 ++ .../fwserver/server_deleteresource_test.go | 129 ++++++- internal/fwserver/server_readresource.go | 25 +- internal/fwserver/server_updateresource.go | 52 ++- .../fwserver/server_updateresource_test.go | 180 ++++++++++ .../server_applyresourcechange.go | 10 +- .../server_applyresourcechange_test.go | 163 +++++++++ internal/toproto5/applyresourcechange.go | 5 + internal/toproto5/applyresourcechange_test.go | 80 +++++ resource/create.go | 12 + resource/delete.go | 7 +- resource/update.go | 12 + 20 files changed, 1347 insertions(+), 73 deletions(-) diff --git a/go.mod b/go.mod index 584c97cf8..58ea938ab 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ toolchain go1.22.7 require ( github.com/google/go-cmp v0.7.0 - github.com/hashicorp/terraform-plugin-go v0.26.1-0.20250219133416-0561ec7f0caf + github.com/hashicorp/terraform-plugin-go v0.26.1-0.20250310103402-e49288281ffd github.com/hashicorp/terraform-plugin-log v0.9.0 ) @@ -28,7 +28,7 @@ require ( golang.org/x/net v0.34.0 // indirect golang.org/x/sys v0.29.0 // indirect golang.org/x/text v0.21.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20241202173237-19429a94021a // indirect - google.golang.org/grpc v1.70.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect + google.golang.org/grpc v1.71.0 // indirect google.golang.org/protobuf v1.36.5 // indirect ) diff --git a/go.sum b/go.sum index a285453e6..bfdeff563 100644 --- a/go.sum +++ b/go.sum @@ -21,8 +21,8 @@ github.com/hashicorp/go-plugin v1.6.3 h1:xgHB+ZUSYeuJi96WtxEjzi23uh7YQpznjGh0U0U github.com/hashicorp/go-plugin v1.6.3/go.mod h1:MRobyh+Wc/nYy1V4KAXUiYfzxoYhs7V1mlH1Z7iY2h0= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/terraform-plugin-go v0.26.1-0.20250219133416-0561ec7f0caf h1:qsHBfGoRp15P8vc95eAVMVO6erkMfpLKZ/6A5lLcR1w= -github.com/hashicorp/terraform-plugin-go v0.26.1-0.20250219133416-0561ec7f0caf/go.mod h1:dUu1RU16sOSKn6w4g+xaAnV0S0wHsVBEY/XH8jv1kx4= +github.com/hashicorp/terraform-plugin-go v0.26.1-0.20250310103402-e49288281ffd h1:g5iwwMyN3PXQ+eyyJGL1rOLPDcD4f0gjUKDsbPabTNU= +github.com/hashicorp/terraform-plugin-go v0.26.1-0.20250310103402-e49288281ffd/go.mod h1:MfDwS/KnIy2QzCwdRtuqIjZ23gpYa9Vm+Z8cFpx8qtU= github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0= github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow= github.com/hashicorp/terraform-registry-address v0.2.4 h1:JXu/zHB2Ymg/TGVCRu10XqNa4Sh2bWcqCNyKWjnCPJA= @@ -54,16 +54,18 @@ github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IU github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= -go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U= -go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg= -go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M= -go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8= -go.opentelemetry.io/otel/sdk v1.32.0 h1:RNxepc9vK59A8XsgZQouW8ue8Gkb4jpWtJm9ge5lEG4= -go.opentelemetry.io/otel/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJCAP9LlJXEjU= -go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU= -go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ= -go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM= -go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= +go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= +go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= +go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= +go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= +go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= +go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk= +go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= +go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= +go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -76,10 +78,10 @@ golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241202173237-19429a94021a h1:hgh8P4EuoxpsuKMXX/To36nOFD7vixReXgn8lPGnt+o= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241202173237-19429a94021a/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU= -google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ= -google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50= +google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= +google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/fromproto5/applyresourcechange.go b/internal/fromproto5/applyresourcechange.go index 08d04d4ac..c32b34387 100644 --- a/internal/fromproto5/applyresourcechange.go +++ b/internal/fromproto5/applyresourcechange.go @@ -17,7 +17,7 @@ import ( // ApplyResourceChangeRequest returns the *fwserver.ApplyResourceChangeRequest // equivalent of a *tfprotov5.ApplyResourceChangeRequest. -func ApplyResourceChangeRequest(ctx context.Context, proto5 *tfprotov5.ApplyResourceChangeRequest, resource resource.Resource, resourceSchema fwschema.Schema, providerMetaSchema fwschema.Schema) (*fwserver.ApplyResourceChangeRequest, diag.Diagnostics) { +func ApplyResourceChangeRequest(ctx context.Context, proto5 *tfprotov5.ApplyResourceChangeRequest, resource resource.Resource, resourceSchema fwschema.Schema, providerMetaSchema fwschema.Schema, identitySchema fwschema.Schema) (*fwserver.ApplyResourceChangeRequest, diag.Diagnostics) { if proto5 == nil { return nil, nil } @@ -40,6 +40,7 @@ func ApplyResourceChangeRequest(ctx context.Context, proto5 *tfprotov5.ApplyReso fw := &fwserver.ApplyResourceChangeRequest{ ResourceSchema: resourceSchema, + IdentitySchema: identitySchema, Resource: resource, } @@ -55,6 +56,12 @@ func ApplyResourceChangeRequest(ctx context.Context, proto5 *tfprotov5.ApplyReso fw.PlannedState = plannedState + plannedIdentity, plannedIdentityDiags := ResourceIdentity(ctx, proto5.PlannedIdentity, identitySchema) + + diags.Append(plannedIdentityDiags...) + + fw.PlannedIdentity = plannedIdentity + priorState, priorStateDiags := State(ctx, proto5.PriorState, resourceSchema) diags.Append(priorStateDiags...) diff --git a/internal/fromproto5/applyresourcechange_test.go b/internal/fromproto5/applyresourcechange_test.go index 859181364..a6b09e011 100644 --- a/internal/fromproto5/applyresourcechange_test.go +++ b/internal/fromproto5/applyresourcechange_test.go @@ -17,6 +17,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/tfsdk" ) @@ -48,6 +49,30 @@ func TestApplyResourceChangeRequest(t *testing.T) { }, } + testIdentityProto5Type := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_identity_attribute": tftypes.String, + }, + } + + testIdentityProto5Value := tftypes.NewValue(testIdentityProto5Type, map[string]tftypes.Value{ + "test_identity_attribute": tftypes.NewValue(tftypes.String, "id-123"), + }) + + testIdentityProto5DynamicValue, err := tfprotov5.NewDynamicValue(testIdentityProto5Type, testIdentityProto5Value) + + if err != nil { + t.Fatalf("unexpected error calling tfprotov5.NewDynamicValue(): %s", err) + } + + testIdentitySchema := identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_identity_attribute": identityschema.StringAttribute{ + RequiredForImport: true, + }, + }, + } + testProviderKeyValue := privatestate.MustMarshalToJson(map[string][]byte{ "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), }) @@ -61,6 +86,7 @@ func TestApplyResourceChangeRequest(t *testing.T) { resourceSchema fwschema.Schema resource resource.Resource providerMetaSchema fwschema.Schema + identitySchema fwschema.Schema expected *fwserver.ApplyResourceChangeRequest expectedDiagnostics diag.Diagnostics }{ @@ -137,6 +163,42 @@ func TestApplyResourceChangeRequest(t *testing.T) { ResourceSchema: testFwSchema, }, }, + "plannedidentity-missing-schema": { + input: &tfprotov5.ApplyResourceChangeRequest{ + PlannedIdentity: &tfprotov5.ResourceIdentityData{ + IdentityData: &testIdentityProto5DynamicValue, + }, + }, + resourceSchema: testFwSchema, + expected: &fwserver.ApplyResourceChangeRequest{ + ResourceSchema: testFwSchema, + }, + expectedDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Unable to Convert Resource Identity", + "An unexpected error was encountered when converting the resource identity from the protocol type. "+ + "Identity data was sent in the protocol to a resource that doesn't support identity.\n\n"+ + "This is always a problem with Terraform or terraform-plugin-framework. Please report this to the provider developer.", + ), + }, + }, + "plannedidentity": { + input: &tfprotov5.ApplyResourceChangeRequest{ + PlannedIdentity: &tfprotov5.ResourceIdentityData{ + IdentityData: &testIdentityProto5DynamicValue, + }, + }, + identitySchema: testIdentitySchema, + resourceSchema: testFwSchema, + expected: &fwserver.ApplyResourceChangeRequest{ + IdentitySchema: testIdentitySchema, + PlannedIdentity: &tfsdk.ResourceIdentity{ + Raw: testIdentityProto5Value, + Schema: testIdentitySchema, + }, + ResourceSchema: testFwSchema, + }, + }, "plannedprivate-malformed-json": { input: &tfprotov5.ApplyResourceChangeRequest{ PlannedPrivate: []byte(`{`), @@ -253,7 +315,7 @@ func TestApplyResourceChangeRequest(t *testing.T) { t.Run(name, func(t *testing.T) { t.Parallel() - got, diags := fromproto5.ApplyResourceChangeRequest(context.Background(), testCase.input, testCase.resource, testCase.resourceSchema, testCase.providerMetaSchema) + got, diags := fromproto5.ApplyResourceChangeRequest(context.Background(), testCase.input, testCase.resource, testCase.resourceSchema, testCase.providerMetaSchema, testCase.identitySchema) if diff := cmp.Diff(got, testCase.expected, cmp.AllowUnexported(privatestate.ProviderData{})); diff != "" { t.Errorf("unexpected difference: %s", diff) diff --git a/internal/fwserver/server_applyresourcechange.go b/internal/fwserver/server_applyresourcechange.go index a11a72e47..2167ab709 100644 --- a/internal/fwserver/server_applyresourcechange.go +++ b/internal/fwserver/server_applyresourcechange.go @@ -17,13 +17,15 @@ import ( // ApplyResourceChangeRequest is the framework server request for the // ApplyResourceChange RPC. type ApplyResourceChangeRequest struct { - Config *tfsdk.Config - PlannedPrivate *privatestate.Data - PlannedState *tfsdk.Plan - PriorState *tfsdk.State - ProviderMeta *tfsdk.Config - ResourceSchema fwschema.Schema - Resource resource.Resource + Config *tfsdk.Config + PlannedPrivate *privatestate.Data + PlannedState *tfsdk.Plan + PlannedIdentity *tfsdk.ResourceIdentity + PriorState *tfsdk.State + ProviderMeta *tfsdk.Config + ResourceSchema fwschema.Schema + IdentitySchema fwschema.Schema + Resource resource.Resource } // ApplyResourceChangeResponse is the framework server response for the @@ -31,6 +33,7 @@ type ApplyResourceChangeRequest struct { type ApplyResourceChangeResponse struct { Diagnostics diag.Diagnostics NewState *tfsdk.State + NewIdentity *tfsdk.ResourceIdentity Private *privatestate.Data } @@ -45,12 +48,14 @@ func (s *Server) ApplyResourceChange(ctx context.Context, req *ApplyResourceChan logging.FrameworkTrace(ctx, "ApplyResourceChange received no PriorState, running CreateResource") createReq := &CreateResourceRequest{ - Config: req.Config, - PlannedPrivate: req.PlannedPrivate, - PlannedState: req.PlannedState, - ProviderMeta: req.ProviderMeta, - ResourceSchema: req.ResourceSchema, - Resource: req.Resource, + Config: req.Config, + PlannedPrivate: req.PlannedPrivate, + PlannedState: req.PlannedState, + PlannedIdentity: req.PlannedIdentity, + ProviderMeta: req.ProviderMeta, + ResourceSchema: req.ResourceSchema, + IdentitySchema: req.IdentitySchema, + Resource: req.Resource, } createResp := &CreateResourceResponse{} @@ -58,6 +63,7 @@ func (s *Server) ApplyResourceChange(ctx context.Context, req *ApplyResourceChan resp.Diagnostics = createResp.Diagnostics resp.NewState = createResp.NewState + resp.NewIdentity = createResp.NewIdentity resp.Private = createResp.Private return @@ -72,6 +78,7 @@ func (s *Server) ApplyResourceChange(ctx context.Context, req *ApplyResourceChan PriorState: req.PriorState, ProviderMeta: req.ProviderMeta, ResourceSchema: req.ResourceSchema, + IdentitySchema: req.IdentitySchema, Resource: req.Resource, } deleteResp := &DeleteResourceResponse{} @@ -80,6 +87,7 @@ func (s *Server) ApplyResourceChange(ctx context.Context, req *ApplyResourceChan resp.Diagnostics = deleteResp.Diagnostics resp.NewState = deleteResp.NewState + resp.NewIdentity = deleteResp.NewIdentity resp.Private = deleteResp.Private return @@ -89,13 +97,15 @@ func (s *Server) ApplyResourceChange(ctx context.Context, req *ApplyResourceChan logging.FrameworkTrace(ctx, "ApplyResourceChange running UpdateResource") updateReq := &UpdateResourceRequest{ - Config: req.Config, - PlannedPrivate: req.PlannedPrivate, - PlannedState: req.PlannedState, - PriorState: req.PriorState, - ProviderMeta: req.ProviderMeta, - ResourceSchema: req.ResourceSchema, - Resource: req.Resource, + Config: req.Config, + PlannedPrivate: req.PlannedPrivate, + PlannedState: req.PlannedState, + PlannedIdentity: req.PlannedIdentity, + PriorState: req.PriorState, + ProviderMeta: req.ProviderMeta, + ResourceSchema: req.ResourceSchema, + IdentitySchema: req.IdentitySchema, + Resource: req.Resource, } updateResp := &UpdateResourceResponse{} @@ -103,5 +113,6 @@ func (s *Server) ApplyResourceChange(ctx context.Context, req *ApplyResourceChan resp.Diagnostics = updateResp.Diagnostics resp.NewState = updateResp.NewState + resp.NewIdentity = updateResp.NewIdentity resp.Private = updateResp.Private } diff --git a/internal/fwserver/server_applyresourcechange_test.go b/internal/fwserver/server_applyresourcechange_test.go index ea4452244..a8565de7d 100644 --- a/internal/fwserver/server_applyresourcechange_test.go +++ b/internal/fwserver/server_applyresourcechange_test.go @@ -18,6 +18,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" @@ -33,6 +34,12 @@ func TestServerApplyResourceChange(t *testing.T) { }, } + testIdentityType := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_id": tftypes.String, + }, + } + testSchema := schema.Schema{ Attributes: map[string]schema.Attribute{ "test_computed": schema.StringAttribute{ @@ -44,6 +51,14 @@ func TestServerApplyResourceChange(t *testing.T) { }, } + testIdentitySchema := identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_id": identityschema.StringAttribute{ + RequiredForImport: true, + }, + }, + } + testEmptyPlan := &tfsdk.Plan{ Raw: tftypes.NewValue(testSchemaType, nil), Schema: testSchema, @@ -84,6 +99,10 @@ func TestServerApplyResourceChange(t *testing.T) { TestRequiredWriteOnly types.String `tfsdk:"test_required_write_only"` } + type testIdentitySchemaData struct { + TestID types.String `tfsdk:"test_id"` + } + testProviderMetaType := tftypes.Object{ AttributeTypes: map[string]tftypes.Type{ "test_provider_meta_attribute": tftypes.String, @@ -243,6 +262,69 @@ func TestServerApplyResourceChange(t *testing.T) { }, Private: testEmptyPrivate}, }, + "create-request-plannedidentity": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ApplyResourceChangeRequest{ + PlannedState: &tfsdk.Plan{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-plannedstate-value"), + }), + Schema: testSchema, + }, + PlannedIdentity: &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(testIdentityType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "id-123"), + }), + Schema: testIdentitySchema, + }, + PriorState: testEmptyState, + IdentitySchema: testIdentitySchema, + ResourceSchema: testSchema, + Resource: &testprovider.ResourceWithIdentity{ + Resource: &testprovider.Resource{ + CreateMethod: func(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var identityData testIdentitySchemaData + + resp.Diagnostics.Append(req.Identity.Get(ctx, &identityData)...) + + if identityData.TestID.ValueString() != "id-123" { + resp.Diagnostics.AddError("Unexpected req.Identity Value", "Got: "+identityData.TestID.ValueString()) + } + + // Prevent missing resource state error diagnostic + var data testSchemaData + + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) + }, + DeleteMethod: func(_ context.Context, _ resource.DeleteRequest, resp *resource.DeleteResponse) { + resp.Diagnostics.AddError("Unexpected Method Call", "Expected: Create, Got: Delete") + }, + UpdateMethod: func(_ context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { + resp.Diagnostics.AddError("Unexpected Method Call", "Expected: Create, Got: Update") + }, + }, + }, + }, + expectedResponse: &fwserver.ApplyResourceChangeResponse{ + NewIdentity: &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(testIdentityType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "id-123"), + }), + Schema: testIdentitySchema, + }, + NewState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-plannedstate-value"), + }), + Schema: testSchema, + }, + Private: testEmptyPrivate}, + }, "create-request-providermeta": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, @@ -376,6 +458,67 @@ func TestServerApplyResourceChange(t *testing.T) { Private: testEmptyPrivate, }, }, + "create-response-newidentity": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ApplyResourceChangeRequest{ + Config: &tfsdk.Config{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + Schema: testSchema, + }, + PlannedState: &tfsdk.Plan{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-plannedstate-value"), + }), + Schema: testSchema, + }, + IdentitySchema: testIdentitySchema, + PriorState: testEmptyState, + ResourceSchema: testSchema, + Resource: &testprovider.ResourceWithIdentity{ + Resource: &testprovider.Resource{ + CreateMethod: func(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + resp.Diagnostics.Append(resp.Identity.Set(ctx, testIdentitySchemaData{ + TestID: types.StringValue("new-id-123"), + })...) + + // Prevent missing resource state error diagnostic + var data testSchemaData + + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) + }, + DeleteMethod: func(_ context.Context, _ resource.DeleteRequest, resp *resource.DeleteResponse) { + resp.Diagnostics.AddError("Unexpected Method Call", "Expected: Create, Got: Delete") + }, + UpdateMethod: func(_ context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { + resp.Diagnostics.AddError("Unexpected Method Call", "Expected: Create, Got: Update") + }, + }, + }, + }, + expectedResponse: &fwserver.ApplyResourceChangeResponse{ + NewIdentity: &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(testIdentityType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "new-id-123"), + }), + Schema: testIdentitySchema, + }, + NewState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-plannedstate-value"), + }), + Schema: testSchema, + }, + Private: testEmptyPrivate, + }, + }, "create-response-newstate-null": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, @@ -826,6 +969,48 @@ func TestServerApplyResourceChange(t *testing.T) { NewState: testEmptyState, }, }, + "delete-response-newidentity": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ApplyResourceChangeRequest{ + PlannedState: testEmptyPlan, + PriorState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-priorstate-value"), + }), + Schema: testSchema, + }, + IdentitySchema: testIdentitySchema, + ResourceSchema: testSchema, + Resource: &testprovider.ResourceWithIdentity{ + Resource: &testprovider.Resource{ + CreateMethod: func(_ context.Context, _ resource.CreateRequest, resp *resource.CreateResponse) { + resp.Diagnostics.AddError("Unexpected Method Call", "Expected: Delete, Got: Create") + }, + DeleteMethod: func(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + if resp.Identity == nil || !resp.Identity.Raw.IsNull() { + resp.Diagnostics.AddError( + "Unexpected resp.Identity", + "expected resp.Identity to be a null object of the schema type.", + ) + } + }, + UpdateMethod: func(_ context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { + resp.Diagnostics.AddError("Unexpected Method Call", "Expected: Delete, Got: Update") + }, + }, + }, + }, + expectedResponse: &fwserver.ApplyResourceChangeResponse{ + NewIdentity: &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(testIdentityType, nil), + Schema: testIdentitySchema, + }, + NewState: testEmptyState, + }, + }, "update-request-config": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, @@ -940,6 +1125,77 @@ func TestServerApplyResourceChange(t *testing.T) { Private: testEmptyPrivate, }, }, + "update-request-plannedidentity": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ApplyResourceChangeRequest{ + Config: &tfsdk.Config{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-new-value"), + }), + Schema: testSchema, + }, + PlannedState: &tfsdk.Plan{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, "test-plannedstate-value"), + "test_required": tftypes.NewValue(tftypes.String, "test-new-value"), + }), + Schema: testSchema, + }, + PlannedIdentity: &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(testIdentityType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "id-123"), + }), + Schema: testIdentitySchema, + }, + PriorState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-old-value"), + }), + Schema: testSchema, + }, + IdentitySchema: testIdentitySchema, + ResourceSchema: testSchema, + Resource: &testprovider.ResourceWithIdentity{ + Resource: &testprovider.Resource{ + CreateMethod: func(_ context.Context, _ resource.CreateRequest, resp *resource.CreateResponse) { + resp.Diagnostics.AddError("Unexpected Method Call", "Expected: Update, Got: Create") + }, + DeleteMethod: func(_ context.Context, _ resource.DeleteRequest, resp *resource.DeleteResponse) { + resp.Diagnostics.AddError("Unexpected Method Call", "Expected: Update, Got: Delete") + }, + UpdateMethod: func(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var identityData testIdentitySchemaData + + resp.Diagnostics.Append(req.Identity.Get(ctx, &identityData)...) + + if identityData.TestID.ValueString() != "id-123" { + resp.Diagnostics.AddError("Unexpected req.Identity Value", "Got: "+identityData.TestID.ValueString()) + } + }, + }, + }, + }, + expectedResponse: &fwserver.ApplyResourceChangeResponse{ + NewIdentity: &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(testIdentityType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "id-123"), + }), + Schema: testIdentitySchema, + }, + NewState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-old-value"), + }), + Schema: testSchema, + }, + Private: testEmptyPrivate, + }, + }, "update-request-priorstate": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, @@ -1280,6 +1536,67 @@ func TestServerApplyResourceChange(t *testing.T) { Private: testEmptyPrivate, }, }, + "update-response-newidentity": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ApplyResourceChangeRequest{ + Config: &tfsdk.Config{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-new-value"), + }), + Schema: testSchema, + }, + PlannedState: &tfsdk.Plan{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, "test-plannedstate-value"), + "test_required": tftypes.NewValue(tftypes.String, "test-new-value"), + }), + Schema: testSchema, + }, + PriorState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-old-value"), + }), + Schema: testSchema, + }, + IdentitySchema: testIdentitySchema, + ResourceSchema: testSchema, + Resource: &testprovider.ResourceWithIdentity{ + Resource: &testprovider.Resource{ + CreateMethod: func(_ context.Context, _ resource.CreateRequest, resp *resource.CreateResponse) { + resp.Diagnostics.AddError("Unexpected Method Call", "Expected: Update, Got: Create") + }, + DeleteMethod: func(_ context.Context, _ resource.DeleteRequest, resp *resource.DeleteResponse) { + resp.Diagnostics.AddError("Unexpected Method Call", "Expected: Update, Got: Delete") + }, + UpdateMethod: func(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + resp.Diagnostics.Append(resp.Identity.Set(ctx, testIdentitySchemaData{ + TestID: types.StringValue("new-id-123"), + })...) + }, + }, + }, + }, + expectedResponse: &fwserver.ApplyResourceChangeResponse{ + NewIdentity: &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(testIdentityType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "new-id-123"), + }), + Schema: testIdentitySchema, + }, + NewState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-old-value"), + }), + Schema: testSchema, + }, + Private: testEmptyPrivate, + }, + }, "update-response-newstate-null": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, diff --git a/internal/fwserver/server_createresource.go b/internal/fwserver/server_createresource.go index d5a0aef2e..0e5b873ac 100644 --- a/internal/fwserver/server_createresource.go +++ b/internal/fwserver/server_createresource.go @@ -20,12 +20,14 @@ import ( // CreateResourceRequest is the framework server request for a create request // with the ApplyResourceChange RPC. type CreateResourceRequest struct { - Config *tfsdk.Config - PlannedPrivate *privatestate.Data - PlannedState *tfsdk.Plan - ProviderMeta *tfsdk.Config - ResourceSchema fwschema.Schema - Resource resource.Resource + Config *tfsdk.Config + PlannedPrivate *privatestate.Data + PlannedState *tfsdk.Plan + PlannedIdentity *tfsdk.ResourceIdentity + ProviderMeta *tfsdk.Config + ResourceSchema fwschema.Schema + IdentitySchema fwschema.Schema + Resource resource.Resource } // CreateResourceResponse is the framework server response for a create request @@ -33,6 +35,7 @@ type CreateResourceRequest struct { type CreateResourceResponse struct { Diagnostics diag.Diagnostics NewState *tfsdk.State + NewIdentity *tfsdk.ResourceIdentity Private *privatestate.Data } @@ -97,12 +100,37 @@ func (s *Server) CreateResource(ctx context.Context, req *CreateResourceRequest, createReq.ProviderMeta = *req.ProviderMeta } + // If the resource supports identity and there is no planned identity data, pre-populate with a null value. + // TODO:ResourceIdentity: This logic is likely useless since plan should already handle this, probably should remove. + if req.PlannedIdentity == nil && req.IdentitySchema != nil { + nullIdentityTfValue := tftypes.NewValue(req.IdentitySchema.Type().TerraformType(ctx), nil) + + req.PlannedIdentity = &tfsdk.ResourceIdentity{ + Schema: req.IdentitySchema, + Raw: nullIdentityTfValue.Copy(), + } + } + + // Pre-populate the new identity with the planned identity. + if req.PlannedIdentity != nil { + createReq.Identity = &tfsdk.ResourceIdentity{ + Schema: req.PlannedIdentity.Schema, + Raw: req.PlannedIdentity.Raw.Copy(), + } + + createResp.Identity = &tfsdk.ResourceIdentity{ + Schema: req.PlannedIdentity.Schema, + Raw: req.PlannedIdentity.Raw.Copy(), + } + } + logging.FrameworkTrace(ctx, "Calling provider defined Resource Create") req.Resource.Create(ctx, createReq, &createResp) logging.FrameworkTrace(ctx, "Called provider defined Resource Create") resp.Diagnostics = createResp.Diagnostics resp.NewState = &createResp.State + resp.NewIdentity = createResp.Identity if !resp.Diagnostics.HasError() && createResp.State.Raw.Equal(nullSchemaData) { detail := "The Terraform Provider unexpectedly returned no resource state after having no errors in the resource creation. " + @@ -132,6 +160,16 @@ func (s *Server) CreateResource(ctx context.Context, req *CreateResourceRequest, return } + if resp.NewIdentity != nil && req.IdentitySchema == nil { + resp.Diagnostics.AddError( + "Unexpected Create Response", + "An unexpected error was encountered when creating the apply response. New identity data was returned by the provider create operation, but the resource does not indicate identity support.\n\n"+ + "This is always a problem with the provider and should be reported to the provider developer.", + ) + + return + } + semanticEqualityReq := SchemaSemanticEqualityRequest{ PriorData: fwschemadata.Data{ Description: fwschemadata.DataDescriptionPlan, diff --git a/internal/fwserver/server_createresource_test.go b/internal/fwserver/server_createresource_test.go index 86bedcb8f..3095638ff 100644 --- a/internal/fwserver/server_createresource_test.go +++ b/internal/fwserver/server_createresource_test.go @@ -18,6 +18,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" @@ -33,6 +34,12 @@ func TestServerCreateResource(t *testing.T) { }, } + testIdentityType := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_id": tftypes.String, + }, + } + testSchemaTypeWriteOnly := tftypes.Object{ AttributeTypes: map[string]tftypes.Type{ "test_required": tftypes.String, @@ -51,6 +58,14 @@ func TestServerCreateResource(t *testing.T) { }, } + testIdentitySchema := identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_id": identityschema.StringAttribute{ + RequiredForImport: true, + }, + }, + } + testSchemaWithSemanticEquals := schema.Schema{ Attributes: map[string]schema.Attribute{ "test_computed": schema.StringAttribute{ @@ -105,6 +120,10 @@ func TestServerCreateResource(t *testing.T) { TestRequired types.String `tfsdk:"test_required"` } + type testIdentitySchemaData struct { + TestID types.String `tfsdk:"test_id"` + } + type testSchemaDataWithSemanticEquals struct { TestComputed types.String `tfsdk:"test_computed"` TestRequired testtypes.StringValueWithSemanticEquals `tfsdk:"test_required"` @@ -244,6 +263,63 @@ func TestServerCreateResource(t *testing.T) { Private: testEmptyPrivate, }, }, + "request-plannedidentity": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.CreateResourceRequest{ + PlannedState: &tfsdk.Plan{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-plannedstate-value"), + }), + Schema: testSchema, + }, + PlannedIdentity: &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(testIdentityType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "id-123"), + }), + Schema: testIdentitySchema, + }, + IdentitySchema: testIdentitySchema, + ResourceSchema: testSchema, + Resource: &testprovider.ResourceWithIdentity{ + Resource: &testprovider.Resource{ + CreateMethod: func(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var identityData testIdentitySchemaData + + resp.Diagnostics.Append(req.Identity.Get(ctx, &identityData)...) + + if identityData.TestID.ValueString() != "id-123" { + resp.Diagnostics.AddError("Unexpected req.Identity Value", "Got: "+identityData.TestID.ValueString()) + } + + // Prevent missing resource state error diagnostic + var data testSchemaData + + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) + }, + }, + }, + }, + expectedResponse: &fwserver.CreateResourceResponse{ + NewIdentity: &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(testIdentityType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "id-123"), + }), + Schema: testIdentitySchema, + }, + NewState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-plannedstate-value"), + }), + Schema: testSchema, + }, + Private: testEmptyPrivate, + }, + }, "request-providermeta": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, @@ -457,6 +533,110 @@ func TestServerCreateResource(t *testing.T) { Private: testEmptyPrivate, }, }, + "response-newidentity": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.CreateResourceRequest{ + PlannedState: &tfsdk.Plan{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-plannedstate-value"), + }), + Schema: testSchema, + }, + IdentitySchema: testIdentitySchema, + ResourceSchema: testSchema, + Resource: &testprovider.ResourceWithIdentity{ + Resource: &testprovider.Resource{ + CreateMethod: func(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + resp.Diagnostics.Append(resp.Identity.Set(ctx, testIdentitySchemaData{ + TestID: types.StringValue("new-id-123"), + })...) + + // Prevent missing resource state error diagnostic + var data testSchemaData + + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) + }, + }, + }, + }, + expectedResponse: &fwserver.CreateResourceResponse{ + NewIdentity: &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(testIdentityType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "new-id-123"), + }), + Schema: testIdentitySchema, + }, + NewState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-plannedstate-value"), + }), + Schema: testSchema, + }, + Private: testEmptyPrivate, + }, + }, + "response-invalid-newidentity": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.CreateResourceRequest{ + PlannedState: &tfsdk.Plan{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-plannedstate-value"), + }), + Schema: testSchema, + }, + ResourceSchema: testSchema, + Resource: &testprovider.ResourceWithIdentity{ + Resource: &testprovider.Resource{ + CreateMethod: func(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + // This resource doesn't indicate identity support (via a schema), so this should raise a diagnostic. + resp.Identity = &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(testIdentityType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "new-id-123"), + }), + Schema: testIdentitySchema, + } + + // Prevent missing resource state error diagnostic + var data testSchemaData + + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) + }, + }, + }, + }, + expectedResponse: &fwserver.CreateResourceResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Unexpected Create Response", + "An unexpected error was encountered when creating the apply response. New identity data was returned by the provider create operation, but the resource does not indicate identity support.\n\n"+ + "This is always a problem with the provider and should be reported to the provider developer.", + ), + }, + NewIdentity: &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(testIdentityType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "new-id-123"), + }), + Schema: testIdentitySchema, + }, + NewState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-plannedstate-value"), + }), + Schema: testSchema, + }, + Private: testEmptyPrivate, + }, + }, "response-newstate-null": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, diff --git a/internal/fwserver/server_deleteresource.go b/internal/fwserver/server_deleteresource.go index 5879b6706..d556badfd 100644 --- a/internal/fwserver/server_deleteresource.go +++ b/internal/fwserver/server_deleteresource.go @@ -23,6 +23,7 @@ type DeleteResourceRequest struct { PriorState *tfsdk.State ProviderMeta *tfsdk.Config ResourceSchema fwschema.Schema + IdentitySchema fwschema.Schema Resource resource.Resource } @@ -31,6 +32,7 @@ type DeleteResourceRequest struct { type DeleteResourceResponse struct { Diagnostics diag.Diagnostics NewState *tfsdk.State + NewIdentity *tfsdk.ResourceIdentity Private *privatestate.Data } @@ -96,6 +98,17 @@ func (s *Server) DeleteResource(ctx context.Context, req *DeleteResourceRequest, resp.Private = req.PlannedPrivate } + // If the resource supports identity pre-populate a null value. + // TODO:ResourceIdentity: This should probably be prior identity, but we don't currently have that in the protocol. + if req.IdentitySchema != nil { + nullIdentityTfValue := tftypes.NewValue(req.IdentitySchema.Type().TerraformType(ctx), nil) + + deleteResp.Identity = &tfsdk.ResourceIdentity{ + Schema: req.IdentitySchema, + Raw: nullIdentityTfValue.Copy(), + } + } + logging.FrameworkTrace(ctx, "Calling provider defined Resource Delete") req.Resource.Delete(ctx, deleteReq, &deleteResp) logging.FrameworkTrace(ctx, "Called provider defined Resource Delete") @@ -108,10 +121,21 @@ func (s *Server) DeleteResource(ctx context.Context, req *DeleteResourceRequest, // Reference: https://github.com/hashicorp/terraform-plugin-framework/issues/863 deleteResp.Private = nil resp.Private = nil + + // If the resource supports identity send a null value. + if req.IdentitySchema != nil { + nullIdentityTfValue := tftypes.NewValue(req.IdentitySchema.Type().TerraformType(ctx), nil) + + deleteResp.Identity = &tfsdk.ResourceIdentity{ + Schema: req.IdentitySchema, + Raw: nullIdentityTfValue.Copy(), + } + } } resp.Diagnostics = deleteResp.Diagnostics resp.NewState = &deleteResp.State + resp.NewIdentity = deleteResp.Identity if deleteResp.Private != nil { if resp.Private == nil { @@ -120,4 +144,14 @@ func (s *Server) DeleteResource(ctx context.Context, req *DeleteResourceRequest, resp.Private.Provider = deleteResp.Private } + + if resp.NewIdentity != nil && req.IdentitySchema == nil { + resp.Diagnostics.AddError( + "Unexpected Delete Response", + "An unexpected error was encountered when creating the apply response. New identity data was returned by the provider delete operation, but the resource does not indicate identity support.\n\n"+ + "This is always a problem with the provider and should be reported to the provider developer.", + ) + + return + } } diff --git a/internal/fwserver/server_deleteresource_test.go b/internal/fwserver/server_deleteresource_test.go index 2042e2764..b47c16c0b 100644 --- a/internal/fwserver/server_deleteresource_test.go +++ b/internal/fwserver/server_deleteresource_test.go @@ -18,6 +18,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/testing/testprovider" "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" @@ -33,6 +34,12 @@ func TestServerDeleteResource(t *testing.T) { }, } + testIdentityType := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_id": tftypes.String, + }, + } + testSchema := schema.Schema{ Attributes: map[string]schema.Attribute{ "test_computed": schema.StringAttribute{ @@ -44,6 +51,14 @@ func TestServerDeleteResource(t *testing.T) { }, } + testIdentitySchema := identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_id": identityschema.StringAttribute{ + RequiredForImport: true, + }, + }, + } + testEmptyState := &tfsdk.State{ Raw: tftypes.NewValue(testSchemaType, nil), Schema: testSchema, @@ -54,6 +69,10 @@ func TestServerDeleteResource(t *testing.T) { TestRequired types.String `tfsdk:"test_required"` } + type testIdentitySchemaData struct { + TestID types.String `tfsdk:"test_id"` + } + testProviderMetaType := tftypes.Object{ AttributeTypes: map[string]tftypes.Type{ "test_provider_meta_attribute": tftypes.String, @@ -334,6 +353,114 @@ func TestServerDeleteResource(t *testing.T) { NewState: testEmptyState, }, }, + "response-newidentity": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.DeleteResourceRequest{ + PriorState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-priorstate-value"), + }), + Schema: testSchema, + }, + IdentitySchema: testIdentitySchema, + ResourceSchema: testSchema, + Resource: &testprovider.Resource{ + DeleteMethod: func(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + if resp.Identity == nil || !resp.Identity.Raw.IsNull() { + resp.Diagnostics.AddError( + "Unexpected resp.Identity", + "expected resp.Identity to be a null object of the schema type.", + ) + } + }, + }, + }, + expectedResponse: &fwserver.DeleteResourceResponse{ + NewIdentity: &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(testIdentityType, nil), + Schema: testIdentitySchema, + }, + NewState: testEmptyState, + }, + }, + "response-newidentity-set-to-null": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.DeleteResourceRequest{ + PriorState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-priorstate-value"), + }), + Schema: testSchema, + }, + IdentitySchema: testIdentitySchema, + ResourceSchema: testSchema, + Resource: &testprovider.Resource{ + DeleteMethod: func(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + // This should be nulled out + resp.Diagnostics.Append(resp.Identity.Set(ctx, testIdentitySchemaData{ + TestID: types.StringValue("new-id-123"), + })...) + }, + }, + }, + expectedResponse: &fwserver.DeleteResourceResponse{ + NewIdentity: &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(testIdentityType, nil), + Schema: testIdentitySchema, + }, + NewState: testEmptyState, + }, + }, + "response-invalid-newidentity": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.DeleteResourceRequest{ + PriorState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-priorstate-value"), + }), + Schema: testSchema, + }, + ResourceSchema: testSchema, + Resource: &testprovider.ResourceWithIdentity{ + Resource: &testprovider.Resource{ + DeleteMethod: func(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + // This should raise a diagnostic + resp.Identity = &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(testIdentityType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "new-id-123"), + }), + Schema: testIdentitySchema, + } + }, + }, + }, + }, + expectedResponse: &fwserver.DeleteResourceResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Unexpected Delete Response", + "An unexpected error was encountered when creating the apply response. New identity data was returned by the provider delete operation, but the resource does not indicate identity support.\n\n"+ + "This is always a problem with the provider and should be reported to the provider developer.", + ), + }, + NewIdentity: &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(testIdentityType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "new-id-123"), + }), + Schema: testIdentitySchema, + }, + NewState: testEmptyState, + }, + }, "response-private": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, @@ -375,7 +502,7 @@ func TestServerDeleteResource(t *testing.T) { Private: testPrivateProvider, }, }, - "response-private-updated": { + "response-private-Deleted": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, }, diff --git a/internal/fwserver/server_readresource.go b/internal/fwserver/server_readresource.go index 6ceeda853..1c56d44c5 100644 --- a/internal/fwserver/server_readresource.go +++ b/internal/fwserver/server_readresource.go @@ -119,31 +119,26 @@ func (s *Server) ReadResource(ctx context.Context, req *ReadResourceRequest, res resp.Private = req.Private } - if req.CurrentIdentity != nil { - readReq.Identity = &tfsdk.ResourceIdentity{ - Schema: req.CurrentIdentity.Schema, - Raw: req.CurrentIdentity.Raw.Copy(), - } - - readResp.Identity = &tfsdk.ResourceIdentity{ - Schema: req.CurrentIdentity.Schema, - Raw: req.CurrentIdentity.Raw.Copy(), - } - } - // If the resource supports identity and there is no current identity data, pre-populate with a null value. // TODO:ResourceIdentity: Is there any reason a provider WOULD NOT want to populate an identity when it supports one? if req.CurrentIdentity == nil && req.IdentitySchema != nil { nullTfValue := tftypes.NewValue(req.IdentitySchema.Type().TerraformType(ctx), nil) - readReq.Identity = &tfsdk.ResourceIdentity{ + req.CurrentIdentity = &tfsdk.ResourceIdentity{ Schema: req.IdentitySchema, Raw: nullTfValue.Copy(), } + } + + if req.CurrentIdentity != nil { + readReq.Identity = &tfsdk.ResourceIdentity{ + Schema: req.CurrentIdentity.Schema, + Raw: req.CurrentIdentity.Raw.Copy(), + } readResp.Identity = &tfsdk.ResourceIdentity{ - Schema: req.IdentitySchema, - Raw: nullTfValue.Copy(), + Schema: req.CurrentIdentity.Schema, + Raw: req.CurrentIdentity.Raw.Copy(), } } diff --git a/internal/fwserver/server_updateresource.go b/internal/fwserver/server_updateresource.go index ad1d9f998..19827b7e3 100644 --- a/internal/fwserver/server_updateresource.go +++ b/internal/fwserver/server_updateresource.go @@ -20,13 +20,15 @@ import ( // UpdateResourceRequest is the framework server request for an update request // with the ApplyResourceChange RPC. type UpdateResourceRequest struct { - Config *tfsdk.Config - PlannedPrivate *privatestate.Data - PlannedState *tfsdk.Plan - PriorState *tfsdk.State - ProviderMeta *tfsdk.Config - ResourceSchema fwschema.Schema - Resource resource.Resource + Config *tfsdk.Config + PlannedPrivate *privatestate.Data + PlannedState *tfsdk.Plan + PlannedIdentity *tfsdk.ResourceIdentity + PriorState *tfsdk.State + ProviderMeta *tfsdk.Config + ResourceSchema fwschema.Schema + IdentitySchema fwschema.Schema + Resource resource.Resource } // UpdateResourceResponse is the framework server response for an update request @@ -34,6 +36,7 @@ type UpdateResourceRequest struct { type UpdateResourceResponse struct { Diagnostics diag.Diagnostics NewState *tfsdk.State + NewIdentity *tfsdk.ResourceIdentity Private *privatestate.Data } @@ -118,12 +121,37 @@ func (s *Server) UpdateResource(ctx context.Context, req *UpdateResourceRequest, resp.Private = req.PlannedPrivate } + // If the resource supports identity and there is no planned identity data, pre-populate with a null value. + // TODO:ResourceIdentity: This logic is likely useless since plan should already handle this, probably should remove. + if req.PlannedIdentity == nil && req.IdentitySchema != nil { + nullIdentityTfValue := tftypes.NewValue(req.IdentitySchema.Type().TerraformType(ctx), nil) + + req.PlannedIdentity = &tfsdk.ResourceIdentity{ + Schema: req.IdentitySchema, + Raw: nullIdentityTfValue.Copy(), + } + } + + // Pre-populate the new identity with the planned identity. + if req.PlannedIdentity != nil { + updateReq.Identity = &tfsdk.ResourceIdentity{ + Schema: req.PlannedIdentity.Schema, + Raw: req.PlannedIdentity.Raw.Copy(), + } + + updateResp.Identity = &tfsdk.ResourceIdentity{ + Schema: req.PlannedIdentity.Schema, + Raw: req.PlannedIdentity.Raw.Copy(), + } + } + logging.FrameworkTrace(ctx, "Calling provider defined Resource Update") req.Resource.Update(ctx, updateReq, &updateResp) logging.FrameworkTrace(ctx, "Called provider defined Resource Update") resp.Diagnostics = updateResp.Diagnostics resp.NewState = &updateResp.State + resp.NewIdentity = updateResp.Identity if !resp.Diagnostics.HasError() && updateResp.State.Raw.Equal(nullSchemaData) { resp.Diagnostics.AddError( @@ -145,6 +173,16 @@ func (s *Server) UpdateResource(ctx context.Context, req *UpdateResourceRequest, return } + if resp.NewIdentity != nil && req.IdentitySchema == nil { + resp.Diagnostics.AddError( + "Unexpected Update Response", + "An unexpected error was encountered when creating the apply response. New identity data was returned by the provider update operation, but the resource does not indicate identity support.\n\n"+ + "This is always a problem with the provider and should be reported to the provider developer.", + ) + + return + } + semanticEqualityReq := SchemaSemanticEqualityRequest{ PriorData: fwschemadata.Data{ Description: fwschemadata.DataDescriptionPlan, diff --git a/internal/fwserver/server_updateresource_test.go b/internal/fwserver/server_updateresource_test.go index 4396e93f1..f17616703 100644 --- a/internal/fwserver/server_updateresource_test.go +++ b/internal/fwserver/server_updateresource_test.go @@ -19,6 +19,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/testing/testtypes" "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" @@ -34,6 +35,12 @@ func TestServerUpdateResource(t *testing.T) { }, } + testIdentityType := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_id": tftypes.String, + }, + } + testSchemaTypeWriteOnly := tftypes.Object{ AttributeTypes: map[string]tftypes.Type{ "test_required": tftypes.String, @@ -52,6 +59,14 @@ func TestServerUpdateResource(t *testing.T) { }, } + testIdentitySchema := identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_id": identityschema.StringAttribute{ + RequiredForImport: true, + }, + }, + } + testSchemaWithSemanticEquals := schema.Schema{ Attributes: map[string]schema.Attribute{ "test_computed": schema.StringAttribute{ @@ -101,6 +116,10 @@ func TestServerUpdateResource(t *testing.T) { TestRequired types.String `tfsdk:"test_required"` } + type testIdentitySchemaData struct { + TestID types.String `tfsdk:"test_id"` + } + type testSchemaDataWithSemanticEquals struct { TestComputed types.String `tfsdk:"test_computed"` TestRequired testtypes.StringValueWithSemanticEquals `tfsdk:"test_required"` @@ -274,6 +293,63 @@ func TestServerUpdateResource(t *testing.T) { Private: testEmptyPrivate, }, }, + "request-plannedidentity": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.UpdateResourceRequest{ + PlannedState: &tfsdk.Plan{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-plannedstate-value"), + }), + Schema: testSchema, + }, + PlannedIdentity: &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(testIdentityType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "id-123"), + }), + Schema: testIdentitySchema, + }, + IdentitySchema: testIdentitySchema, + ResourceSchema: testSchema, + Resource: &testprovider.ResourceWithIdentity{ + Resource: &testprovider.Resource{ + UpdateMethod: func(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var identityData testIdentitySchemaData + + resp.Diagnostics.Append(req.Identity.Get(ctx, &identityData)...) + + if identityData.TestID.ValueString() != "id-123" { + resp.Diagnostics.AddError("Unexpected req.Identity Value", "Got: "+identityData.TestID.ValueString()) + } + + // Prevent missing resource state error diagnostic + var data testSchemaData + + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) + }, + }, + }, + }, + expectedResponse: &fwserver.UpdateResourceResponse{ + NewIdentity: &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(testIdentityType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "id-123"), + }), + Schema: testIdentitySchema, + }, + NewState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-plannedstate-value"), + }), + Schema: testSchema, + }, + Private: testEmptyPrivate, + }, + }, "request-priorstate": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, @@ -699,6 +775,110 @@ func TestServerUpdateResource(t *testing.T) { Private: testEmptyPrivate, }, }, + "response-newidentity": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.UpdateResourceRequest{ + PlannedState: &tfsdk.Plan{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-plannedstate-value"), + }), + Schema: testSchema, + }, + IdentitySchema: testIdentitySchema, + ResourceSchema: testSchema, + Resource: &testprovider.ResourceWithIdentity{ + Resource: &testprovider.Resource{ + UpdateMethod: func(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + resp.Diagnostics.Append(resp.Identity.Set(ctx, testIdentitySchemaData{ + TestID: types.StringValue("new-id-123"), + })...) + + // Prevent missing resource state error diagnostic + var data testSchemaData + + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) + }, + }, + }, + }, + expectedResponse: &fwserver.UpdateResourceResponse{ + NewIdentity: &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(testIdentityType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "new-id-123"), + }), + Schema: testIdentitySchema, + }, + NewState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-plannedstate-value"), + }), + Schema: testSchema, + }, + Private: testEmptyPrivate, + }, + }, + "response-invalid-newidentity": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.UpdateResourceRequest{ + PlannedState: &tfsdk.Plan{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-plannedstate-value"), + }), + Schema: testSchema, + }, + ResourceSchema: testSchema, + Resource: &testprovider.ResourceWithIdentity{ + Resource: &testprovider.Resource{ + UpdateMethod: func(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + // This resource doesn't indicate identity support (via a schema), so this should raise a diagnostic. + resp.Identity = &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(testIdentityType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "new-id-123"), + }), + Schema: testIdentitySchema, + } + + // Prevent missing resource state error diagnostic + var data testSchemaData + + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) + }, + }, + }, + }, + expectedResponse: &fwserver.UpdateResourceResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Unexpected Update Response", + "An unexpected error was encountered when creating the apply response. New identity data was returned by the provider update operation, but the resource does not indicate identity support.\n\n"+ + "This is always a problem with the provider and should be reported to the provider developer.", + ), + }, + NewIdentity: &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(testIdentityType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "new-id-123"), + }), + Schema: testIdentitySchema, + }, + NewState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-plannedstate-value"), + }), + Schema: testSchema, + }, + Private: testEmptyPrivate, + }, + }, "response-newstate-null": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, diff --git a/internal/proto5server/server_applyresourcechange.go b/internal/proto5server/server_applyresourcechange.go index e4e8bb92e..717d071f2 100644 --- a/internal/proto5server/server_applyresourcechange.go +++ b/internal/proto5server/server_applyresourcechange.go @@ -36,6 +36,14 @@ func (s *Server) ApplyResourceChange(ctx context.Context, proto5Req *tfprotov5.A return toproto5.ApplyResourceChangeResponse(ctx, fwResp), nil } + identitySchema, diags := s.FrameworkServer.ResourceIdentitySchema(ctx, proto5Req.TypeName) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto5.ApplyResourceChangeResponse(ctx, fwResp), nil + } + providerMetaSchema, diags := s.FrameworkServer.ProviderMetaSchema(ctx) fwResp.Diagnostics.Append(diags...) @@ -44,7 +52,7 @@ func (s *Server) ApplyResourceChange(ctx context.Context, proto5Req *tfprotov5.A return toproto5.ApplyResourceChangeResponse(ctx, fwResp), nil } - fwReq, diags := fromproto5.ApplyResourceChangeRequest(ctx, proto5Req, resource, resourceSchema, providerMetaSchema) + fwReq, diags := fromproto5.ApplyResourceChangeRequest(ctx, proto5Req, resource, resourceSchema, providerMetaSchema, identitySchema) fwResp.Diagnostics.Append(diags...) diff --git a/internal/proto5server/server_applyresourcechange_test.go b/internal/proto5server/server_applyresourcechange_test.go index 54484786c..6c17d703b 100644 --- a/internal/proto5server/server_applyresourcechange_test.go +++ b/internal/proto5server/server_applyresourcechange_test.go @@ -18,6 +18,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/provider" "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/types" ) @@ -34,6 +35,20 @@ func TestServerApplyResourceChange(t *testing.T) { testEmptyDynamicValue, _ := tfprotov5.NewDynamicValue(testSchemaType, tftypes.NewValue(testSchemaType, nil)) + testIdentityType := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_id": tftypes.String, + }, + } + + testPlannedIdentityValue := testNewDynamicValue(t, testIdentityType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "id-123"), + }) + + testNewIdentityDynamicValue := testNewDynamicValue(t, testIdentityType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "new-id-123"), + }) + testSchema := schema.Schema{ Attributes: map[string]schema.Attribute{ "test_computed": schema.StringAttribute{ @@ -45,6 +60,14 @@ func TestServerApplyResourceChange(t *testing.T) { }, } + testIdentitySchema := identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_id": identityschema.StringAttribute{ + RequiredForImport: true, + }, + }, + } + type testSchemaData struct { TestComputed types.String `tfsdk:"test_computed"` TestRequired types.String `tfsdk:"test_required"` @@ -194,6 +217,79 @@ func TestServerApplyResourceChange(t *testing.T) { }), }, }, + "create-request-plannedidentity": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + ResourcesMethod: func(_ context.Context) []func() resource.Resource { + return []func() resource.Resource{ + func() resource.Resource { + return &testprovider.ResourceWithIdentity{ + Resource: &testprovider.Resource{ + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema + }, + MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "test_resource" + }, + CreateMethod: func(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var identityData struct { + TestID types.String `tfsdk:"test_id"` + } + + resp.Diagnostics.Append(req.Identity.Get(ctx, &identityData)...) + + if identityData.TestID.ValueString() != "id-123" { + resp.Diagnostics.AddError("Unexpected req.Identity", identityData.TestID.ValueString()) + } + + // Prevent missing resource state error diagnostic + var data testSchemaData + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) + }, + DeleteMethod: func(_ context.Context, _ resource.DeleteRequest, resp *resource.DeleteResponse) { + resp.Diagnostics.AddError("Unexpected Method Call", "Expected: Create, Got: Delete") + }, + UpdateMethod: func(_ context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { + resp.Diagnostics.AddError("Unexpected Method Call", "Expected: Create, Got: Update") + }, + }, + IdentitySchemaMethod: func(ctx context.Context, req resource.IdentitySchemaRequest, resp *resource.IdentitySchemaResponse) { + resp.IdentitySchema = testIdentitySchema + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.ApplyResourceChangeRequest{ + Config: testNewDynamicValue(t, testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + PlannedState: testNewDynamicValue(t, testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, "test-plannedstate-value"), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + PlannedIdentity: &tfprotov5.ResourceIdentityData{ + IdentityData: testPlannedIdentityValue, + }, + PriorState: &testEmptyDynamicValue, + TypeName: "test_resource", + }, + expectedResponse: &tfprotov5.ApplyResourceChangeResponse{ + NewState: testNewDynamicValue(t, testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, "test-plannedstate-value"), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + NewIdentity: &tfprotov5.ResourceIdentityData{ + IdentityData: testPlannedIdentityValue, + }, + }, + }, "create-request-providermeta": { server: &Server{ FrameworkServer: fwserver.Server{ @@ -372,6 +468,73 @@ func TestServerApplyResourceChange(t *testing.T) { }), }, }, + "create-response-newidentity": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + ResourcesMethod: func(_ context.Context) []func() resource.Resource { + return []func() resource.Resource{ + func() resource.Resource { + return &testprovider.ResourceWithIdentity{ + Resource: &testprovider.Resource{ + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema + }, + MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "test_resource" + }, + CreateMethod: func(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + identityData := struct { + TestID types.String `tfsdk:"test_id"` + }{ + TestID: types.StringValue("new-id-123"), + } + resp.Diagnostics.Append(resp.Identity.Set(ctx, identityData)...) + + var data testSchemaData + + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) + }, + DeleteMethod: func(_ context.Context, _ resource.DeleteRequest, resp *resource.DeleteResponse) { + resp.Diagnostics.AddError("Unexpected Method Call", "Expected: Create, Got: Delete") + }, + UpdateMethod: func(_ context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { + resp.Diagnostics.AddError("Unexpected Method Call", "Expected: Create, Got: Update") + }, + }, + IdentitySchemaMethod: func(ctx context.Context, req resource.IdentitySchemaRequest, resp *resource.IdentitySchemaResponse) { + resp.IdentitySchema = testIdentitySchema + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.ApplyResourceChangeRequest{ + Config: testNewDynamicValue(t, testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + PlannedState: testNewDynamicValue(t, testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, "test-plannedstate-value"), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + PriorState: &testEmptyDynamicValue, + TypeName: "test_resource", + }, + expectedResponse: &tfprotov5.ApplyResourceChangeResponse{ + NewIdentity: &tfprotov5.ResourceIdentityData{ + IdentityData: testNewIdentityDynamicValue, + }, + NewState: testNewDynamicValue(t, testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, "test-plannedstate-value"), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + }, + }, "create-response-newstate-null": { server: &Server{ FrameworkServer: fwserver.Server{ diff --git a/internal/toproto5/applyresourcechange.go b/internal/toproto5/applyresourcechange.go index 289373999..1b5da0df6 100644 --- a/internal/toproto5/applyresourcechange.go +++ b/internal/toproto5/applyresourcechange.go @@ -27,6 +27,11 @@ func ApplyResourceChangeResponse(ctx context.Context, fw *fwserver.ApplyResource proto5.Diagnostics = append(proto5.Diagnostics, Diagnostics(ctx, diags)...) proto5.NewState = newState + newIdentity, diags := ResourceIdentity(ctx, fw.NewIdentity) + + proto5.Diagnostics = append(proto5.Diagnostics, Diagnostics(ctx, diags)...) + proto5.NewIdentity = newIdentity + newPrivate, diags := fw.Private.Bytes(ctx) proto5.Diagnostics = append(proto5.Diagnostics, Diagnostics(ctx, diags)...) diff --git a/internal/toproto5/applyresourcechange_test.go b/internal/toproto5/applyresourcechange_test.go index 85f8c99dd..63911e17c 100644 --- a/internal/toproto5/applyresourcechange_test.go +++ b/internal/toproto5/applyresourcechange_test.go @@ -15,6 +15,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" "github.com/hashicorp/terraform-plugin-framework/internal/toproto5" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/tfsdk" ) @@ -38,6 +39,22 @@ func TestApplyResourceChangeResponse(t *testing.T) { t.Fatalf("unexpected error calling tfprotov5.NewDynamicValue(): %s", err) } + testIdentityProto5Type := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_id": tftypes.String, + }, + } + + testIdentityProto5Value := tftypes.NewValue(testIdentityProto5Type, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "id-123"), + }) + + testIdentityProto5DynamicValue, err := tfprotov5.NewDynamicValue(testIdentityProto5Type, testIdentityProto5Value) + + if err != nil { + t.Fatalf("unexpected error calling tfprotov5.NewDynamicValue(): %s", err) + } + testState := &tfsdk.State{ Raw: testProto5Value, Schema: schema.Schema{ @@ -60,6 +77,28 @@ func TestApplyResourceChangeResponse(t *testing.T) { }, } + testIdentity := &tfsdk.ResourceIdentity{ + Raw: testIdentityProto5Value, + Schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_id": identityschema.StringAttribute{ + RequiredForImport: true, + }, + }, + }, + } + + testIdentityInvalid := &tfsdk.ResourceIdentity{ + Raw: testIdentityProto5Value, + Schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_id": identityschema.BoolAttribute{ + RequiredForImport: true, + }, + }, + }, + } + testProviderKeyValue := privatestate.MustMarshalToJson(map[string][]byte{ "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), }) @@ -131,6 +170,37 @@ func TestApplyResourceChangeResponse(t *testing.T) { }, }, }, + "diagnostics-invalid-newidentity": { + input: &fwserver.ApplyResourceChangeResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewWarningDiagnostic("test warning summary", "test warning details"), + diag.NewErrorDiagnostic("test error summary", "test error details"), + }, + NewIdentity: testIdentityInvalid, + }, + expected: &tfprotov5.ApplyResourceChangeResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityWarning, + Summary: "test warning summary", + Detail: "test warning details", + }, + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "test error summary", + Detail: "test error details", + }, + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "Unable to Convert Resource Identity", + Detail: "An unexpected error was encountered when converting the resource identity to the protocol type. " + + "This is always an issue in terraform-plugin-framework used to implement the provider and should be reported to the provider developers.\n\n" + + "Please report this to the provider developer:\n\n" + + "Unable to create DynamicValue: AttributeName(\"test_id\"): unexpected value type string, tftypes.Bool values must be of type bool", + }, + }, + }, + }, "newstate": { input: &fwserver.ApplyResourceChangeResponse{ NewState: testState, @@ -139,6 +209,16 @@ func TestApplyResourceChangeResponse(t *testing.T) { NewState: &testProto5DynamicValue, }, }, + "newidentity": { + input: &fwserver.ApplyResourceChangeResponse{ + NewIdentity: testIdentity, + }, + expected: &tfprotov5.ApplyResourceChangeResponse{ + NewIdentity: &tfprotov5.ResourceIdentityData{ + IdentityData: &testIdentityProto5DynamicValue, + }, + }, + }, "private": { input: &fwserver.ApplyResourceChangeResponse{ Private: &privatestate.Data{ diff --git a/resource/create.go b/resource/create.go index 8831f8370..920cc5dd3 100644 --- a/resource/create.go +++ b/resource/create.go @@ -23,6 +23,10 @@ type CreateRequest struct { // Plan is the planned state for the resource. Plan tfsdk.Plan + // Identity is the planned identity for the resource. If the resource does not + // support identity, this value will not be set. + Identity *tfsdk.ResourceIdentity + // ProviderMeta is metadata from the provider_meta block of the module. ProviderMeta tfsdk.Config } @@ -37,6 +41,14 @@ type CreateResponse struct { // should be set during the resource's Create operation. State tfsdk.State + // Identity is the identity of the resource following the Create operation. + // This field is pre-populated from CreateRequest.Identity and + // should be set during the resource's Create operation. + // + // If the resource does not support identity, this value will not be set and will + // raise a diagnostic if set by the resource's Create operation. + Identity *tfsdk.ResourceIdentity + // Private is the private state resource data following the Create operation. // This field is not pre-populated as there is no pre-existing private state // data during the resource's Create operation. diff --git a/resource/delete.go b/resource/delete.go index ab81a6c92..8281dffa1 100644 --- a/resource/delete.go +++ b/resource/delete.go @@ -33,10 +33,13 @@ type DeleteRequest struct { // should set values on the DeleteResponse as appropriate. type DeleteResponse struct { // State is the state of the resource following the Delete operation. - // This field is pre-populated from UpdateResourceRequest.Plan and - // should be set during the resource's Update operation. + // This field is pre-populated from DeleteRequest.State and + // should be set during the resource's Delete operation. State tfsdk.State + // Identity is the identity of the resource following the Delete operation. + Identity *tfsdk.ResourceIdentity + // Private is the private state resource data following the Delete // operation. This field is pre-populated from DeleteRequest.Private and // can be modified during the resource's Delete operation in cases where diff --git a/resource/update.go b/resource/update.go index 0dceaf8ab..d832b24c3 100644 --- a/resource/update.go +++ b/resource/update.go @@ -27,6 +27,10 @@ type UpdateRequest struct { // operation. State tfsdk.State + // Identity is the planned identity for the resource. If the resource does not + // support identity, this value will not be set. + Identity *tfsdk.ResourceIdentity + // ProviderMeta is metadata from the provider_meta block of the module. ProviderMeta tfsdk.Config @@ -49,6 +53,14 @@ type UpdateResponse struct { // should be set during the resource's Update operation. State tfsdk.State + // Identity is the identity of the resource following the Update operation. + // This field is pre-populated from UpdateRequest.Identity and + // should be set during the resource's Update operation. + // + // If the resource does not support identity, this value will not be set and will + // raise a diagnostic if set by the resource's Update operation. + Identity *tfsdk.ResourceIdentity + // Private is the private state resource data following the Update operation. // This field is pre-populated from UpdateRequest.Private and // can be modified during the resource's Update operation. From bb07ce6561f35ee8c1299efa365152f92a6b0737 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Fri, 14 Mar 2025 08:39:39 -0400 Subject: [PATCH 19/20] protov6 implementation of apply resource --- internal/fromproto6/applyresourcechange.go | 9 +- .../fromproto6/applyresourcechange_test.go | 64 ++++++- .../server_applyresourcechange.go | 10 +- .../server_applyresourcechange_test.go | 163 ++++++++++++++++++ internal/toproto6/applyresourcechange.go | 5 + internal/toproto6/applyresourcechange_test.go | 80 +++++++++ 6 files changed, 328 insertions(+), 3 deletions(-) diff --git a/internal/fromproto6/applyresourcechange.go b/internal/fromproto6/applyresourcechange.go index f48eb856b..5620ffe40 100644 --- a/internal/fromproto6/applyresourcechange.go +++ b/internal/fromproto6/applyresourcechange.go @@ -17,7 +17,7 @@ import ( // ApplyResourceChangeRequest returns the *fwserver.ApplyResourceChangeRequest // equivalent of a *tfprotov6.ApplyResourceChangeRequest. -func ApplyResourceChangeRequest(ctx context.Context, proto6 *tfprotov6.ApplyResourceChangeRequest, resource resource.Resource, resourceSchema fwschema.Schema, providerMetaSchema fwschema.Schema) (*fwserver.ApplyResourceChangeRequest, diag.Diagnostics) { +func ApplyResourceChangeRequest(ctx context.Context, proto6 *tfprotov6.ApplyResourceChangeRequest, resource resource.Resource, resourceSchema fwschema.Schema, providerMetaSchema fwschema.Schema, identitySchema fwschema.Schema) (*fwserver.ApplyResourceChangeRequest, diag.Diagnostics) { if proto6 == nil { return nil, nil } @@ -40,6 +40,7 @@ func ApplyResourceChangeRequest(ctx context.Context, proto6 *tfprotov6.ApplyReso fw := &fwserver.ApplyResourceChangeRequest{ ResourceSchema: resourceSchema, + IdentitySchema: identitySchema, Resource: resource, } @@ -55,6 +56,12 @@ func ApplyResourceChangeRequest(ctx context.Context, proto6 *tfprotov6.ApplyReso fw.PlannedState = plannedState + plannedIdentity, plannedIdentityDiags := ResourceIdentity(ctx, proto6.PlannedIdentity, identitySchema) + + diags.Append(plannedIdentityDiags...) + + fw.PlannedIdentity = plannedIdentity + priorState, priorStateDiags := State(ctx, proto6.PriorState, resourceSchema) diags.Append(priorStateDiags...) diff --git a/internal/fromproto6/applyresourcechange_test.go b/internal/fromproto6/applyresourcechange_test.go index 9f845412e..d29913fb3 100644 --- a/internal/fromproto6/applyresourcechange_test.go +++ b/internal/fromproto6/applyresourcechange_test.go @@ -17,6 +17,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/tfsdk" ) @@ -48,6 +49,30 @@ func TestApplyResourceChangeRequest(t *testing.T) { }, } + testIdentityProto6Type := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_identity_attribute": tftypes.String, + }, + } + + testIdentityProto6Value := tftypes.NewValue(testIdentityProto6Type, map[string]tftypes.Value{ + "test_identity_attribute": tftypes.NewValue(tftypes.String, "id-123"), + }) + + testIdentityProto6DynamicValue, err := tfprotov6.NewDynamicValue(testIdentityProto6Type, testIdentityProto6Value) + + if err != nil { + t.Fatalf("unexpected error calling tfprotov6.NewDynamicValue(): %s", err) + } + + testIdentitySchema := identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_identity_attribute": identityschema.StringAttribute{ + RequiredForImport: true, + }, + }, + } + testProviderKeyValue := privatestate.MustMarshalToJson(map[string][]byte{ "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), }) @@ -61,6 +86,7 @@ func TestApplyResourceChangeRequest(t *testing.T) { resourceSchema fwschema.Schema resource resource.Resource providerMetaSchema fwschema.Schema + identitySchema fwschema.Schema expected *fwserver.ApplyResourceChangeRequest expectedDiagnostics diag.Diagnostics }{ @@ -137,6 +163,42 @@ func TestApplyResourceChangeRequest(t *testing.T) { ResourceSchema: testFwSchema, }, }, + "plannedidentity-missing-schema": { + input: &tfprotov6.ApplyResourceChangeRequest{ + PlannedIdentity: &tfprotov6.ResourceIdentityData{ + IdentityData: &testIdentityProto6DynamicValue, + }, + }, + resourceSchema: testFwSchema, + expected: &fwserver.ApplyResourceChangeRequest{ + ResourceSchema: testFwSchema, + }, + expectedDiagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Unable to Convert Resource Identity", + "An unexpected error was encountered when converting the resource identity from the protocol type. "+ + "Identity data was sent in the protocol to a resource that doesn't support identity.\n\n"+ + "This is always a problem with Terraform or terraform-plugin-framework. Please report this to the provider developer.", + ), + }, + }, + "plannedidentity": { + input: &tfprotov6.ApplyResourceChangeRequest{ + PlannedIdentity: &tfprotov6.ResourceIdentityData{ + IdentityData: &testIdentityProto6DynamicValue, + }, + }, + identitySchema: testIdentitySchema, + resourceSchema: testFwSchema, + expected: &fwserver.ApplyResourceChangeRequest{ + IdentitySchema: testIdentitySchema, + PlannedIdentity: &tfsdk.ResourceIdentity{ + Raw: testIdentityProto6Value, + Schema: testIdentitySchema, + }, + ResourceSchema: testFwSchema, + }, + }, "plannedprivate-malformed-json": { input: &tfprotov6.ApplyResourceChangeRequest{ PlannedPrivate: []byte(`{`), @@ -253,7 +315,7 @@ func TestApplyResourceChangeRequest(t *testing.T) { t.Run(name, func(t *testing.T) { t.Parallel() - got, diags := fromproto6.ApplyResourceChangeRequest(context.Background(), testCase.input, testCase.resource, testCase.resourceSchema, testCase.providerMetaSchema) + got, diags := fromproto6.ApplyResourceChangeRequest(context.Background(), testCase.input, testCase.resource, testCase.resourceSchema, testCase.providerMetaSchema, testCase.identitySchema) if diff := cmp.Diff(got, testCase.expected, cmp.AllowUnexported(privatestate.ProviderData{})); diff != "" { t.Errorf("unexpected difference: %s", diff) diff --git a/internal/proto6server/server_applyresourcechange.go b/internal/proto6server/server_applyresourcechange.go index 0762368b1..85fc2dc11 100644 --- a/internal/proto6server/server_applyresourcechange.go +++ b/internal/proto6server/server_applyresourcechange.go @@ -36,6 +36,14 @@ func (s *Server) ApplyResourceChange(ctx context.Context, proto6Req *tfprotov6.A return toproto6.ApplyResourceChangeResponse(ctx, fwResp), nil } + identitySchema, diags := s.FrameworkServer.ResourceIdentitySchema(ctx, proto6Req.TypeName) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto6.ApplyResourceChangeResponse(ctx, fwResp), nil + } + providerMetaSchema, diags := s.FrameworkServer.ProviderMetaSchema(ctx) fwResp.Diagnostics.Append(diags...) @@ -44,7 +52,7 @@ func (s *Server) ApplyResourceChange(ctx context.Context, proto6Req *tfprotov6.A return toproto6.ApplyResourceChangeResponse(ctx, fwResp), nil } - fwReq, diags := fromproto6.ApplyResourceChangeRequest(ctx, proto6Req, resource, resourceSchema, providerMetaSchema) + fwReq, diags := fromproto6.ApplyResourceChangeRequest(ctx, proto6Req, resource, resourceSchema, providerMetaSchema, identitySchema) fwResp.Diagnostics.Append(diags...) diff --git a/internal/proto6server/server_applyresourcechange_test.go b/internal/proto6server/server_applyresourcechange_test.go index 66c12933d..93b4f40e0 100644 --- a/internal/proto6server/server_applyresourcechange_test.go +++ b/internal/proto6server/server_applyresourcechange_test.go @@ -18,6 +18,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/provider" "github.com/hashicorp/terraform-plugin-framework/provider/metaschema" "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/types" ) @@ -34,6 +35,20 @@ func TestServerApplyResourceChange(t *testing.T) { testEmptyDynamicValue, _ := tfprotov6.NewDynamicValue(testSchemaType, tftypes.NewValue(testSchemaType, nil)) + testIdentityType := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_id": tftypes.String, + }, + } + + testPlannedIdentityValue := testNewDynamicValue(t, testIdentityType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "id-123"), + }) + + testNewIdentityDynamicValue := testNewDynamicValue(t, testIdentityType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "new-id-123"), + }) + testSchema := schema.Schema{ Attributes: map[string]schema.Attribute{ "test_computed": schema.StringAttribute{ @@ -45,6 +60,14 @@ func TestServerApplyResourceChange(t *testing.T) { }, } + testIdentitySchema := identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_id": identityschema.StringAttribute{ + RequiredForImport: true, + }, + }, + } + type testSchemaData struct { TestComputed types.String `tfsdk:"test_computed"` TestRequired types.String `tfsdk:"test_required"` @@ -194,6 +217,79 @@ func TestServerApplyResourceChange(t *testing.T) { }), }, }, + "create-request-plannedidentity": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + ResourcesMethod: func(_ context.Context) []func() resource.Resource { + return []func() resource.Resource{ + func() resource.Resource { + return &testprovider.ResourceWithIdentity{ + Resource: &testprovider.Resource{ + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema + }, + MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "test_resource" + }, + CreateMethod: func(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var identityData struct { + TestID types.String `tfsdk:"test_id"` + } + + resp.Diagnostics.Append(req.Identity.Get(ctx, &identityData)...) + + if identityData.TestID.ValueString() != "id-123" { + resp.Diagnostics.AddError("Unexpected req.Identity", identityData.TestID.ValueString()) + } + + // Prevent missing resource state error diagnostic + var data testSchemaData + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) + }, + DeleteMethod: func(_ context.Context, _ resource.DeleteRequest, resp *resource.DeleteResponse) { + resp.Diagnostics.AddError("Unexpected Method Call", "Expected: Create, Got: Delete") + }, + UpdateMethod: func(_ context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { + resp.Diagnostics.AddError("Unexpected Method Call", "Expected: Create, Got: Update") + }, + }, + IdentitySchemaMethod: func(ctx context.Context, req resource.IdentitySchemaRequest, resp *resource.IdentitySchemaResponse) { + resp.IdentitySchema = testIdentitySchema + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov6.ApplyResourceChangeRequest{ + Config: testNewDynamicValue(t, testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + PlannedState: testNewDynamicValue(t, testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, "test-plannedstate-value"), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + PlannedIdentity: &tfprotov6.ResourceIdentityData{ + IdentityData: testPlannedIdentityValue, + }, + PriorState: &testEmptyDynamicValue, + TypeName: "test_resource", + }, + expectedResponse: &tfprotov6.ApplyResourceChangeResponse{ + NewState: testNewDynamicValue(t, testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, "test-plannedstate-value"), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + NewIdentity: &tfprotov6.ResourceIdentityData{ + IdentityData: testPlannedIdentityValue, + }, + }, + }, "create-request-providermeta": { server: &Server{ FrameworkServer: fwserver.Server{ @@ -372,6 +468,73 @@ func TestServerApplyResourceChange(t *testing.T) { }), }, }, + "create-response-newidentity": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + ResourcesMethod: func(_ context.Context) []func() resource.Resource { + return []func() resource.Resource{ + func() resource.Resource { + return &testprovider.ResourceWithIdentity{ + Resource: &testprovider.Resource{ + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema + }, + MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "test_resource" + }, + CreateMethod: func(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + identityData := struct { + TestID types.String `tfsdk:"test_id"` + }{ + TestID: types.StringValue("new-id-123"), + } + resp.Diagnostics.Append(resp.Identity.Set(ctx, identityData)...) + + var data testSchemaData + + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) + }, + DeleteMethod: func(_ context.Context, _ resource.DeleteRequest, resp *resource.DeleteResponse) { + resp.Diagnostics.AddError("Unexpected Method Call", "Expected: Create, Got: Delete") + }, + UpdateMethod: func(_ context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { + resp.Diagnostics.AddError("Unexpected Method Call", "Expected: Create, Got: Update") + }, + }, + IdentitySchemaMethod: func(ctx context.Context, req resource.IdentitySchemaRequest, resp *resource.IdentitySchemaResponse) { + resp.IdentitySchema = testIdentitySchema + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov6.ApplyResourceChangeRequest{ + Config: testNewDynamicValue(t, testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + PlannedState: testNewDynamicValue(t, testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, "test-plannedstate-value"), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + PriorState: &testEmptyDynamicValue, + TypeName: "test_resource", + }, + expectedResponse: &tfprotov6.ApplyResourceChangeResponse{ + NewIdentity: &tfprotov6.ResourceIdentityData{ + IdentityData: testNewIdentityDynamicValue, + }, + NewState: testNewDynamicValue(t, testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, "test-plannedstate-value"), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + }, + }, "create-response-newstate-null": { server: &Server{ FrameworkServer: fwserver.Server{ diff --git a/internal/toproto6/applyresourcechange.go b/internal/toproto6/applyresourcechange.go index c3d158f0a..d47230d46 100644 --- a/internal/toproto6/applyresourcechange.go +++ b/internal/toproto6/applyresourcechange.go @@ -27,6 +27,11 @@ func ApplyResourceChangeResponse(ctx context.Context, fw *fwserver.ApplyResource proto6.Diagnostics = append(proto6.Diagnostics, Diagnostics(ctx, diags)...) proto6.NewState = newState + newIdentity, diags := ResourceIdentity(ctx, fw.NewIdentity) + + proto6.Diagnostics = append(proto6.Diagnostics, Diagnostics(ctx, diags)...) + proto6.NewIdentity = newIdentity + newPrivate, diags := fw.Private.Bytes(ctx) proto6.Diagnostics = append(proto6.Diagnostics, Diagnostics(ctx, diags)...) diff --git a/internal/toproto6/applyresourcechange_test.go b/internal/toproto6/applyresourcechange_test.go index 5084841d1..19a9f6144 100644 --- a/internal/toproto6/applyresourcechange_test.go +++ b/internal/toproto6/applyresourcechange_test.go @@ -15,6 +15,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" "github.com/hashicorp/terraform-plugin-framework/internal/toproto6" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/tfsdk" ) @@ -38,6 +39,22 @@ func TestApplyResourceChangeResponse(t *testing.T) { t.Fatalf("unexpected error calling tfprotov6.NewDynamicValue(): %s", err) } + testIdentityProto6Type := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_id": tftypes.String, + }, + } + + testIdentityProto6Value := tftypes.NewValue(testIdentityProto6Type, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "id-123"), + }) + + testIdentityProto6DynamicValue, err := tfprotov6.NewDynamicValue(testIdentityProto6Type, testIdentityProto6Value) + + if err != nil { + t.Fatalf("unexpected error calling tfprotov6.NewDynamicValue(): %s", err) + } + testState := &tfsdk.State{ Raw: testProto6Value, Schema: schema.Schema{ @@ -60,6 +77,28 @@ func TestApplyResourceChangeResponse(t *testing.T) { }, } + testIdentity := &tfsdk.ResourceIdentity{ + Raw: testIdentityProto6Value, + Schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_id": identityschema.StringAttribute{ + RequiredForImport: true, + }, + }, + }, + } + + testIdentityInvalid := &tfsdk.ResourceIdentity{ + Raw: testIdentityProto6Value, + Schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_id": identityschema.BoolAttribute{ + RequiredForImport: true, + }, + }, + }, + } + testProviderKeyValue := privatestate.MustMarshalToJson(map[string][]byte{ "providerKeyOne": []byte(`{"pKeyOne": {"k0": "zero", "k1": 1}}`), }) @@ -131,6 +170,37 @@ func TestApplyResourceChangeResponse(t *testing.T) { }, }, }, + "diagnostics-invalid-newidentity": { + input: &fwserver.ApplyResourceChangeResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewWarningDiagnostic("test warning summary", "test warning details"), + diag.NewErrorDiagnostic("test error summary", "test error details"), + }, + NewIdentity: testIdentityInvalid, + }, + expected: &tfprotov6.ApplyResourceChangeResponse{ + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityWarning, + Summary: "test warning summary", + Detail: "test warning details", + }, + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "test error summary", + Detail: "test error details", + }, + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Unable to Convert Resource Identity", + Detail: "An unexpected error was encountered when converting the resource identity to the protocol type. " + + "This is always an issue in terraform-plugin-framework used to implement the provider and should be reported to the provider developers.\n\n" + + "Please report this to the provider developer:\n\n" + + "Unable to create DynamicValue: AttributeName(\"test_id\"): unexpected value type string, tftypes.Bool values must be of type bool", + }, + }, + }, + }, "newstate": { input: &fwserver.ApplyResourceChangeResponse{ NewState: testState, @@ -139,6 +209,16 @@ func TestApplyResourceChangeResponse(t *testing.T) { NewState: &testProto6DynamicValue, }, }, + "newidentity": { + input: &fwserver.ApplyResourceChangeResponse{ + NewIdentity: testIdentity, + }, + expected: &tfprotov6.ApplyResourceChangeResponse{ + NewIdentity: &tfprotov6.ResourceIdentityData{ + IdentityData: &testIdentityProto6DynamicValue, + }, + }, + }, "private": { input: &fwserver.ApplyResourceChangeResponse{ Private: &privatestate.Data{ From df3e558e7df0c0cf0061e8088f0661fa41cc42e9 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Fri, 14 Mar 2025 08:42:36 -0400 Subject: [PATCH 20/20] update go dep --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 58ea938ab..fd586aba3 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ toolchain go1.22.7 require ( github.com/google/go-cmp v0.7.0 - github.com/hashicorp/terraform-plugin-go v0.26.1-0.20250310103402-e49288281ffd + github.com/hashicorp/terraform-plugin-go v0.26.1-0.20250314120210-d406d3409aef github.com/hashicorp/terraform-plugin-log v0.9.0 ) diff --git a/go.sum b/go.sum index bfdeff563..d841220e8 100644 --- a/go.sum +++ b/go.sum @@ -21,8 +21,8 @@ github.com/hashicorp/go-plugin v1.6.3 h1:xgHB+ZUSYeuJi96WtxEjzi23uh7YQpznjGh0U0U github.com/hashicorp/go-plugin v1.6.3/go.mod h1:MRobyh+Wc/nYy1V4KAXUiYfzxoYhs7V1mlH1Z7iY2h0= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/terraform-plugin-go v0.26.1-0.20250310103402-e49288281ffd h1:g5iwwMyN3PXQ+eyyJGL1rOLPDcD4f0gjUKDsbPabTNU= -github.com/hashicorp/terraform-plugin-go v0.26.1-0.20250310103402-e49288281ffd/go.mod h1:MfDwS/KnIy2QzCwdRtuqIjZ23gpYa9Vm+Z8cFpx8qtU= +github.com/hashicorp/terraform-plugin-go v0.26.1-0.20250314120210-d406d3409aef h1:RtNtj/RAsw/ef2bpeMlCDo+HsREcQVyJ+20AzikH3kw= +github.com/hashicorp/terraform-plugin-go v0.26.1-0.20250314120210-d406d3409aef/go.mod h1:MfDwS/KnIy2QzCwdRtuqIjZ23gpYa9Vm+Z8cFpx8qtU= github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0= github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow= github.com/hashicorp/terraform-registry-address v0.2.4 h1:JXu/zHB2Ymg/TGVCRu10XqNa4Sh2bWcqCNyKWjnCPJA=