From 41c79c47f7f5c2dc4d95c5efb555fc2b28074523 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20T=C3=B6lle?= Date: Wed, 15 Nov 2023 07:48:36 +0100 Subject: [PATCH] refactor: move data.hcloud_datacenter(s) to Plugin Framework (#779) Related to #752 Co-authored-by: Jonas Lammler --- go.mod | 1 + go.sum | 30 +- hcloud/plugin_provider.go | 7 +- hcloud/provider.go | 3 - hcloud/provider_test.go | 3 - internal/datacenter/data_source.go | 432 ++++++++++++------ .../e2etests/datacenter/data_source_test.go | 104 ++++- internal/e2etests/firewall/resource_test.go | 7 +- internal/e2etests/testing.go | 1 + internal/hcclient/provider.go | 28 ++ internal/testdata/r/null_resource.tf.tmpl | 9 + 11 files changed, 456 insertions(+), 169 deletions(-) create mode 100644 internal/hcclient/provider.go create mode 100644 internal/testdata/r/null_resource.tf.tmpl diff --git a/go.mod b/go.mod index 26fc7fdba..4c9acb58b 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/hashicorp/go-hclog v1.5.0 github.com/hashicorp/go-multierror v1.1.1 github.com/hashicorp/terraform-plugin-framework v1.4.2 + github.com/hashicorp/terraform-plugin-framework-validators v0.12.0 github.com/hashicorp/terraform-plugin-go v0.19.0 github.com/hashicorp/terraform-plugin-log v0.9.0 github.com/hashicorp/terraform-plugin-mux v0.12.0 diff --git a/go.sum b/go.sum index eb757055f..f7e8b7552 100644 --- a/go.sum +++ b/go.sum @@ -46,8 +46,6 @@ github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -81,12 +79,10 @@ github.com/hashicorp/terraform-exec v0.19.0 h1:FpqZ6n50Tk95mItTSS9BjeOVUb4eg81Sp github.com/hashicorp/terraform-exec v0.19.0/go.mod h1:tbxUpe3JKruE9Cuf65mycSIT8KiNPZ0FkuTE3H4urQg= github.com/hashicorp/terraform-json v0.17.1 h1:eMfvh/uWggKmY7Pmb3T85u86E2EQg6EQHgyRwf3RkyA= github.com/hashicorp/terraform-json v0.17.1/go.mod h1:Huy6zt6euxaY9knPAFKjUITn8QxUFIe9VuSzb4zn/0o= -github.com/hashicorp/terraform-plugin-framework v1.4.0 h1:WKbtCRtNrjsh10eA7NZvC/Qyr7zp77j+D21aDO5th9c= -github.com/hashicorp/terraform-plugin-framework v1.4.0/go.mod h1:XC0hPcQbBvlbxwmjxuV/8sn8SbZRg4XwGMs22f+kqV0= -github.com/hashicorp/terraform-plugin-framework v1.4.1 h1:ZC29MoB3Nbov6axHdgPbMz7799pT5H8kIrM8YAsaVrs= -github.com/hashicorp/terraform-plugin-framework v1.4.1/go.mod h1:XC0hPcQbBvlbxwmjxuV/8sn8SbZRg4XwGMs22f+kqV0= github.com/hashicorp/terraform-plugin-framework v1.4.2 h1:P7a7VP1GZbjc4rv921Xy5OckzhoiO3ig6SGxwelD2sI= github.com/hashicorp/terraform-plugin-framework v1.4.2/go.mod h1:GWl3InPFZi2wVQmdVnINPKys09s9mLmTZr95/ngLnbY= +github.com/hashicorp/terraform-plugin-framework-validators v0.12.0 h1:HOjBuMbOEzl7snOdOoUfE2Jgeto6JOjLVQ39Ls2nksc= +github.com/hashicorp/terraform-plugin-framework-validators v0.12.0/go.mod h1:jfHGE/gzjxYz6XoUwi/aYiiKrJDeutQNUtGQXkaHklg= github.com/hashicorp/terraform-plugin-go v0.19.0 h1:BuZx/6Cp+lkmiG0cOBk6Zps0Cb2tmqQpDM3iAtnhDQU= github.com/hashicorp/terraform-plugin-go v0.19.0/go.mod h1:EhRSkEPNoylLQntYsk5KrDHTZJh9HQoumZXbOGOXmec= github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0= @@ -103,10 +99,6 @@ github.com/hashicorp/terraform-svchost v0.1.1 h1:EZZimZ1GxdqFRinZ1tpJwVxxt49xc/S github.com/hashicorp/terraform-svchost v0.1.1/go.mod h1:mNsjQfZyf/Jhz35v6/0LWcv26+X7JPS+buii2c9/ctc= github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE= github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= -github.com/hetznercloud/hcloud-go v1.49.0 h1:b/dKaptbbahWrkzYilWi42UgyfpiQ0Qh9ZFvyAgAVoo= -github.com/hetznercloud/hcloud-go v1.49.0/go.mod h1:VzDWThl47lOnZXY0q5/LPFD+M62pfe/52TV+mOrpp9Q= -github.com/hetznercloud/hcloud-go v1.51.0 h1:Gsjh+GeSH1ZZwOhVBLDxqRFEJSctDu6Jva9YDnNYlk4= -github.com/hetznercloud/hcloud-go v1.51.0/go.mod h1:VzDWThl47lOnZXY0q5/LPFD+M62pfe/52TV+mOrpp9Q= github.com/hetznercloud/hcloud-go v1.52.0 h1:3r9pEulTOBB9BoArSgpQYUQVTy+Xjkg0k/QAU4c6dQ8= github.com/hetznercloud/hcloud-go v1.52.0/go.mod h1:VzDWThl47lOnZXY0q5/LPFD+M62pfe/52TV+mOrpp9Q= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= @@ -189,10 +181,6 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= -golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck= -golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= -golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= -golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA= golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g= golang.org/x/exp v0.0.0-20230809150735-7b3493d9a819 h1:EDuYyU/MkFXllv9QF9819VlI9a4tzGuCbhG0ExK9o1U= @@ -209,10 +197,6 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= -golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8= -golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= -golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= -golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg= golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -235,10 +219,6 @@ golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= -golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -246,8 +226,8 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuX golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= -golang.org/x/term v0.12.0 h1:/ZfYdc3zq+q02Rv9vGqTeSItdzZTSNDmfTi0mBAuidU= -golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/term v0.14.0 h1:LGK9IlZ8T9jvdy6cTdfKUCltatMFOehAQo9SRC46UQ8= +golang.org/x/term v0.14.0/go.mod h1:TySc+nGkYR6qt8km8wUhuFRTVSMIX3XPR58y2lC8vww= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -255,8 +235,6 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/hcloud/plugin_provider.go b/hcloud/plugin_provider.go index 82e6e6cd0..443a1223d 100644 --- a/hcloud/plugin_provider.go +++ b/hcloud/plugin_provider.go @@ -14,7 +14,9 @@ import ( "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/hetznercloud/hcloud-go/hcloud" + "github.com/hetznercloud/terraform-provider-hcloud/internal/datacenter" "github.com/hetznercloud/terraform-provider-hcloud/internal/util/tflogutil" ) @@ -149,7 +151,10 @@ func (p *PluginProvider) Configure(ctx context.Context, req provider.ConfigureRe // The data source type name is determined by the DataSource implementing // the Metadata method. All data sources must have unique names. func (p *PluginProvider) DataSources(_ context.Context) []func() datasource.DataSource { - return []func() datasource.DataSource{} + return []func() datasource.DataSource{ + datacenter.NewDataSource, + datacenter.NewDataSourceList, + } } // Resources returns a slice of functions to instantiate each Resource diff --git a/hcloud/provider.go b/hcloud/provider.go index d2d7dd7f9..e84a4e4cf 100644 --- a/hcloud/provider.go +++ b/hcloud/provider.go @@ -20,7 +20,6 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hetznercloud/hcloud-go/hcloud" "github.com/hetznercloud/terraform-provider-hcloud/internal/certificate" - "github.com/hetznercloud/terraform-provider-hcloud/internal/datacenter" "github.com/hetznercloud/terraform-provider-hcloud/internal/floatingip" "github.com/hetznercloud/terraform-provider-hcloud/internal/image" "github.com/hetznercloud/terraform-provider-hcloud/internal/loadbalancer" @@ -98,8 +97,6 @@ func Provider() *schema.Provider { DataSourcesMap: map[string]*schema.Resource{ certificate.DataSourceType: certificate.DataSource(), certificate.DataSourceListType: certificate.DataSourceList(), - datacenter.DataSourceListType: datacenter.DataSourceList(), - datacenter.DataSourceType: datacenter.DataSource(), firewall.DataSourceType: firewall.DataSource(), firewall.DataSourceListType: firewall.DataSourceList(), floatingip.DataSourceType: floatingip.DataSource(), diff --git a/hcloud/provider_test.go b/hcloud/provider_test.go index b2966d3f0..c3308fd43 100644 --- a/hcloud/provider_test.go +++ b/hcloud/provider_test.go @@ -4,7 +4,6 @@ import ( "testing" "github.com/hetznercloud/terraform-provider-hcloud/internal/certificate" - "github.com/hetznercloud/terraform-provider-hcloud/internal/datacenter" "github.com/hetznercloud/terraform-provider-hcloud/internal/firewall" "github.com/hetznercloud/terraform-provider-hcloud/internal/floatingip" "github.com/hetznercloud/terraform-provider-hcloud/internal/image" @@ -69,8 +68,6 @@ func TestProvider_DataSources(t *testing.T) { expectedDataSources := []string{ certificate.DataSourceType, certificate.DataSourceListType, - datacenter.DataSourceType, - datacenter.DataSourceListType, firewall.DataSourceType, firewall.DataSourceListType, floatingip.DataSourceType, diff --git a/internal/datacenter/data_source.go b/internal/datacenter/data_source.go index 42017cef3..85edc4ca5 100644 --- a/internal/datacenter/data_source.go +++ b/internal/datacenter/data_source.go @@ -8,9 +8,14 @@ import ( "strconv" "strings" - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-framework-validators/datasourcevalidator" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hetznercloud/hcloud-go/hcloud" "github.com/hetznercloud/terraform-provider-hcloud/internal/hcclient" ) @@ -23,176 +28,343 @@ const ( DataSourceListType = "hcloud_datacenters" ) -// getCommonDataSchema returns a new common schema used by all datacenter data sources. -func getCommonDataSchema() map[string]*schema.Schema { - return map[string]*schema.Schema{ - "id": { - Type: schema.TypeInt, +type datacenterResourceData struct { + ID types.Int64 `tfsdk:"id"` + Name types.String `tfsdk:"name"` + Description types.String `tfsdk:"description"` + Location types.Map `tfsdk:"location"` + SupportedServerTypeIds types.List `tfsdk:"supported_server_type_ids"` + AvailableServerTypeIds types.List `tfsdk:"available_server_type_ids"` +} + +var datacenterResourceDataAttrTypes = map[string]attr.Type{ + "id": types.Int64Type, + "name": types.StringType, + "description": types.StringType, + "location": types.MapType{ElemType: types.StringType}, + "supported_server_type_ids": types.ListType{ElemType: types.Int64Type}, + "available_server_type_ids": types.ListType{ElemType: types.Int64Type}, +} + +func newDatacenterResourceData(ctx context.Context, in *hcloud.Datacenter) (datacenterResourceData, diag.Diagnostics) { + var data datacenterResourceData + var diags diag.Diagnostics + var newDiags diag.Diagnostics + + data.ID = types.Int64Value(int64(in.ID)) + data.Name = types.StringValue(in.Name) + data.Description = types.StringValue(in.Description) + + data.Location, newDiags = types.MapValue(types.StringType, map[string]attr.Value{ + "id": types.StringValue(strconv.Itoa(in.Location.ID)), + "name": types.StringValue(in.Location.Name), + "description": types.StringValue(in.Location.Description), + "country": types.StringValue(in.Location.Country), + "city": types.StringValue(in.Location.City), + "latitude": types.StringValue(fmt.Sprintf("%f", in.Location.Latitude)), + "longitude": types.StringValue(fmt.Sprintf("%f", in.Location.Longitude)), + }) + diags.Append(newDiags...) + + supportedServerTypeIds := make([]int64, len(in.ServerTypes.Supported)) + for i, v := range in.ServerTypes.Supported { + supportedServerTypeIds[i] = int64(v.ID) + } + availableServerTypeIds := make([]int64, len(in.ServerTypes.Available)) + for i, v := range in.ServerTypes.Available { + availableServerTypeIds[i] = int64(v.ID) + } + sort.Slice(supportedServerTypeIds, func(i, j int) bool { return supportedServerTypeIds[i] < supportedServerTypeIds[j] }) + sort.Slice(availableServerTypeIds, func(i, j int) bool { return availableServerTypeIds[i] < availableServerTypeIds[j] }) + + data.SupportedServerTypeIds, newDiags = types.ListValueFrom(ctx, types.Int64Type, supportedServerTypeIds) + diags.Append(newDiags...) + data.AvailableServerTypeIds, newDiags = types.ListValueFrom(ctx, types.Int64Type, availableServerTypeIds) + diags.Append(newDiags...) + + return data, diags +} + +func getCommonDataSchema() map[string]schema.Attribute { + return map[string]schema.Attribute{ + "id": schema.Int64Attribute{ Optional: true, Computed: true, }, - "name": { - Type: schema.TypeString, + "name": schema.StringAttribute{ Optional: true, Computed: true, }, - "description": { - Type: schema.TypeString, + "description": schema.StringAttribute{ Computed: true, }, - "location": { - Type: schema.TypeMap, - Computed: true, + // TODO: Refactor to SingleNestedAttribute in v2 + "location": schema.MapAttribute{ + Computed: true, + ElementType: types.StringType, }, - "supported_server_type_ids": { - Type: schema.TypeList, - Computed: true, - Elem: &schema.Schema{Type: schema.TypeInt}, + "supported_server_type_ids": schema.ListAttribute{ + Computed: true, + ElementType: types.Int64Type, }, - "available_server_type_ids": { - Type: schema.TypeList, - Computed: true, - Elem: &schema.Schema{Type: schema.TypeInt}, + "available_server_type_ids": schema.ListAttribute{ + Computed: true, + ElementType: types.Int64Type, }, } } -// DataSource creates a new Terraform schema for the Hetzner Cloud Datacenter -// data source. -func DataSource() *schema.Resource { - return &schema.Resource{ - ReadContext: dataSourceHcloudDatacenterRead, - Schema: getCommonDataSchema(), +// Single +var _ datasource.DataSource = (*datacenterDataSource)(nil) +var _ datasource.DataSourceWithConfigure = (*datacenterDataSource)(nil) +var _ datasource.DataSourceWithConfigValidators = (*datacenterDataSource)(nil) + +type datacenterDataSource struct { + client *hcloud.Client +} + +func NewDataSource() datasource.DataSource { + return &datacenterDataSource{} +} + +// Metadata should return the full name of the data source. +func (d *datacenterDataSource) Metadata(_ context.Context, _ datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = DataSourceType +} + +// Configure enables provider-level data or clients to be set in the +// provider-defined DataSource type. It is separately executed for each +// ReadDataSource RPC. +func (d *datacenterDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + var newDiags diag.Diagnostics + + d.client, newDiags = hcclient.ConfigureClient(req.ProviderData) + resp.Diagnostics.Append(newDiags...) + if resp.Diagnostics.HasError() { + return } } -// DataSourceList creates a new Terraform schema for the Hetzner Cloud Datacenters data source. -func DataSourceList() *schema.Resource { - return &schema.Resource{ - ReadContext: dataSourceHcloudDatacenterListRead, - Schema: map[string]*schema.Schema{ - "datacenter_ids": { - Type: schema.TypeList, - Optional: true, - Deprecated: "Use datacenters list instead", - Elem: &schema.Schema{Type: schema.TypeString}, - }, - "names": { - Type: schema.TypeList, - Computed: true, - Deprecated: "Use datacenters list instead", - Elem: &schema.Schema{Type: schema.TypeString}, - }, - "descriptions": { - Type: schema.TypeList, - Computed: true, - Deprecated: "Use datacenters list instead", - Elem: &schema.Schema{Type: schema.TypeString}, - }, - "datacenters": { - Type: schema.TypeList, - Computed: true, - Elem: &schema.Resource{ - Schema: getCommonDataSchema(), - }, - }, - }, +// Schema should return the schema for this data source. +func (d *datacenterDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema.Attributes = getCommonDataSchema() + + resp.Schema.MarkdownDescription = ` +Provides details about a specific Hetzner Cloud Datacenter. +Use this resource to get detailed information about specific datacenter. + +## Example Usage +` + "```" + `hcl +data "hcloud_datacenter" "ds_1" { + name = "fsn1-dc14" +} +data "hcloud_datacenter" "ds_2" { + id = 4 +} +` + "```" +} + +// ConfigValidators returns a list of ConfigValidators. Each ConfigValidator's Validate method will be called when validating the data source. +func (d *datacenterDataSource) ConfigValidators(_ context.Context) []datasource.ConfigValidator { + return []datasource.ConfigValidator{ + datasourcevalidator.ExactlyOneOf( + path.MatchRoot("id"), + path.MatchRoot("name"), + ), } } -func dataSourceHcloudDatacenterRead(ctx context.Context, data *schema.ResourceData, m interface{}) diag.Diagnostics { - client := m.(*hcloud.Client) +// Read is called when the provider must read data source values in +// order to update state. Config values should be read from the +// ReadRequest and new state values set on the ReadResponse. +func (d *datacenterDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var data datacenterResourceData - if id, ok := data.GetOk("id"); ok { - d, _, err := client.Datacenter.GetByID(ctx, id.(int)) + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + var result *hcloud.Datacenter + var err error + + switch { + case !data.ID.IsNull(): + result, _, err = d.client.Datacenter.GetByID(ctx, int(data.ID.ValueInt64())) if err != nil { - return hcclient.ErrorToDiag(err) + resp.Diagnostics.Append(hcclient.APIErrorDiagnostics(err)...) + return } - if d == nil { - return diag.Errorf("no datacenter found with id %d", id) + if result == nil { + resp.Diagnostics.AddError( + "Resource not found", + fmt.Sprintf("No datacenter found with id %s.", data.ID.String()), + ) + return } - setDatacenterSchema(data, d) - return nil - } - if name, ok := data.GetOk("name"); ok { - d, _, err := client.Datacenter.GetByName(ctx, name.(string)) + case !data.Name.IsNull(): + result, _, err = d.client.Datacenter.GetByName(ctx, data.Name.ValueString()) if err != nil { - return hcclient.ErrorToDiag(err) + resp.Diagnostics.Append(hcclient.APIErrorDiagnostics(err)...) } - if d == nil { - return diag.Errorf("no datacenter found with name %v", name) + if result == nil { + resp.Diagnostics.AddError( + "Resource not found", + fmt.Sprintf("No datacenter found with name %s.", data.Name.String()), + ) + return } - setDatacenterSchema(data, d) - return nil + default: + // Should not happen, see [datacenterDataSource.ConfigValidators] + resp.Diagnostics.AddError("Unexpected internal error", "") + return } - return diag.Errorf("please specify an id, or a name to lookup for a datacenter") + data, diags := newDatacenterResourceData(ctx, result) + resp.Diagnostics.Append(diags...) + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } -func setDatacenterSchema(d *schema.ResourceData, dc *hcloud.Datacenter) { - for key, val := range getDatacenterAttributes(dc) { - if key == "id" { - d.SetId(strconv.Itoa(val.(int))) - } else { - d.Set(key, val) - } - } +// List +var _ datasource.DataSource = (*datacenterDataSourceList)(nil) +var _ datasource.DataSourceWithConfigure = (*datacenterDataSourceList)(nil) + +type datacenterDataSourceList struct { + client *hcloud.Client +} + +func NewDataSourceList() datasource.DataSource { + return &datacenterDataSourceList{} } -func getDatacenterAttributes(dc *hcloud.Datacenter) map[string]interface{} { - supported := make([]int, len(dc.ServerTypes.Supported)) +// Metadata should return the full name of the data source. +func (d *datacenterDataSourceList) Metadata(_ context.Context, _ datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = DataSourceListType +} - for i, v := range dc.ServerTypes.Supported { - supported[i] = v.ID - } - available := make([]int, len(dc.ServerTypes.Available)) - for i, v := range dc.ServerTypes.Available { - available[i] = v.ID +// Configure enables provider-level data or clients to be set in the +// provider-defined DataSource type. It is separately executed for each +// ReadDataSource RPC. +func (d *datacenterDataSourceList) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + var newDiags diag.Diagnostics + + d.client, newDiags = hcclient.ConfigureClient(req.ProviderData) + resp.Diagnostics.Append(newDiags...) + if resp.Diagnostics.HasError() { + return } - sort.Ints(available) - sort.Ints(supported) - - return map[string]interface{}{ - "id": dc.ID, - "name": dc.Name, - "description": dc.Description, - "location": map[string]string{ - "id": strconv.Itoa(dc.Location.ID), - "name": dc.Location.Name, - "description": dc.Location.Description, - "country": dc.Location.Country, - "city": dc.Location.City, - "latitude": fmt.Sprintf("%f", dc.Location.Latitude), - "longitude": fmt.Sprintf("%f", dc.Location.Longitude), +} + +// Schema should return the schema for this data source. +func (d *datacenterDataSourceList) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema.Attributes = map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Optional: true, + }, + "datacenter_ids": schema.ListAttribute{ + Optional: true, + DeprecationMessage: "Use datacenters list instead", + ElementType: types.StringType, + }, + "names": schema.ListAttribute{ + Optional: true, + DeprecationMessage: "Use datacenters list instead", + ElementType: types.StringType, + }, + "descriptions": schema.ListAttribute{ + Optional: true, + DeprecationMessage: "Use datacenters list instead", + ElementType: types.StringType, }, - "supported_server_type_ids": supported, - "available_server_type_ids": available, + "datacenters": schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: getCommonDataSchema(), + }, + Computed: true, + }, + } + + resp.Schema.MarkdownDescription = ` +Provides details about a specific Hetzner Cloud Datacenter. +Use this resource to get detailed information about specific datacenter. + +## Example Usage +` + "```" + `hcl +data "hcloud_datacenter" "ds_1" { + name = "fsn1-dc8" +} +data "hcloud_datacenter" "ds_2" { + id = 4 +} +` + "```" +} + +type datacenterListResourceData struct { + ID types.String `tfsdk:"id"` + DatacenterIDs types.List `tfsdk:"datacenter_ids"` + Names types.List `tfsdk:"names"` + Descriptions types.List `tfsdk:"descriptions"` + Datacenters types.List `tfsdk:"datacenters"` +} + +func newDatacenterListResourceData(ctx context.Context, in []*hcloud.Datacenter) (datacenterListResourceData, diag.Diagnostics) { + var data datacenterListResourceData + var diags diag.Diagnostics + var newDiags diag.Diagnostics + + datacenterIDs := make([]string, len(in)) + names := make([]string, len(in)) + descriptions := make([]string, len(in)) + datacenters := make([]datacenterResourceData, len(in)) + + for i, item := range in { + datacenterIDs[i] = strconv.Itoa(item.ID) + names[i] = item.Name + descriptions[i] = item.Description + + datacenter, newDiags := newDatacenterResourceData(ctx, item) + diags.Append(newDiags...) + datacenters[i] = datacenter } + + data.ID = types.StringValue(fmt.Sprintf("%x", sha1.Sum([]byte(strings.Join(datacenterIDs, ""))))) + + data.DatacenterIDs, newDiags = types.ListValueFrom(ctx, types.StringType, datacenterIDs) + diags.Append(newDiags...) + data.Names, newDiags = types.ListValueFrom(ctx, types.StringType, names) + diags.Append(newDiags...) + data.Descriptions, newDiags = types.ListValueFrom(ctx, types.StringType, descriptions) + diags.Append(newDiags...) + + data.Datacenters, newDiags = types.ListValueFrom(ctx, types.ObjectType{AttrTypes: datacenterResourceDataAttrTypes}, datacenters) + diags.Append(newDiags...) + + return data, diags } -func dataSourceHcloudDatacenterListRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - client := m.(*hcloud.Client) - dcs, err := client.Datacenter.All(ctx) - if err != nil { - return hcclient.ErrorToDiag(err) +// Read is called when the provider must read data source values in +// order to update state. Config values should be read from the +// ReadRequest and new state values set on the ReadResponse. +func (d *datacenterDataSourceList) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var data datacenterListResourceData + + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return } - names := make([]string, len(dcs)) - descriptions := make([]string, len(dcs)) - ids := make([]string, len(dcs)) - tfDatacenters := make([]map[string]interface{}, len(dcs)) - for i, datacenter := range dcs { - ids[i] = strconv.Itoa(datacenter.ID) - descriptions[i] = datacenter.Description - names[i] = datacenter.Name + var result []*hcloud.Datacenter + var err error - tfDatacenters[i] = getDatacenterAttributes(datacenter) + result, err = d.client.Datacenter.All(ctx) + if err != nil { + resp.Diagnostics.Append(hcclient.APIErrorDiagnostics(err)...) + return } - d.SetId(fmt.Sprintf("%x", sha1.Sum([]byte(strings.Join(ids, ""))))) - d.Set("datacenter_ids", ids) - d.Set("names", names) - d.Set("descriptions", descriptions) - d.Set("datacenters", tfDatacenters) - return nil + data, diags := newDatacenterListResourceData(ctx, result) + resp.Diagnostics.Append(diags...) + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } diff --git a/internal/e2etests/datacenter/data_source_test.go b/internal/e2etests/datacenter/data_source_test.go index af0aadaf4..81ffcfbc1 100644 --- a/internal/e2etests/datacenter/data_source_test.go +++ b/internal/e2etests/datacenter/data_source_test.go @@ -3,10 +3,10 @@ package datacenter_test import ( "testing" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hetznercloud/terraform-provider-hcloud/internal/datacenter" "github.com/hetznercloud/terraform-provider-hcloud/internal/e2etests" - - "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hetznercloud/terraform-provider-hcloud/internal/testtemplate" ) @@ -43,6 +43,61 @@ func TestAccHcloudDataSourceDatacenterTest(t *testing.T) { }) } +func TestAccHcloudDataSourceDatacenter_UpgradePluginFramework(t *testing.T) { + tmplMan := testtemplate.Manager{} + + dcByName := &datacenter.DData{ + DatacenterName: "fsn1-dc14", + } + dcByName.SetRName("dc_by_name") + dcByID := &datacenter.DData{ + DatacenterID: "4", + } + dcByID.SetRName("dc_by_id") + resource.ParallelTest(t, resource.TestCase{ + PreCheck: e2etests.PreCheck(t), + Steps: []resource.TestStep{ + { + ExternalProviders: map[string]resource.ExternalProvider{ + "hcloud": { + VersionConstraint: "1.42.1", + Source: "hetznercloud/hcloud", + }, + "null": { + VersionConstraint: "3.2.1", + Source: "hashicorp/null", + }, + }, + + Config: tmplMan.Render(t, + "testdata/d/hcloud_datacenter", dcByName, + "testdata/d/hcloud_datacenter", dcByID, + "testdata/r/null_resource", dcByName, + "testdata/r/null_resource", dcByID, + ), + }, + { + ExternalProviders: map[string]resource.ExternalProvider{ + "null": { + VersionConstraint: "3.2.1", + Source: "hashicorp/null", + }, + }, + ProtoV6ProviderFactories: e2etests.ProtoV6ProviderFactories(), + + Config: tmplMan.Render(t, + "testdata/d/hcloud_datacenter", dcByName, + "testdata/d/hcloud_datacenter", dcByID, + "testdata/r/null_resource", dcByName, + "testdata/r/null_resource", dcByID, + ), + + PlanOnly: true, + }, + }, + }) +} + func TestAccHcloudDataSourceDatacentersTest(t *testing.T) { tmplMan := testtemplate.Manager{} @@ -79,3 +134,48 @@ func TestAccHcloudDataSourceDatacentersTest(t *testing.T) { }, }) } + +func TestAccHcloudDataSourceDatacenters_UpgradePluginFramework(t *testing.T) { + tmplMan := testtemplate.Manager{} + + datacentersD := &datacenter.DDataList{} + datacentersD.SetRName("ds") + resource.ParallelTest(t, resource.TestCase{ + PreCheck: e2etests.PreCheck(t), + Steps: []resource.TestStep{ + { + ExternalProviders: map[string]resource.ExternalProvider{ + "hcloud": { + VersionConstraint: "1.44.1", + Source: "hetznercloud/hcloud", + }, + "null": { + VersionConstraint: "3.2.1", + Source: "hashicorp/null", + }, + }, + + Config: tmplMan.Render(t, + "testdata/d/hcloud_datacenters", datacentersD, + "testdata/r/null_resource", datacentersD, + ), + }, + { + ExternalProviders: map[string]resource.ExternalProvider{ + "null": { + VersionConstraint: "3.2.1", + Source: "hashicorp/null", + }, + }, + ProtoV6ProviderFactories: e2etests.ProtoV6ProviderFactories(), + + Config: tmplMan.Render(t, + "testdata/d/hcloud_datacenters", datacentersD, + "testdata/r/null_resource", datacentersD, + ), + + PlanOnly: true, + }, + }, + }) +} diff --git a/internal/e2etests/firewall/resource_test.go b/internal/e2etests/firewall/resource_test.go index 8aa0c0dd4..7565241b9 100644 --- a/internal/e2etests/firewall/resource_test.go +++ b/internal/e2etests/firewall/resource_test.go @@ -4,13 +4,12 @@ import ( "fmt" "testing" - "github.com/hetznercloud/terraform-provider-hcloud/internal/e2etests" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/stretchr/testify/assert" - "github.com/hetznercloud/terraform-provider-hcloud/internal/firewall" - - "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hetznercloud/hcloud-go/hcloud" + "github.com/hetznercloud/terraform-provider-hcloud/internal/e2etests" + "github.com/hetznercloud/terraform-provider-hcloud/internal/firewall" "github.com/hetznercloud/terraform-provider-hcloud/internal/testsupport" "github.com/hetznercloud/terraform-provider-hcloud/internal/testtemplate" ) diff --git a/internal/e2etests/testing.go b/internal/e2etests/testing.go index 1e499ff9b..368e7fc54 100644 --- a/internal/e2etests/testing.go +++ b/internal/e2etests/testing.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hetznercloud/hcloud-go/hcloud" tfhcloud "github.com/hetznercloud/terraform-provider-hcloud/hcloud" ) diff --git a/internal/hcclient/provider.go b/internal/hcclient/provider.go new file mode 100644 index 000000000..8241cdb4e --- /dev/null +++ b/internal/hcclient/provider.go @@ -0,0 +1,28 @@ +package hcclient + +import ( + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/diag" + + "github.com/hetznercloud/hcloud-go/hcloud" +) + +func ConfigureClient(providerData any) (*hcloud.Client, diag.Diagnostics) { + var diagnostics diag.Diagnostics + + if providerData == nil { + return nil, diagnostics + } + + client, ok := providerData.(*hcloud.Client) + if !ok { + diagnostics.AddError( + "Unexpected Configure Type", + fmt.Sprintf("Expected *hcloud.Client, got: %T. Please report this issue to the provider developers.", providerData), + ) + return nil, diagnostics + } + + return client, diagnostics +} diff --git a/internal/testdata/r/null_resource.tf.tmpl b/internal/testdata/r/null_resource.tf.tmpl new file mode 100644 index 000000000..d302bce87 --- /dev/null +++ b/internal/testdata/r/null_resource.tf.tmpl @@ -0,0 +1,9 @@ +// This template can be used to work around a bug/limitation in terraform-test-framework, which only checks for plan +// changes in resources, not in datasources or outputs. +// You can pass any [testtemplate.DataCommon] to it. + +resource "null_resource" "{{ .RName }}" { + triggers = { + resource = jsonencode({{ .TFID }}) + } +}