diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 161a6d0922932bda37ea8ac5087492a2bea916e8..f050bebe91296e19e0bd0c5447c1f1fa0fa3d1d1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -51,7 +51,6 @@ jobs: name: Terraform Provider Acceptance Tests needs: build runs-on: ubuntu-latest - timeout-minutes: 15 strategy: fail-fast: false matrix: @@ -75,8 +74,8 @@ jobs: SMALLSTEP_API_URL: https://gateway.smallstep.com/api SMALLSTEP_API_TOKEN: ${{ secrets.SMALLSTEP_API_TOKEN }} run: | - go test -v -cover ./internal/provider/ - timeout-minutes: 10 + go test -v -cover ./internal/provider/ -timeout 20m + timeout-minutes: 20 sweep: runs-on: ubuntu-latest needs: test diff --git a/Makefile b/Makefile index e81458c20855d6a707499424d287d9b3c4d6282e..27b15a797e8ce24a92d383999528738f6139f830 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ default: testacc # Run acceptance tests .PHONY: testacc testacc: - TF_ACC_LOG=INFO TF_ACC=1 go test ./... -v -timeout 10m + TF_ACC_LOG=INFO TF_ACC=1 go test ./... -v -timeout 20m sweep: TF_ACC_LOG=INFO TF_ACC=1 go test ./... -v -timeout 10m -sweep="1" diff --git a/docs/data-sources/agent_configuration.md b/docs/data-sources/agent_configuration.md new file mode 100644 index 0000000000000000000000000000000000000000..84bd1087d550a4e14b39d456be67b3ff9d4f875e --- /dev/null +++ b/docs/data-sources/agent_configuration.md @@ -0,0 +1,35 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "smallstep_agent_configuration Data Source - terraform-provider-smallstep" +subcategory: "" +description: |- + The agent configuration describes the attestation authority used by the agent to grant workload certificates. +--- + +# smallstep_agent_configuration (Data Source) + +The agent configuration describes the attestation authority used by the agent to grant workload certificates. + +## Example Usage + +```terraform +data "smallstep_agent_configuration" "agent1" { + id = "0496154c-ea90-4642-a2b9-96e76e69d219" +} +``` + +<!-- schema generated by tfplugindocs --> +## Schema + +### Required + +- `id` (String) A UUID identifying this agent configuration. Generated server-side on creation. + +### Read-Only + +- `attestation_slug` (String) The slug of the attestation authority the agent connects to to get a certificate. +- `authority_id` (String) UUID identifying the authority the agent uses to generate endpoint certificates. +- `name` (String) The name of this agent configuration. +- `provisioner_name` (String) The name of the provisioner on the authority the agent uses to generate endpoint certificates. + + diff --git a/docs/data-sources/endpoint_configuration.md b/docs/data-sources/endpoint_configuration.md new file mode 100644 index 0000000000000000000000000000000000000000..f61b33bc8717d6fe51e919c7b1a8373d1b9748a4 --- /dev/null +++ b/docs/data-sources/endpoint_configuration.md @@ -0,0 +1,104 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "smallstep_endpoint_configuration Data Source - terraform-provider-smallstep" +subcategory: "" +description: |- + Configuration for a managed endpoint +--- + +# smallstep_endpoint_configuration (Data Source) + +Configuration for a managed endpoint + +## Example Usage + +```terraform +data "smallstep_endpoint_configuration" "ep1" { + id = "2c495e72-cbcf-440c-aa34-7736d76645e7" +} +``` + +<!-- schema generated by tfplugindocs --> +## Schema + +### Required + +- `id` (String) A UUID identifying this endpoint configuration. Generated server-side when the endpoint configuration is created. + +### Read-Only + +- `authority_id` (String) UUID identifying the authority that will issue certificates for the endpoint. +- `certificate_info` (Attributes) Details on a managed certificate (see [below for nested schema](#nestedatt--certificate_info)) +- `hooks` (Attributes) The collection of commands to run when a certificate for a managed endpoint is signed or renewed. (see [below for nested schema](#nestedatt--hooks)) +- `key_info` (Attributes) The attributes of the cryptographic key. (see [below for nested schema](#nestedatt--key_info)) +- `kind` (String) The kind of endpoint this configuration applies to. Allowed values: `DEVICE` `WORKLOAD` `PEOPLE` +- `name` (String) The name of the endpoint configuration. +- `provisioner_name` (String) Name of the provisioner on the authority that will authorize certificates for the endpoint. +- `reload_info` (Attributes) The properties used to reload a service. (see [below for nested schema](#nestedatt--reload_info)) + +<a id="nestedatt--certificate_info"></a> +### Nested Schema for `certificate_info` + +Read-Only: + +- `crt_file` (String) The filepath where the certificate is to be stored. +- `duration` (String) The certificate lifetime. Parsed as a [Golang duration](https://pkg.go.dev/time#ParseDuration). +- `gid` (Number) GID of the files where the certificate is stored. +- `key_file` (String) The filepath where the key is to be stored. +- `mode` (Number) Permission bits of the files where the certificate is stored. +- `root_file` (String) The filepath where the root certificate is to be stored. +- `type` (String) The type of certificate Allowed values: `X509` `SSH_USER` `SSH_HOST` +- `uid` (Number) UID of the files where the certificate is stored. + + +<a id="nestedatt--hooks"></a> +### Nested Schema for `hooks` + +Read-Only: + +- `renew` (Attributes) A list of commands to run before and after a certificate is granted. (see [below for nested schema](#nestedatt--hooks--renew)) +- `sign` (Attributes) A list of commands to run before and after a certificate is granted. (see [below for nested schema](#nestedatt--hooks--sign)) + +<a id="nestedatt--hooks--renew"></a> +### Nested Schema for `hooks.renew` + +Read-Only: + +- `after` (List of String) List of commands to run after the operation. +- `before` (List of String) List of commands to run before the operation. +- `on_error` (List of String) List of commands to run when the operation fails. +- `shell` (String) The shell to use to execute the commands. + + +<a id="nestedatt--hooks--sign"></a> +### Nested Schema for `hooks.sign` + +Read-Only: + +- `after` (List of String) List of commands to run after the operation. +- `before` (List of String) List of commands to run before the operation. +- `on_error` (List of String) List of commands to run when the operation fails. +- `shell` (String) The shell to use to execute the commands. + + + +<a id="nestedatt--key_info"></a> +### Nested Schema for `key_info` + +Read-Only: + +- `format` (String) The format used to encode the private key. For X509 keys the default format is SEC 1 for ECDSA keys, PKCS#1 for RSA keys and PKCS#8 for ED25519 keys. For SSH keys the default format is always the OPENSSH format. Allowed values: `DEFAULT` `PKCS8` `OPENSSH` `DER` +- `pub_file` (String) A CSR or SSH public key to use instead of generating one. +- `type` (String) The key type used. The current DEFAULT type is ECDSA_P256. Allowed values: `DEFAULT` `ECDSA_P256` `ECDSA_P384` `ECDSA_P521` `RSA_2048` `RSA_3072` `RSA_4096` `ED25519` + + +<a id="nestedatt--reload_info"></a> +### Nested Schema for `reload_info` + +Read-Only: + +- `method` (String) Ways an endpoint can reload a certificate. `AUTOMATIC` means the process is able to detect and reload new certificates automatically. `CUSTOM` means a custom command must be run to trigger the workload to reload the certificates. `SIGNAL` will configure the agent to send a signal to the process in pidFile. Allowed values: `AUTOMATIC` `CUSTOM` `SIGNAL` +- `pid_file` (String) File that holds the pid of the process to signal. Required when method is SIGNAL. +- `signal` (Number) The signal to send to a process when a certificate should be reloaded. Required when method is SIGNAL. + + diff --git a/docs/data-sources/managed_configuration.md b/docs/data-sources/managed_configuration.md new file mode 100644 index 0000000000000000000000000000000000000000..9215d8192bf7e60e8aa5f0bc2e3d28dfe45131d6 --- /dev/null +++ b/docs/data-sources/managed_configuration.md @@ -0,0 +1,62 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "smallstep_managed_configuration Data Source - terraform-provider-smallstep" +subcategory: "" +description: |- + The agent and managed endpoints used in one host. +--- + +# smallstep_managed_configuration (Data Source) + +The agent and managed endpoints used in one host. + +## Example Usage + +```terraform +data "smallstep_managed_configuration" "mc1" { + id = "2e891be8-e1c4-4d27-a10c-62be6ccb6a5e" +} +``` + +<!-- schema generated by tfplugindocs --> +## Schema + +### Required + +- `id` (String) UUID identifying this managed configuration. + +### Read-Only + +- `agent_configuration_id` (String) UUID identifying the agent configuration. +- `host_id` (String) UUID identifying the host this managed configuration is for. Will be generated on server-side if not provided. +- `managed_endpoints` (Attributes Set) All the information used by an agent to grant a certificate to an endpoint. Exactly one of `x509CertificateData` or `sshCertificateData` must be set and must match the endpoint configuration certificate info type. (see [below for nested schema](#nestedatt--managed_endpoints)) +- `name` (String) The name of this managed configuration. + +<a id="nestedatt--managed_endpoints"></a> +### Nested Schema for `managed_endpoints` + +Read-Only: + +- `endpoint_configuration_id` (String) UUID identifying the endpoint configuration. +- `id` (String) UUID identifying this managed endpoint. Generated server-side on creation. +- `ssh_certificate_data` (Attributes) Contains the information to include when granting an SSH certificate to an endpoint. (see [below for nested schema](#nestedatt--managed_endpoints--ssh_certificate_data)) +- `x509_certificate_data` (Attributes) Contains the information to include when granting an x509 certificate to an endpoint. (see [below for nested schema](#nestedatt--managed_endpoints--x509_certificate_data)) + +<a id="nestedatt--managed_endpoints--ssh_certificate_data"></a> +### Nested Schema for `managed_endpoints.ssh_certificate_data` + +Read-Only: + +- `key_id` (String) The key ID to include in the endpoint certificate. +- `principals` (Set of String) The principals to include in the endpoint certificate. + + +<a id="nestedatt--managed_endpoints--x509_certificate_data"></a> +### Nested Schema for `managed_endpoints.x509_certificate_data` + +Read-Only: + +- `common_name` (String) The Common Name to be used in the subject of the endpoint certificate. +- `sans` (Set of String) The list of SANs to include in the endpoint certificate. + + diff --git a/docs/resources/agent_configuration.md b/docs/resources/agent_configuration.md new file mode 100644 index 0000000000000000000000000000000000000000..8ff1728df252330117474bde2759f57f0dd929a9 --- /dev/null +++ b/docs/resources/agent_configuration.md @@ -0,0 +1,48 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "smallstep_agent_configuration Resource - terraform-provider-smallstep" +subcategory: "" +description: |- + The agent configuration describes the attestation authority used by the agent to grant workload certificates. +--- + +# smallstep_agent_configuration (Resource) + +The agent configuration describes the attestation authority used by the agent to grant workload certificates. + +## Example Usage + +```terraform +resource "smallstep_agent_configuration" "agent1" { + name = "Agent1" + authority_id = smallstep_authority.agents_authority.id + provisioner_name = smallstep_provisioner.acme_attest.name + attestation_slug = smallstep_attestation_authority.aa.slug + depends_on = [smallstep_provisioner.acme_attest] +} +``` + +<!-- schema generated by tfplugindocs --> +## Schema + +### Required + +- `authority_id` (String) UUID identifying the authority the agent uses to generate endpoint certificates. +- `name` (String) The name of this agent configuration. +- `provisioner_name` (String) The name of the provisioner on the authority the agent uses to generate endpoint certificates. + +### Optional + +- `attestation_slug` (String) The slug of the attestation authority the agent connects to to get a certificate. + +### Read-Only + +- `id` (String) A UUID identifying this agent configuration. Generated server-side on creation. + +## Import + +Import is supported using the following syntax: + +```shell +terraform import smallstep_agent_configuration.ac 7422eaa5-5521-49b8-b7af-aad8f1740165 +``` diff --git a/docs/resources/endpoint_configuration.md b/docs/resources/endpoint_configuration.md new file mode 100644 index 0000000000000000000000000000000000000000..3a0f297d536e6c2a5a4ee10513d3053313ae9b73 --- /dev/null +++ b/docs/resources/endpoint_configuration.md @@ -0,0 +1,178 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "smallstep_endpoint_configuration Resource - terraform-provider-smallstep" +subcategory: "" +description: |- + Configuration for a managed endpoint +--- + +# smallstep_endpoint_configuration (Resource) + +Configuration for a managed endpoint + +## Example Usage + +```terraform +resource "smallstep_endpoint_configuration" "x509" { + name = "My DB" + kind = "WORKLOAD" + + authority_id = smallstep_authority.endpoints.id + provisioner_name = smallstep_provisioner.endpoints_x5c.name + + certificate_info = { + type = "X509" + duration = "168h" + crt_file = "db.crt" + key_file = "db.key" + root_file = "ca.crt" + uid = 1001 + gid = 999 + mode = 256 + } + + hooks = { + renew = { + shell = "/bin/sh" + before = ["echo renewing"] + after = ["echo renewed"] + on_error = ["echo failed renew"] + } + sign = { + shell = "/bin/bash" + before = ["echo signing"] + after = ["echo signed"] + on_error = ["echo failed sign"] + } + } + + key_info = { + format = "DEFAULT" + type = "ECDSA_P256" + pub_file = "file.csr" + } + + reload_info = { + method = "SIGNAL" + pid_file = "db.pid" + signal = 1 + } +} + +resource "smallstep_endpoint_configuration" "ssh" { + name = "SSH" + kind = "PEOPLE" + authority_id = smallstep_authority.endpoints.id + provisioner_name = smallstep_provisioner.endpoints_x5c.name + certificate_info = { + type = "SSH_USER" + } + key_info = { + type = "RSA_2048" + format = "OPENSSH" + } +} +``` + +<!-- schema generated by tfplugindocs --> +## Schema + +### Required + +- `authority_id` (String) UUID identifying the authority that will issue certificates for the endpoint. +- `certificate_info` (Attributes) Details on a managed certificate (see [below for nested schema](#nestedatt--certificate_info)) +- `key_info` (Attributes) The attributes of the cryptographic key. (see [below for nested schema](#nestedatt--key_info)) +- `kind` (String) The kind of endpoint this configuration applies to. Allowed values: `DEVICE` `WORKLOAD` `PEOPLE` +- `name` (String) The name of the endpoint configuration. +- `provisioner_name` (String) Name of the provisioner on the authority that will authorize certificates for the endpoint. + +### Optional + +- `hooks` (Attributes) The collection of commands to run when a certificate for a managed endpoint is signed or renewed. (see [below for nested schema](#nestedatt--hooks)) +- `reload_info` (Attributes) The properties used to reload a service. (see [below for nested schema](#nestedatt--reload_info)) + +### Read-Only + +- `id` (String) A UUID identifying this endpoint configuration. Generated server-side when the endpoint configuration is created. + +<a id="nestedatt--certificate_info"></a> +### Nested Schema for `certificate_info` + +Required: + +- `type` (String) The type of certificate Allowed values: `X509` `SSH_USER` `SSH_HOST` + +Optional: + +- `crt_file` (String) The filepath where the certificate is to be stored. +- `duration` (String) The certificate lifetime. Parsed as a [Golang duration](https://pkg.go.dev/time#ParseDuration). +- `gid` (Number) GID of the files where the certificate is stored. +- `key_file` (String) The filepath where the key is to be stored. +- `mode` (Number) Permission bits of the files where the certificate is stored. +- `root_file` (String) The filepath where the root certificate is to be stored. +- `uid` (Number) UID of the files where the certificate is stored. + + +<a id="nestedatt--key_info"></a> +### Nested Schema for `key_info` + +Required: + +- `format` (String) The format used to encode the private key. For X509 keys the default format is SEC 1 for ECDSA keys, PKCS#1 for RSA keys and PKCS#8 for ED25519 keys. For SSH keys the default format is always the OPENSSH format. Allowed values: `DEFAULT` `PKCS8` `OPENSSH` `DER` +- `type` (String) The key type used. The current DEFAULT type is ECDSA_P256. Allowed values: `DEFAULT` `ECDSA_P256` `ECDSA_P384` `ECDSA_P521` `RSA_2048` `RSA_3072` `RSA_4096` `ED25519` + +Optional: + +- `pub_file` (String) A CSR or SSH public key to use instead of generating one. + + +<a id="nestedatt--hooks"></a> +### Nested Schema for `hooks` + +Optional: + +- `renew` (Attributes) A list of commands to run before and after a certificate is granted. (see [below for nested schema](#nestedatt--hooks--renew)) +- `sign` (Attributes) A list of commands to run before and after a certificate is granted. (see [below for nested schema](#nestedatt--hooks--sign)) + +<a id="nestedatt--hooks--renew"></a> +### Nested Schema for `hooks.renew` + +Optional: + +- `after` (List of String) List of commands to run after the operation. +- `before` (List of String) List of commands to run before the operation. +- `on_error` (List of String) List of commands to run when the operation fails. +- `shell` (String) The shell to use to execute the commands. + + +<a id="nestedatt--hooks--sign"></a> +### Nested Schema for `hooks.sign` + +Optional: + +- `after` (List of String) List of commands to run after the operation. +- `before` (List of String) List of commands to run before the operation. +- `on_error` (List of String) List of commands to run when the operation fails. +- `shell` (String) The shell to use to execute the commands. + + + +<a id="nestedatt--reload_info"></a> +### Nested Schema for `reload_info` + +Required: + +- `method` (String) Ways an endpoint can reload a certificate. `AUTOMATIC` means the process is able to detect and reload new certificates automatically. `CUSTOM` means a custom command must be run to trigger the workload to reload the certificates. `SIGNAL` will configure the agent to send a signal to the process in pidFile. Allowed values: `AUTOMATIC` `CUSTOM` `SIGNAL` + +Optional: + +- `pid_file` (String) File that holds the pid of the process to signal. Required when method is SIGNAL. +- `signal` (Number) The signal to send to a process when a certificate should be reloaded. Required when method is SIGNAL. + +## Import + +Import is supported using the following syntax: + +```shell +terraform import smallstep_endpoint_configuration.ep1 7bcde9eb-daf8-4be7-9d82-21da3ff60e81 +``` diff --git a/docs/resources/managed_configuration.md b/docs/resources/managed_configuration.md new file mode 100644 index 0000000000000000000000000000000000000000..1643366ecd9670024c472f0ad9b710a314f2c587 --- /dev/null +++ b/docs/resources/managed_configuration.md @@ -0,0 +1,100 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "smallstep_managed_configuration Resource - terraform-provider-smallstep" +subcategory: "" +description: |- + The agent and managed endpoints used in one host. +--- + +# smallstep_managed_configuration (Resource) + +The agent and managed endpoints used in one host. + +## Example Usage + +```terraform +resource "smallstep_managed_configuration" "mc" { + agent_configuration_id = smallstep_agent_configuration.agent1.id + host_id = "9cdaf513-3296-4037-bd9b-d0634f51cd79" + name = "DB Server" + managed_endpoints = [ + { + endpoint_configuration_id = smallstep_endpoint_configuration.x509.id + x509_certificate_data = { + common_name = "db" + sans = [ + "db", + "db.default", + "db.default.svc", + "db.defaulst.svc.cluster.local", + ] + } + }, + { + endpoint_configuration_id = smallstep_endpoint_configuration.ssh.id + ssh_certificate_data = { + key_id = "abc" + principals = ["ops", "eng"] + } + }, + ] +} +``` + +<!-- schema generated by tfplugindocs --> +## Schema + +### Required + +- `agent_configuration_id` (String) UUID identifying the agent configuration. +- `managed_endpoints` (Attributes Set) All the information used by an agent to grant a certificate to an endpoint. Exactly one of `x509CertificateData` or `sshCertificateData` must be set and must match the endpoint configuration certificate info type. (see [below for nested schema](#nestedatt--managed_endpoints)) +- `name` (String) The name of this managed configuration. + +### Optional + +- `host_id` (String) UUID identifying the host this managed configuration is for. Will be generated on server-side if not provided. + +### Read-Only + +- `id` (String) UUID identifying this managed configuration. + +<a id="nestedatt--managed_endpoints"></a> +### Nested Schema for `managed_endpoints` + +Required: + +- `endpoint_configuration_id` (String) UUID identifying the endpoint configuration. + +Optional: + +- `ssh_certificate_data` (Attributes) Contains the information to include when granting an SSH certificate to an endpoint. (see [below for nested schema](#nestedatt--managed_endpoints--ssh_certificate_data)) +- `x509_certificate_data` (Attributes) Contains the information to include when granting an x509 certificate to an endpoint. (see [below for nested schema](#nestedatt--managed_endpoints--x509_certificate_data)) + +Read-Only: + +- `id` (String) UUID identifying this managed endpoint. Generated server-side on creation. + +<a id="nestedatt--managed_endpoints--ssh_certificate_data"></a> +### Nested Schema for `managed_endpoints.ssh_certificate_data` + +Required: + +- `key_id` (String) The key ID to include in the endpoint certificate. +- `principals` (Set of String) The principals to include in the endpoint certificate. + + +<a id="nestedatt--managed_endpoints--x509_certificate_data"></a> +### Nested Schema for `managed_endpoints.x509_certificate_data` + +Required: + +- `common_name` (String) The Common Name to be used in the subject of the endpoint certificate. +- `sans` (Set of String) The list of SANs to include in the endpoint certificate. + +## Import + +Import is supported using the following syntax: + +```shell +terraform import smallstep_managed_configuration.mc 411009f5-b30d-4c16-818f-cefcc71d86b4 +``` diff --git a/e2e/managed.tf b/e2e/managed.tf index 83ff4b43b8f7d2e5f45a2214c9af6cfc4b376665..1505d82d93ced1a5adf5c6e58fd74968363593bc 100644 --- a/e2e/managed.tf +++ b/e2e/managed.tf @@ -62,3 +62,98 @@ resource "smallstep_provisioner_webhook" "devices" { collection_slug = smallstep_collection.tpms.slug depends_on = [smallstep_collection.tpms] } + +resource "smallstep_agent_configuration" "agent1" { + authority_id = smallstep_authority.agents.id + provisioner_name = smallstep_provisioner.agents.name + name = "Agent1" + attestation_slug = smallstep_attestation_authority.aa.slug + depends_on = [smallstep_provisioner.agents] +} + +resource "smallstep_endpoint_configuration" "ep_x509" { + name = "My DB" + kind = "WORKLOAD" + + authority_id = smallstep_authority.endpoints.id + provisioner_name = smallstep_provisioner.endpoints.name + + certificate_info = { + type = "X509" + duration = "168h" + crt_file = "db.crt" + key_file = "db.key" + root_file = "ca.crt" + uid = 1001 + gid = 999 + mode = 256 + } + + hooks = { + renew = { + shell = "/bin/sh" + before = ["echo renewing"] + after = ["echo renewed"] + on_error = ["echo failed renew"] + } + sign = { + shell = "/bin/bash" + before = ["echo signing"] + after = ["echo signed"] + on_error = ["echo failed sign"] + } + } + + key_info = { + format = "DEFAULT" + type = "ECDSA_P256" + pub_file = "file.csr" + } + + reload_info = { + method = "SIGNAL" + pid_file = "db.pid" + signal = 1 + } +} + +resource "smallstep_endpoint_configuration" "ep_ssh" { + name = "SSH" + kind = "PEOPLE" + authority_id = smallstep_authority.agents.id + provisioner_name = smallstep_provisioner.agents.name + certificate_info = { + type = "SSH_USER" + } + key_info = { + type = "RSA_2048" + format = "OPENSSH" + } +} + +resource "smallstep_managed_configuration" "mc" { + agent_configuration_id = smallstep_agent_configuration.agent1.id + host_id = "9cdaf513-3296-4037-bd9b-d0634f51cd79" + name = "DB Server" + managed_endpoints = [ + { + endpoint_configuration_id = smallstep_endpoint_configuration.ep_x509.id + x509_certificate_data = { + common_name = "db" + sans = [ + "db", + "db.default", + "db.default.svc", + "db.defaulst.svc.cluster.local", + ] + } + }, + { + endpoint_configuration_id = smallstep_endpoint_configuration.ep_ssh.id + ssh_certificate_data = { + key_id = "abc" + principals = ["ops"] + } + }, + ] +} diff --git a/e2e/output.tf b/e2e/output.tf new file mode 100644 index 0000000000000000000000000000000000000000..b833485a4db9b775b430fe6051ea19c5e599387f --- /dev/null +++ b/e2e/output.tf @@ -0,0 +1,15 @@ +output "agent_configuration_id" { + value = smallstep_agent_configuration.agent1.id +} + +output "ep_x509_id" { + value = smallstep_endpoint_configuration.ep_x509.id +} + +output "ep_ssh_id" { + value = smallstep_endpoint_configuration.ep_ssh.id +} + +output "managed_configuration_id" { + value = smallstep_managed_configuration.mc.id +} diff --git a/examples/data-sources/smallstep_agent_configuration/data-source.tf b/examples/data-sources/smallstep_agent_configuration/data-source.tf new file mode 100644 index 0000000000000000000000000000000000000000..594dedf57884fa1e81eb678b663e800e5ef836ad --- /dev/null +++ b/examples/data-sources/smallstep_agent_configuration/data-source.tf @@ -0,0 +1,4 @@ + +data "smallstep_agent_configuration" "agent1" { + id = "0496154c-ea90-4642-a2b9-96e76e69d219" +} diff --git a/examples/data-sources/smallstep_endpoint_configuration/data-source.tf b/examples/data-sources/smallstep_endpoint_configuration/data-source.tf new file mode 100644 index 0000000000000000000000000000000000000000..348a8c1c12e9e3a4f2298456ed8b51ecbc97796f --- /dev/null +++ b/examples/data-sources/smallstep_endpoint_configuration/data-source.tf @@ -0,0 +1,4 @@ + +data "smallstep_endpoint_configuration" "ep1" { + id = "2c495e72-cbcf-440c-aa34-7736d76645e7" +} diff --git a/examples/data-sources/smallstep_managed_configuration/data-source.tf b/examples/data-sources/smallstep_managed_configuration/data-source.tf new file mode 100644 index 0000000000000000000000000000000000000000..cdaa4bd906c93b653e790b98dde34b8876351028 --- /dev/null +++ b/examples/data-sources/smallstep_managed_configuration/data-source.tf @@ -0,0 +1,4 @@ + +data "smallstep_managed_configuration" "mc1" { + id = "2e891be8-e1c4-4d27-a10c-62be6ccb6a5e" +} diff --git a/examples/data-sources/smallstep_managed_configuration/smallstep_managed_configuration/data-source.tf b/examples/data-sources/smallstep_managed_configuration/smallstep_managed_configuration/data-source.tf new file mode 100644 index 0000000000000000000000000000000000000000..cdaa4bd906c93b653e790b98dde34b8876351028 --- /dev/null +++ b/examples/data-sources/smallstep_managed_configuration/smallstep_managed_configuration/data-source.tf @@ -0,0 +1,4 @@ + +data "smallstep_managed_configuration" "mc1" { + id = "2e891be8-e1c4-4d27-a10c-62be6ccb6a5e" +} diff --git a/examples/resources/smallstep_agent_configuration/import.sh b/examples/resources/smallstep_agent_configuration/import.sh new file mode 100644 index 0000000000000000000000000000000000000000..24c82c0e79f449cf93bd1aadfa9f589aac25e757 --- /dev/null +++ b/examples/resources/smallstep_agent_configuration/import.sh @@ -0,0 +1 @@ +terraform import smallstep_agent_configuration.ac 7422eaa5-5521-49b8-b7af-aad8f1740165 diff --git a/examples/resources/smallstep_agent_configuration/resource.tf b/examples/resources/smallstep_agent_configuration/resource.tf new file mode 100644 index 0000000000000000000000000000000000000000..eccfe6971c41a62172be0c07ec2c787669ec1b30 --- /dev/null +++ b/examples/resources/smallstep_agent_configuration/resource.tf @@ -0,0 +1,8 @@ + +resource "smallstep_agent_configuration" "agent1" { + name = "Agent1" + authority_id = smallstep_authority.agents_authority.id + provisioner_name = smallstep_provisioner.acme_attest.name + attestation_slug = smallstep_attestation_authority.aa.slug + depends_on = [smallstep_provisioner.acme_attest] +} diff --git a/examples/resources/smallstep_endpoint_configuration/import.sh b/examples/resources/smallstep_endpoint_configuration/import.sh new file mode 100644 index 0000000000000000000000000000000000000000..4367f43546d2634eea2c10a3ced83844e158ce54 --- /dev/null +++ b/examples/resources/smallstep_endpoint_configuration/import.sh @@ -0,0 +1 @@ +terraform import smallstep_endpoint_configuration.ep1 7bcde9eb-daf8-4be7-9d82-21da3ff60e81 diff --git a/examples/resources/smallstep_endpoint_configuration/resource.tf b/examples/resources/smallstep_endpoint_configuration/resource.tf new file mode 100644 index 0000000000000000000000000000000000000000..4c711a3c2b617d2a7e1fa396aa6043e429662fbf --- /dev/null +++ b/examples/resources/smallstep_endpoint_configuration/resource.tf @@ -0,0 +1,60 @@ + +resource "smallstep_endpoint_configuration" "x509" { + name = "My DB" + kind = "WORKLOAD" + + authority_id = smallstep_authority.endpoints.id + provisioner_name = smallstep_provisioner.endpoints_x5c.name + + certificate_info = { + type = "X509" + duration = "168h" + crt_file = "db.crt" + key_file = "db.key" + root_file = "ca.crt" + uid = 1001 + gid = 999 + mode = 256 + } + + hooks = { + renew = { + shell = "/bin/sh" + before = ["echo renewing"] + after = ["echo renewed"] + on_error = ["echo failed renew"] + } + sign = { + shell = "/bin/bash" + before = ["echo signing"] + after = ["echo signed"] + on_error = ["echo failed sign"] + } + } + + key_info = { + format = "DEFAULT" + type = "ECDSA_P256" + pub_file = "file.csr" + } + + reload_info = { + method = "SIGNAL" + pid_file = "db.pid" + signal = 1 + } +} + +resource "smallstep_endpoint_configuration" "ssh" { + name = "SSH" + kind = "PEOPLE" + authority_id = smallstep_authority.endpoints.id + provisioner_name = smallstep_provisioner.endpoints_x5c.name + certificate_info = { + type = "SSH_USER" + } + key_info = { + type = "RSA_2048" + format = "OPENSSH" + } +} diff --git a/examples/resources/smallstep_managed_configuration/import.sh b/examples/resources/smallstep_managed_configuration/import.sh new file mode 100644 index 0000000000000000000000000000000000000000..bf0d69e60a802891d21237800926b820d11fbf1f --- /dev/null +++ b/examples/resources/smallstep_managed_configuration/import.sh @@ -0,0 +1 @@ +terraform import smallstep_managed_configuration.mc 411009f5-b30d-4c16-818f-cefcc71d86b4 diff --git a/examples/resources/smallstep_managed_configuration/resource.tf b/examples/resources/smallstep_managed_configuration/resource.tf new file mode 100644 index 0000000000000000000000000000000000000000..1137e181bfc6c43568fd79d7274295a4ab6f181c --- /dev/null +++ b/examples/resources/smallstep_managed_configuration/resource.tf @@ -0,0 +1,27 @@ + +resource "smallstep_managed_configuration" "mc" { + agent_configuration_id = smallstep_agent_configuration.agent1.id + host_id = "9cdaf513-3296-4037-bd9b-d0634f51cd79" + name = "DB Server" + managed_endpoints = [ + { + endpoint_configuration_id = smallstep_endpoint_configuration.x509.id + x509_certificate_data = { + common_name = "db" + sans = [ + "db", + "db.default", + "db.default.svc", + "db.defaulst.svc.cluster.local", + ] + } + }, + { + endpoint_configuration_id = smallstep_endpoint_configuration.ssh.id + ssh_certificate_data = { + key_id = "abc" + principals = ["ops", "eng"] + } + }, + ] +} diff --git a/internal/provider/agent_configuration/data_source.go b/internal/provider/agent_configuration/data_source.go new file mode 100644 index 0000000000000000000000000000000000000000..fc3883d0a338db8f817cd2341c99eb142f75f33e --- /dev/null +++ b/internal/provider/agent_configuration/data_source.go @@ -0,0 +1,137 @@ +package agent_configuration + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-log/tflog" + v20230301 "github.com/smallstep/terraform-provider-smallstep/internal/apiclient/v20230301" + "github.com/smallstep/terraform-provider-smallstep/internal/provider/utils" +) + +var _ datasource.DataSourceWithConfigure = (*DataSource)(nil) + +func NewDataSource() datasource.DataSource { + return &DataSource{} +} + +// DataSource implements data.smallstep_agent_configuration +type DataSource struct { + client *v20230301.Client +} + +func (a *DataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = typeName +} + +// Configure adds the Smallstep API client to the data source. +func (a *DataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + // Prevent panic if the provider has not been configured. + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(*v20230301.Client) + + if !ok { + resp.Diagnostics.AddError( + "Get Smallstep API client from provider", + fmt.Sprintf("Expected *v20230301.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + return + } + + a.client = client +} + +func (a *DataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var config Model + + // Read Terraform configuration data into the model + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + + if resp.Diagnostics.HasError() { + return + } + + id := config.ID.ValueString() + + httpResp, err := a.client.GetAgentConfiguration(ctx, id, &v20230301.GetAgentConfigurationParams{}) + if err != nil { + resp.Diagnostics.AddError( + "Smallstep API Client Error", + fmt.Sprintf("Failed to read agent configuration %q: %v", id, err), + ) + return + } + defer httpResp.Body.Close() + + if httpResp.StatusCode != http.StatusOK { + resp.Diagnostics.AddError( + "Smallstep API Response Error", + fmt.Sprintf("Received status %d reading agent configuration %q: %s", httpResp.StatusCode, id, utils.APIErrorMsg(httpResp.Body)), + ) + return + } + + ac := &v20230301.AgentConfiguration{} + if err := json.NewDecoder(httpResp.Body).Decode(ac); err != nil { + resp.Diagnostics.AddError( + "Smallstep API Client Error", + fmt.Sprintf("Failed to unmarshal agent configuration %q: %v", id, err), + ) + return + } + + remote, d := fromAPI(ctx, ac, req.Config) + resp.Diagnostics.Append(d...) + if resp.Diagnostics.HasError() { + return + } + + tflog.Trace(ctx, fmt.Sprintf("read agent configuration %q data source", id)) + + resp.Diagnostics.Append(resp.State.Set(ctx, &remote)...) +} + +func (d *DataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { + component, props, err := utils.Describe("agentConfiguration") + if err != nil { + resp.Diagnostics.AddError( + "Parse Smallstep OpenAPI spec", + err.Error(), + ) + return + } + + resp.Schema = schema.Schema{ + MarkdownDescription: component, + + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + MarkdownDescription: props["id"], + Required: true, + }, + "attestation_slug": schema.StringAttribute{ + MarkdownDescription: props["attestationSlug"], + Computed: true, + }, + "authority_id": schema.StringAttribute{ + MarkdownDescription: props["authorityID"], + Computed: true, + }, + "provisioner_name": schema.StringAttribute{ + MarkdownDescription: props["provisioner"], + Computed: true, + }, + "name": schema.StringAttribute{ + MarkdownDescription: props["name"], + Computed: true, + }, + }, + } +} diff --git a/internal/provider/agent_configuration/model.go b/internal/provider/agent_configuration/model.go new file mode 100644 index 0000000000000000000000000000000000000000..2f85f662f0948e8adabf8acf237194d07fecd4f8 --- /dev/null +++ b/internal/provider/agent_configuration/model.go @@ -0,0 +1,47 @@ +package agent_configuration + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/types" + v20230301 "github.com/smallstep/terraform-provider-smallstep/internal/apiclient/v20230301" + "github.com/smallstep/terraform-provider-smallstep/internal/provider/utils" +) + +const typeName = "smallstep_agent_configuration" + +type Model struct { + ID types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + Provisioner types.String `tfsdk:"provisioner_name"` + AuthorityID types.String `tfsdk:"authority_id"` + AttestationSlug types.String `tfsdk:"attestation_slug"` +} + +func fromAPI(ctx context.Context, ac *v20230301.AgentConfiguration, state utils.AttributeGetter) (*Model, diag.Diagnostics) { + var diags diag.Diagnostics + + model := &Model{ + ID: types.StringValue(utils.Deref(ac.Id)), + Name: types.StringValue(ac.Name), + Provisioner: types.StringValue(ac.Provisioner), + AuthorityID: types.StringValue(ac.AuthorityID), + } + + attestationSlug, d := utils.ToOptionalString(ctx, ac.AttestationSlug, state, path.Root("attestation_slug")) + diags = append(diags, d...) + model.AttestationSlug = attestationSlug + + return model, diags +} + +func toAPI(model *Model) *v20230301.AgentConfiguration { + return &v20230301.AgentConfiguration{ + Name: model.Name.ValueString(), + AuthorityID: model.AuthorityID.ValueString(), + Provisioner: model.Provisioner.ValueString(), + AttestationSlug: model.AttestationSlug.ValueStringPointer(), + } +} diff --git a/internal/provider/agent_configuration/resource.go b/internal/provider/agent_configuration/resource.go new file mode 100644 index 0000000000000000000000000000000000000000..6dfd4ff0ac3bcd9f7502845f58c7b48cb784b5ee --- /dev/null +++ b/internal/provider/agent_configuration/resource.go @@ -0,0 +1,246 @@ +package agent_configuration + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + + "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-log/tflog" + v20230301 "github.com/smallstep/terraform-provider-smallstep/internal/apiclient/v20230301" + "github.com/smallstep/terraform-provider-smallstep/internal/provider/utils" +) + +var _ resource.ResourceWithImportState = (*Resource)(nil) + +func NewResource() resource.Resource { + return &Resource{} +} + +// Resource defines the resource implementation. +type Resource struct { + client *v20230301.Client +} + +func (r *Resource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = typeName +} + +// Configure adds the Smallstep API client to the resource. +func (r *Resource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + // Prevent panic if the provider has not been configured. + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(*v20230301.Client) + + if !ok { + resp.Diagnostics.AddError( + "Get Smallstep API client from provider", + fmt.Sprintf("Expected *v20230301.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + return + } + + r.client = client +} + +func (r *Resource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + state := &Model{} + + resp.Diagnostics.Append(req.State.Get(ctx, state)...) + + if resp.Diagnostics.HasError() { + return + } + + id := state.ID.ValueString() + + httpResp, err := r.client.GetAgentConfiguration(ctx, id, &v20230301.GetAgentConfigurationParams{}) + if err != nil { + resp.Diagnostics.AddError( + "Smallstep API Client Error", + fmt.Sprintf("Failed to read agent configuration %q: %v", id, err), + ) + return + } + defer httpResp.Body.Close() + + if httpResp.StatusCode == http.StatusNotFound { + resp.State.RemoveResource(ctx) + return + } + + if httpResp.StatusCode != http.StatusOK { + resp.Diagnostics.AddError( + "Smallstep API Response Error", + fmt.Sprintf("Received status %d reading agent configuration %q: %s", httpResp.StatusCode, id, utils.APIErrorMsg(httpResp.Body)), + ) + return + } + + ac := &v20230301.AgentConfiguration{} + if err := json.NewDecoder(httpResp.Body).Decode(ac); err != nil { + resp.Diagnostics.AddError( + "Smallstep API Client Error", + fmt.Sprintf("Failed to unmarshal agent configuration %q: %v", id, err), + ) + return + } + + remote, d := fromAPI(ctx, ac, req.State) + resp.Diagnostics.Append(d...) + if resp.Diagnostics.HasError() { + return + } + + tflog.Trace(ctx, fmt.Sprintf("read agent configuration %q resource", id)) + + resp.Diagnostics.Append(resp.State.Set(ctx, &remote)...) +} + +func (r *Resource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + component, props, err := utils.Describe("agentConfiguration") + if err != nil { + resp.Diagnostics.AddError( + "Parse Smallstep OpenAPI spec", + err.Error(), + ) + return + } + + resp.Schema = schema.Schema{ + MarkdownDescription: component, + + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + MarkdownDescription: props["id"], + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "name": schema.StringAttribute{ + MarkdownDescription: props["name"], + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "authority_id": schema.StringAttribute{ + MarkdownDescription: props["authorityID"], + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "provisioner_name": schema.StringAttribute{ + MarkdownDescription: props["provisioner"], + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "attestation_slug": schema.StringAttribute{ + MarkdownDescription: props["attestationSlug"], + Optional: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + }, + } +} + +func (a *Resource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan Model + + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + + if resp.Diagnostics.HasError() { + return + } + + reqBody := toAPI(&plan) + + httpResp, err := a.client.PostAgentConfigurations(ctx, &v20230301.PostAgentConfigurationsParams{}, *reqBody) + if err != nil { + resp.Diagnostics.AddError( + "Smallstep API Client Error", + fmt.Sprintf("Failed to create agent configuration %q: %v", plan.Name.ValueString(), err), + ) + return + } + defer httpResp.Body.Close() + + if httpResp.StatusCode != http.StatusCreated { + resp.Diagnostics.AddError( + "Smallstep API Response Error", + fmt.Sprintf("Received status %d creating agent configuration %q: %s", httpResp.StatusCode, plan.Name.ValueString(), utils.APIErrorMsg(httpResp.Body)), + ) + return + } + + ac := &v20230301.AgentConfiguration{} + if err := json.NewDecoder(httpResp.Body).Decode(ac); err != nil { + resp.Diagnostics.AddError( + "Smallstep API Client Error", + fmt.Sprintf("Failed to unmarshal agent configuration %q: %v", plan.Name.ValueString(), err), + ) + return + } + + state, diags := fromAPI(ctx, ac, req.Plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + tflog.Trace(ctx, fmt.Sprintf("create agent configuration %q resource", plan.Name.ValueString())) + + resp.Diagnostics.Append(resp.State.Set(ctx, state)...) +} + +func (r *Resource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + // Update not supported. All changes require replacement. +} + +func (a *Resource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state Model + + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + + if resp.Diagnostics.HasError() { + return + } + + id := state.ID.ValueString() + + httpResp, err := a.client.DeleteAgentConfiguration(ctx, id, &v20230301.DeleteAgentConfigurationParams{}) + if err != nil { + resp.Diagnostics.AddError( + "Smallstep API Client Error", + fmt.Sprintf("Failed to delete agent configuration %s: %v", id, err), + ) + return + } + defer httpResp.Body.Close() + + if httpResp.StatusCode != http.StatusNoContent { + resp.Diagnostics.AddError( + "Smallstep API Response Error", + fmt.Sprintf("Received status %d deleting agent configuration %s: %s", httpResp.StatusCode, id, utils.APIErrorMsg(httpResp.Body)), + ) + return + } +} + +func (r *Resource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} diff --git a/internal/provider/endpoint_configuration/data_source.go b/internal/provider/endpoint_configuration/data_source.go new file mode 100644 index 0000000000000000000000000000000000000000..fe5d1065fc27d801c3ec8140b56ba75c836734cb --- /dev/null +++ b/internal/provider/endpoint_configuration/data_source.go @@ -0,0 +1,313 @@ +package endpoint_configuration + +import ( + "context" + "encoding/json" + "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" + v20230301 "github.com/smallstep/terraform-provider-smallstep/internal/apiclient/v20230301" + "github.com/smallstep/terraform-provider-smallstep/internal/provider/utils" +) + +var _ datasource.DataSourceWithConfigure = (*DataSource)(nil) + +func NewDataSource() datasource.DataSource { + return &DataSource{} +} + +// DataSource implements data.smallstep_endpoint_configuration +type DataSource struct { + client *v20230301.Client +} + +func (a *DataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = typeName +} + +// Configure adds the Smallstep API client to the data source. +func (a *DataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + // Prevent panic if the provider has not been configured. + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(*v20230301.Client) + + if !ok { + resp.Diagnostics.AddError( + "Get Smallstep API client from provider", + fmt.Sprintf("Expected *v20230301.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + return + } + + a.client = client +} + +func (a *DataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var config Model + + // Read Terraform configuration data into the model + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + + if resp.Diagnostics.HasError() { + return + } + + id := config.ID.ValueString() + + httpResp, err := a.client.GetEndpointConfiguration(ctx, id, &v20230301.GetEndpointConfigurationParams{}) + if err != nil { + resp.Diagnostics.AddError( + "Smallstep API Client Error", + fmt.Sprintf("Failed to read endpoint configuration %q: %v", id, err), + ) + return + } + defer httpResp.Body.Close() + + if httpResp.StatusCode != http.StatusOK { + resp.Diagnostics.AddError( + "Smallstep API Response Error", + fmt.Sprintf("Received status %d reading endpoint configuration %q: %s", httpResp.StatusCode, id, utils.APIErrorMsg(httpResp.Body)), + ) + return + } + + ac := &v20230301.EndpointConfiguration{} + if err := json.NewDecoder(httpResp.Body).Decode(ac); err != nil { + resp.Diagnostics.AddError( + "Smallstep API Client Error", + fmt.Sprintf("Failed to unmarshal endpoint configuration %q: %v", id, err), + ) + return + } + + remote, d := fromAPI(ctx, ac, req.Config) + resp.Diagnostics.Append(d...) + if resp.Diagnostics.HasError() { + return + } + + tflog.Trace(ctx, fmt.Sprintf("read endpoint configuration %q data source", id)) + + resp.Diagnostics.Append(resp.State.Set(ctx, &remote)...) +} + +func (d *DataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { + component, props, err := utils.Describe("endpointConfiguration") + if err != nil { + resp.Diagnostics.AddError( + "Parse Smallstep OpenAPI spec", + err.Error(), + ) + return + } + + certInfo, certInfoProps, err := utils.Describe("endpointCertificateInfo") + if err != nil { + resp.Diagnostics.AddError( + "Parse Smallstep OpenAPI spec", + err.Error(), + ) + return + } + + _, hookProps, err := utils.Describe("endpointHook") + if err != nil { + resp.Diagnostics.AddError( + "Parse Smallstep OpenAPI spec", + err.Error(), + ) + return + } + + hooks, hooksProps, err := utils.Describe("endpointHooks") + if err != nil { + resp.Diagnostics.AddError( + "Parse Smallstep OpenAPI spec", + err.Error(), + ) + return + } + + keyInfo, keyInfoProps, err := utils.Describe("endpointKeyInfo") + if err != nil { + resp.Diagnostics.AddError( + "Parse Smallstep OpenAPI spec", + err.Error(), + ) + return + } + + reloadInfo, reloadInfoProps, err := utils.Describe("endpointReloadInfo") + if err != nil { + resp.Diagnostics.AddError( + "Parse Smallstep OpenAPI spec", + err.Error(), + ) + return + } + + resp.Schema = schema.Schema{ + MarkdownDescription: component, + + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + MarkdownDescription: props["id"], + Required: true, + }, + "kind": schema.StringAttribute{ + MarkdownDescription: props["kind"], + Computed: true, + }, + "name": schema.StringAttribute{ + MarkdownDescription: props["name"], + Computed: true, + }, + "authority_id": schema.StringAttribute{ + MarkdownDescription: props["authorityID"], + Computed: true, + }, + "provisioner_name": schema.StringAttribute{ + MarkdownDescription: props["provisioner"], + Computed: true, + }, + "key_info": schema.SingleNestedAttribute{ + Computed: true, + MarkdownDescription: keyInfo, + Attributes: map[string]schema.Attribute{ + "format": schema.StringAttribute{ + Computed: true, + MarkdownDescription: keyInfoProps["format"], + }, + "pub_file": schema.StringAttribute{ + Computed: true, + MarkdownDescription: keyInfoProps["pubFile"], + }, + "type": schema.StringAttribute{ + Computed: true, + MarkdownDescription: keyInfoProps["type"], + }, + }, + }, + "reload_info": schema.SingleNestedAttribute{ + Computed: true, + MarkdownDescription: reloadInfo, + Attributes: map[string]schema.Attribute{ + "method": schema.StringAttribute{ + Computed: true, + MarkdownDescription: reloadInfoProps["method"], + }, + "pid_file": schema.StringAttribute{ + Computed: true, + MarkdownDescription: reloadInfoProps["pidFile"], + }, + "signal": schema.Int64Attribute{ + Computed: true, + MarkdownDescription: reloadInfoProps["signal"], + }, + }, + }, + "hooks": schema.SingleNestedAttribute{ + Computed: true, + MarkdownDescription: hooks, + Attributes: map[string]schema.Attribute{ + "sign": schema.SingleNestedAttribute{ + Computed: true, + MarkdownDescription: hooksProps["sign"], + Attributes: map[string]schema.Attribute{ + "shell": schema.StringAttribute{ + Computed: true, + MarkdownDescription: hookProps["shell"], + }, + "before": schema.ListAttribute{ + ElementType: types.StringType, + Computed: true, + MarkdownDescription: hookProps["before"], + }, + "after": schema.ListAttribute{ + ElementType: types.StringType, + Computed: true, + MarkdownDescription: hookProps["after"], + }, + "on_error": schema.ListAttribute{ + ElementType: types.StringType, + Computed: true, + MarkdownDescription: hookProps["onError"], + }, + }, + }, + "renew": schema.SingleNestedAttribute{ + Computed: true, + MarkdownDescription: hooksProps["renew"], + Attributes: map[string]schema.Attribute{ + "shell": schema.StringAttribute{ + Computed: true, + MarkdownDescription: hookProps["shell"], + }, + "before": schema.ListAttribute{ + ElementType: types.StringType, + Computed: true, + MarkdownDescription: hookProps["before"], + }, + "after": schema.ListAttribute{ + ElementType: types.StringType, + Computed: true, + MarkdownDescription: hookProps["after"], + }, + "on_error": schema.ListAttribute{ + ElementType: types.StringType, + Computed: true, + MarkdownDescription: hookProps["onError"], + }, + }, + }, + }, + }, + "certificate_info": schema.SingleNestedAttribute{ + Computed: true, + MarkdownDescription: certInfo, + Attributes: map[string]schema.Attribute{ + "type": schema.StringAttribute{ + MarkdownDescription: certInfoProps["type"], + Computed: true, + }, + "duration": schema.StringAttribute{ + MarkdownDescription: certInfoProps["duration"], + Computed: true, + }, + "crt_file": schema.StringAttribute{ + MarkdownDescription: certInfoProps["crtFile"], + Computed: true, + }, + "key_file": schema.StringAttribute{ + MarkdownDescription: certInfoProps["keyFile"], + Computed: true, + }, + "root_file": schema.StringAttribute{ + MarkdownDescription: certInfoProps["rootFile"], + Computed: true, + }, + "uid": schema.Int64Attribute{ + MarkdownDescription: certInfoProps["uid"], + Computed: true, + }, + "gid": schema.Int64Attribute{ + MarkdownDescription: certInfoProps["gid"], + Computed: true, + }, + "mode": schema.Int64Attribute{ + MarkdownDescription: certInfoProps["mode"], + Computed: true, + }, + }, + }, + }, + } +} diff --git a/internal/provider/endpoint_configuration/model.go b/internal/provider/endpoint_configuration/model.go new file mode 100644 index 0000000000000000000000000000000000000000..7df0a6fc2648239232fd02625dfdd151075d3cc2 --- /dev/null +++ b/internal/provider/endpoint_configuration/model.go @@ -0,0 +1,341 @@ +package endpoint_configuration + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "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-framework/types/basetypes" + v20230301 "github.com/smallstep/terraform-provider-smallstep/internal/apiclient/v20230301" + "github.com/smallstep/terraform-provider-smallstep/internal/provider/utils" +) + +const typeName = "smallstep_endpoint_configuration" + +type Model struct { + ID types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + AuthorityID types.String `tfsdk:"authority_id"` + Provisioner types.String `tfsdk:"provisioner_name"` + Kind types.String `tfsdk:"kind"` + CertificateInfo *CertificateInfoModel `tfsdk:"certificate_info"` + KeyInfo *KeyInfoModel `tfsdk:"key_info"` + // ReloadInfo and Hooks are optional. Need to use Object type to support + // the "unknown" state. + ReloadInfo types.Object `tfsdk:"reload_info"` + Hooks types.Object `tfsdk:"hooks"` +} + +type CertificateInfoModel struct { + Type types.String `tfsdk:"type"` + CrtFile types.String `tfsdk:"crt_file"` + KeyFile types.String `tfsdk:"key_file"` + RootFile types.String `tfsdk:"root_file"` + Duration types.String `tfsdk:"duration"` + GID types.Int64 `tfsdk:"gid"` + UID types.Int64 `tfsdk:"uid"` + Mode types.Int64 `tfsdk:"mode"` +} + +func (ci CertificateInfoModel) toAPI() v20230301.EndpointCertificateInfo { + return v20230301.EndpointCertificateInfo{ + Type: v20230301.EndpointCertificateInfoType(ci.Type.ValueString()), + Duration: ci.Duration.ValueStringPointer(), + CrtFile: ci.CrtFile.ValueStringPointer(), + KeyFile: ci.KeyFile.ValueStringPointer(), + RootFile: ci.RootFile.ValueStringPointer(), + Uid: utils.ToIntPointer(ci.UID.ValueInt64Pointer()), + Gid: utils.ToIntPointer(ci.GID.ValueInt64Pointer()), + Mode: utils.ToIntPointer(ci.Mode.ValueInt64Pointer()), + } +} + +type HookModel struct { + Shell types.String `tfsdk:"shell"` + Before types.List `tfsdk:"before"` + After types.List `tfsdk:"after"` + OnError types.List `tfsdk:"on_error"` +} + +var hookObjectType = types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "shell": types.StringType, + "before": types.ListType{ + ElemType: types.StringType, + }, + "after": types.ListType{ + ElemType: types.StringType, + }, + "on_error": types.ListType{ + ElemType: types.StringType, + }, + }, +} + +func (h *HookModel) toAPI(ctx context.Context) (*v20230301.EndpointHook, diag.Diagnostics) { + var diags diag.Diagnostics + + if h == nil { + return nil, diags + } + + var before *[]string + var after *[]string + var onError *[]string + + if !h.Before.IsNull() { + diags.Append(h.Before.ElementsAs(ctx, &before, false)...) + } + if !h.After.IsNull() { + diags.Append(h.After.ElementsAs(ctx, &after, false)...) + } + if !h.OnError.IsNull() { + diags.Append(h.OnError.ElementsAs(ctx, &onError, false)...) + } + + return &v20230301.EndpointHook{ + Shell: h.Shell.ValueStringPointer(), + Before: before, + After: after, + OnError: onError, + }, diags +} + +type HooksModel struct { + Sign types.Object `tfsdk:"sign"` + Renew types.Object `tfsdk:"renew"` +} + +var hooksObjectType = map[string]attr.Type{ + "sign": hookObjectType, + "renew": hookObjectType, +} + +func hookToAPI(ctx context.Context, hook types.Object) (*v20230301.EndpointHook, diag.Diagnostics) { + hookModel := &HookModel{} + diags := hook.As(ctx, &hookModel, basetypes.ObjectAsOptions{ + UnhandledUnknownAsEmpty: true, + }) + + h, d := hookModel.toAPI(ctx) + diags.Append(d...) + + return h, diags +} + +func (h *HooksModel) toAPI(ctx context.Context) (*v20230301.EndpointHooks, diag.Diagnostics) { + var diags diag.Diagnostics + + if h == nil { + return nil, diags + } + + sign, d := hookToAPI(ctx, h.Sign) + diags.Append(d...) + + renew, d := hookToAPI(ctx, h.Renew) + diags.Append(d...) + + return &v20230301.EndpointHooks{ + Sign: sign, + Renew: renew, + }, diags +} + +type KeyInfoModel struct { + Format types.String `tfsdk:"format"` + PubFile types.String `tfsdk:"pub_file"` + Type types.String `tfsdk:"type"` +} + +func (ki *KeyInfoModel) toAPI() *v20230301.EndpointKeyInfo { + if ki == nil { + return nil + } + + return &v20230301.EndpointKeyInfo{ + Format: utils.ToStringPointer[string, v20230301.EndpointKeyInfoFormat](ki.Format.ValueStringPointer()), + PubFile: ki.PubFile.ValueStringPointer(), + Type: utils.ToStringPointer[string, v20230301.EndpointKeyInfoType](ki.Type.ValueStringPointer()), + } +} + +type ReloadInfoModel struct { + Method types.String `tfsdk:"method"` + PIDFile types.String `tfsdk:"pid_file"` + Signal types.Int64 `tfsdk:"signal"` +} + +var reloadInfoType = map[string]attr.Type{ + "method": types.StringType, + "pid_file": types.StringType, + "signal": types.Int64Type, +} + +func (ri *ReloadInfoModel) toAPI() *v20230301.EndpointReloadInfo { + if ri == nil { + return nil + } + + return &v20230301.EndpointReloadInfo{ + Method: v20230301.EndpointReloadInfoMethod(ri.Method.ValueString()), + PidFile: ri.PIDFile.ValueStringPointer(), + Signal: utils.ToIntPointer(ri.Signal.ValueInt64Pointer()), + } +} + +func fromAPI(ctx context.Context, ec *v20230301.EndpointConfiguration, state utils.AttributeGetter) (*Model, diag.Diagnostics) { + var diags diag.Diagnostics + + ciDuration, d := utils.ToEqualString(ctx, ec.CertificateInfo.Duration, state, path.Root("certificate_info").AtName("duration"), utils.IsDurationEqual) + diags = append(diags, d...) + + ciCrtFile, d := utils.ToOptionalString(ctx, ec.CertificateInfo.CrtFile, state, path.Root("certificate_info").AtName("crt_file")) + diags = append(diags, d...) + + ciKeyFile, d := utils.ToOptionalString(ctx, ec.CertificateInfo.KeyFile, state, path.Root("certificate_info").AtName("key_file")) + diags = append(diags, d...) + + ciRootFile, d := utils.ToOptionalString(ctx, ec.CertificateInfo.RootFile, state, path.Root("certificate_info").AtName("root_file")) + diags = append(diags, d...) + + ciUID, d := utils.ToOptionalInt(ctx, ec.CertificateInfo.Uid, state, path.Root("certificate_info").AtName("uid")) + diags = append(diags, d...) + + ciGID, d := utils.ToOptionalInt(ctx, ec.CertificateInfo.Gid, state, path.Root("certificate_info").AtName("gid")) + diags = append(diags, d...) + + ciMode, d := utils.ToOptionalInt(ctx, ec.CertificateInfo.Mode, state, path.Root("certificate_info").AtName("mode")) + diags = append(diags, d...) + + model := &Model{ + ID: types.StringValue(utils.Deref(ec.Id)), + Name: types.StringValue(ec.Name), + Kind: types.StringValue(string(ec.Kind)), + AuthorityID: types.StringValue(ec.AuthorityID), + Provisioner: types.StringValue(ec.Provisioner), + CertificateInfo: &CertificateInfoModel{ + Type: types.StringValue(string(ec.CertificateInfo.Type)), + Duration: ciDuration, + CrtFile: ciCrtFile, + KeyFile: ciKeyFile, + RootFile: ciRootFile, + UID: ciUID, + GID: ciGID, + Mode: ciMode, + }, + } + + if ec.Hooks != nil { + sign, d := hookFromAPI(ctx, ec.Hooks.Sign, path.Root("hooks").AtName("sign"), state) + diags.Append(d...) + + renew, d := hookFromAPI(ctx, ec.Hooks.Renew, path.Root("hooks").AtName("renew"), state) + diags.Append(d...) + + hooksObj, d := basetypes.NewObjectValue(hooksObjectType, map[string]attr.Value{ + "sign": sign, + "renew": renew, + }) + diags.Append(d...) + + model.Hooks = hooksObj + } else { + model.Hooks = basetypes.NewObjectNull(hooksObjectType) + } + + if ec.KeyInfo != nil { + format, d := utils.ToOptionalString(ctx, ec.KeyInfo.Format, state, path.Root("key_info").AtName("format")) + diags = append(diags, d...) + + pubFile, d := utils.ToOptionalString(ctx, ec.KeyInfo.PubFile, state, path.Root("key_info").AtName("pub_file")) + diags = append(diags, d...) + + typ, d := utils.ToOptionalString(ctx, ec.KeyInfo.Type, state, path.Root("key_info").AtName("type")) + diags = append(diags, d...) + + model.KeyInfo = &KeyInfoModel{ + Format: format, + PubFile: pubFile, + Type: typ, + } + } + + if ec.ReloadInfo != nil { + pidFile, d := utils.ToOptionalString(ctx, ec.ReloadInfo.PidFile, state, path.Root("reload_info").AtName("method")) + diags.Append(d...) + + signal, d := utils.ToOptionalInt(ctx, ec.ReloadInfo.Signal, state, path.Root("reload_info").AtName("signal")) + diags.Append(d...) + + reloadInfoObject, d := basetypes.NewObjectValue(reloadInfoType, map[string]attr.Value{ + "method": types.StringValue(string(ec.ReloadInfo.Method)), + "pid_file": pidFile, + "signal": signal, + }) + diags.Append(d...) + model.ReloadInfo = reloadInfoObject + } else { + model.ReloadInfo = basetypes.NewObjectNull(reloadInfoType) + } + + return model, diags +} + +func toAPI(ctx context.Context, model *Model) (*v20230301.EndpointConfiguration, diag.Diagnostics) { + hooksModel := &HooksModel{} + diags := model.Hooks.As(ctx, &hooksModel, basetypes.ObjectAsOptions{ + UnhandledUnknownAsEmpty: true, + }) + hooks, d := hooksModel.toAPI(ctx) + diags.Append(d...) + + reloadInfo := &ReloadInfoModel{} + d = model.ReloadInfo.As(ctx, &reloadInfo, basetypes.ObjectAsOptions{ + UnhandledUnknownAsEmpty: true, + }) + diags.Append(d...) + + return &v20230301.EndpointConfiguration{ + Name: model.Name.ValueString(), + Kind: v20230301.EndpointConfigurationKind(model.Kind.ValueString()), + AuthorityID: model.AuthorityID.ValueString(), + Provisioner: model.Provisioner.ValueString(), + CertificateInfo: model.CertificateInfo.toAPI(), + KeyInfo: model.KeyInfo.toAPI(), + Hooks: hooks, + ReloadInfo: reloadInfo.toAPI(), + }, diags +} + +func hookFromAPI(ctx context.Context, hook *v20230301.EndpointHook, hookPath path.Path, state utils.AttributeGetter) (types.Object, diag.Diagnostics) { + var diags diag.Diagnostics + + if hook == nil { + return basetypes.NewObjectNull(hookObjectType.AttrTypes), diags + } + + shell, d := utils.ToOptionalString(ctx, hook.Shell, state, hookPath.AtName("shell")) + diags = append(diags, d...) + + before, d := utils.ToOptionalList(ctx, hook.Before, state, hookPath.AtName("before")) + diags = append(diags, d...) + + after, d := utils.ToOptionalList(ctx, hook.After, state, hookPath.AtName("after")) + diags = append(diags, d...) + + onError, d := utils.ToOptionalList(ctx, hook.OnError, state, hookPath.AtName("on_error")) + diags = append(diags, d...) + + obj, d := basetypes.NewObjectValue(hookObjectType.AttrTypes, map[string]attr.Value{ + "shell": shell, + "before": before, + "after": after, + "on_error": onError, + }) + diags.Append(d...) + + return obj, diags +} diff --git a/internal/provider/endpoint_configuration/resource.go b/internal/provider/endpoint_configuration/resource.go new file mode 100644 index 0000000000000000000000000000000000000000..e72a36238aee80898e6a619081104a603756a26b --- /dev/null +++ b/internal/provider/endpoint_configuration/resource.go @@ -0,0 +1,472 @@ +package endpoint_configuration + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + + "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/int64planmodifier" + "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" + v20230301 "github.com/smallstep/terraform-provider-smallstep/internal/apiclient/v20230301" + "github.com/smallstep/terraform-provider-smallstep/internal/provider/utils" +) + +var _ resource.ResourceWithImportState = (*Resource)(nil) + +func NewResource() resource.Resource { + return &Resource{} +} + +// Resource defines the resource implementation. +type Resource struct { + client *v20230301.Client +} + +func (r *Resource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = typeName +} + +// Configure adds the Smallstep API client to the resource. +func (r *Resource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + // Prevent panic if the provider has not been configured. + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(*v20230301.Client) + + if !ok { + resp.Diagnostics.AddError( + "Get Smallstep API client from provider", + fmt.Sprintf("Expected *v20230301.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + return + } + + r.client = client +} + +func (r *Resource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + state := &Model{} + resp.Diagnostics.Append(req.State.Get(ctx, state)...) + + if resp.Diagnostics.HasError() { + return + } + + id := state.ID.ValueString() + + httpResp, err := r.client.GetEndpointConfiguration(ctx, id, &v20230301.GetEndpointConfigurationParams{}) + if err != nil { + resp.Diagnostics.AddError( + "Smallstep API Client Error", + fmt.Sprintf("Failed to read endpoint configuration %q: %v", id, err), + ) + return + } + defer httpResp.Body.Close() + + if httpResp.StatusCode == http.StatusNotFound { + resp.State.RemoveResource(ctx) + return + } + + if httpResp.StatusCode != http.StatusOK { + resp.Diagnostics.AddError( + "Smallstep API Response Error", + fmt.Sprintf("Received status %d reading endpoint configuration %q: %s", httpResp.StatusCode, id, utils.APIErrorMsg(httpResp.Body)), + ) + return + } + + ac := &v20230301.EndpointConfiguration{} + if err := json.NewDecoder(httpResp.Body).Decode(ac); err != nil { + resp.Diagnostics.AddError( + "Smallstep API Client Error", + fmt.Sprintf("Failed to unmarshal endpoint configuration %q: %v", id, err), + ) + return + } + + remote, d := fromAPI(ctx, ac, req.State) + resp.Diagnostics.Append(d...) + if resp.Diagnostics.HasError() { + return + } + + tflog.Trace(ctx, fmt.Sprintf("read endpoint configuration %q resource", id)) + + resp.Diagnostics.Append(resp.State.Set(ctx, &remote)...) +} + +func (r *Resource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + component, props, err := utils.Describe("endpointConfiguration") + if err != nil { + resp.Diagnostics.AddError( + "Parse Smallstep OpenAPI spec", + err.Error(), + ) + return + } + + certInfo, certInfoProps, err := utils.Describe("endpointCertificateInfo") + if err != nil { + resp.Diagnostics.AddError( + "Parse Smallstep OpenAPI spec", + err.Error(), + ) + return + } + + _, hookProps, err := utils.Describe("endpointHook") + if err != nil { + resp.Diagnostics.AddError( + "Parse Smallstep OpenAPI spec", + err.Error(), + ) + return + } + + hooks, hooksProps, err := utils.Describe("endpointHooks") + if err != nil { + resp.Diagnostics.AddError( + "Parse Smallstep OpenAPI spec", + err.Error(), + ) + return + } + + keyInfo, keyInfoProps, err := utils.Describe("endpointKeyInfo") + if err != nil { + resp.Diagnostics.AddError( + "Parse Smallstep OpenAPI spec", + err.Error(), + ) + return + } + + reloadInfo, reloadInfoProps, err := utils.Describe("endpointReloadInfo") + if err != nil { + resp.Diagnostics.AddError( + "Parse Smallstep OpenAPI spec", + err.Error(), + ) + return + } + + resp.Schema = schema.Schema{ + MarkdownDescription: component, + + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + MarkdownDescription: props["id"], + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "name": schema.StringAttribute{ + MarkdownDescription: props["name"], + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "kind": schema.StringAttribute{ + MarkdownDescription: props["kind"], + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "authority_id": schema.StringAttribute{ + MarkdownDescription: props["authorityID"], + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "provisioner_name": schema.StringAttribute{ + MarkdownDescription: props["provisioner"], + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "key_info": schema.SingleNestedAttribute{ + // This object is not required by the API but a default object + // will always be returned with both format and type set to + // "DEFAULT". To avoid "inconsistent result after apply" errors + // require these fields to be set explicitly in terraform. + Required: true, + MarkdownDescription: keyInfo, + Attributes: map[string]schema.Attribute{ + "type": schema.StringAttribute{ + Required: true, + MarkdownDescription: keyInfoProps["type"], + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "format": schema.StringAttribute{ + Required: true, + MarkdownDescription: keyInfoProps["format"], + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "pub_file": schema.StringAttribute{ + Optional: true, + MarkdownDescription: keyInfoProps["pubFile"], + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + }, + }, + "reload_info": schema.SingleNestedAttribute{ + Optional: true, + Computed: true, + MarkdownDescription: reloadInfo, + Attributes: map[string]schema.Attribute{ + "method": schema.StringAttribute{ + Required: true, + MarkdownDescription: reloadInfoProps["method"], + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "pid_file": schema.StringAttribute{ + Optional: true, + MarkdownDescription: reloadInfoProps["pidFile"], + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "signal": schema.Int64Attribute{ + Optional: true, + MarkdownDescription: reloadInfoProps["signal"], + PlanModifiers: []planmodifier.Int64{ + int64planmodifier.RequiresReplace(), + }, + }, + }, + }, + "hooks": schema.SingleNestedAttribute{ + Optional: true, + Computed: true, + MarkdownDescription: hooks, + Attributes: map[string]schema.Attribute{ + "sign": schema.SingleNestedAttribute{ + Optional: true, + MarkdownDescription: hooksProps["sign"], + Attributes: map[string]schema.Attribute{ + "shell": schema.StringAttribute{ + Optional: true, + MarkdownDescription: hookProps["shell"], + }, + "before": schema.ListAttribute{ + ElementType: types.StringType, + Optional: true, + MarkdownDescription: hookProps["before"], + }, + "after": schema.ListAttribute{ + ElementType: types.StringType, + Optional: true, + MarkdownDescription: hookProps["after"], + }, + "on_error": schema.ListAttribute{ + ElementType: types.StringType, + Optional: true, + MarkdownDescription: hookProps["onError"], + }, + }, + }, + "renew": schema.SingleNestedAttribute{ + Optional: true, + MarkdownDescription: hooksProps["renew"], + Attributes: map[string]schema.Attribute{ + "shell": schema.StringAttribute{ + Optional: true, + MarkdownDescription: hookProps["shell"], + }, + "before": schema.ListAttribute{ + ElementType: types.StringType, + Optional: true, + MarkdownDescription: hookProps["before"], + }, + "after": schema.ListAttribute{ + ElementType: types.StringType, + Optional: true, + MarkdownDescription: hookProps["after"], + }, + "on_error": schema.ListAttribute{ + ElementType: types.StringType, + Optional: true, + MarkdownDescription: hookProps["onError"], + }, + }, + }, + }, + }, + "certificate_info": schema.SingleNestedAttribute{ + Required: true, + MarkdownDescription: certInfo, + Attributes: map[string]schema.Attribute{ + "type": schema.StringAttribute{ + MarkdownDescription: certInfoProps["type"], + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "duration": schema.StringAttribute{ + MarkdownDescription: certInfoProps["duration"], + Optional: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "crt_file": schema.StringAttribute{ + MarkdownDescription: certInfoProps["crtFile"], + Optional: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "key_file": schema.StringAttribute{ + MarkdownDescription: certInfoProps["keyFile"], + Optional: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "root_file": schema.StringAttribute{ + MarkdownDescription: certInfoProps["rootFile"], + Optional: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "uid": schema.Int64Attribute{ + MarkdownDescription: certInfoProps["uid"], + Optional: true, + PlanModifiers: []planmodifier.Int64{ + int64planmodifier.RequiresReplace(), + }, + }, + "gid": schema.Int64Attribute{ + MarkdownDescription: certInfoProps["gid"], + Optional: true, + PlanModifiers: []planmodifier.Int64{ + int64planmodifier.RequiresReplace(), + }, + }, + "mode": schema.Int64Attribute{ + MarkdownDescription: certInfoProps["mode"], + Optional: true, + PlanModifiers: []planmodifier.Int64{ + int64planmodifier.RequiresReplace(), + }, + }, + }, + }, + }, + } +} + +func (a *Resource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + plan := &Model{} + resp.Diagnostics.Append(req.Plan.Get(ctx, plan)...) + + if resp.Diagnostics.HasError() { + return + } + + reqBody, diags := toAPI(ctx, plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + httpResp, err := a.client.PostEndpointConfigurations(ctx, &v20230301.PostEndpointConfigurationsParams{}, *reqBody) + if err != nil { + resp.Diagnostics.AddError( + "Smallstep API Client Error", + fmt.Sprintf("Failed to create endpoint configuration %q: %v", plan.Name.ValueString(), err), + ) + return + } + defer httpResp.Body.Close() + + if httpResp.StatusCode != http.StatusCreated { + resp.Diagnostics.AddError( + "Smallstep API Response Error", + fmt.Sprintf("Received status %d creating endpoint configuration %q: %s", httpResp.StatusCode, plan.Name.ValueString(), utils.APIErrorMsg(httpResp.Body)), + ) + return + } + + ac := &v20230301.EndpointConfiguration{} + if err := json.NewDecoder(httpResp.Body).Decode(ac); err != nil { + resp.Diagnostics.AddError( + "Smallstep API Client Error", + fmt.Sprintf("Failed to unmarshal endpoint configuration %q: %v", plan.Name.ValueString(), err), + ) + return + } + + state, diags := fromAPI(ctx, ac, req.Plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + tflog.Trace(ctx, fmt.Sprintf("create endpoint configuration %q resource", plan.Name.ValueString())) + + resp.Diagnostics.Append(resp.State.Set(ctx, state)...) +} + +func (r *Resource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + // Update not supported. All changes require replacement. +} + +func (a *Resource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + state := &Model{} + resp.Diagnostics.Append(req.State.Get(ctx, state)...) + + if resp.Diagnostics.HasError() { + return + } + + id := state.ID.ValueString() + + httpResp, err := a.client.DeleteEndpointConfiguration(ctx, id, &v20230301.DeleteEndpointConfigurationParams{}) + if err != nil { + resp.Diagnostics.AddError( + "Smallstep API Client Error", + fmt.Sprintf("Failed to delete endpoint configuration %s: %v", id, err), + ) + return + } + defer httpResp.Body.Close() + + if httpResp.StatusCode != http.StatusNoContent { + resp.Diagnostics.AddError( + "Smallstep API Response Error", + fmt.Sprintf("Received status %d deleting endpoint configuration %s: %s", httpResp.StatusCode, id, utils.APIErrorMsg(httpResp.Body)), + ) + return + } +} + +func (r *Resource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} diff --git a/internal/provider/managed_configuration/data_source.go b/internal/provider/managed_configuration/data_source.go new file mode 100644 index 0000000000000000000000000000000000000000..2675c370b4b323335af767593cbc8b675d5975dd --- /dev/null +++ b/internal/provider/managed_configuration/data_source.go @@ -0,0 +1,207 @@ +package managed_configuration + +import ( + "context" + "encoding/json" + "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" + v20230301 "github.com/smallstep/terraform-provider-smallstep/internal/apiclient/v20230301" + "github.com/smallstep/terraform-provider-smallstep/internal/provider/utils" +) + +var _ datasource.DataSourceWithConfigure = (*DataSource)(nil) + +func NewDataSource() datasource.DataSource { + return &DataSource{} +} + +// DataSource implements data.smallstep_managed_configuration +type DataSource struct { + client *v20230301.Client +} + +func (a *DataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = typeName +} + +// Configure adds the Smallstep API client to the data source. +func (a *DataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + // Prevent panic if the provider has not been configured. + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(*v20230301.Client) + + if !ok { + resp.Diagnostics.AddError( + "Get Smallstep API client from provider", + fmt.Sprintf("Expected *v20230301.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + return + } + + a.client = client +} + +func (a *DataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var config Model + + // Read Terraform configuration data into the model + resp.Diagnostics.Append(req.Config.Get(ctx, &config)...) + + if resp.Diagnostics.HasError() { + return + } + + id := config.ID.ValueString() + + httpResp, err := a.client.GetManagedConfiguration(ctx, id, &v20230301.GetManagedConfigurationParams{}) + if err != nil { + resp.Diagnostics.AddError( + "Smallstep API Client Error", + fmt.Sprintf("Failed to read managed configuration %q: %v", id, err), + ) + return + } + defer httpResp.Body.Close() + + if httpResp.StatusCode != http.StatusOK { + resp.Diagnostics.AddError( + "Smallstep API Response Error", + fmt.Sprintf("Received status %d reading managed configuration %q: %s", httpResp.StatusCode, id, utils.APIErrorMsg(httpResp.Body)), + ) + return + } + + ac := &v20230301.ManagedConfiguration{} + if err := json.NewDecoder(httpResp.Body).Decode(ac); err != nil { + resp.Diagnostics.AddError( + "Smallstep API Client Error", + fmt.Sprintf("Failed to unmarshal managed configuration %q: %v", id, err), + ) + return + } + + remote, d := fromAPI(ctx, ac, req.Config) + resp.Diagnostics.Append(d...) + if resp.Diagnostics.HasError() { + return + } + + tflog.Trace(ctx, fmt.Sprintf("read managed configuration %q data source", id)) + + resp.Diagnostics.Append(resp.State.Set(ctx, &remote)...) +} + +func (d *DataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { + component, props, err := utils.Describe("managedConfiguration") + if err != nil { + resp.Diagnostics.AddError( + "Parse Smallstep OpenAPI spec", + err.Error(), + ) + return + } + + me, meProps, err := utils.Describe("managedEndpoint") + if err != nil { + resp.Diagnostics.AddError( + "Parse Smallstep OpenAPI spec", + err.Error(), + ) + return + } + + x509, x509Props, err := utils.Describe("endpointX509CertificateData") + if err != nil { + resp.Diagnostics.AddError( + "Parse Smallstep OpenAPI spec", + err.Error(), + ) + return + } + + ssh, sshProps, err := utils.Describe("endpointSSHCertificateData") + if err != nil { + resp.Diagnostics.AddError( + "Parse Smallstep OpenAPI spec", + err.Error(), + ) + return + } + + resp.Schema = schema.Schema{ + MarkdownDescription: component, + + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + MarkdownDescription: props["id"], + Required: true, + }, + "name": schema.StringAttribute{ + MarkdownDescription: props["name"], + Computed: true, + }, + "agent_configuration_id": schema.StringAttribute{ + MarkdownDescription: props["agentConfigurationID"], + Computed: true, + }, + "host_id": schema.StringAttribute{ + MarkdownDescription: props["hostID"], + Computed: true, + }, + "managed_endpoints": schema.SetNestedAttribute{ + MarkdownDescription: me, + Computed: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + MarkdownDescription: meProps["id"], + Computed: true, + }, + "endpoint_configuration_id": schema.StringAttribute{ + MarkdownDescription: meProps["endpointConfigurationID"], + Computed: true, + }, + "x509_certificate_data": schema.SingleNestedAttribute{ + MarkdownDescription: x509, + Computed: true, + Attributes: map[string]schema.Attribute{ + "common_name": schema.StringAttribute{ + MarkdownDescription: x509Props["commonName"], + Computed: true, + }, + "sans": schema.SetAttribute{ + ElementType: types.StringType, + MarkdownDescription: x509Props["sans"], + Computed: true, + }, + }, + }, + "ssh_certificate_data": schema.SingleNestedAttribute{ + MarkdownDescription: ssh, + Computed: true, + Attributes: map[string]schema.Attribute{ + "key_id": schema.StringAttribute{ + MarkdownDescription: sshProps["keyID"], + Computed: true, + }, + "principals": schema.SetAttribute{ + ElementType: types.StringType, + MarkdownDescription: sshProps["principals"], + Computed: true, + }, + }, + }, + }, + }, + }, + }, + } +} diff --git a/internal/provider/managed_configuration/model.go b/internal/provider/managed_configuration/model.go new file mode 100644 index 0000000000000000000000000000000000000000..3ee9ee4b2b0c2c0f0eb8a7bed98849474fa4f64f --- /dev/null +++ b/internal/provider/managed_configuration/model.go @@ -0,0 +1,161 @@ +package managed_configuration + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/types" + v20230301 "github.com/smallstep/terraform-provider-smallstep/internal/apiclient/v20230301" + "github.com/smallstep/terraform-provider-smallstep/internal/provider/utils" +) + +const typeName = "smallstep_managed_configuration" + +type Model struct { + ID types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + AgentConfigurationID types.String `tfsdk:"agent_configuration_id"` + HostID types.String `tfsdk:"host_id"` + ManagedEndpoints []ManagedEndpoint `tfsdk:"managed_endpoints"` +} + +func (model Model) toAPI(ctx context.Context) (v20230301.ManagedConfiguration, diag.Diagnostics) { + var diags diag.Diagnostics + + mc := v20230301.ManagedConfiguration{ + Id: model.ID.ValueStringPointer(), + Name: model.Name.ValueString(), + AgentConfigurationID: model.AgentConfigurationID.ValueString(), + HostID: model.HostID.ValueStringPointer(), + } + + for _, me := range model.ManagedEndpoints { + me, d := me.toAPI(ctx) + diags.Append(d...) + mc.ManagedEndpoints = append(mc.ManagedEndpoints, me) + } + + return mc, diags +} + +type ManagedEndpoint struct { + ID types.String `tfsdk:"id"` + EndpointConfigurationID types.String `tfsdk:"endpoint_configuration_id"` + SSHCertificateData *SSHCertificateData `tfsdk:"ssh_certificate_data"` + X509CertificateData *X509CertificateData `tfsdk:"x509_certificate_data"` +} + +func (me *ManagedEndpoint) toAPI(ctx context.Context) (v20230301.ManagedEndpoint, diag.Diagnostics) { + x509, diags := me.X509CertificateData.toAPI(ctx) + ssh, d := me.SSHCertificateData.toAPI(ctx) + diags.Append(d...) + + return v20230301.ManagedEndpoint{ + Id: me.ID.ValueStringPointer(), + EndpointConfigurationID: me.EndpointConfigurationID.ValueString(), + X509CertificateData: x509, + SshCertificateData: ssh, + }, diags +} + +type SSHCertificateData struct { + KeyID types.String `tfsdk:"key_id"` + Principals types.Set `tfsdk:"principals"` +} + +func (ssh *SSHCertificateData) toAPI(ctx context.Context) (*v20230301.EndpointSSHCertificateData, diag.Diagnostics) { + if ssh == nil { + return nil, diag.Diagnostics{} + } + var principals []string + diags := ssh.Principals.ElementsAs(ctx, &principals, false) + + return &v20230301.EndpointSSHCertificateData{ + KeyID: ssh.KeyID.ValueString(), + Principals: principals, + }, diags +} + +type X509CertificateData struct { + CommonName types.String `tfsdk:"common_name"` + SANs types.Set `tfsdk:"sans"` +} + +func (x509 *X509CertificateData) toAPI(ctx context.Context) (*v20230301.EndpointX509CertificateData, diag.Diagnostics) { + if x509 == nil { + return nil, diag.Diagnostics{} + } + + var sans []string + diags := x509.SANs.ElementsAs(ctx, &sans, false) + + return &v20230301.EndpointX509CertificateData{ + CommonName: x509.CommonName.ValueString(), + Sans: sans, + }, diags +} + +func fromAPI(ctx context.Context, mc *v20230301.ManagedConfiguration, state utils.AttributeGetter) (*Model, diag.Diagnostics) { + var diags diag.Diagnostics + + model := &Model{ + ID: types.StringValue(utils.Deref(mc.Id)), + Name: types.StringValue(mc.Name), + AgentConfigurationID: types.StringValue(mc.AgentConfigurationID), + HostID: types.StringValue(utils.Deref(mc.HostID)), + } + + for i, me := range mc.ManagedEndpoints { + p := path.Root("managed_endpoints").AtListIndex(i) + ssh, d := fromSSHCertificateData(ctx, me.SshCertificateData, state, p) + diags.Append(d...) + + x509, d := fromX509CertificateData(ctx, me.X509CertificateData, state, p) + diags.Append(d...) + + model.ManagedEndpoints = append(model.ManagedEndpoints, ManagedEndpoint{ + ID: types.StringValue(utils.Deref(me.Id)), + EndpointConfigurationID: types.StringValue(me.EndpointConfigurationID), + SSHCertificateData: ssh, + X509CertificateData: x509, + }) + } + + return model, diags +} + +func fromSSHCertificateData(ctx context.Context, ssh *v20230301.EndpointSSHCertificateData, state utils.AttributeGetter, p path.Path) (*SSHCertificateData, diag.Diagnostics) { + if ssh == nil { + return nil, diag.Diagnostics{} + } + + values := make([]attr.Value, len(ssh.Principals)) + for i, s := range ssh.Principals { + values[i] = types.StringValue(s) + } + principals, diags := types.SetValue(types.StringType, values) + + return &SSHCertificateData{ + KeyID: types.StringValue(ssh.KeyID), + Principals: principals, + }, diags +} + +func fromX509CertificateData(ctx context.Context, x509 *v20230301.EndpointX509CertificateData, state utils.AttributeGetter, p path.Path) (*X509CertificateData, diag.Diagnostics) { + if x509 == nil { + return nil, diag.Diagnostics{} + } + + values := make([]attr.Value, len(x509.Sans)) + for i, s := range x509.Sans { + values[i] = types.StringValue(s) + } + sans, diags := types.SetValue(types.StringType, values) + + return &X509CertificateData{ + CommonName: types.StringValue(x509.CommonName), + SANs: sans, + }, diags +} diff --git a/internal/provider/managed_configuration/resource.go b/internal/provider/managed_configuration/resource.go new file mode 100644 index 0000000000000000000000000000000000000000..ec2e765ea3d1b139d982d93cb94ab729f573bbed --- /dev/null +++ b/internal/provider/managed_configuration/resource.go @@ -0,0 +1,340 @@ +package managed_configuration + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + + "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/setplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + v20230301 "github.com/smallstep/terraform-provider-smallstep/internal/apiclient/v20230301" + "github.com/smallstep/terraform-provider-smallstep/internal/provider/utils" +) + +var _ resource.ResourceWithImportState = (*Resource)(nil) + +func NewResource() resource.Resource { + return &Resource{} +} + +// Resource defines the resource implementation. +type Resource struct { + client *v20230301.Client +} + +func (r *Resource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = typeName +} + +// Configure adds the Smallstep API client to the resource. +func (r *Resource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + // Prevent panic if the provider has not been configured. + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(*v20230301.Client) + + if !ok { + resp.Diagnostics.AddError( + "Get Smallstep API client from provider", + fmt.Sprintf("Expected *v20230301.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + return + } + + r.client = client +} + +func (r *Resource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + state := &Model{} + + resp.Diagnostics.Append(req.State.Get(ctx, state)...) + + if resp.Diagnostics.HasError() { + return + } + + id := state.ID.ValueString() + + httpResp, err := r.client.GetManagedConfiguration(ctx, id, &v20230301.GetManagedConfigurationParams{}) + if err != nil { + resp.Diagnostics.AddError( + "Smallstep API Client Error", + fmt.Sprintf("Failed to read managed configuration %q: %v", id, err), + ) + return + } + defer httpResp.Body.Close() + + if httpResp.StatusCode == http.StatusNotFound { + resp.State.RemoveResource(ctx) + return + } + + if httpResp.StatusCode != http.StatusOK { + resp.Diagnostics.AddError( + "Smallstep API Response Error", + fmt.Sprintf("Received status %d reading managed configuration %q: %s", httpResp.StatusCode, id, utils.APIErrorMsg(httpResp.Body)), + ) + return + } + + ac := &v20230301.ManagedConfiguration{} + if err := json.NewDecoder(httpResp.Body).Decode(ac); err != nil { + resp.Diagnostics.AddError( + "Smallstep API Client Error", + fmt.Sprintf("Failed to unmarshal managed configuration %q: %v", id, err), + ) + return + } + + remote, d := fromAPI(ctx, ac, req.State) + if d.HasError() { + resp.Diagnostics.Append(d...) + return + } + + tflog.Trace(ctx, fmt.Sprintf("read managed configuration %q resource", id)) + + resp.Diagnostics.Append(resp.State.Set(ctx, &remote)...) +} + +func (r *Resource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + component, props, err := utils.Describe("managedConfiguration") + if err != nil { + resp.Diagnostics.AddError( + "Parse Smallstep OpenAPI spec", + err.Error(), + ) + return + } + + me, meProps, err := utils.Describe("managedEndpoint") + if err != nil { + resp.Diagnostics.AddError( + "Parse Smallstep OpenAPI spec", + err.Error(), + ) + return + } + + x509, x509Props, err := utils.Describe("endpointX509CertificateData") + if err != nil { + resp.Diagnostics.AddError( + "Parse Smallstep OpenAPI spec", + err.Error(), + ) + return + } + + ssh, sshProps, err := utils.Describe("endpointSSHCertificateData") + if err != nil { + resp.Diagnostics.AddError( + "Parse Smallstep OpenAPI spec", + err.Error(), + ) + return + } + + resp.Schema = schema.Schema{ + MarkdownDescription: component, + + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + MarkdownDescription: props["id"], + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "name": schema.StringAttribute{ + MarkdownDescription: props["name"], + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "agent_configuration_id": schema.StringAttribute{ + MarkdownDescription: props["agentConfigurationID"], + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "host_id": schema.StringAttribute{ + MarkdownDescription: props["hostID"], + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "managed_endpoints": schema.SetNestedAttribute{ + MarkdownDescription: me, + Required: true, + PlanModifiers: []planmodifier.Set{ + setplanmodifier.RequiresReplace(), + }, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + MarkdownDescription: meProps["id"], + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "endpoint_configuration_id": schema.StringAttribute{ + MarkdownDescription: meProps["endpointConfigurationID"], + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "x509_certificate_data": schema.SingleNestedAttribute{ + MarkdownDescription: x509, + Optional: true, + Attributes: map[string]schema.Attribute{ + "common_name": schema.StringAttribute{ + MarkdownDescription: x509Props["commonName"], + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "sans": schema.SetAttribute{ + ElementType: types.StringType, + MarkdownDescription: x509Props["sans"], + Required: true, + PlanModifiers: []planmodifier.Set{ + setplanmodifier.RequiresReplace(), + }, + }, + }, + }, + "ssh_certificate_data": schema.SingleNestedAttribute{ + MarkdownDescription: ssh, + Optional: true, + Attributes: map[string]schema.Attribute{ + "key_id": schema.StringAttribute{ + MarkdownDescription: sshProps["keyID"], + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "principals": schema.SetAttribute{ + ElementType: types.StringType, + MarkdownDescription: sshProps["principals"], + Required: true, + PlanModifiers: []planmodifier.Set{ + setplanmodifier.RequiresReplace(), + }, + }, + }, + }, + }, + }, + }, + }, + } +} + +func (a *Resource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan Model + + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + + if resp.Diagnostics.HasError() { + return + } + + reqBody, diags := plan.toAPI(ctx) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + httpResp, err := a.client.PostManagedConfigurations(ctx, &v20230301.PostManagedConfigurationsParams{}, reqBody) + if err != nil { + resp.Diagnostics.AddError( + "Smallstep API Client Error", + fmt.Sprintf("Failed to create managed configuration %q: %v", plan.Name.ValueString(), err), + ) + return + } + defer httpResp.Body.Close() + + if httpResp.StatusCode != http.StatusCreated { + resp.Diagnostics.AddError( + "Smallstep API Response Error", + fmt.Sprintf("Received status %d creating managed configuration %q: %s", httpResp.StatusCode, plan.Name.ValueString(), utils.APIErrorMsg(httpResp.Body)), + ) + return + } + + mc := &v20230301.ManagedConfiguration{} + if err := json.NewDecoder(httpResp.Body).Decode(mc); err != nil { + resp.Diagnostics.AddError( + "Smallstep API Client Error", + fmt.Sprintf("Failed to unmarshal managed configuration %q: %v", plan.Name.ValueString(), err), + ) + return + } + + state, diags := fromAPI(ctx, mc, req.Plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + tflog.Trace(ctx, fmt.Sprintf("create managed configuration %q resource", plan.Name.ValueString())) + + resp.Diagnostics.Append(resp.State.Set(ctx, state)...) +} + +func (r *Resource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + // Update not supported. All changes require replacement. +} + +func (a *Resource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state Model + + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + + if resp.Diagnostics.HasError() { + return + } + + id := state.ID.ValueString() + + httpResp, err := a.client.DeleteManagedConfiguration(ctx, id, &v20230301.DeleteManagedConfigurationParams{}) + if err != nil { + resp.Diagnostics.AddError( + "Smallstep API Client Error", + fmt.Sprintf("Failed to delete managed configuration %s: %v", id, err), + ) + return + } + defer httpResp.Body.Close() + + if httpResp.StatusCode != http.StatusNoContent { + resp.Diagnostics.AddError( + "Smallstep API Response Error", + fmt.Sprintf("Received status %d deleting managed configuration %s: %s", httpResp.StatusCode, id, utils.APIErrorMsg(httpResp.Body)), + ) + return + } +} + +func (r *Resource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} diff --git a/internal/provider/managed_workload_data_source_test.go b/internal/provider/managed_workload_data_source_test.go new file mode 100644 index 0000000000000000000000000000000000000000..161f6db335a760a1bedb62861ddb4d97dadcf770 --- /dev/null +++ b/internal/provider/managed_workload_data_source_test.go @@ -0,0 +1,118 @@ +package provider + +import ( + "fmt" + "regexp" + "strconv" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/smallstep/terraform-provider-smallstep/internal/provider/utils" +) + +func TestAccManagedWorkloadDataSource(t *testing.T) { + authority := utils.NewAuthority(t) + provisioner, _ := utils.NewOIDCProvisioner(t, authority.Id) + collection := utils.NewCollection(t) + attest := utils.FixAttestationAuthority(t, collection.Slug) + ac := utils.NewAgentConfiguration(t, authority.Id, provisioner.Name, *attest.Slug) + ec := utils.NewEndpointConfiguration(t, authority.Id, provisioner.Name) + mc := utils.NewManagedConfiguration(t, *ac.Id, *ec.Id) + + managedConfig := fmt.Sprintf(` +data "smallstep_managed_configuration" "mc" { + id = %q +} +`, *mc.Id) + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: managedConfig, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("data.smallstep_managed_configuration.mc", "id", *mc.Id), + resource.TestCheckResourceAttr("data.smallstep_managed_configuration.mc", "agent_configuration_id", *ac.Id), + resource.TestCheckResourceAttr("data.smallstep_managed_configuration.mc", "host_id", utils.Deref(mc.HostID)), + resource.TestCheckResourceAttr("data.smallstep_managed_configuration.mc", "name", mc.Name), + resource.TestCheckResourceAttr("data.smallstep_managed_configuration.mc", "managed_endpoints.#", "1"), + resource.TestCheckResourceAttr("data.smallstep_managed_configuration.mc", "managed_endpoints.0.endpoint_configuration_id", *ec.Id), + resource.TestCheckResourceAttr("data.smallstep_managed_configuration.mc", "managed_endpoints.0.id", *mc.ManagedEndpoints[0].Id), + resource.TestCheckResourceAttr("data.smallstep_managed_configuration.mc", "managed_endpoints.0.x509_certificate_data.common_name", mc.ManagedEndpoints[0].X509CertificateData.CommonName), + resource.TestCheckResourceAttr("data.smallstep_managed_configuration.mc", "managed_endpoints.0.x509_certificate_data.sans.#", "1"), + ), + }, + }, + }) + + agentConfig := fmt.Sprintf(` +data "smallstep_agent_configuration" "agent" { + id = %q +} +`, *ac.Id) + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: agentConfig, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("data.smallstep_agent_configuration.agent", "id", *ac.Id), + resource.TestCheckResourceAttr("data.smallstep_agent_configuration.agent", "authority_id", authority.Id), + resource.TestCheckResourceAttr("data.smallstep_agent_configuration.agent", "name", ac.Name), + resource.TestCheckResourceAttr("data.smallstep_agent_configuration.agent", "provisioner_name", ac.Provisioner), + resource.TestCheckResourceAttr("data.smallstep_agent_configuration.agent", "attestation_slug", *ac.AttestationSlug), + ), + }, + }, + }) + + endpointConfig := fmt.Sprintf(` +data "smallstep_endpoint_configuration" "ep" { + id = %q +} +`, *ec.Id) + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: endpointConfig, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestMatchResourceAttr("data.smallstep_endpoint_configuration.ep", "id", regexp.MustCompile(`^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`)), + resource.TestCheckResourceAttr("data.smallstep_endpoint_configuration.ep", "authority_id", authority.Id), + resource.TestCheckResourceAttr("data.smallstep_endpoint_configuration.ep", "name", ec.Name), + resource.TestCheckResourceAttr("data.smallstep_endpoint_configuration.ep", "provisioner_name", ec.Provisioner), + resource.TestCheckResourceAttr("data.smallstep_endpoint_configuration.ep", "certificate_info.type", string(ec.CertificateInfo.Type)), + resource.TestCheckResourceAttr("data.smallstep_endpoint_configuration.ep", "certificate_info.duration", utils.Deref(ec.CertificateInfo.Duration)), + resource.TestCheckResourceAttr("data.smallstep_endpoint_configuration.ep", "certificate_info.crt_file", utils.Deref(ec.CertificateInfo.CrtFile)), + resource.TestCheckResourceAttr("data.smallstep_endpoint_configuration.ep", "certificate_info.root_file", utils.Deref(ec.CertificateInfo.RootFile)), + resource.TestCheckResourceAttr("data.smallstep_endpoint_configuration.ep", "certificate_info.key_file", utils.Deref(ec.CertificateInfo.KeyFile)), + resource.TestCheckResourceAttr("data.smallstep_endpoint_configuration.ep", "certificate_info.uid", strconv.Itoa(utils.Deref(ec.CertificateInfo.Uid))), + resource.TestCheckResourceAttr("data.smallstep_endpoint_configuration.ep", "certificate_info.gid", strconv.Itoa(utils.Deref(ec.CertificateInfo.Gid))), + resource.TestCheckResourceAttr("data.smallstep_endpoint_configuration.ep", "certificate_info.mode", strconv.Itoa(utils.Deref(ec.CertificateInfo.Mode))), + resource.TestCheckResourceAttr("data.smallstep_endpoint_configuration.ep", "hooks.sign.shell", utils.Deref(ec.Hooks.Sign.Shell)), + resource.TestCheckResourceAttr("data.smallstep_endpoint_configuration.ep", "hooks.sign.before.#", "1"), + resource.TestCheckResourceAttr("data.smallstep_endpoint_configuration.ep", "hooks.sign.before.0", (*ec.Hooks.Sign.Before)[0]), + resource.TestCheckResourceAttr("data.smallstep_endpoint_configuration.ep", "hooks.sign.after.#", "1"), + resource.TestCheckResourceAttr("data.smallstep_endpoint_configuration.ep", "hooks.sign.after.0", (*ec.Hooks.Sign.After)[0]), + resource.TestCheckResourceAttr("data.smallstep_endpoint_configuration.ep", "hooks.sign.on_error.#", "1"), + resource.TestCheckResourceAttr("data.smallstep_endpoint_configuration.ep", "hooks.sign.on_error.0", (*ec.Hooks.Sign.OnError)[0]), + resource.TestCheckResourceAttr("data.smallstep_endpoint_configuration.ep", "hooks.renew.shell", utils.Deref(ec.Hooks.Renew.Shell)), + resource.TestCheckResourceAttr("data.smallstep_endpoint_configuration.ep", "hooks.renew.before.#", "1"), + resource.TestCheckResourceAttr("data.smallstep_endpoint_configuration.ep", "hooks.renew.before.0", (*ec.Hooks.Renew.Before)[0]), + resource.TestCheckResourceAttr("data.smallstep_endpoint_configuration.ep", "hooks.renew.after.#", "1"), + resource.TestCheckResourceAttr("data.smallstep_endpoint_configuration.ep", "hooks.renew.after.0", (*ec.Hooks.Renew.After)[0]), + resource.TestCheckResourceAttr("data.smallstep_endpoint_configuration.ep", "hooks.renew.on_error.#", "1"), + resource.TestCheckResourceAttr("data.smallstep_endpoint_configuration.ep", "hooks.renew.on_error.0", (*ec.Hooks.Renew.OnError)[0]), + resource.TestCheckResourceAttr("data.smallstep_endpoint_configuration.ep", "key_info.type", string(utils.Deref(ec.KeyInfo.Type))), + resource.TestCheckResourceAttr("data.smallstep_endpoint_configuration.ep", "key_info.format", string(utils.Deref(ec.KeyInfo.Format))), + resource.TestCheckResourceAttr("data.smallstep_endpoint_configuration.ep", "key_info.pub_file", utils.Deref(ec.KeyInfo.PubFile)), + resource.TestCheckResourceAttr("data.smallstep_endpoint_configuration.ep", "reload_info.method", string(ec.ReloadInfo.Method)), + resource.TestCheckResourceAttr("data.smallstep_endpoint_configuration.ep", "reload_info.signal", strconv.Itoa(utils.Deref(ec.ReloadInfo.Signal))), + resource.TestCheckResourceAttr("data.smallstep_endpoint_configuration.ep", "reload_info.pid_file", utils.Deref(ec.ReloadInfo.PidFile)), + ), + }, + }, + }) +} diff --git a/internal/provider/managed_workloads_resource_test.go b/internal/provider/managed_workloads_resource_test.go new file mode 100644 index 0000000000000000000000000000000000000000..1144c8bb954ccb3082a461c10cee47ef69794622 --- /dev/null +++ b/internal/provider/managed_workloads_resource_test.go @@ -0,0 +1,308 @@ +package provider + +import ( + "fmt" + "regexp" + "testing" + + "github.com/google/uuid" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/smallstep/terraform-provider-smallstep/internal/provider/utils" + "github.com/stretchr/testify/require" +) + +func TestAccManagedConfigurationResource(t *testing.T) { + attestorRoot, _ := utils.CACerts(t) + slug := utils.Slug(t) + hostID := uuid.New().String() + config := fmt.Sprintf(` +resource "smallstep_collection" "tpms" { + slug = %q +} + +resource "smallstep_collection_instance" "server1" { + id = "urn:ek:sha256:RAzbOveN1Y45fYubuTxu5jOXWtOK1HbfZ7yHjBuWlyE=" + data = "{}" + collection_slug = smallstep_collection.tpms.slug + depends_on = [smallstep_collection.tpms] +} + +resource "smallstep_attestation_authority" "aa" { + name = "tfprovider%s" + catalog = smallstep_collection.tpms.slug + attestor_roots = %q + depends_on = [smallstep_collection.tpms] +} + +resource "smallstep_authority" "agents" { + subdomain = %q + name = "Agents Authority" + type = "devops" + admin_emails = ["andrew@smallstep.com"] +} + +resource "smallstep_provisioner" "agents" { + authority_id = smallstep_authority.agents.id + name = "Agents" + type = "ACME_ATTESTATION" + acme_attestation = { + attestation_formats = ["tpm"] + attestation_roots = [smallstep_attestation_authority.aa.root] + } +} + +resource "smallstep_provisioner_webhook" "devices" { + authority_id = smallstep_authority.agents.id + provisioner_id = smallstep_provisioner.agents.id + name = "devices" + kind = "ENRICHING" + cert_type = "X509" + server_type = "HOSTED_ATTESTATION" + collection_slug = smallstep_collection.tpms.slug + depends_on = [smallstep_collection.tpms] +} + +resource "smallstep_agent_configuration" "agent1" { + authority_id = smallstep_authority.agents.id + provisioner_name = smallstep_provisioner.agents.name + name = "Agent1" + attestation_slug = smallstep_attestation_authority.aa.slug + depends_on = [smallstep_provisioner.agents] +} + +resource "smallstep_endpoint_configuration" "ep1" { + name = "My DB" + kind = "WORKLOAD" + + # this would generally be a different authority + authority_id = smallstep_authority.agents.id + + # this would generally be an x5c provisioner + provisioner_name = smallstep_provisioner.agents.name + + certificate_info = { + type = "X509" + duration = "168h" + crt_file = "db.crt" + key_file = "db.key" + root_file = "ca.crt" + uid = 1001 + gid = 999 + mode = 256 + } + + hooks = { + renew = { + shell = "/bin/sh" + before = [ + "echo renewing 1", + "echo renewing 2", + "echo renewing 3", + ] + after = [ + "echo renewed 1", + "echo renewew 2", + "echo renewed 3", + ] + on_error = [ + "echo failed renew 1", + "echo failed renew 2", + "echo failed renew 3", + ] + } + sign = { + shell = "/bin/bash" + before = [ + "echo signing 1", + "echo signing 2", + "echo signing 3", + ] + after = [ + "echo signed 1", + "echo signed 2", + "echo signed 3", + ] + on_error = [ + "echo failed sign 1", + "echo failed sign 2", + "echo failed sign 3", + ] + } + } + + key_info = { + format = "DER" + type = "ECDSA_P256" + pub_file = "file.csr" + } + + + reload_info = { + method = "SIGNAL" + pid_file = "db.pid" + signal = 1 + } +} + +resource "smallstep_endpoint_configuration" "ep2" { + name = "SSH" + kind = "PEOPLE" + authority_id = smallstep_authority.agents.id + provisioner_name = smallstep_provisioner.agents.name + certificate_info = { + type = "SSH_USER" + } + + key_info = { + type = "DEFAULT" + format = "DEFAULT" + } + + reload_info = { + method = "AUTOMATIC" + } + hooks = { + sign = {} + } +} + + +resource "smallstep_managed_configuration" "mc" { + agent_configuration_id = smallstep_agent_configuration.agent1.id + host_id = %q + name = "Foo MC" + managed_endpoints = [ + { + endpoint_configuration_id = smallstep_endpoint_configuration.ep1.id + x509_certificate_data = { + common_name = "db" + sans = [ + "db", + "db.default", + "db.default.svc", + "db.defaulst.svc.cluster.local", + ] + } + }, + ] +} + +resource "smallstep_managed_configuration" "mc2" { + agent_configuration_id = smallstep_agent_configuration.agent1.id + name = "Multiple Endpoints" + managed_endpoints = [ + { + endpoint_configuration_id = smallstep_endpoint_configuration.ep1.id + x509_certificate_data = { + common_name = "db" + sans = ["db.internal"] + } + }, + { + endpoint_configuration_id = smallstep_endpoint_configuration.ep2.id + ssh_certificate_data = { + key_id = "abc" + principals = [ + "ops", + "eng", + "sec", + ] + } + }, + ] +} +`, slug, slug, attestorRoot, slug, hostID) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + require.NoError(t, utils.SweepAttestationAuthorities()) + }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: config, + Check: resource.ComposeAggregateTestCheckFunc( + // agent + resource.TestMatchResourceAttr("smallstep_agent_configuration.agent1", "id", regexp.MustCompile(`^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`)), + resource.TestMatchResourceAttr("smallstep_agent_configuration.agent1", "authority_id", regexp.MustCompile(`^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`)), + resource.TestCheckResourceAttr("smallstep_agent_configuration.agent1", "provisioner_name", "Agents"), + + // x509 endpoint + resource.TestMatchResourceAttr("smallstep_endpoint_configuration.ep1", "id", regexp.MustCompile(`^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`)), + resource.TestCheckResourceAttr("smallstep_endpoint_configuration.ep1", "name", "My DB"), + resource.TestCheckResourceAttr("smallstep_endpoint_configuration.ep1", "kind", "WORKLOAD"), + resource.TestCheckResourceAttr("smallstep_endpoint_configuration.ep1", "certificate_info.type", "X509"), + resource.TestCheckResourceAttr("smallstep_endpoint_configuration.ep1", "certificate_info.duration", "168h"), + resource.TestCheckResourceAttr("smallstep_endpoint_configuration.ep1", "certificate_info.crt_file", "db.crt"), + resource.TestCheckResourceAttr("smallstep_endpoint_configuration.ep1", "certificate_info.key_file", "db.key"), + resource.TestCheckResourceAttr("smallstep_endpoint_configuration.ep1", "certificate_info.root_file", "ca.crt"), + resource.TestCheckResourceAttr("smallstep_endpoint_configuration.ep1", "certificate_info.uid", "1001"), + resource.TestCheckResourceAttr("smallstep_endpoint_configuration.ep1", "certificate_info.gid", "999"), + resource.TestCheckResourceAttr("smallstep_endpoint_configuration.ep1", "certificate_info.mode", "256"), + resource.TestCheckResourceAttr("smallstep_endpoint_configuration.ep1", "hooks.renew.shell", "/bin/sh"), + resource.TestCheckResourceAttr("smallstep_endpoint_configuration.ep1", "hooks.renew.before.#", "3"), + resource.TestCheckResourceAttr("smallstep_endpoint_configuration.ep1", "hooks.renew.after.#", "3"), + resource.TestCheckResourceAttr("smallstep_endpoint_configuration.ep1", "hooks.renew.on_error.#", "3"), + resource.TestCheckResourceAttr("smallstep_endpoint_configuration.ep1", "hooks.sign.shell", "/bin/bash"), + resource.TestCheckResourceAttr("smallstep_endpoint_configuration.ep1", "hooks.sign.before.#", "3"), + resource.TestCheckResourceAttr("smallstep_endpoint_configuration.ep1", "hooks.sign.after.#", "3"), + resource.TestCheckResourceAttr("smallstep_endpoint_configuration.ep1", "hooks.sign.on_error.#", "3"), + resource.TestCheckResourceAttr("smallstep_endpoint_configuration.ep1", "key_info.type", "ECDSA_P256"), + resource.TestCheckResourceAttr("smallstep_endpoint_configuration.ep1", "key_info.format", "DER"), + resource.TestCheckResourceAttr("smallstep_endpoint_configuration.ep1", "key_info.pub_file", "file.csr"), + resource.TestCheckResourceAttr("smallstep_endpoint_configuration.ep1", "reload_info.method", "SIGNAL"), + resource.TestCheckResourceAttr("smallstep_endpoint_configuration.ep1", "reload_info.pid_file", "db.pid"), + resource.TestCheckResourceAttr("smallstep_endpoint_configuration.ep1", "reload_info.signal", "1"), + // ssh endpoint + resource.TestMatchResourceAttr("smallstep_endpoint_configuration.ep2", "id", regexp.MustCompile(`^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`)), + resource.TestCheckResourceAttr("smallstep_endpoint_configuration.ep2", "name", "SSH"), + resource.TestCheckResourceAttr("smallstep_endpoint_configuration.ep2", "kind", "PEOPLE"), + resource.TestCheckResourceAttr("smallstep_endpoint_configuration.ep2", "certificate_info.type", "SSH_USER"), + resource.TestCheckResourceAttr("smallstep_endpoint_configuration.ep2", "key_info.type", "DEFAULT"), + resource.TestCheckResourceAttr("smallstep_endpoint_configuration.ep2", "key_info.format", "DEFAULT"), + resource.TestCheckResourceAttr("smallstep_endpoint_configuration.ep2", "reload_info.method", "AUTOMATIC"), + // managed configuration + resource.TestMatchResourceAttr("smallstep_managed_configuration.mc", "id", regexp.MustCompile(`^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`)), + resource.TestCheckResourceAttr("smallstep_managed_configuration.mc", "host_id", hostID), + resource.TestCheckResourceAttr("smallstep_managed_configuration.mc", "name", "Foo MC"), + resource.TestCheckResourceAttr("smallstep_managed_configuration.mc", "managed_endpoints.#", "1"), + resource.TestMatchResourceAttr("smallstep_managed_configuration.mc", "managed_endpoints.0.endpoint_configuration_id", regexp.MustCompile(`^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`)), + resource.TestCheckResourceAttr("smallstep_managed_configuration.mc", "managed_endpoints.0.x509_certificate_data.common_name", "db"), + resource.TestCheckResourceAttr("smallstep_managed_configuration.mc", "managed_endpoints.0.x509_certificate_data.sans.#", "4"), + // managed configuration 2 + resource.TestMatchResourceAttr("smallstep_managed_configuration.mc2", "id", regexp.MustCompile(`^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`)), + resource.TestMatchResourceAttr("smallstep_managed_configuration.mc2", "host_id", regexp.MustCompile(`^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`)), + resource.TestCheckResourceAttr("smallstep_managed_configuration.mc2", "name", "Multiple Endpoints"), + resource.TestCheckResourceAttr("smallstep_managed_configuration.mc2", "managed_endpoints.#", "2"), + ), + }, + { + ResourceName: "smallstep_agent_configuration.agent1", + ImportState: true, + ImportStateVerify: true, + }, + { + ResourceName: "smallstep_endpoint_configuration.ep1", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"certificate_info.duration"}, + }, + { + ResourceName: "smallstep_endpoint_configuration.ep2", + ImportState: true, + ImportStateVerify: true, + }, + { + ResourceName: "smallstep_managed_configuration.mc", + ImportState: true, + ImportStateVerify: true, + }, + { + ResourceName: "smallstep_managed_configuration.mc2", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index d8d984e903608af95cb42994bdd2ecd0a1d8ddd4..d364ea9b40bb664f4a60931628215303dd51c9ed 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -13,10 +13,13 @@ import ( "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/types" v20230301 "github.com/smallstep/terraform-provider-smallstep/internal/apiclient/v20230301" + "github.com/smallstep/terraform-provider-smallstep/internal/provider/agent_configuration" "github.com/smallstep/terraform-provider-smallstep/internal/provider/attestation_authority" "github.com/smallstep/terraform-provider-smallstep/internal/provider/authority" "github.com/smallstep/terraform-provider-smallstep/internal/provider/collection" "github.com/smallstep/terraform-provider-smallstep/internal/provider/collection_instance" + "github.com/smallstep/terraform-provider-smallstep/internal/provider/endpoint_configuration" + "github.com/smallstep/terraform-provider-smallstep/internal/provider/managed_configuration" "github.com/smallstep/terraform-provider-smallstep/internal/provider/provisioner" "github.com/smallstep/terraform-provider-smallstep/internal/provider/webhook" ) @@ -172,6 +175,9 @@ func (p *SmallstepProvider) Resources(ctx context.Context) []func() resource.Res collection_instance.NewResource, attestation_authority.NewResource, webhook.NewResource, + agent_configuration.NewResource, + endpoint_configuration.NewResource, + managed_configuration.NewResource, } } @@ -183,6 +189,9 @@ func (p *SmallstepProvider) DataSources(ctx context.Context) []func() datasource collection_instance.NewDataSource, attestation_authority.NewDataSource, webhook.NewDataSource, + agent_configuration.NewDataSource, + endpoint_configuration.NewDataSource, + managed_configuration.NewDataSource, } } diff --git a/internal/provider/utils/testutils.go b/internal/provider/utils/testutils.go index 5b5df5d56fa14079d5822ad56ae496cee93a16bc..5ed872ab30d4397597fbee6d98b3de5e3b201c5d 100644 --- a/internal/provider/utils/testutils.go +++ b/internal/provider/utils/testutils.go @@ -294,7 +294,8 @@ func SweepAttestationAuthorities() error { } for _, aa := range list { - if !strings.HasPrefix(aa.Name, "tfprovider") { + // API e2e tests create one named "foo" + if !strings.HasPrefix(aa.Name, "tfprovider") && aa.Name != "foo" { continue } resp, err := client.DeleteAttestationAuthority(context.Background(), *aa.Id, &v20230301.DeleteAttestationAuthorityParams{})