diff --git a/hcloud/plugin_provider.go b/hcloud/plugin_provider.go index a1858841f..29b3c22bb 100644 --- a/hcloud/plugin_provider.go +++ b/hcloud/plugin_provider.go @@ -20,6 +20,7 @@ import ( "github.com/hetznercloud/hcloud-go/hcloud" "github.com/hetznercloud/terraform-provider-hcloud/internal/datacenter" "github.com/hetznercloud/terraform-provider-hcloud/internal/location" + "github.com/hetznercloud/terraform-provider-hcloud/internal/sshkey" "github.com/hetznercloud/terraform-provider-hcloud/internal/util/tflogutil" ) @@ -171,6 +172,8 @@ func (p *PluginProvider) DataSources(_ context.Context) []func() datasource.Data datacenter.NewDataSourceList, location.NewDataSource, location.NewDataSourceList, + sshkey.NewDataSource, + sshkey.NewDataSourceList, } } @@ -180,5 +183,7 @@ func (p *PluginProvider) DataSources(_ context.Context) []func() datasource.Data // The resource type name is determined by the Resource implementing // the Metadata method. All resources must have unique names. func (p *PluginProvider) Resources(_ context.Context) []func() resource.Resource { - return []func() resource.Resource{} + return []func() resource.Resource{ + sshkey.NewResource, + } } diff --git a/hcloud/provider.go b/hcloud/provider.go index 0392b471e..e31cbca05 100644 --- a/hcloud/provider.go +++ b/hcloud/provider.go @@ -28,7 +28,6 @@ import ( "github.com/hetznercloud/terraform-provider-hcloud/internal/rdns" "github.com/hetznercloud/terraform-provider-hcloud/internal/server" "github.com/hetznercloud/terraform-provider-hcloud/internal/servertype" - "github.com/hetznercloud/terraform-provider-hcloud/internal/sshkey" "github.com/hetznercloud/terraform-provider-hcloud/internal/volume" ) @@ -96,7 +95,6 @@ func Provider() *schema.Provider { server.NetworkResourceType: server.NetworkResource(), server.ResourceType: server.Resource(), snapshot.ResourceType: snapshot.Resource(), - sshkey.ResourceType: sshkey.Resource(), volume.AttachmentResourceType: volume.AttachmentResource(), volume.ResourceType: volume.Resource(), placementgroup.ResourceType: placementgroup.Resource(), @@ -122,8 +120,6 @@ func Provider() *schema.Provider { server.DataSourceListType: server.DataSourceList(), servertype.DataSourceType: servertype.DataSource(), servertype.DataSourceListType: servertype.ServerTypesDataSource(), - sshkey.DataSourceType: sshkey.DataSource(), - sshkey.DataSourceListType: sshkey.DataSourceList(), volume.DataSourceType: volume.DataSource(), volume.DataSourceListType: volume.DataSourceList(), }, diff --git a/hcloud/provider_test.go b/hcloud/provider_test.go index a42a70683..11447900d 100644 --- a/hcloud/provider_test.go +++ b/hcloud/provider_test.go @@ -15,7 +15,6 @@ import ( "github.com/hetznercloud/terraform-provider-hcloud/internal/server" "github.com/hetznercloud/terraform-provider-hcloud/internal/servertype" "github.com/hetznercloud/terraform-provider-hcloud/internal/snapshot" - "github.com/hetznercloud/terraform-provider-hcloud/internal/sshkey" "github.com/hetznercloud/terraform-provider-hcloud/internal/volume" "github.com/stretchr/testify/assert" ) @@ -48,7 +47,6 @@ func TestProvider_Resources(t *testing.T) { server.NetworkResourceType, server.ResourceType, snapshot.ResourceType, - sshkey.ResourceType, volume.AttachmentResourceType, volume.ResourceType, placementgroup.ResourceType, @@ -85,8 +83,6 @@ func TestProvider_DataSources(t *testing.T) { server.DataSourceListType, servertype.DataSourceType, servertype.DataSourceListType, - sshkey.DataSourceType, - sshkey.DataSourceListType, volume.DataSourceType, volume.DataSourceListType, } diff --git a/internal/sshkey/common.go b/internal/sshkey/common.go new file mode 100644 index 000000000..58ac34268 --- /dev/null +++ b/internal/sshkey/common.go @@ -0,0 +1,34 @@ +package sshkey + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/types" + + "github.com/hetznercloud/hcloud-go/hcloud" + "github.com/hetznercloud/terraform-provider-hcloud/internal/util/resourceutil" +) + +type resourceData struct { + ID types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + Fingerprint types.String `tfsdk:"fingerprint"` + PublicKey types.String `tfsdk:"public_key"` + Labels types.Map `tfsdk:"labels"` +} + +func populateResourceData(ctx context.Context, data *resourceData, in *hcloud.SSHKey) diag.Diagnostics { + var diags diag.Diagnostics + var newDiags diag.Diagnostics + + data.ID = resourceutil.IDStringValue(in.ID) + data.Name = types.StringValue(in.Name) + data.Fingerprint = types.StringValue(in.Fingerprint) + data.PublicKey = types.StringValue(in.PublicKey) + + data.Labels, newDiags = resourceutil.LabelsMapValueFrom(ctx, in.Labels) + diags.Append(newDiags...) + + return diags +} diff --git a/internal/sshkey/data_source.go b/internal/sshkey/data_source.go index 78ab1b2a1..34b63b379 100644 --- a/internal/sshkey/data_source.go +++ b/internal/sshkey/data_source.go @@ -2,192 +2,225 @@ package sshkey import ( "context" + _ "embed" "fmt" + "maps" - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-framework-validators/datasourcevalidator" + "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/util/datasourceutil" "github.com/hetznercloud/terraform-provider-hcloud/internal/util/hcloudutil" ) -const ( - // DataSourceType is the type name of the Hetzner Cloud SSH Key data source. - DataSourceType = "hcloud_ssh_key" +// DataSourceType is the type name of the Hetzner Cloud SSH Key data source. +const DataSourceType = "hcloud_ssh_key" - // DataSourceListType is the type name of the Hetzner Cloud SSH Keys data source. - DataSourceListType = "hcloud_ssh_keys" -) +type resourceDataWithSelector struct { + ID types.Int64 `tfsdk:"id"` + Name types.String `tfsdk:"name"` + Fingerprint types.String `tfsdk:"fingerprint"` + PublicKey types.String `tfsdk:"public_key"` + Labels types.Map `tfsdk:"labels"` -// getCommonDataSchema returns a new common schema used by all ssh key data sources. -func getCommonDataSchema() map[string]*schema.Schema { - return map[string]*schema.Schema{ - "id": { - Type: schema.TypeInt, - Optional: true, - Computed: true, + Selector types.String `tfsdk:"selector"` + WithSelector types.String `tfsdk:"with_selector"` +} + +func populateResourceDataWithSelector(ctx context.Context, data *resourceDataWithSelector, in *hcloud.SSHKey) diag.Diagnostics { + var diags diag.Diagnostics + + var resourceDataWithoutSelector resourceData + diags.Append(populateResourceData(ctx, &resourceDataWithoutSelector, in)...) + + data.ID = types.Int64Value(int64(in.ID)) + data.Name = resourceDataWithoutSelector.Name + data.Fingerprint = resourceDataWithoutSelector.Fingerprint + data.PublicKey = resourceDataWithoutSelector.PublicKey + data.Labels = resourceDataWithoutSelector.Labels + + return diags +} + +func getCommonDataSourceSchema() map[string]schema.Attribute { + return map[string]schema.Attribute{ + "id": schema.Int64Attribute{ + MarkdownDescription: "ID of the SSH key.", + Optional: true, + Computed: true, }, - "name": { - Type: schema.TypeString, - Computed: true, - Optional: true, + "name": schema.StringAttribute{ + MarkdownDescription: "Name of the SSH key.", + Optional: true, + Computed: true, }, - "fingerprint": { - Type: schema.TypeString, - Computed: true, - Optional: true, + "fingerprint": schema.StringAttribute{ + MarkdownDescription: "Fingerprint of the SSH key.", + Optional: true, + Computed: true, }, - "public_key": { - Type: schema.TypeString, - Computed: true, + "public_key": schema.StringAttribute{ + MarkdownDescription: "Public key of the SSH key pair.", + Optional: true, + Computed: true, }, - "labels": { - Type: schema.TypeMap, - Computed: true, + "labels": schema.MapAttribute{ + MarkdownDescription: "User-defined [labels](https://docs.hetzner.cloud/#labels) (key-value pairs) for the resource.", + ElementType: types.StringType, + Optional: true, + Computed: true, }, } } -// DataSource creates a new Terraform schema for the hcloud_ssh_key data -// source. -func DataSource() *schema.Resource { - return &schema.Resource{ - ReadContext: dataSourceHcloudSSHKeyRead, - Schema: datasourceutil.MergeSchema( - getCommonDataSchema(), - map[string]*schema.Schema{ - "selector": { - Type: schema.TypeString, - Optional: true, - Deprecated: "Please use the with_selector property instead.", - ConflictsWith: []string{"with_selector"}, - }, - "with_selector": { - Type: schema.TypeString, - Optional: true, - ConflictsWith: []string{"selector"}, - }, - }, - ), +// Single +var _ datasource.DataSource = (*dataSource)(nil) +var _ datasource.DataSourceWithConfigure = (*dataSource)(nil) +var _ datasource.DataSourceWithConfigValidators = (*dataSource)(nil) + +type dataSource struct { + client *hcloud.Client +} + +func NewDataSource() datasource.DataSource { + return &dataSource{} +} + +// Metadata should return the full name of the data source. +func (d *dataSource) 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 *dataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + var newDiags diag.Diagnostics + + d.client, newDiags = hcloudutil.ConfigureClient(req.ProviderData) + resp.Diagnostics.Append(newDiags...) + if resp.Diagnostics.HasError() { + return } } -// DataSourceList creates a new Terraform schema for the hcloud_ssh_keys data -// source. -func DataSourceList() *schema.Resource { - return &schema.Resource{ - ReadContext: dataSourceHcloudSSHKeyListRead, - Schema: map[string]*schema.Schema{ - "with_selector": { - Type: schema.TypeString, - Optional: true, - }, - "ssh_keys": { - Type: schema.TypeList, - Computed: true, - Elem: &schema.Resource{ - Schema: getCommonDataSchema(), - }, - }, +//go:embed data_source.md +var dataSourceMarkdownDescription string + +// Schema should return the schema for this data source. +func (d *dataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema.Attributes = getCommonDataSourceSchema() + maps.Copy(resp.Schema.Attributes, map[string]schema.Attribute{ + "selector": schema.StringAttribute{ + Optional: true, + DeprecationMessage: "Please use the with_selector property instead.", + }, + "with_selector": schema.StringAttribute{ + Optional: true, }, + }) + resp.Schema.MarkdownDescription = dataSourceMarkdownDescription +} + +// ConfigValidators returns a list of ConfigValidators. Each ConfigValidator's Validate method will be called when validating the data source. +func (d *dataSource) ConfigValidators(_ context.Context) []datasource.ConfigValidator { + return []datasource.ConfigValidator{ + datasourcevalidator.ExactlyOneOf( + path.MatchRoot("id"), + path.MatchRoot("name"), + path.MatchRoot("fingerprint"), + path.MatchRoot("selector"), + path.MatchRoot("with_selector"), + ), } } -func dataSourceHcloudSSHKeyRead(ctx context.Context, d *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 *dataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var data resourceDataWithSelector + + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + var result *hcloud.SSHKey + var err error - if id, ok := d.GetOk("id"); ok { - s, _, err := client.SSHKey.GetByID(ctx, id.(int)) + switch { + case !data.ID.IsNull(): + result, _, err = d.client.SSHKey.GetByID(ctx, int(data.ID.ValueInt64())) if err != nil { - return hcloudutil.ErrorToDiag(err) + resp.Diagnostics.Append(hcloudutil.APIErrorDiagnostics(err)...) + return } - if s == nil { - return diag.Errorf("no sshkey found with id %d", id) + if result == nil { + resp.Diagnostics.AddError( + "Resource not found", + fmt.Sprintf("No ssh key found with id %s.", data.ID.String()), + ) + return } - setSSHKeySchema(d, s) - return nil - } - if name, ok := d.GetOk("name"); ok { - s, _, err := client.SSHKey.GetByName(ctx, name.(string)) + case !data.Name.IsNull(): + result, _, err = d.client.SSHKey.GetByName(ctx, data.Name.ValueString()) if err != nil { - return hcloudutil.ErrorToDiag(err) + resp.Diagnostics.Append(hcloudutil.APIErrorDiagnostics(err)...) + return } - if s == nil { - return diag.Errorf("no sshkey found with name %v", name) + if result == nil { + resp.Diagnostics.AddError( + "Resource not found", + fmt.Sprintf("No ssh key found with name %s.", data.Name.String()), + ) + return } - setSSHKeySchema(d, s) - return nil - } - if fingerprint, ok := d.GetOk("fingerprint"); ok { - s, _, err := client.SSHKey.GetByFingerprint(ctx, fingerprint.(string)) + case !data.Fingerprint.IsNull(): + result, _, err = d.client.SSHKey.GetByFingerprint(ctx, data.Fingerprint.ValueString()) if err != nil { - return hcloudutil.ErrorToDiag(err) + resp.Diagnostics.Append(hcloudutil.APIErrorDiagnostics(err)...) + return } - if s == nil { - return diag.Errorf("no sshkey found with fingerprint %v", fingerprint) + if result == nil { + resp.Diagnostics.AddError( + "Resource not found", + fmt.Sprintf("No ssh key found with fingerprint %s.", data.Fingerprint.String()), + ) + return } - setSSHKeySchema(d, s) - return nil - } + case !data.WithSelector.IsNull() || !data.Selector.IsNull(): + opts := hcloud.SSHKeyListOpts{} - var selector string - if v := d.Get("with_selector").(string); v != "" { - selector = v - } else if v := d.Get("selector").(string); v != "" { - selector = v - } - if selector != "" { - var allKeys []*hcloud.SSHKey - opts := hcloud.SSHKeyListOpts{ - ListOpts: hcloud.ListOpts{ - LabelSelector: selector, - }, + if !data.WithSelector.IsNull() { + opts.LabelSelector = data.WithSelector.ValueString() + } else if !data.Selector.IsNull() { + opts.LabelSelector = data.Selector.ValueString() } - allKeys, err := client.SSHKey.AllWithOpts(ctx, opts) + + allKeys, err := d.client.SSHKey.AllWithOpts(ctx, opts) if err != nil { - return hcloudutil.ErrorToDiag(err) + resp.Diagnostics.Append(hcloudutil.APIErrorDiagnostics(err)...) + return } - if len(allKeys) == 0 { - return diag.Errorf("no sshkey found for selector %q", selector) - } - if len(allKeys) > 1 { - return diag.Errorf("more than one sshkey found for selector %q", selector) - } - setSSHKeySchema(d, allKeys[0]) - return nil - } - return diag.Errorf("please specify a id, a name, a fingerprint or a selector to lookup the sshkey") -} - -func dataSourceHcloudSSHKeyListRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - client := m.(*hcloud.Client) - - labelSelector := d.Get("with_selector") - labelSelectorStr, _ := labelSelector.(string) - opts := hcloud.SSHKeyListOpts{ - ListOpts: hcloud.ListOpts{ - LabelSelector: labelSelectorStr, - }, - } - allKeys, err := client.SSHKey.AllWithOpts(ctx, opts) - if err != nil { - return hcloudutil.ErrorToDiag(err) - } - - id := "" - tfKeys := make([]map[string]interface{}, len(allKeys)) - for i, key := range allKeys { - if id != "" { - id += "-" + var newDiag diag.Diagnostic + result, newDiag = datasourceutil.GetOneResultForLabelSelector("ssh key", allKeys, opts.LabelSelector) + if newDiag != nil { + resp.Diagnostics.Append(newDiag) + return } - id += fmt.Sprintf("%d", key.ID) - tfKeys[i] = getSSHKeyAttributes(key) } - d.SetId(id) - d.Set("ssh_keys", tfKeys) + resp.Diagnostics.Append(populateResourceDataWithSelector(ctx, &data, result)...) + if resp.Diagnostics.HasError() { + return + } - return nil + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } diff --git a/internal/sshkey/data_source.md b/internal/sshkey/data_source.md new file mode 100644 index 000000000..ee34c4318 --- /dev/null +++ b/internal/sshkey/data_source.md @@ -0,0 +1,31 @@ +Provides details about a specific Hetzner Cloud SSH Key. + +This resource is useful if you want to use a non-terraform managed SSH Key. + +## Example Usage + +```hcl +data "hcloud_ssh_key" "ssh_key_1" { + id = "1234" +} + +data "hcloud_ssh_key" "ssh_key_2" { + name = "my-ssh-key" +} + +data "hcloud_ssh_key" "ssh_key_3" { + fingerprint = "43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8" +} + +data "hcloud_ssh_key" "ssh_key_4" { + with_selector = "key=value" +} + +resource "hcloud_server" "main" { + ssh_keys = [ + data.hcloud_ssh_key.ssh_key_1.id, + data.hcloud_ssh_key.ssh_key_2.id, + data.hcloud_ssh_key.ssh_key_3.id, + ] +} +``` diff --git a/internal/sshkey/data_source_list.go b/internal/sshkey/data_source_list.go new file mode 100644 index 000000000..e4e069402 --- /dev/null +++ b/internal/sshkey/data_source_list.go @@ -0,0 +1,165 @@ +package sshkey + +import ( + "context" + _ "embed" + "strconv" + "strings" + + "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/types" + + "github.com/hetznercloud/hcloud-go/hcloud" + "github.com/hetznercloud/terraform-provider-hcloud/internal/util/hcloudutil" + "github.com/hetznercloud/terraform-provider-hcloud/internal/util/resourceutil" +) + +// DataSourceListType is the type name of the Hetzner Cloud SSH Keys data source. +const DataSourceListType = "hcloud_ssh_keys" + +var _ datasource.DataSource = (*dataSourceList)(nil) +var _ datasource.DataSourceWithConfigure = (*dataSourceList)(nil) + +type dataSourceList struct { + client *hcloud.Client +} + +func NewDataSourceList() datasource.DataSource { + return &dataSourceList{} +} + +// Metadata should return the full name of the data source. +func (d *dataSourceList) Metadata(_ context.Context, _ datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = DataSourceListType +} + +// 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 *dataSourceList) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + var newDiags diag.Diagnostics + + d.client, newDiags = hcloudutil.ConfigureClient(req.ProviderData) + resp.Diagnostics.Append(newDiags...) + if resp.Diagnostics.HasError() { + return + } +} + +//go:embed data_source_list.md +var dataSourceListMarkdownDescription string + +// Schema should return the schema for this data source. +func (d *dataSourceList) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema.Attributes = map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Optional: true, + }, + "ssh_keys": schema.ListNestedAttribute{ + NestedObject: schema.NestedAttributeObject{ + Attributes: getCommonDataSourceSchema(), + }, + Computed: true, + }, + "with_selector": schema.StringAttribute{ + Optional: true, + }, + } + + resp.Schema.MarkdownDescription = dataSourceListMarkdownDescription +} + +type resourceDataList struct { + ID types.String `tfsdk:"id"` + SSHKeys types.List `tfsdk:"ssh_keys"` + + WithSelector types.String `tfsdk:"with_selector"` +} + +func populateResourceDataList(ctx context.Context, data *resourceDataList, in []*hcloud.SSHKey) diag.Diagnostics { + var diags diag.Diagnostics + var newDiags diag.Diagnostics + + // This type is required because the SDK version had an `int` ID field inside the data source list, + // but a `string` ID field for the resource & single data source. + type resourceDataList struct { + ID types.Int64 `tfsdk:"id"` + Name types.String `tfsdk:"name"` + Fingerprint types.String `tfsdk:"fingerprint"` + PublicKey types.String `tfsdk:"public_key"` + Labels types.Map `tfsdk:"labels"` + } + var populateResourceDataList = func(ctx context.Context, data *resourceDataList, in *hcloud.SSHKey) diag.Diagnostics { + var diags diag.Diagnostics + var newDiags diag.Diagnostics + + data.ID = types.Int64Value(int64(in.ID)) + data.Name = types.StringValue(in.Name) + data.Fingerprint = types.StringValue(in.Fingerprint) + data.PublicKey = types.StringValue(in.PublicKey) + + data.Labels, newDiags = resourceutil.LabelsMapValueFrom(ctx, in.Labels) + diags.Append(newDiags...) + + return diags + } + + sshKeyIDs := make([]string, len(in)) + sshKeys := make([]resourceDataList, len(in)) + + for i, item := range in { + sshKeyIDs[i] = strconv.Itoa(item.ID) + + var sshKey resourceDataList + diags.Append(populateResourceDataList(ctx, &sshKey, item)...) + sshKeys[i] = sshKey + } + + data.ID = types.StringValue(strings.Join(sshKeyIDs, "-")) + data.SSHKeys, newDiags = types.ListValueFrom(ctx, types.ObjectType{AttrTypes: map[string]attr.Type{ + "id": types.Int64Type, + "name": types.StringType, + "fingerprint": types.StringType, + "public_key": types.StringType, + "labels": types.MapType{ElemType: types.StringType}, + }}, sshKeys) + diags.Append(newDiags...) + + return diags +} + +// 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 *dataSourceList) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var data resourceDataList + + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + var result []*hcloud.SSHKey + var err error + + opts := hcloud.SSHKeyListOpts{} + if !data.WithSelector.IsNull() { + opts.LabelSelector = data.WithSelector.ValueString() + } + + result, err = d.client.SSHKey.AllWithOpts(ctx, opts) + if err != nil { + resp.Diagnostics.Append(hcloudutil.APIErrorDiagnostics(err)...) + return + } + + resp.Diagnostics.Append(populateResourceDataList(ctx, &data, result)...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} diff --git a/internal/sshkey/data_source_list.md b/internal/sshkey/data_source_list.md new file mode 100644 index 000000000..462706482 --- /dev/null +++ b/internal/sshkey/data_source_list.md @@ -0,0 +1,17 @@ +Provides details about Hetzner Cloud SSH Keys. + +This resource is useful if you want to use a non-terraform managed SSH Key. + +## Example Usage + +```hcl +data "hcloud_ssh_keys" "all_ssh_keys" {} + +data "hcloud_ssh_keys" "ssh_keys_by_label_selector" { + with_selector = "foo=bar" +} + +resource "hcloud_server" "main" { + ssh_keys = data.hcloud_ssh_keys.all_ssh_keys.ssh_keys.*.name +} +``` diff --git a/internal/sshkey/data_source_list_test.go b/internal/sshkey/data_source_list_test.go new file mode 100644 index 000000000..bcef6717c --- /dev/null +++ b/internal/sshkey/data_source_list_test.go @@ -0,0 +1,107 @@ +package sshkey_test + +import ( + "fmt" + "testing" + + "github.com/hetznercloud/terraform-provider-hcloud/internal/sshkey" + "github.com/hetznercloud/terraform-provider-hcloud/internal/teste2e" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + + "github.com/hetznercloud/terraform-provider-hcloud/internal/testtemplate" +) + +func TestAccHcloudDataSourceSSHKeysTest(t *testing.T) { + res := sshkey.NewRData(t, "ssh-key-ds-test") + + sshKeysByLabelSelector := &sshkey.DDataList{ + LabelSelector: fmt.Sprintf("key=${%s.labels[\"key\"]}", res.TFID()), + } + sshKeysByLabelSelector.SetRName("key_by_sel") + + sshKeysAll := &sshkey.DDataList{} + sshKeysAll.SetRName("all_keys_sel") + + tmplMan := testtemplate.Manager{} + resource.ParallelTest(t, resource.TestCase{ + PreCheck: teste2e.PreCheck(t), + ProtoV6ProviderFactories: teste2e.ProtoV6ProviderFactories(), + Steps: []resource.TestStep{ + { + Config: tmplMan.Render(t, + "testdata/r/hcloud_ssh_key", res, + ), + }, + { + Config: tmplMan.Render(t, + "testdata/r/hcloud_ssh_key", res, + "testdata/d/hcloud_ssh_keys", sshKeysByLabelSelector, + "testdata/d/hcloud_ssh_keys", sshKeysAll, + ), + + Check: resource.ComposeTestCheckFunc( + resource.TestCheckTypeSetElemNestedAttrs(sshKeysByLabelSelector.TFID(), "ssh_keys.*", + map[string]string{ + "name": fmt.Sprintf("%s--%d", res.Name, tmplMan.RandInt), + "public_key": res.PublicKey, + }, + ), + + resource.TestCheckTypeSetElemNestedAttrs(sshKeysAll.TFID(), "ssh_keys.*", + map[string]string{ + "name": fmt.Sprintf("%s--%d", res.Name, tmplMan.RandInt), + "public_key": res.PublicKey, + }, + ), + ), + }, + }, + }) +} + +func TestAccHcloudDataSourceSSHKeys_UpgradePluginFramework(t *testing.T) { + tmplMan := testtemplate.Manager{} + + res := sshkey.NewRData(t, "ssh-key-ds-test") + + sshKeysByLabelSelector := &sshkey.DDataList{ + LabelSelector: fmt.Sprintf("key=${%s.labels[\"key\"]}", res.TFID()), + } + sshKeysByLabelSelector.SetRName("key_by_sel") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: teste2e.PreCheck(t), + Steps: []resource.TestStep{ + { + ExternalProviders: map[string]resource.ExternalProvider{ + "hcloud": { + VersionConstraint: "1.44.1", + Source: "hetznercloud/hcloud", + }, + }, + + Config: tmplMan.Render(t, + "testdata/r/hcloud_ssh_key", res, + + "testdata/d/hcloud_ssh_keys", sshKeysByLabelSelector, + + "testdata/r/terraform_data_resource", sshKeysByLabelSelector, + ), + }, + { + ProtoV6ProviderFactories: teste2e.ProtoV6ProviderFactories(), + + Config: tmplMan.Render(t, + "testdata/r/hcloud_ssh_key", res, + + "testdata/d/hcloud_ssh_keys", sshKeysByLabelSelector, + + "testdata/r/terraform_data_resource", sshKeysByLabelSelector, + ), + + PlanOnly: true, + }, + }, + }) +} diff --git a/internal/sshkey/data_source_test.go b/internal/sshkey/data_source_test.go index 1e602e108..9fa2d0043 100644 --- a/internal/sshkey/data_source_test.go +++ b/internal/sshkey/data_source_test.go @@ -8,6 +8,7 @@ import ( "github.com/hetznercloud/terraform-provider-hcloud/internal/teste2e" "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hetznercloud/terraform-provider-hcloud/internal/testsupport" "github.com/hetznercloud/terraform-provider-hcloud/internal/testtemplate" ) @@ -65,49 +66,72 @@ func TestAccHcloudDataSourceSSHKeyTest(t *testing.T) { }) } -func TestAccHcloudDataSourceSSHKeysTest(t *testing.T) { - res := sshkey.NewRData(t, "ssh-key-ds-test") +func TestAccHcloudDatasourceSSHKey_UpgradePluginFramework(t *testing.T) { + tmplMan := testtemplate.Manager{} - keyBySel := &sshkey.DDataList{ + res := sshkey.NewRData(t, "datasource-test") + sshKeyByName := &sshkey.DData{ + SSHKeyName: res.TFID() + ".name", + } + sshKeyByName.SetRName("sshkey_by_name") + sshKeyByID := &sshkey.DData{ + SSHKeyID: res.TFID() + ".id", + } + sshKeyByID.SetRName("sshkey_by_id") + sshKeyBySel := &sshkey.DData{ LabelSelector: fmt.Sprintf("key=${%s.labels[\"key\"]}", res.TFID()), } - keyBySel.SetRName("key_by_sel") - - allKeysSel := &sshkey.DDataList{} - allKeysSel.SetRName("all_keys_sel") + sshKeyBySel.SetRName("sshkey_by_sel") - tmplMan := testtemplate.Manager{} resource.ParallelTest(t, resource.TestCase{ - PreCheck: teste2e.PreCheck(t), - ProtoV6ProviderFactories: teste2e.ProtoV6ProviderFactories(), + PreCheck: teste2e.PreCheck(t), Steps: []resource.TestStep{ { + ExternalProviders: map[string]resource.ExternalProvider{ + "hcloud": { + VersionConstraint: "1.44.1", + Source: "hetznercloud/hcloud", + }, + }, + Config: tmplMan.Render(t, "testdata/r/hcloud_ssh_key", res, ), }, { + ExternalProviders: map[string]resource.ExternalProvider{ + "hcloud": { + VersionConstraint: "1.44.1", + Source: "hetznercloud/hcloud", + }, + }, + Config: tmplMan.Render(t, "testdata/r/hcloud_ssh_key", res, - "testdata/d/hcloud_ssh_keys", keyBySel, - "testdata/d/hcloud_ssh_keys", allKeysSel, + "testdata/d/hcloud_ssh_key", sshKeyByName, + "testdata/d/hcloud_ssh_key", sshKeyByID, + "testdata/d/hcloud_ssh_key", sshKeyBySel, + + "testdata/r/terraform_data_resource", sshKeyByName, + "testdata/r/terraform_data_resource", sshKeyByID, + "testdata/r/terraform_data_resource", sshKeyBySel, ), + }, + { + ProtoV6ProviderFactories: teste2e.ProtoV6ProviderFactories(), - Check: resource.ComposeTestCheckFunc( - resource.TestCheckTypeSetElemNestedAttrs(keyBySel.TFID(), "ssh_keys.*", - map[string]string{ - "name": fmt.Sprintf("%s--%d", res.Name, tmplMan.RandInt), - "public_key": res.PublicKey, - }, - ), - - resource.TestCheckTypeSetElemNestedAttrs(allKeysSel.TFID(), "ssh_keys.*", - map[string]string{ - "name": fmt.Sprintf("%s--%d", res.Name, tmplMan.RandInt), - "public_key": res.PublicKey, - }, - ), + Config: tmplMan.Render(t, + "testdata/r/hcloud_ssh_key", res, + "testdata/d/hcloud_ssh_key", sshKeyByName, + "testdata/d/hcloud_ssh_key", sshKeyByID, + "testdata/d/hcloud_ssh_key", sshKeyBySel, + + "testdata/r/terraform_data_resource", sshKeyByName, + "testdata/r/terraform_data_resource", sshKeyByID, + "testdata/r/terraform_data_resource", sshKeyBySel, ), + + PlanOnly: true, }, }, }) diff --git a/internal/sshkey/resource.go b/internal/sshkey/resource.go index 02a0d9627..22ce1a8e7 100644 --- a/internal/sshkey/resource.go +++ b/internal/sshkey/resource.go @@ -2,203 +2,214 @@ package sshkey import ( "context" - "log" - "strconv" - "strings" + _ "embed" - "github.com/hashicorp/go-cty/cty" - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hetznercloud/hcloud-go/hcloud" "github.com/hetznercloud/terraform-provider-hcloud/internal/util/hcloudutil" - "golang.org/x/crypto/ssh" + "github.com/hetznercloud/terraform-provider-hcloud/internal/util/resourceutil" ) // ResourceType is the type name of the Hetzner Cloud SSH Key resource. const ResourceType = "hcloud_ssh_key" -// Resource creates a Terraform schema for the hcloud_ssh_key resource. -func Resource() *schema.Resource { - return &schema.Resource{ - CreateContext: resourceSSHKeyCreate, - ReadContext: resourceSSHKeyRead, - UpdateContext: resourceSSHKeyUpdate, - DeleteContext: resourceSSHKeyDelete, - Importer: &schema.ResourceImporter{ - StateContext: schema.ImportStatePassthroughContext, - }, +var _ resource.Resource = (*resourceImpl)(nil) +var _ resource.ResourceWithConfigure = (*resourceImpl)(nil) +var _ resource.ResourceWithImportState = (*resourceImpl)(nil) - Schema: map[string]*schema.Schema{ - "name": { - Type: schema.TypeString, - Required: true, - }, - "public_key": { - Type: schema.TypeString, - Required: true, - ForceNew: true, - DiffSuppressFunc: resourceSSHKeyPublicKeyDiffSuppress, +type resourceImpl struct { + client *hcloud.Client +} + +func NewResource() resource.Resource { + return &resourceImpl{} +} + +// Metadata should return the full name of the data source. +func (r *resourceImpl) Metadata(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = ResourceType +} + +// 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 (r *resourceImpl) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + var newDiags diag.Diagnostics + + r.client, newDiags = hcloudutil.ConfigureClient(req.ProviderData) + resp.Diagnostics.Append(newDiags...) + if resp.Diagnostics.HasError() { + return + } +} + +//go:embed resource.md +var resourceMarkdownDescription string + +func (r *resourceImpl) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema.Attributes = map[string]schema.Attribute{ + "id": schema.StringAttribute{ + MarkdownDescription: "ID of the SSH key.", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), }, - "fingerprint": { - Type: schema.TypeString, - Computed: true, + }, + "name": schema.StringAttribute{ + MarkdownDescription: "Name of the SSH key.", + Required: true, + }, + "public_key": schema.StringAttribute{ + MarkdownDescription: "Public key of the SSH key pair. If this is a file, it can be read using the `file` interpolation function.", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), }, - "labels": { - Type: schema.TypeMap, - Optional: true, - ValidateDiagFunc: func(i interface{}, path cty.Path) diag.Diagnostics { // nolint:revive - if ok, err := hcloud.ValidateResourceLabels(i.(map[string]interface{})); !ok { - return diag.Errorf(err.Error()) - } - return nil - }, + }, + "fingerprint": schema.StringAttribute{ + MarkdownDescription: "Fingerprint of the SSH public key.", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), }, }, + "labels": resourceutil.LabelsSchema(), } + resp.Schema.MarkdownDescription = resourceMarkdownDescription } -func resourceSSHKeyPublicKeyDiffSuppress(_, old, new string, d *schema.ResourceData) bool { - fingerprint := d.Get("fingerprint").(string) - if new != "" && fingerprint != "" { - publicKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(new)) - if err != nil { - return false - } - return ssh.FingerprintLegacyMD5(publicKey) == fingerprint +func (r *resourceImpl) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data resourceData + + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return } - return strings.TrimSpace(old) == strings.TrimSpace(new) -} -func resourceSSHKeyCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - client := m.(*hcloud.Client) opts := hcloud.SSHKeyCreateOpts{ - Name: d.Get("name").(string), - PublicKey: d.Get("public_key").(string), + Name: data.Name.ValueString(), + PublicKey: data.PublicKey.ValueString(), } - if labels, ok := d.GetOk("labels"); ok { - tmpLabels := make(map[string]string) - for k, v := range labels.(map[string]interface{}) { - tmpLabels[k] = v.(string) - } - opts.Labels = tmpLabels + if !data.Labels.IsNull() { + hcloudutil.TerraformLabelsToHCloud(ctx, data.Labels, &opts.Labels) } - sshKey, _, err := client.SSHKey.Create(ctx, opts) + in, _, err := r.client.SSHKey.Create(ctx, opts) if err != nil { - return hcloudutil.ErrorToDiag(err) + resp.Diagnostics.Append(hcloudutil.APIErrorDiagnostics(err)...) + return + } + + resp.Diagnostics.Append(populateResourceData(ctx, &data, in)...) + if resp.Diagnostics.HasError() { + return } - d.SetId(strconv.Itoa(sshKey.ID)) - return resourceSSHKeyRead(ctx, d, m) + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } -func resourceSSHKeyRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - client := m.(*hcloud.Client) +func (r *resourceImpl) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data resourceData - sshKeyID, err := strconv.Atoi(d.Id()) - if err != nil { - log.Printf("[WARN] invalid SSH key id (%s), removing from state: %v", d.Id(), err) - d.SetId("") - return nil + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + id, newDiags := resourceutil.ParseID(data.ID) + resp.Diagnostics.Append(newDiags...) + if resp.Diagnostics.HasError() { + return } - sshKey, _, err := client.SSHKey.GetByID(ctx, sshKeyID) + in, _, err := r.client.SSHKey.GetByID(ctx, id) if err != nil { - return hcloudutil.ErrorToDiag(err) + resp.Diagnostics.Append(hcloudutil.APIErrorDiagnostics(err)...) + return } - if sshKey == nil { - log.Printf("[WARN] SSH key (%s) not found, removing from state", d.Id()) - d.SetId("") - return nil + + if in == nil { + resp.State.RemoveResource(ctx) + return } - setSSHKeySchema(d, sshKey) + resp.Diagnostics.Append(populateResourceData(ctx, &data, in)...) + if resp.Diagnostics.HasError() { + return + } - return nil + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } -func resourceSSHKeyUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - client := m.(*hcloud.Client) +func (r *resourceImpl) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var data, plan resourceData - sshKeyID, err := strconv.Atoi(d.Id()) - if err != nil { - log.Printf("[WARN] invalid SSH key id (%s), removing from state: %v", d.Id(), err) - d.SetId("") - return nil - } - - if d.HasChange("name") { - name := d.Get("name").(string) - _, _, err := client.SSHKey.Update(ctx, &hcloud.SSHKey{ID: sshKeyID}, hcloud.SSHKeyUpdateOpts{ - Name: name, - }) - if err != nil { - if hcerr, ok := err.(hcloud.Error); ok && hcerr.Code == hcloud.ErrorCodeNotFound { - log.Printf("[WARN] SSH key (%s) not found, removing from state", d.Id()) - d.SetId("") - return nil - } - return hcloudutil.ErrorToDiag(err) - } + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return } - if d.HasChange("labels") { - labels := make(map[string]string) - for k, v := range d.Get("labels").(map[string]interface{}) { - labels[k] = v.(string) - } - _, _, err := client.SSHKey.Update(ctx, &hcloud.SSHKey{ID: sshKeyID}, hcloud.SSHKeyUpdateOpts{ - Labels: labels, - }) - if err != nil { - if hcerr, ok := err.(hcloud.Error); ok && hcerr.Code == hcloud.ErrorCodeNotFound { - log.Printf("[WARN] SSH key (%s) not found, removing from state", d.Id()) - d.SetId("") - return nil - } - return hcloudutil.ErrorToDiag(err) - } + + opts := hcloud.SSHKeyUpdateOpts{} + + if !plan.Name.Equal(data.Name) { + opts.Name = plan.Name.ValueString() } - return resourceSSHKeyRead(ctx, d, m) -} + if !plan.Labels.Equal(data.Labels) { + hcloudutil.TerraformLabelsToHCloud(ctx, plan.Labels, &opts.Labels) + } -func resourceSSHKeyDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { - client := m.(*hcloud.Client) + id, newDiags := resourceutil.ParseID(data.ID) + resp.Diagnostics.Append(newDiags...) + if resp.Diagnostics.HasError() { + return + } - sshKeyID, err := strconv.Atoi(d.Id()) + in, _, err := r.client.SSHKey.Update(ctx, &hcloud.SSHKey{ID: id}, opts) if err != nil { - log.Printf("[WARN] invalid SSH key id (%s), removing from state: %v", d.Id(), err) - d.SetId("") - return nil - } - if _, err := client.SSHKey.Delete(ctx, &hcloud.SSHKey{ID: sshKeyID}); err != nil { - if hcerr, ok := err.(hcloud.Error); ok && hcerr.Code == hcloud.ErrorCodeNotFound { - // SSH key has already been deleted - return nil - } - return hcloudutil.ErrorToDiag(err) + resp.Diagnostics.Append(hcloudutil.APIErrorDiagnostics(err)...) + return + } + + resp.Diagnostics.Append(populateResourceData(ctx, &data, in)...) + if resp.Diagnostics.HasError() { + return } - return nil + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } -func setSSHKeySchema(d *schema.ResourceData, s *hcloud.SSHKey) { - for key, val := range getSSHKeyAttributes(s) { - if key == "id" { - d.SetId(strconv.Itoa(val.(int))) - } else { - d.Set(key, val) - } +func (r *resourceImpl) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var data resourceData + + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return } -} -func getSSHKeyAttributes(s *hcloud.SSHKey) map[string]interface{} { - return map[string]interface{}{ - "id": s.ID, - "name": s.Name, - "fingerprint": s.Fingerprint, - "public_key": s.PublicKey, - "labels": s.Labels, + id, newDiags := resourceutil.ParseID(data.ID) + resp.Diagnostics.Append(newDiags...) + if resp.Diagnostics.HasError() { + return } + + _, err := r.client.SSHKey.Delete(ctx, &hcloud.SSHKey{ID: id}) + if err != nil { + if hcloudutil.APIErrorIsNotFound(err) { // SSH key does not exist + return + } + + resp.Diagnostics.Append(hcloudutil.APIErrorDiagnostics(err)...) + return + } +} +func (r *resourceImpl) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) } diff --git a/internal/sshkey/resource.md b/internal/sshkey/resource.md new file mode 100644 index 000000000..80c68d52c --- /dev/null +++ b/internal/sshkey/resource.md @@ -0,0 +1,11 @@ +Provides a Hetzner Cloud SSH key resource to manage SSH keys for server access. + +## Example Usage + +```hcl +# Create a new SSH key +resource "hcloud_ssh_key" "default" { + name = "Terraform Example" + public_key = file("~/.ssh/id_rsa.pub") +} +``` diff --git a/internal/sshkey/resource_test.go b/internal/sshkey/resource_test.go index 66e08890a..1192acc99 100644 --- a/internal/sshkey/resource_test.go +++ b/internal/sshkey/resource_test.go @@ -31,9 +31,10 @@ func TestSSHKeyResource_Basic(t *testing.T) { Config: tmplMan.Render(t, "testdata/r/hcloud_ssh_key", res), Check: resource.ComposeTestCheckFunc( testsupport.CheckResourceExists(res.TFID(), sshkey.ByID(t, &sk)), - resource.TestCheckResourceAttr(res.TFID(), "name", - fmt.Sprintf("basic-ssh-key--%d", tmplMan.RandInt)), + resource.TestCheckResourceAttr(res.TFID(), "name", fmt.Sprintf("basic-ssh-key--%d", tmplMan.RandInt)), resource.TestCheckResourceAttr(res.TFID(), "public_key", res.PublicKey), + resource.TestCheckResourceAttrSet(res.TFID(), "fingerprint"), + resource.TestCheckResourceAttr(res.TFID(), "labels.key", res.Labels["key"]), ), }, { @@ -50,11 +51,43 @@ func TestSSHKeyResource_Basic(t *testing.T) { "testdata/r/hcloud_ssh_key", resRenamed, ), Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttr(resRenamed.TFID(), "name", - fmt.Sprintf("basic-ssh-key-renamed--%d", tmplMan.RandInt)), + resource.TestCheckResourceAttr(resRenamed.TFID(), "name", fmt.Sprintf("basic-ssh-key-renamed--%d", tmplMan.RandInt)), resource.TestCheckResourceAttr(resRenamed.TFID(), "public_key", res.PublicKey), ), }, }, }) } + +func TestAccSSHKeyResource_UpgradePluginFramework(t *testing.T) { + tmplMan := testtemplate.Manager{} + + res := sshkey.NewRData(t, "upgrade-plugin-framework-test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: teste2e.PreCheck(t), + Steps: []resource.TestStep{ + { + ExternalProviders: map[string]resource.ExternalProvider{ + "hcloud": { + VersionConstraint: "1.45.0", + Source: "hetznercloud/hcloud", + }, + }, + + Config: tmplMan.Render(t, + "testdata/r/hcloud_ssh_key", res, + ), + }, + { + ProtoV6ProviderFactories: teste2e.ProtoV6ProviderFactories(), + + Config: tmplMan.Render(t, + "testdata/r/hcloud_ssh_key", res, + ), + + PlanOnly: true, + }, + }, + }) +}