Implement key pair resource (#588)

* Revert "Revert "Implement key pair resource (#578)" (#581)"

This reverts commit 600847a2ea.

* feat: Update iaas SDK module version; Use beta API in key pair resource
This commit is contained in:
João Palet 2024-11-11 11:08:05 +00:00 committed by GitHub
parent fc27f65925
commit b1f928f6be
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 1282 additions and 138 deletions

View file

@ -0,0 +1,36 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "stackit_key_pair Data Source - stackit"
subcategory: ""
description: |-
Key pair resource schema. Must have a region specified in the provider configuration.
~> This resource is in beta and may be subject to breaking changes in the future. Use with caution. See our guide https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/guides/opting_into_beta_resources for how to opt-in to use beta resources.
---
# stackit_key_pair (Data Source)
Key pair resource schema. Must have a `region` specified in the provider configuration.
~> This resource is in beta and may be subject to breaking changes in the future. Use with caution. See our [guide](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/guides/opting_into_beta_resources) for how to opt-in to use beta resources.
## Example Usage
```terraform
data "stackit_key_pair" "example" {
name = "example-key-pair-name"
}
```
<!-- schema generated by tfplugindocs -->
## Schema
### Required
- `name` (String) The name of the SSH key pair.
### Read-Only
- `fingerprint` (String) The fingerprint of the public SSH key.
- `id` (String) Terraform's internal resource ID. It takes the value of the key pair "`name`".
- `labels` (Map of String) Labels are key-value string pairs which can be attached to a resource container.
- `public_key` (String) A string representation of the public SSH key. E.g., `ssh-rsa <key_data>` or `ssh-ed25519 <key-data>`.

View file

@ -3,13 +3,13 @@
page_title: "stackit_public_ip Data Source - stackit"
subcategory: ""
description: |-
Volume resource schema. Must have a region specified in the provider configuration.
Public IP resource schema. Must have a region specified in the provider configuration.
~> This resource is in beta and may be subject to breaking changes in the future. Use with caution. See our guide https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/guides/opting_into_beta_resources for how to opt-in to use beta resources.
---
# stackit_public_ip (Data Source)
Volume resource schema. Must have a `region` specified in the provider configuration.
Public IP resource schema. Must have a `region` specified in the provider configuration.
~> This resource is in beta and may be subject to breaking changes in the future. Use with caution. See our [guide](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/guides/opting_into_beta_resources) for how to opt-in to use beta resources.

View file

@ -0,0 +1,74 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "stackit_key_pair Resource - stackit"
subcategory: ""
description: |-
Key pair resource schema. Must have a region specified in the provider configuration. Allows uploading an SSH public key to be used for server authentication.
Usage with server
```terraform
resource "stackitkeypair" "keypair" {
name = "example-key-pair"
publickey = chomp(file("path/to/idrsa.pub"))
}
resource "stackitserver" "example-server" {
projectid = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
name = "example-server"
bootvolume = {
size = 64
sourcetype = "image"
sourceid = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}
availabilityzone = "eu01-1"
machinetype = "g1.1"
keypairname = "example-key-pair"
}
~> This resource is in beta and may be subject to breaking changes in the future. Use with caution. See our guide https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/guides/opting_into_beta_resources for how to opt-in to use beta resources.
---
# stackit_key_pair (Resource)
Key pair resource schema. Must have a `region` specified in the provider configuration. Allows uploading an SSH public key to be used for server authentication.
### Usage with server
```terraform
resource "stackit_key_pair" "keypair" {
name = "example-key-pair"
public_key = chomp(file("path/to/id_rsa.pub"))
}
resource "stackit_server" "example-server" {
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
name = "example-server"
boot_volume = {
size = 64
source_type = "image"
source_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}
availability_zone = "eu01-1"
machine_type = "g1.1"
keypair_name = "example-key-pair"
}
~> This resource is in beta and may be subject to breaking changes in the future. Use with caution. See our [guide](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/guides/opting_into_beta_resources) for how to opt-in to use beta resources.
<!-- schema generated by tfplugindocs -->
## Schema
### Required
- `name` (String) The name of the SSH key pair.
- `public_key` (String) A string representation of the public SSH key. E.g., `ssh-rsa <key_data>` or `ssh-ed25519 <key-data>`.
### Optional
- `labels` (Map of String) Labels are key-value string pairs which can be attached to a resource container.
### Read-Only
- `fingerprint` (String) The fingerprint of the public SSH key.
- `id` (String) Terraform's internal resource ID. It takes the value of the key pair "`name`".

View file

@ -6,26 +6,44 @@ description: |-
Server resource schema. Must have a region specified in the provider configuration.
~> This resource is in beta and may be subject to breaking changes in the future. Use with caution. See our guide https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/guides/opting_into_beta_resources for how to opt-in to use beta resources.
Example Usage
Boot from volume
resource "stackit_server" "boot-from-volume" {
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
name = "example-server"
boot_volume = {
size = 64
source_type = "image"
source_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}
availability_zone = "eu01-1"
machine_type = "g1.1"
keypair_name = "example-keypair"
With key pair
```terraform
resource "stackitkeypair" "keypair" {
name = "example-key-pair"
publickey = chomp(file("path/to/idrsa.pub"))
}
resource "stackitserver" "user-data-from-file" {
projectid = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
bootvolume = {
size = 64
sourcetype = "image"
sourceid = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}
name = "example-server"
machinetype = "g1.1"
keypairname = stackitkeypair.keypair.name
userdata = file("${path.module}/cloud-init.yaml")
}
```
Boot from volume
```terraform
resource "stackitserver" "boot-from-volume" {
projectid = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
name = "example-server"
bootvolume = {
size = 64
sourcetype = "image"
sourceid = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}
availabilityzone = "eu01-1"
machinetype = "g1.1"
keypairname = "example-keypair"
}
```
Boot from existing volume
resource "stackit_volume" "example-volume" {
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
```terraform
resource "stackitvolume" "example-volume" {
projectid = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
size = 12
source = {
type = "image"
@ -34,129 +52,117 @@ description: |-
name = "example-volume"
availability_zone = "eu01-1"
}
resource "stackit_server" "boot-from-volume" {
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
resource "stackitserver" "boot-from-volume" {
projectid = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
name = "example-server"
boot_volume = {
source_type = "volume"
source_id = stackit_volume.example-volume.volume_id
bootvolume = {
sourcetype = "volume"
sourceid = stackitvolume.example-volume.volumeid
}
availability_zone = "eu01-1"
machine_type = "g1.1"
keypair_name = "example-keypair"
availabilityzone = "eu01-1"
machinetype = "g1.1"
keypairname = stackitkeypair.keypair.name
}
```
Network setup
resource "stackit_server" "server-with-network" {
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
```terraform
resource "stackitserver" "server-with-network" {
projectid = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
name = "example-server"
boot_volume = {
bootvolume = {
size = 64
source_type = "image"
source_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
sourcetype = "image"
sourceid = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}
machine_type = "g1.1"
keypair_name = "example-keypair"
machinetype = "g1.1"
keypairname = stackitkey_pair.keypair.name
}
resource "stackit_network" "network" {
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
resource "stackitnetwork" "network" {
projectid = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
name = "example-network"
nameservers = ["192.0.2.0", "198.51.100.0", "203.0.113.0"]
ipv4_prefix_length = 24
ipv4prefixlength = 24
}
resource "stackit_security_group" "sec-group" {
resource "stackitsecuritygroup" "sec-group" {
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
name = "example-security-group"
stateful = true
}
resource "stackit_security_group_rule" "rule" {
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
security_group_id = stackit_security_group.sec-group.security_group_id
resource "stackitsecuritygrouprule" "rule" {
projectid = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
securitygroupid = stackitsecuritygroup.sec-group.securitygroupid
direction = "ingress"
ether_type = "IPv4"
}
resource "stackit_network_interface" "nic" {
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
network_id = stackit_network.network.network_id
security_group_ids = [stackit_security_group.sec-group.security_group_id]
resource "stackitnetworkinterface" "nic" {
projectid = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
networkid = stackitnetwork.network.networkid
securitygroupids = [stackitsecuritygroup.sec-group.securitygroupid]
}
resource "stackit_public_ip" "public-ip" {
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
network_interface_id = stackit_network_interface.nic.network_interface_id
resource "stackitpublicip" "public-ip" {
projectid = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
networkinterfaceid = stackitnetworkinterface.nic.networkinterface_id
}
resource "stackit_server_network_interface_attach" "nic-attachment" {
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
server_id = stackit_server.server-with-network.server_id
network_interface_id = stackit_network_interface.nic.network_interface_id
resource "stackitservernetworkinterfaceattach" "nic-attachment" {
projectid = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
serverid = stackitserver.server-with-network.serverid
networkinterfaceid = stackitnetworkinterface.nic.networkinterfaceid
}
```
Server with attached volume
resource "stackit_volume" "example-volume" {
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
```terraform
resource "stackitvolume" "example-volume" {
projectid = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
size = 12
performance_class = "storage_premium_perf6"
performanceclass = "storagepremiumperf6"
name = "example-volume"
availability_zone = "eu01-1"
availabilityzone = "eu01-1"
}
resource "stackit_server" "server-with-volume" {
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
resource "stackitserver" "server-with-volume" {
projectid = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
name = "example-server"
boot_volume = {
bootvolume = {
size = 64
source_type = "image"
source_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
sourcetype = "image"
sourceid = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}
availability_zone = "eu01-1"
machine_type = "g1.1"
keypair_name = "example-keypair"
availabilityzone = "eu01-1"
machinetype = "g1.1"
keypairname = stackitkeypair.keypair.name
}
resource "stackit_server_volume_attach" "attach_volume" {
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
server_id = stackit_server.server-with-volume.server_id
volume_id = stackit_volume.example-volume.volume_id
resource "stackitservervolumeattach" "attachvolume" {
projectid = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
serverid = stackitserver.server-with-volume.serverid
volumeid = stackitvolume.example-volume.volume_id
}
```
Server with user data (cloud-init)
resource "stackit_server" "user-data" {
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
boot_volume = {
```terraform
resource "stackitserver" "user-data" {
projectid = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
bootvolume = {
size = 64
source_type = "image"
source_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
sourcetype = "image"
sourceid = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}
name = "example-server"
machine_type = "g1.1"
keypair_name = "example-keypair"
user_data = "#!/bin/bash\n/bin/su"
machinetype = "g1.1"
keypairname = stackitkeypair.keypair.name
userdata = "#!/bin/bash\n/bin/su"
}
resource "stackit_server" "user-data-from-file" {
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
boot_volume = {
resource "stackitserver" "user-data-from-file" {
projectid = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
bootvolume = {
size = 64
source_type = "image"
source_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
sourcetype = "image"
sourceid = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}
name = "example-server"
machine_type = "g1.1"
keypair_name = "example-keypair"
user_data = file("${path.module}/cloud-init.yaml")
machinetype = "g1.1"
keypairname = stackitkeypair.keypair.name
userdata = file("${path.module}/cloud-init.yaml")
}
```
---
# stackit_server (Resource)
@ -167,6 +173,28 @@ Server resource schema. Must have a region specified in the provider configurati
## Example Usage
### With key pair
```terraform
resource "stackit_key_pair" "keypair" {
name = "example-key-pair"
public_key = chomp(file("path/to/id_rsa.pub"))
}
resource "stackit_server" "user-data-from-file" {
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
boot_volume = {
size = 64
source_type = "image"
source_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}
name = "example-server"
machine_type = "g1.1"
keypair_name = stackit_key_pair.keypair.name
user_data = file("${path.module}/cloud-init.yaml")
}
```
### Boot from volume
```terraform
resource "stackit_server" "boot-from-volume" {
@ -206,7 +234,7 @@ resource "stackit_server" "boot-from-volume" {
}
availability_zone = "eu01-1"
machine_type = "g1.1"
keypair_name = "example-keypair"
keypair_name = stackit_key_pair.keypair.name
}
```
@ -222,7 +250,7 @@ resource "stackit_server" "server-with-network" {
source_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}
machine_type = "g1.1"
keypair_name = "example-keypair"
keypair_name = stackit_key_pair.keypair.name
}
resource "stackit_network" "network" {
@ -284,7 +312,7 @@ resource "stackit_server" "server-with-volume" {
}
availability_zone = "eu01-1"
machine_type = "g1.1"
keypair_name = "example-keypair"
keypair_name = stackit_key_pair.keypair.name
}
resource "stackit_server_volume_attach" "attach_volume" {
@ -306,7 +334,7 @@ resource "stackit_server" "user-data" {
}
name = "example-server"
machine_type = "g1.1"
keypair_name = "example-keypair"
keypair_name = stackit_key_pair.keypair.name
user_data = "#!/bin/bash\n/bin/su"
}
@ -319,7 +347,7 @@ resource "stackit_server" "user-data-from-file" {
}
name = "example-server"
machine_type = "g1.1"
keypair_name = "example-keypair"
keypair_name = stackit_key_pair.keypair.name
user_data = file("${path.module}/cloud-init.yaml")
}

View file

@ -0,0 +1,3 @@
data "stackit_key_pair" "example" {
name = "example-key-pair-name"
}

3
go.mod
View file

@ -14,7 +14,8 @@ require (
github.com/stackitcloud/stackit-sdk-go/core v0.14.0
github.com/stackitcloud/stackit-sdk-go/services/argus v0.11.0
github.com/stackitcloud/stackit-sdk-go/services/dns v0.11.0
github.com/stackitcloud/stackit-sdk-go/services/iaas v0.15.0
github.com/stackitcloud/stackit-sdk-go/services/iaas v0.16.0
github.com/stackitcloud/stackit-sdk-go/services/iaasalpha v0.1.12-alpha
github.com/stackitcloud/stackit-sdk-go/services/loadbalancer v0.17.0
github.com/stackitcloud/stackit-sdk-go/services/logme v0.20.0
github.com/stackitcloud/stackit-sdk-go/services/mariadb v0.20.0

6
go.sum
View file

@ -155,8 +155,10 @@ github.com/stackitcloud/stackit-sdk-go/services/authorization v0.4.0 h1:WXSIE4Kf
github.com/stackitcloud/stackit-sdk-go/services/authorization v0.4.0/go.mod h1:8spVqlPqZrvQQ63Qodbydk3qsZx7lr963ECft+sqFhY=
github.com/stackitcloud/stackit-sdk-go/services/dns v0.11.0 h1:+OZ82DwFy4JIJThadVjvll5kUWjHPSLbUIF65njsNBk=
github.com/stackitcloud/stackit-sdk-go/services/dns v0.11.0/go.mod h1:mv8U7kuclXo+0VpDHtBCkve/3i9h1yT+RAId/MUi+C8=
github.com/stackitcloud/stackit-sdk-go/services/iaas v0.15.0 h1:bPNv+PuSykBcKCYVXHiYOcqNP+KLCA7XMFSY4V6J6ug=
github.com/stackitcloud/stackit-sdk-go/services/iaas v0.15.0/go.mod h1:YfuN+eXuqr846xeRyW2Vf1JM2jU0ikeQa76dDI66RsM=
github.com/stackitcloud/stackit-sdk-go/services/iaas v0.16.0 h1:geyW780gqNxzSsPvmlxy3kUUJaRA4eiF9V3b2Ibcdjs=
github.com/stackitcloud/stackit-sdk-go/services/iaas v0.16.0/go.mod h1:YfuN+eXuqr846xeRyW2Vf1JM2jU0ikeQa76dDI66RsM=
github.com/stackitcloud/stackit-sdk-go/services/iaasalpha v0.1.12-alpha h1:jwpif4t2gthmKmCXsQ84rmtDdcZkw4QQTFiCd7nTW8M=
github.com/stackitcloud/stackit-sdk-go/services/iaasalpha v0.1.12-alpha/go.mod h1:nW/6vvumUHA7o1/JOOqsrEOBNrRHombEKB1U4jmg2wU=
github.com/stackitcloud/stackit-sdk-go/services/loadbalancer v0.17.0 h1:06CGP64CEk3Zg6i9kZCMRdmCzLLiyMWQqGK1teBr9Oc=
github.com/stackitcloud/stackit-sdk-go/services/loadbalancer v0.17.0/go.mod h1:JL94zc8K0ebWs+DBGXR28vNCF0EFV54ZLUtrlXOvWgA=
github.com/stackitcloud/stackit-sdk-go/services/logme v0.20.0 h1:V0UGP7JEa4Q8SsZFUJsKgLGaoPruLn2KVKnqQtaoWCU=

View file

@ -12,6 +12,7 @@ import (
"github.com/stackitcloud/stackit-sdk-go/core/config"
"github.com/stackitcloud/stackit-sdk-go/core/utils"
"github.com/stackitcloud/stackit-sdk-go/services/iaas"
"github.com/stackitcloud/stackit-sdk-go/services/iaasalpha"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/testutil"
)
@ -96,6 +97,14 @@ var publicIpResource = map[string]string{
"network_interface_id": testutil.IaaSNetworkInterfaceId,
}
// Key pair resource data
var keyPairResource = map[string]string{
"name": "key-pair-name",
"public_key": `ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIDsPd27M449akqCtdFg2+AmRVJz6eWio0oMP9dVg7XZ`,
"label1": "value1",
"label1-updated": "value1-updated",
}
func networkResourceConfig(name, nameservers string) string {
return fmt.Sprintf(`
resource "stackit_network" "network" {
@ -323,6 +332,13 @@ func testAccPublicIpConfig(publicIpResourceConfig string) string {
)
}
func testAccKeyPairConfig(keyPairResourceConfig string) string {
return fmt.Sprintf("%s\n\n%s",
testutil.IaaSProviderConfig(),
keyPairResourceConfig,
)
}
func TestAccNetworkArea(t *testing.T) {
resource.Test(t, resource.TestCase{
ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories,
@ -1113,6 +1129,117 @@ func TestAccPublicIp(t *testing.T) {
})
}
func TestAccKeyPair(t *testing.T) {
resource.Test(t, resource.TestCase{
ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories,
CheckDestroy: testAccCheckIaaSKeyPairDestroy,
Steps: []resource.TestStep{
// Creation
{
Config: testAccKeyPairConfig(
fmt.Sprintf(`
resource "stackit_key_pair" "key_pair" {
name = "%s"
public_key = "%s"
labels = {
"label1" = "%s"
}
}
`,
keyPairResource["name"],
keyPairResource["public_key"],
keyPairResource["label1"],
),
),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("stackit_key_pair.key_pair", "name", keyPairResource["name"]),
resource.TestCheckResourceAttr("stackit_key_pair.key_pair", "labels.label1", keyPairResource["label1"]),
resource.TestCheckResourceAttr("stackit_key_pair.key_pair", "public_key", keyPairResource["public_key"]),
resource.TestCheckResourceAttrSet("stackit_key_pair.key_pair", "fingerprint"),
),
},
// Data source
{
Config: fmt.Sprintf(`
%s
data "stackit_key_pair" "key_pair" {
name = stackit_key_pair.key_pair.name
}
`,
testAccKeyPairConfig(
fmt.Sprintf(`
resource "stackit_key_pair" "key_pair" {
name = "%s"
public_key = "%s"
labels = {
"label1" = "%s"
}
}
`,
keyPairResource["name"],
keyPairResource["public_key"],
keyPairResource["label1"],
),
),
),
Check: resource.ComposeAggregateTestCheckFunc(
// Instance
resource.TestCheckResourceAttr("data.stackit_key_pair.key_pair", "name", keyPairResource["name"]),
resource.TestCheckResourceAttr("data.stackit_key_pair.key_pair", "public_key", keyPairResource["public_key"]),
resource.TestCheckResourceAttr("stackit_key_pair.key_pair", "labels.label1", keyPairResource["label1"]),
resource.TestCheckResourceAttrPair(
"stackit_key_pair.key_pair", "fingerprint",
"data.stackit_key_pair.key_pair", "fingerprint",
),
),
},
// Import
{
ResourceName: "stackit_key_pair.key_pair",
ImportStateIdFunc: func(s *terraform.State) (string, error) {
r, ok := s.RootModule().Resources["stackit_key_pair.key_pair"]
if !ok {
return "", fmt.Errorf("couldn't find resource stackit_key_pair.key_pair")
}
keyPairName, ok := r.Primary.Attributes["name"]
if !ok {
return "", fmt.Errorf("couldn't find attribute name")
}
return keyPairName, nil
},
ImportState: true,
ImportStateVerify: true,
},
// Update
{
Config: testAccKeyPairConfig(
fmt.Sprintf(`
resource "stackit_key_pair" "key_pair" {
name = "%s"
public_key = "%s"
labels = {
"label1" = "%s"
}
}
`,
keyPairResource["name"],
keyPairResource["public_key"],
keyPairResource["label1-updated"],
),
),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("stackit_key_pair.key_pair", "name", keyPairResource["name"]),
resource.TestCheckResourceAttr("stackit_key_pair.key_pair", "labels.label1", keyPairResource["label1-updated"]),
resource.TestCheckResourceAttrSet("stackit_key_pair.key_pair", "fingerprint"),
),
},
// Deletion is done by the framework implicitly
},
})
}
func testAccCheckNetworkAreaDestroy(s *terraform.State) error {
ctx := context.Background()
var client *iaas.APIClient
@ -1388,3 +1515,49 @@ func testAccCheckIaaSPublicIpDestroy(s *terraform.State) error {
}
return nil
}
func testAccCheckIaaSKeyPairDestroy(s *terraform.State) error {
ctx := context.Background()
var client *iaasalpha.APIClient
var err error
if testutil.IaaSCustomEndpoint == "" {
client, err = iaasalpha.NewAPIClient(
config.WithRegion("eu01"),
)
} else {
client, err = iaasalpha.NewAPIClient(
config.WithEndpoint(testutil.IaaSCustomEndpoint),
)
}
if err != nil {
return fmt.Errorf("creating client: %w", err)
}
keyPairsToDestroy := []string{}
for _, rs := range s.RootModule().Resources {
if rs.Type != "stackit_key_pair" {
continue
}
// Key pair terraform ID: "[name]"
keyPairsToDestroy = append(keyPairsToDestroy, rs.Primary.ID)
}
keyPairsResp, err := client.ListKeyPairsExecute(ctx)
if err != nil {
return fmt.Errorf("getting key pairs: %w", err)
}
keyPairs := *keyPairsResp.Items
for i := range keyPairs {
if keyPairs[i].Name == nil {
continue
}
if utils.Contains(keyPairsToDestroy, *keyPairs[i].Name) {
err := client.DeleteKeyPairExecute(ctx, *keyPairs[i].Name)
if err != nil {
return fmt.Errorf("destroying key pair %s during CheckDestroy: %w", *keyPairs[i].Name, err)
}
}
}
return nil
}

View file

@ -0,0 +1,25 @@
package keypair
const exampleUsageWithServer = `
### Usage with server` + "\n" +
"```terraform" + `
resource "stackit_key_pair" "keypair" {
name = "example-key-pair"
public_key = chomp(file("path/to/id_rsa.pub"))
}
resource "stackit_server" "example-server" {
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
name = "example-server"
boot_volume = {
size = 64
source_type = "image"
source_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}
availability_zone = "eu01-1"
machine_type = "g1.1"
keypair_name = "example-key-pair"
}
`

View file

@ -0,0 +1,155 @@
package keypair
import (
"context"
"fmt"
"net/http"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/stackitcloud/stackit-sdk-go/core/config"
"github.com/stackitcloud/stackit-sdk-go/core/oapierror"
"github.com/stackitcloud/stackit-sdk-go/services/iaas"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features"
)
// keyPairDataSourceBetaCheckDone is used to prevent multiple checks for beta resources.
// This is a workaround for the lack of a global state in the provider and
// needs to exist because the Configure method is called twice.
var keyPairDataSourceBetaCheckDone bool
// Ensure the implementation satisfies the expected interfaces.
var (
_ datasource.DataSource = &keyPairDataSource{}
)
// NewVolumeDataSource is a helper function to simplify the provider implementation.
func NewKeyPairDataSource() datasource.DataSource {
return &keyPairDataSource{}
}
// keyPairDataSource is the data source implementation.
type keyPairDataSource struct {
client *iaas.APIClient
}
// Metadata returns the data source type name.
func (d *keyPairDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_key_pair"
}
func (d *keyPairDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
// Prevent panic if the provider has not been configured.
if req.ProviderData == nil {
return
}
var apiClient *iaas.APIClient
var err error
providerData, ok := req.ProviderData.(core.ProviderData)
if !ok {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData))
return
}
if !keyPairDataSourceBetaCheckDone {
features.CheckBetaResourcesEnabled(ctx, &providerData, &resp.Diagnostics, "stackit_key_pair", "data source")
if resp.Diagnostics.HasError() {
return
}
keyPairDataSourceBetaCheckDone = true
}
if providerData.IaaSCustomEndpoint != "" {
apiClient, err = iaas.NewAPIClient(
config.WithCustomAuth(providerData.RoundTripper),
config.WithEndpoint(providerData.IaaSCustomEndpoint),
)
} else {
apiClient, err = iaas.NewAPIClient(
config.WithCustomAuth(providerData.RoundTripper),
config.WithRegion(providerData.Region),
)
}
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Configuring client: %v. This is an error related to the provider configuration, not to the data source configuration", err))
return
}
d.client = apiClient
tflog.Info(ctx, "iaas client configured")
}
// Schema defines the schema for the resource.
func (r *keyPairDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
description := "Key pair resource schema. Must have a `region` specified in the provider configuration."
resp.Schema = schema.Schema{
MarkdownDescription: features.AddBetaDescription(description),
Description: description,
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Description: "Terraform's internal resource ID. It takes the value of the key pair \"`name`\".",
Computed: true,
},
"name": schema.StringAttribute{
Description: "The name of the SSH key pair.",
Required: true,
},
"public_key": schema.StringAttribute{
Description: "A string representation of the public SSH key. E.g., `ssh-rsa <key_data>` or `ssh-ed25519 <key-data>`.",
Computed: true,
},
"fingerprint": schema.StringAttribute{
Description: "The fingerprint of the public SSH key.",
Computed: true,
},
"labels": schema.MapAttribute{
Description: "Labels are key-value string pairs which can be attached to a resource container.",
ElementType: types.StringType,
Computed: true,
},
},
}
}
// Read refreshes the Terraform state with the latest data.
func (r *keyPairDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform
var model Model
diags := req.Config.Get(ctx, &model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
name := model.Name.ValueString()
ctx = tflog.SetField(ctx, "name", name)
keypairResp, err := r.client.GetKeyPair(ctx, name).Execute()
if err != nil {
oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped
if ok && oapiErr.StatusCode == http.StatusNotFound {
resp.State.RemoveResource(ctx)
return
}
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading key pair", fmt.Sprintf("Calling API: %v", err))
return
}
// Map response body to schema
err = mapFields(ctx, keypairResp, &model)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading key pair", fmt.Sprintf("Processing API payload: %v", err))
return
}
// Set refreshed state
diags = resp.State.Set(ctx, model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
tflog.Info(ctx, "Key pair read")
}

View file

@ -0,0 +1,411 @@
package keypair
import (
"context"
"fmt"
"net/http"
"strings"
"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-framework/types"
"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/stackitcloud/stackit-sdk-go/core/config"
"github.com/stackitcloud/stackit-sdk-go/core/oapierror"
"github.com/stackitcloud/stackit-sdk-go/services/iaas"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features"
)
// resourceBetaCheckDone is used to prevent multiple checks for beta resources.
// This is a workaround for the lack of a global state in the provider and
// needs to exist because the Configure method is called twice.
var resourceBetaCheckDone bool
// Ensure the implementation satisfies the expected interfaces.
var (
_ resource.Resource = &keyPairResource{}
_ resource.ResourceWithConfigure = &keyPairResource{}
_ resource.ResourceWithImportState = &keyPairResource{}
)
type Model struct {
Id types.String `tfsdk:"id"` // needed by TF
Name types.String `tfsdk:"name"`
PublicKey types.String `tfsdk:"public_key"`
Fingerprint types.String `tfsdk:"fingerprint"`
Labels types.Map `tfsdk:"labels"`
}
// NewKeyPairResource is a helper function to simplify the provider implementation.
func NewKeyPairResource() resource.Resource {
return &keyPairResource{}
}
// keyPairResource is the resource implementation.
type keyPairResource struct {
client *iaas.APIClient
}
// Metadata returns the resource type name.
func (r *keyPairResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_key_pair"
}
// Configure adds the provider configured client to the resource.
func (r *keyPairResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
// Prevent panic if the provider has not been configured.
if req.ProviderData == nil {
return
}
providerData, ok := req.ProviderData.(core.ProviderData)
if !ok {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData))
return
}
if !resourceBetaCheckDone {
features.CheckBetaResourcesEnabled(ctx, &providerData, &resp.Diagnostics, "stackit_key_pair", "resource")
if resp.Diagnostics.HasError() {
return
}
resourceBetaCheckDone = true
}
var apiClient *iaas.APIClient
var err error
if providerData.IaaSCustomEndpoint != "" {
ctx = tflog.SetField(ctx, "iaas_custom_endpoint", providerData.IaaSCustomEndpoint)
apiClient, err = iaas.NewAPIClient(
config.WithCustomAuth(providerData.RoundTripper),
config.WithEndpoint(providerData.IaaSCustomEndpoint),
)
} else {
apiClient, err = iaas.NewAPIClient(
config.WithCustomAuth(providerData.RoundTripper),
config.WithRegion(providerData.Region),
)
}
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Configuring client: %v. This is an error related to the provider configuration, not to the resource configuration", err))
return
}
r.client = apiClient
tflog.Info(ctx, "iaas client configured")
}
// Schema defines the schema for the resource.
func (r *keyPairResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
description := "Key pair resource schema. Must have a `region` specified in the provider configuration. Allows uploading an SSH public key to be used for server authentication."
resp.Schema = schema.Schema{
MarkdownDescription: features.AddBetaDescription(description + "\n\n" + exampleUsageWithServer),
Description: description,
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Description: "Terraform's internal resource ID. It takes the value of the key pair \"`name`\".",
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"name": schema.StringAttribute{
Description: "The name of the SSH key pair.",
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"public_key": schema.StringAttribute{
Description: "A string representation of the public SSH key. E.g., `ssh-rsa <key_data>` or `ssh-ed25519 <key-data>`.",
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
},
"fingerprint": schema.StringAttribute{
Description: "The fingerprint of the public SSH key.",
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"labels": schema.MapAttribute{
Description: "Labels are key-value string pairs which can be attached to a resource container.",
ElementType: types.StringType,
Optional: true,
},
},
}
}
// ModifyPlan will be called in the Plan phase.
// It will check if the plan contains a change that requires replacement. If yes, it will show a warning to the user.
func (r *keyPairResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform
// If the state is empty we are creating a new resource
// If the plan is empty we are deleting the resource
// In both cases we don't need to check for replacement
if req.Plan.Raw.IsNull() || req.State.Raw.IsNull() {
return
}
var planModel Model
diags := req.Plan.Get(ctx, &planModel)
resp.Diagnostics.Append(diags...)
var stateModel Model
diags = req.State.Get(ctx, &stateModel)
resp.Diagnostics.Append(diags...)
if planModel.PublicKey.ValueString() != stateModel.PublicKey.ValueString() {
core.LogAndAddWarning(ctx, &resp.Diagnostics, "Key pair public key change", "Changing the public key will trigger a replacement of the key pair resource. The new key pair will not be valid to access servers on which the old key was used, as the key is only registered during server creation.")
}
}
// Create creates the resource and sets the initial Terraform state.
func (r *keyPairResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform
// Retrieve values from plan
var model Model
diags := req.Plan.Get(ctx, &model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
name := model.Name.ValueString()
ctx = tflog.SetField(ctx, "name", name)
// Generate API request body from model
payload, err := toCreatePayload(ctx, &model)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating key pair", fmt.Sprintf("Creating API payload: %v", err))
return
}
// Create new key pair
keyPair, err := r.client.CreateKeyPair(ctx).CreateKeyPairPayload(*payload).Execute()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating key pair", fmt.Sprintf("Calling API: %v", err))
return
}
// Map response body to schema
err = mapFields(ctx, keyPair, &model)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating key pair", fmt.Sprintf("Processing API payload: %v", err))
return
}
// Set state to fully populated data
diags = resp.State.Set(ctx, model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
tflog.Info(ctx, "Key pair created")
}
// Read refreshes the Terraform state with the latest data.
func (r *keyPairResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform
var model Model
diags := req.State.Get(ctx, &model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
name := model.Name.ValueString()
ctx = tflog.SetField(ctx, "name", name)
keyPairResp, err := r.client.GetKeyPair(ctx, name).Execute()
if err != nil {
oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped
if ok && oapiErr.StatusCode == http.StatusNotFound {
resp.State.RemoveResource(ctx)
return
}
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading key pair", fmt.Sprintf("Calling API: %v", err))
return
}
// Map response body to schema
err = mapFields(ctx, keyPairResp, &model)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading key pair", fmt.Sprintf("Processing API payload: %v", err))
return
}
// Set refreshed state
diags = resp.State.Set(ctx, model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
tflog.Info(ctx, "Key pair read")
}
// Update updates the resource and sets the updated Terraform state on success.
func (r *keyPairResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform
// Retrieve values from plan
var model Model
diags := req.Plan.Get(ctx, &model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
name := model.Name.ValueString()
ctx = tflog.SetField(ctx, "name", name)
// Retrieve values from state
var stateModel Model
diags = req.State.Get(ctx, &stateModel)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
// Generate API request body from model
payload, err := toUpdatePayload(ctx, &model, stateModel.Labels)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating key pair", fmt.Sprintf("Creating API payload: %v", err))
return
}
// Update existing key pair
updatedKeyPair, err := r.client.UpdateKeyPair(ctx, name).UpdateKeyPairPayload(*payload).Execute()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating key pair", fmt.Sprintf("Calling API: %v", err))
return
}
err = mapFields(ctx, updatedKeyPair, &model)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating key pair", fmt.Sprintf("Processing API payload: %v", err))
return
}
diags = resp.State.Set(ctx, model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
tflog.Info(ctx, "key pair updated")
}
// Delete deletes the resource and removes the Terraform state on success.
func (r *keyPairResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform
// Retrieve values from state
var model Model
diags := req.State.Get(ctx, &model)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
name := model.Name.ValueString()
ctx = tflog.SetField(ctx, "name", name)
// Delete existing key pair
err := r.client.DeleteKeyPair(ctx, name).Execute()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting key pair", fmt.Sprintf("Calling API: %v", err))
return
}
tflog.Info(ctx, "Key pair deleted")
}
// ImportState imports a resource into the Terraform state on success.
// The expected format of the resource import identifier is: project_id,key_pair_id
func (r *keyPairResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
idParts := strings.Split(req.ID, core.Separator)
if len(idParts) != 1 || idParts[0] == "" {
core.LogAndAddError(ctx, &resp.Diagnostics,
"Error importing key pair",
fmt.Sprintf("Expected import identifier with format: [name] Got: %q", req.ID),
)
return
}
name := idParts[0]
ctx = tflog.SetField(ctx, "name", name)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("name"), name)...)
tflog.Info(ctx, "Key pair state imported")
}
func mapFields(ctx context.Context, keyPairResp *iaas.Keypair, model *Model) error {
if keyPairResp == nil {
return fmt.Errorf("response input is nil")
}
if model == nil {
return fmt.Errorf("model input is nil")
}
var name string
if model.Name.ValueString() != "" {
name = model.Name.ValueString()
} else if keyPairResp.Name != nil {
name = *keyPairResp.Name
} else {
return fmt.Errorf("key pair name not present")
}
model.Id = types.StringValue(name)
model.PublicKey = types.StringPointerValue(keyPairResp.PublicKey)
model.Fingerprint = types.StringPointerValue(keyPairResp.Fingerprint)
labels, diags := types.MapValueFrom(ctx, types.StringType, map[string]interface{}{})
if diags.HasError() {
return fmt.Errorf("converting labels to StringValue map: %w", core.DiagsToError(diags))
}
if keyPairResp.Labels != nil && len(*keyPairResp.Labels) != 0 {
var diags diag.Diagnostics
labels, diags = types.MapValueFrom(ctx, types.StringType, *keyPairResp.Labels)
if diags.HasError() {
return fmt.Errorf("converting labels to StringValue map: %w", core.DiagsToError(diags))
}
} else if model.Labels.IsNull() {
labels = types.MapNull(types.StringType)
}
model.Labels = labels
return nil
}
func toCreatePayload(ctx context.Context, model *Model) (*iaas.CreateKeyPairPayload, error) {
if model == nil {
return nil, fmt.Errorf("nil model")
}
labels, err := conversion.ToStringInterfaceMap(ctx, model.Labels)
if err != nil {
return nil, fmt.Errorf("converting to Go map: %w", err)
}
return &iaas.CreateKeyPairPayload{
Name: conversion.StringValueToPointer(model.Name),
PublicKey: conversion.StringValueToPointer(model.PublicKey),
Labels: &labels,
}, nil
}
func toUpdatePayload(ctx context.Context, model *Model, currentLabels types.Map) (*iaas.UpdateKeyPairPayload, error) {
if model == nil {
return nil, fmt.Errorf("nil model")
}
labels, err := conversion.ToJSONMapPartialUpdatePayload(ctx, currentLabels, model.Labels)
if err != nil {
return nil, fmt.Errorf("converting to Go map: %w", err)
}
return &iaas.UpdateKeyPairPayload{
Labels: &labels,
}, nil
}

View file

@ -0,0 +1,211 @@
package keypair
import (
"context"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/stackitcloud/stackit-sdk-go/core/utils"
"github.com/stackitcloud/stackit-sdk-go/services/iaas"
)
func TestMapFields(t *testing.T) {
tests := []struct {
description string
state Model
input *iaas.Keypair
expected Model
isValid bool
}{
{
"default_values",
Model{
Name: types.StringValue("name"),
},
&iaas.Keypair{
Name: utils.Ptr("name"),
},
Model{
Id: types.StringValue("name"),
Name: types.StringValue("name"),
PublicKey: types.StringNull(),
Fingerprint: types.StringNull(),
Labels: types.MapNull(types.StringType),
},
true,
},
{
"simple_values",
Model{
Name: types.StringValue("name"),
},
&iaas.Keypair{
Name: utils.Ptr("name"),
PublicKey: utils.Ptr("public_key"),
Fingerprint: utils.Ptr("fingerprint"),
Labels: &map[string]interface{}{
"key": "value",
},
},
Model{
Id: types.StringValue("name"),
Name: types.StringValue("name"),
PublicKey: types.StringValue("public_key"),
Fingerprint: types.StringValue("fingerprint"),
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{
"key": types.StringValue("value"),
}),
},
true,
},
{
"empty_labels",
Model{
Name: types.StringValue("name"),
},
&iaas.Keypair{
Name: utils.Ptr("name"),
PublicKey: utils.Ptr("public_key"),
Fingerprint: utils.Ptr("fingerprint"),
Labels: &map[string]interface{}{},
},
Model{
Id: types.StringValue("name"),
Name: types.StringValue("name"),
PublicKey: types.StringValue("public_key"),
Fingerprint: types.StringValue("fingerprint"),
Labels: types.MapNull(types.StringType),
},
true,
},
{
"response_nil_fail",
Model{},
nil,
Model{},
false,
},
{
"no_resource_id",
Model{},
&iaas.Keypair{
PublicKey: utils.Ptr("public_key"),
Fingerprint: utils.Ptr("fingerprint"),
Labels: &map[string]interface{}{},
},
Model{},
false,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
err := mapFields(context.Background(), tt.input, &tt.state)
if !tt.isValid && err == nil {
t.Fatalf("Should have failed")
}
if tt.isValid && err != nil {
t.Fatalf("Should not have failed: %v", err)
}
if tt.isValid {
diff := cmp.Diff(tt.state, tt.expected)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
}
})
}
}
func TestToCreatePayload(t *testing.T) {
tests := []struct {
description string
input *Model
expected *iaas.CreateKeyPairPayload
isValid bool
}{
{
"default_ok",
&Model{
Name: types.StringValue("name"),
PublicKey: types.StringValue("public_key"),
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{
"key1": types.StringValue("value1"),
"key2": types.StringValue("value2"),
}),
},
&iaas.CreateKeyPairPayload{
Name: utils.Ptr("name"),
PublicKey: utils.Ptr("public_key"),
Labels: &map[string]interface{}{
"key1": "value1",
"key2": "value2",
},
},
true,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
output, err := toCreatePayload(context.Background(), tt.input)
if !tt.isValid && err == nil {
t.Fatalf("Should have failed")
}
if tt.isValid && err != nil {
t.Fatalf("Should not have failed: %v", err)
}
if tt.isValid {
diff := cmp.Diff(output, tt.expected, cmp.AllowUnexported(iaas.NullableString{}))
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
}
})
}
}
func TestToUpdatePayload(t *testing.T) {
tests := []struct {
description string
input *Model
expected *iaas.UpdateKeyPairPayload
isValid bool
}{
{
"default_ok",
&Model{
Name: types.StringValue("name"),
PublicKey: types.StringValue("public_key"),
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{
"key1": types.StringValue("value1"),
"key2": types.StringValue("value2"),
}),
},
&iaas.UpdateKeyPairPayload{
Labels: &map[string]interface{}{
"key1": "value1",
"key2": "value2",
},
},
true,
},
}
for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
output, err := toUpdatePayload(context.Background(), tt.input, types.MapNull(types.StringType))
if !tt.isValid && err == nil {
t.Fatalf("Should have failed")
}
if tt.isValid && err != nil {
t.Fatalf("Should not have failed: %v", err)
}
if tt.isValid {
diff := cmp.Diff(output, tt.expected, cmp.AllowUnexported(iaas.NullableString{}))
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
}
})
}
}

View file

@ -181,7 +181,7 @@ func (d *networkInterfaceDataSource) Read(ctx context.Context, req datasource.Re
ctx = tflog.SetField(ctx, "network_id", networkId)
ctx = tflog.SetField(ctx, "network_interface_id", networkInterfaceId)
networkInterfaceResp, err := d.client.GetNIC(ctx, projectId, networkId, networkInterfaceId).Execute()
networkInterfaceResp, err := d.client.GetNic(ctx, projectId, networkId, networkInterfaceId).Execute()
if err != nil {
oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped
if ok && oapiErr.StatusCode == http.StatusNotFound {

View file

@ -271,7 +271,7 @@ func (r *networkInterfaceResource) Create(ctx context.Context, req resource.Crea
}
// Create new network interface
networkInterface, err := r.client.CreateNIC(ctx, projectId, networkId).CreateNICPayload(*payload).Execute()
networkInterface, err := r.client.CreateNic(ctx, projectId, networkId).CreateNicPayload(*payload).Execute()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network interface", fmt.Sprintf("Calling API: %v", err))
return
@ -311,7 +311,7 @@ func (r *networkInterfaceResource) Read(ctx context.Context, req resource.ReadRe
ctx = tflog.SetField(ctx, "network_id", networkId)
ctx = tflog.SetField(ctx, "network_interface_id", networkInterfaceId)
networkInterfaceResp, err := r.client.GetNIC(ctx, projectId, networkId, networkInterfaceId).Execute()
networkInterfaceResp, err := r.client.GetNic(ctx, projectId, networkId, networkInterfaceId).Execute()
if err != nil {
oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped
if ok && oapiErr.StatusCode == http.StatusNotFound {
@ -368,7 +368,7 @@ func (r *networkInterfaceResource) Update(ctx context.Context, req resource.Upda
return
}
// Update existing network
nicResp, err := r.client.UpdateNIC(ctx, projectId, networkId, networkInterfaceId).UpdateNICPayload(*payload).Execute()
nicResp, err := r.client.UpdateNic(ctx, projectId, networkId, networkInterfaceId).UpdateNicPayload(*payload).Execute()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network interface", fmt.Sprintf("Calling API: %v", err))
return
@ -405,7 +405,7 @@ func (r *networkInterfaceResource) Delete(ctx context.Context, req resource.Dele
ctx = tflog.SetField(ctx, "network_interface_id", networkInterfaceId)
// Delete existing network interface
err := r.client.DeleteNIC(ctx, projectId, networkId, networkInterfaceId).Execute()
err := r.client.DeleteNic(ctx, projectId, networkId, networkInterfaceId).Execute()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting network interface", fmt.Sprintf("Calling API: %v", err))
return
@ -544,7 +544,7 @@ func mapFields(ctx context.Context, networkInterfaceResp *iaas.NIC, model *Model
return nil
}
func toCreatePayload(ctx context.Context, model *Model) (*iaas.CreateNICPayload, error) {
func toCreatePayload(ctx context.Context, model *Model) (*iaas.CreateNicPayload, error) {
if model == nil {
return nil, fmt.Errorf("nil model")
}
@ -586,7 +586,7 @@ func toCreatePayload(ctx context.Context, model *Model) (*iaas.CreateNICPayload,
labelPayload = &labelMap
}
return &iaas.CreateNICPayload{
return &iaas.CreateNicPayload{
AllowedAddresses: allowedAddressesPayload,
SecurityGroups: &modelSecurityGroups,
Labels: labelPayload,
@ -599,7 +599,7 @@ func toCreatePayload(ctx context.Context, model *Model) (*iaas.CreateNICPayload,
}, nil
}
func toUpdatePayload(ctx context.Context, model *Model, currentLabels types.Map) (*iaas.UpdateNICPayload, error) {
func toUpdatePayload(ctx context.Context, model *Model, currentLabels types.Map) (*iaas.UpdateNicPayload, error) {
if model == nil {
return nil, fmt.Errorf("nil model")
}
@ -637,7 +637,7 @@ func toUpdatePayload(ctx context.Context, model *Model, currentLabels types.Map)
labelPayload = &labelMap
}
return &iaas.UpdateNICPayload{
return &iaas.UpdateNicPayload{
AllowedAddresses: &allowedAddressesPayload,
SecurityGroups: &modelSecurityGroups,
Labels: labelPayload,

View file

@ -194,7 +194,7 @@ func TestToCreatePayload(t *testing.T) {
tests := []struct {
description string
input *Model
expected *iaas.CreateNICPayload
expected *iaas.CreateNicPayload
isValid bool
}{
{
@ -210,7 +210,7 @@ func TestToCreatePayload(t *testing.T) {
}),
Security: types.BoolValue(true),
},
&iaas.CreateNICPayload{
&iaas.CreateNicPayload{
Name: utils.Ptr("name"),
SecurityGroups: &[]string{
"sg1",
@ -236,7 +236,7 @@ func TestToCreatePayload(t *testing.T) {
AllowedAddresses: types.ListNull(types.StringType),
},
&iaas.CreateNICPayload{
&iaas.CreateNicPayload{
Name: utils.Ptr("name"),
SecurityGroups: &[]string{
"sg1",
@ -270,7 +270,7 @@ func TestToUpdatePayload(t *testing.T) {
tests := []struct {
description string
input *Model
expected *iaas.UpdateNICPayload
expected *iaas.UpdateNicPayload
isValid bool
}{
{
@ -286,7 +286,7 @@ func TestToUpdatePayload(t *testing.T) {
}),
Security: types.BoolValue(true),
},
&iaas.UpdateNICPayload{
&iaas.UpdateNicPayload{
Name: utils.Ptr("name"),
SecurityGroups: &[]string{
"sg1",
@ -312,7 +312,7 @@ func TestToUpdatePayload(t *testing.T) {
AllowedAddresses: types.ListNull(types.StringType),
},
&iaas.UpdateNICPayload{
&iaas.UpdateNicPayload{
Name: utils.Ptr("name"),
SecurityGroups: &[]string{
"sg1",

View file

@ -169,7 +169,7 @@ func (r *networkInterfaceAttachResource) Create(ctx context.Context, req resourc
ctx = tflog.SetField(ctx, "network_interface_id", networkInterfaceId)
// Create new network interface attachment
err := r.client.AddNICToServer(ctx, projectId, serverId, networkInterfaceId).Execute()
err := r.client.AddNicToServer(ctx, projectId, serverId, networkInterfaceId).Execute()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error attaching network interface to server", fmt.Sprintf("Calling API: %v", err))
return
@ -208,7 +208,7 @@ func (r *networkInterfaceAttachResource) Read(ctx context.Context, req resource.
networkInterfaceId := model.NetworkInterfaceId.ValueString()
ctx = tflog.SetField(ctx, "network_interface_id", networkInterfaceId)
nics, err := r.client.ListServerNICs(ctx, projectId, serverId).Execute()
nics, err := r.client.ListServerNics(ctx, projectId, serverId).Execute()
if err != nil {
oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped
if ok && oapiErr.StatusCode == http.StatusNotFound {
@ -267,7 +267,7 @@ func (r *networkInterfaceAttachResource) Delete(ctx context.Context, req resourc
ctx = tflog.SetField(ctx, "network_interface_id", network_interfaceId)
// Remove network_interface from server
err := r.client.RemoveNICFromServer(ctx, projectId, serverId, network_interfaceId).Execute()
err := r.client.RemoveNicFromServer(ctx, projectId, serverId, network_interfaceId).Execute()
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error removing network interface from server", fmt.Sprintf("Calling API: %v", err))
return

View file

@ -89,8 +89,8 @@ func (d *publicIpDataSource) Configure(ctx context.Context, req datasource.Confi
// Schema defines the schema for the resource.
func (r *publicIpDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: features.AddBetaDescription("Volume resource schema. Must have a `region` specified in the provider configuration."),
Description: "Volume resource schema. Must have a `region` specified in the provider configuration.",
MarkdownDescription: features.AddBetaDescription("Public IP resource schema. Must have a `region` specified in the provider configuration."),
Description: "Public IP resource schema. Must have a `region` specified in the provider configuration.",
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Description: "Terraform's internal datasource ID. It is structured as \"`project_id`,`public_ip_id`\".",

View file

@ -5,6 +5,28 @@ Server resource schema. Must have a region specified in the provider configurati
~> This resource is in beta and may be subject to breaking changes in the future. Use with caution. See our [guide](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/guides/opting_into_beta_resources) for how to opt-in to use beta resources.
## Example Usage` + "\n" + `
### With key pair` + "\n" +
"```terraform" + `
resource "stackit_key_pair" "keypair" {
name = "example-key-pair"
public_key = chomp(file("path/to/id_rsa.pub"))
}
resource "stackit_server" "user-data-from-file" {
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
boot_volume = {
size = 64
source_type = "image"
source_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}
name = "example-server"
machine_type = "g1.1"
keypair_name = stackit_key_pair.keypair.name
user_data = file("${path.module}/cloud-init.yaml")
}
` + "\n```" + `
### Boot from volume` + "\n" +
"```terraform" + `
@ -45,7 +67,7 @@ resource "stackit_server" "boot-from-volume" {
}
availability_zone = "eu01-1"
machine_type = "g1.1"
keypair_name = "example-keypair"
keypair_name = stackit_key_pair.keypair.name
}
` + "\n```" + `
@ -61,7 +83,7 @@ resource "stackit_server" "server-with-network" {
source_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}
machine_type = "g1.1"
keypair_name = "example-keypair"
keypair_name = stackit_key_pair.keypair.name
}
resource "stackit_network" "network" {
@ -123,7 +145,7 @@ resource "stackit_server" "server-with-volume" {
}
availability_zone = "eu01-1"
machine_type = "g1.1"
keypair_name = "example-keypair"
keypair_name = stackit_key_pair.keypair.name
}
resource "stackit_server_volume_attach" "attach_volume" {
@ -145,7 +167,7 @@ resource "stackit_server" "user-data" {
}
name = "example-server"
machine_type = "g1.1"
keypair_name = "example-keypair"
keypair_name = stackit_key_pair.keypair.name
user_data = "#!/bin/bash\n/bin/su"
}
@ -158,7 +180,7 @@ resource "stackit_server" "user-data-from-file" {
}
name = "example-server"
machine_type = "g1.1"
keypair_name = "example-keypair"
keypair_name = stackit_key_pair.keypair.name
user_data = file("${path.module}/cloud-init.yaml")
}
` + "\n```"

View file

@ -563,7 +563,7 @@ func mapFields(ctx context.Context, serverResp *iaas.Server, model *Model) error
} else if serverResp.Id != nil {
serverId = *serverResp.Id
} else {
return fmt.Errorf("Server id not present")
return fmt.Errorf("server id not present")
}
idParts := []string{

View file

@ -14,6 +14,7 @@ import (
argusScrapeConfig "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/argus/scrapeconfig"
dnsRecordSet "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/dns/recordset"
dnsZone "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/dns/zone"
iaasKeyPair "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/keypair"
iaasNetwork "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/network"
iaasNetworkArea "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/networkarea"
iaasNetworkAreaRoute "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/networkarearoute"
@ -412,6 +413,7 @@ func (p *Provider) DataSources(_ context.Context) []func() datasource.DataSource
iaasNetworkInterface.NewNetworkInterfaceDataSource,
iaasVolume.NewVolumeDataSource,
iaasPublicIp.NewPublicIpDataSource,
iaasKeyPair.NewKeyPairDataSource,
iaasServer.NewServerDataSource,
iaasSecurityGroup.NewSecurityGroupDataSource,
iaasSecurityGroupRule.NewSecurityGroupRuleDataSource,
@ -462,6 +464,7 @@ func (p *Provider) Resources(_ context.Context) []func() resource.Resource {
iaasNetworkInterface.NewNetworkInterfaceResource,
iaasVolume.NewVolumeResource,
iaasPublicIp.NewPublicIpResource,
iaasKeyPair.NewKeyPairResource,
iaasVolumeAttach.NewVolumeAttachResource,
iaasNetworkInterfaceAttach.NewNetworkInterfaceAttachResource,
iaasServiceAccountAttach.NewServiceAccountAttachResource,