mirror of
https://github.com/ovh/terraform-provider-ovh.git
synced 2026-01-11 20:07:09 +00:00
feat(okms): add datasource and resource okms_secret
Signed-off-by: alexGNX <alexandre.gagneux12@gmail.com>
This commit is contained in:
parent
85f6824a7e
commit
9180ef475c
17 changed files with 2537 additions and 0 deletions
|
|
@ -130,6 +130,7 @@ In order to run the full suite of Acceptance tests you will need to have the fol
|
|||
- a [Load Balancer](https://www.ovh.ie/solutions/load-balancer/)
|
||||
- a registered [Domain](https://www.ovh.ie/domains/)
|
||||
- a [Cloud Project](https://www.ovh.ie/public-cloud/instances/)
|
||||
- a [KMS](https://www.ovhcloud.com/en-gb/identity-security-operations/key-management-service/)
|
||||
|
||||
You will also need to setup your [OVH API](https://api.ovh.com) credentials. (see [documentation](https://www.terraform.io/docs/providers/ovh/index.html#configuration-reference))
|
||||
|
||||
|
|
@ -169,6 +170,7 @@ export OVH_DOMAIN_NS3_HOST_TEST="..."
|
|||
export OVH_DOMAIN_DS_RECORD_ALGORITHM_TEST="..."
|
||||
export OVH_DOMAIN_DS_RECORD_PUBLIC_KEY_TEST="..."
|
||||
export OVH_DOMAIN_DS_RECORD_TAG_TEST="..."
|
||||
export OVH_OKMS="..."
|
||||
|
||||
$ make testacc
|
||||
```
|
||||
|
|
|
|||
90
docs/data-sources/okms_secret.md
Normal file
90
docs/data-sources/okms_secret.md
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
---
|
||||
subcategory : "Key Management Service (KMS)"
|
||||
---
|
||||
|
||||
# ovh_okms_secret (Data Source)
|
||||
|
||||
Retrieves metadata (and optionally the payload) of a secret stored in OVHcloud KMS.
|
||||
|
||||
> WARNING: If `include_data = true` the secret value is stored in cleartext (JSON) in the Terraform state file. Marked **Sensitive** only hides it from CLI output. If you use this option it is recommended to protect your state with encryption and access controls.
|
||||
|
||||
## Example Usage
|
||||
|
||||
Get the latest secret version (metadata only):
|
||||
|
||||
```terraform
|
||||
data "ovh_okms_secret" "latest" {
|
||||
okms_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
path = "app/api_credentials"
|
||||
}
|
||||
```
|
||||
|
||||
Get the latest secret version including its data:
|
||||
|
||||
```terraform
|
||||
data "ovh_okms_secret" "latest_with_data" {
|
||||
okms_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
path = "app/api_credentials"
|
||||
include_data = true
|
||||
}
|
||||
|
||||
locals {
|
||||
secret_obj = jsondecode(data.ovh_okms_secret.latest_with_data.data)
|
||||
}
|
||||
|
||||
output "api_key" {
|
||||
value = local.secret_obj.api_key
|
||||
sensitive = true
|
||||
}
|
||||
```
|
||||
|
||||
Get a specific version including its payload:
|
||||
|
||||
```terraform
|
||||
data "ovh_okms_secret" "v3" {
|
||||
okms_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
path = "app/api_credentials"
|
||||
version = 3
|
||||
include_data = true
|
||||
}
|
||||
```
|
||||
|
||||
## Argument Reference
|
||||
|
||||
The following arguments are supported:
|
||||
|
||||
### Required
|
||||
|
||||
- `okms_id` (String) OKMS service ID that owns the secret.
|
||||
- `path` (String) Secret path (identifier within the OKMS instance).
|
||||
|
||||
### Optional
|
||||
|
||||
- `version` (Number) Specific version to retrieve. If omitted, the latest (current) version is selected.
|
||||
- `include_data` (Boolean) If true, retrieves the secret payload (`data` attribute). Defaults to false. When false only metadata is returned.
|
||||
|
||||
## Attributes Reference (Read-Only)
|
||||
|
||||
In addition to the arguments above, the following attributes are exported:
|
||||
|
||||
- `version` (Number) The resolved version number (requested or current latest).
|
||||
- `data` (String, Sensitive) Raw JSON secret payload (present only if `include_data` is true).
|
||||
- `metadata` (Block) Secret metadata:
|
||||
- `cas_required` (Boolean)
|
||||
- `created_at` (String)
|
||||
- `updated_at` (String)
|
||||
- `current_version` (Number)
|
||||
- `oldest_version` (Number)
|
||||
- `max_versions` (Number)
|
||||
- `deactivate_version_after` (String)
|
||||
- `custom_metadata` (Map of String)
|
||||
- `iam` (Block) IAM resource metadata:
|
||||
- `display_name` (String)
|
||||
- `id` (String)
|
||||
- `tags` (Map of String)
|
||||
- `urn` (String)
|
||||
|
||||
## Behavior & Notes
|
||||
|
||||
- The `data` attribute retains the raw JSON returned by the API. Use `jsondecode()` to work with individual keys.
|
||||
- Changing only `include_data` (true -> false) will cause the `data` attribute to become null in subsequent refreshes (state no longer holds the payload).
|
||||
131
docs/resources/okms_secret.md
Normal file
131
docs/resources/okms_secret.md
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
|
||||
---
|
||||
subcategory : "Key Management Service (KMS)"
|
||||
---
|
||||
|
||||
# ovh_okms_secret (Resource)
|
||||
|
||||
Manages a secret stored in OVHcloud KMS.
|
||||
|
||||
> WARNING: `version.data` is marked **Sensitive** but still ends up in the state file. To mitigate that, it is recommended to protect your state with encryption and access controls. Avoid committing it to source control.
|
||||
|
||||
## Example Usage
|
||||
|
||||
Create a secret whose value is a JSON object. Use `jsonencode()` to produce a deterministic JSON string (ordering/whitespace) to minimize diffs.
|
||||
|
||||
```terraform
|
||||
resource "ovh_okms_secret" "example" {
|
||||
okms_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
path = "app/api_credentials"
|
||||
|
||||
metadata = {
|
||||
max_versions = 10 # keep last 10 versions
|
||||
cas_required = true # enforce optimistic concurrency control (server will require current secret version on the cas attribute to allow update)
|
||||
deactivate_version_after = "0s" # keep versions active indefinitely (example)
|
||||
custom_metadata = {
|
||||
environment = "prod"
|
||||
owner = "payments-team"
|
||||
}
|
||||
}
|
||||
|
||||
# Initial version (will create version 1)
|
||||
version = {
|
||||
data = jsonencode({
|
||||
api_key = var.api_key
|
||||
api_secret = var.api_secret
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
# Reading a field from the secret version data
|
||||
locals {
|
||||
secret_json = jsondecode(ovh_okms_secret.example.version.data)
|
||||
}
|
||||
|
||||
output "api_key" {
|
||||
value = local.secret_json.api_key
|
||||
sensitive = true
|
||||
}
|
||||
```
|
||||
|
||||
Updating the secret (new version) while enforcing optimistic concurrency control using CAS:
|
||||
|
||||
```terraform
|
||||
resource "ovh_okms_secret" "example" {
|
||||
okms_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
path = "app/api_credentials"
|
||||
|
||||
# Ensure no concurrent update happened: set cas to the current version
|
||||
# (metadata.current_version is populated after first apply)
|
||||
cas = ovh_okms_secret.example.metadata.current_version
|
||||
|
||||
metadata = {
|
||||
cas_required = true
|
||||
}
|
||||
|
||||
version = {
|
||||
data = jsonencode({
|
||||
api_key = var.api_key
|
||||
api_secret = var.new_api_secret # changed value -> creates new version
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Argument Reference
|
||||
|
||||
The following arguments are supported:
|
||||
|
||||
### Required
|
||||
|
||||
- `okms_id` (String) ID of the OKMS service to create the secret in.
|
||||
- `path` (String) Secret path (acts as the secret identifier within the OKMS instance). Immutable after creation.
|
||||
- `version` (Block) Definition of the version to create/update. See Version Block below. (On updates providing a new `version.data` creates a new version.)
|
||||
|
||||
### Optional
|
||||
|
||||
- `cas` (Number) Check‑and‑set parameter used only on update (if `cas_required` metadata is set to true) to enforce optimistic concurrency control: its value must equal the current secret version (`metadata.current_version`) for the update to succeed. Ignored on create.
|
||||
- `metadata` (Block) Secret metadata configuration (subset of fields are user-settable). See Metadata Block below.
|
||||
|
||||
### Metadata Block
|
||||
|
||||
User configurable attributes inside `metadata`:
|
||||
|
||||
- `cas_required` (Boolean) If true, the server will enforce optimistic concurrency control by requiring the `cas` parameter to match the current version number on every write (update) request.
|
||||
- `custom_metadata` (Map of String) Arbitrary key/value metadata.
|
||||
- `deactivate_version_after` (String) Duration (e.g. `"24h"`) after which a version is deactivated. `"0s"` (default) means never automatically deactivate.
|
||||
- `max_versions` (Number) Number of versions to retain (default 10). Older versions beyond this limit are pruned.
|
||||
|
||||
Computed (read‑only) metadata attributes:
|
||||
|
||||
- `created_at` (String) Creation timestamp of the secret.
|
||||
- `updated_at` (String) Last update timestamp.
|
||||
- `current_version` (Number) Current (latest) version number.
|
||||
- `oldest_version` (Number) Oldest retained version number.
|
||||
|
||||
### Version Block
|
||||
|
||||
Required attribute:
|
||||
|
||||
- `data` (String, Sensitive) Secret payload. Commonly set with `jsonencode(...)` so that Terraform comparisons are stable. Any valid JSON (object, array, string, number, bool) is accepted. Changing `data` creates a new secret version.
|
||||
|
||||
Computed (read‑only) attributes:
|
||||
|
||||
- `id` (Number) Version number.
|
||||
- `created_at` (String) Version creation timestamp.
|
||||
- `deactivated_at` (String) Deactivation timestamp if the version was deactivated.
|
||||
- `state` (String) Version state (e.g. `ACTIVE`).
|
||||
|
||||
## Attributes Reference (Read-Only)
|
||||
|
||||
In addition to arguments above, the following attributes are exported:
|
||||
|
||||
- `iam` (Block) IAM metadata: `display_name`, `id`, `tags`, `urn`.
|
||||
- `metadata.*` computed fields as listed above.
|
||||
- `version.*` computed fields as listed above.
|
||||
|
||||
## Behavior & Notes
|
||||
|
||||
- Updating with a new `version.data` performs an API PUT that creates a new version; the previous version remains (subject to `max_versions`).
|
||||
- If `cas_required` is true, all write operations must include a correct `cas` query parameter (the expected current version number). Set `cas = ovh_okms_secret.example.metadata.current_version` to enforce optimistic concurrency. A mismatch causes the API to reject the update (preventing overwriting unseen changes).
|
||||
- `cas` is ignored on create (no existing version).
|
||||
14
examples/data-sources/okms_secret/example_1.tf
Normal file
14
examples/data-sources/okms_secret/example_1.tf
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
data "ovh_okms_secret" "latest_with_data" {
|
||||
okms_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
path = "app/api_credentials"
|
||||
include_data = true
|
||||
}
|
||||
|
||||
locals {
|
||||
secret_obj = jsondecode(data.ovh_okms_secret.latest_with_data.data)
|
||||
}
|
||||
|
||||
output "api_key" {
|
||||
value = local.secret_obj.api_key
|
||||
sensitive = true
|
||||
}
|
||||
4
examples/data-sources/okms_secret/example_2.tf
Normal file
4
examples/data-sources/okms_secret/example_2.tf
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
data "ovh_okms_secret" "latest" {
|
||||
okms_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
path = "app/api_credentials"
|
||||
}
|
||||
6
examples/data-sources/okms_secret/example_3.tf
Normal file
6
examples/data-sources/okms_secret/example_3.tf
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
data "ovh_okms_secret" "v3" {
|
||||
okms_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
path = "app/api_credentials"
|
||||
version = 3
|
||||
include_data = true
|
||||
}
|
||||
32
examples/resources/okms_secret/example_1.tf
Normal file
32
examples/resources/okms_secret/example_1.tf
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
resource "ovh_okms_secret" "example" {
|
||||
okms_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
path = "app/api_credentials"
|
||||
|
||||
metadata = {
|
||||
max_versions = 10 # keep last 10 versions
|
||||
cas_required = true # enforce optimistic concurrency control (server will require current secret version on the cas attribute to allow update)
|
||||
deactivate_version_after = "0s" # keep versions active indefinitely (example)
|
||||
custom_metadata = {
|
||||
environment = "prod"
|
||||
owner = "payments-team"
|
||||
}
|
||||
}
|
||||
|
||||
# Initial version (will create version 1)
|
||||
version = {
|
||||
data = jsonencode({
|
||||
api_key = var.api_key
|
||||
api_secret = var.api_secret
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
# Reading a field from the secret version data
|
||||
locals {
|
||||
secret_json = jsondecode(ovh_okms_secret.example.version.data)
|
||||
}
|
||||
|
||||
output "api_key" {
|
||||
value = local.secret_json.api_key
|
||||
sensitive = true
|
||||
}
|
||||
19
examples/resources/okms_secret/example_2.tf
Normal file
19
examples/resources/okms_secret/example_2.tf
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
resource "ovh_okms_secret" "example" {
|
||||
okms_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
path = "app/api_credentials"
|
||||
|
||||
# Ensure no concurrent update happened: set cas to the current version
|
||||
# (metadata.current_version is populated after first apply)
|
||||
cas = ovh_okms_secret.example.metadata.current_version
|
||||
|
||||
metadata = {
|
||||
cas_required = true
|
||||
}
|
||||
|
||||
version = {
|
||||
data = jsonencode({
|
||||
api_key = var.api_key
|
||||
api_secret = var.new_api_secret # changed value -> creates new version
|
||||
})
|
||||
}
|
||||
}
|
||||
126
ovh/data_okms_secret.go
Normal file
126
ovh/data_okms_secret.go
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
package ovh
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-framework/datasource"
|
||||
ovhtypes "github.com/ovh/terraform-provider-ovh/v2/ovh/types"
|
||||
)
|
||||
|
||||
var _ datasource.DataSourceWithConfigure = (*okmsSecretDataSource)(nil)
|
||||
|
||||
func NewOkmsSecretDataSource() datasource.DataSource {
|
||||
return &okmsSecretDataSource{}
|
||||
}
|
||||
|
||||
type okmsSecretDataSource struct {
|
||||
config *Config
|
||||
}
|
||||
|
||||
func (d *okmsSecretDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
|
||||
resp.TypeName = req.ProviderTypeName + "_okms_secret"
|
||||
}
|
||||
|
||||
func (d *okmsSecretDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
|
||||
if req.ProviderData == nil {
|
||||
return
|
||||
}
|
||||
|
||||
config, ok := req.ProviderData.(*Config)
|
||||
if !ok {
|
||||
resp.Diagnostics.AddError(
|
||||
"Unexpected Data Source Configure Type",
|
||||
fmt.Sprintf("Expected *Config, got: %T. Please report this issue to the provider developers.", req.ProviderData),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
d.config = config
|
||||
}
|
||||
|
||||
func (d *okmsSecretDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) {
|
||||
resp.Schema = OkmsSecretDataSourceSchema(ctx)
|
||||
}
|
||||
|
||||
func (d *okmsSecretDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
|
||||
var configModel OkmsSecretDataSourceModel
|
||||
|
||||
// Read Terraform configuration into the lightweight DS model
|
||||
resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
base := "/v2/okms/resource/" + url.PathEscape(configModel.OkmsId.ValueString()) + "/secret/" + url.PathEscape(configModel.Path.ValueString())
|
||||
versionProvided := !configModel.Version.IsNull() && !configModel.Version.IsUnknown() && configModel.Version.ValueInt64() > 0
|
||||
includeDataRequested := configModel.IncludeData.ValueBool()
|
||||
|
||||
metaEndpoint := base
|
||||
if !versionProvided { // only add includeData for latest case; version endpoint handled separately
|
||||
if includeDataRequested {
|
||||
metaEndpoint += "?includeData=true"
|
||||
}
|
||||
}
|
||||
var apiModel OkmsSecretModel
|
||||
if err := d.config.OVHClient.Get(metaEndpoint, &apiModel); err != nil {
|
||||
resp.Diagnostics.AddError(
|
||||
fmt.Sprintf("Error calling Get %s", metaEndpoint),
|
||||
err.Error(),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
configModel.Iam = apiModel.Iam
|
||||
configModel.Metadata = apiModel.Metadata
|
||||
|
||||
if versionProvided {
|
||||
verEndpoint := base + "/version/" + fmt.Sprintf("%d", configModel.Version.ValueInt64())
|
||||
if includeDataRequested {
|
||||
verEndpoint += "?includeData=true"
|
||||
}
|
||||
var ver struct {
|
||||
Id int64 `json:"id"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
Data json.RawMessage `json:"data"`
|
||||
State string `json:"state"`
|
||||
// deactivatedAt may appear
|
||||
DeactivatedAt *string `json:"deactivatedAt"`
|
||||
}
|
||||
if err := d.config.OVHClient.Get(verEndpoint, &ver); err == nil {
|
||||
if includeDataRequested && len(ver.Data) > 0 && string(ver.Data) != "null" {
|
||||
configModel.Data = ovhtypes.NewTfStringValue(string(ver.Data))
|
||||
}
|
||||
} else {
|
||||
resp.Diagnostics.AddError(
|
||||
fmt.Sprintf("Error calling Get %s", verEndpoint),
|
||||
err.Error(),
|
||||
)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
configModel.Version = apiModel.Metadata.CurrentVersion
|
||||
if includeDataRequested {
|
||||
if !apiModel.Version.Data.IsNull() && !apiModel.Version.Data.IsUnknown() {
|
||||
configModel.Data = apiModel.Version.Data
|
||||
} else {
|
||||
if !apiModel.Metadata.CurrentVersion.IsNull() && !apiModel.Metadata.CurrentVersion.IsUnknown() && apiModel.Metadata.CurrentVersion.ValueInt64() > 0 {
|
||||
verEndpoint := base + "/version/" + fmt.Sprintf("%d", apiModel.Metadata.CurrentVersion.ValueInt64()) + "?includeData=true"
|
||||
var ver struct {
|
||||
Data json.RawMessage `json:"data"`
|
||||
}
|
||||
if err := d.config.OVHClient.Get(verEndpoint, &ver); err == nil {
|
||||
if len(ver.Data) > 0 && string(ver.Data) != "null" {
|
||||
configModel.Data = ovhtypes.NewTfStringValue(string(ver.Data))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save state
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, &configModel)...)
|
||||
}
|
||||
1002
ovh/data_okms_secret_gen.go
Normal file
1002
ovh/data_okms_secret_gen.go
Normal file
File diff suppressed because it is too large
Load diff
96
ovh/data_okms_secret_test.go
Normal file
96
ovh/data_okms_secret_test.go
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
package ovh
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-testing/helper/acctest"
|
||||
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
|
||||
)
|
||||
|
||||
// Step 1 config: create secret version 1 and read latest with data.
|
||||
// NOTE: resource and data source must be prefixed with provider name 'ovh_'.
|
||||
const testAccOkmsSecretDataSourceConfigV1 = `
|
||||
resource "ovh_okms_secret" "test" {
|
||||
okms_id = "%s"
|
||||
path = "%s"
|
||||
version = {
|
||||
data = jsonencode({ initial = "v1" })
|
||||
}
|
||||
}
|
||||
|
||||
data "ovh_okms_secret" "latest" {
|
||||
okms_id = ovh_okms_secret.test.okms_id
|
||||
path = ovh_okms_secret.test.path
|
||||
include_data = true
|
||||
}
|
||||
`
|
||||
|
||||
// Step 2 config: update same resource (cas=1) to create version 2, then read latest, explicit v1 and v2.
|
||||
const testAccOkmsSecretDataSourceConfigV2 = `
|
||||
resource "ovh_okms_secret" "test" {
|
||||
okms_id = "%s"
|
||||
path = "%s"
|
||||
cas = 1
|
||||
version = {
|
||||
data = jsonencode({ initial = "v1", second = "v2" })
|
||||
}
|
||||
}
|
||||
|
||||
data "ovh_okms_secret" "latest" {
|
||||
okms_id = ovh_okms_secret.test.okms_id
|
||||
path = ovh_okms_secret.test.path
|
||||
include_data = true
|
||||
}
|
||||
|
||||
data "ovh_okms_secret" "v1" {
|
||||
okms_id = ovh_okms_secret.test.okms_id
|
||||
path = ovh_okms_secret.test.path
|
||||
version = 1
|
||||
include_data = true
|
||||
}
|
||||
|
||||
data "ovh_okms_secret" "v2" {
|
||||
okms_id = ovh_okms_secret.test.okms_id
|
||||
path = ovh_okms_secret.test.path
|
||||
version = 2
|
||||
include_data = true
|
||||
}
|
||||
`
|
||||
|
||||
func TestAccOkmsSecretDataSource_latestAndVersions(t *testing.T) {
|
||||
okmsID := os.Getenv("OVH_OKMS")
|
||||
if okmsID == "" {
|
||||
checkEnvOrSkip(t, "OVH_OKMS")
|
||||
}
|
||||
path := fmt.Sprintf("tfacc-%s", acctest.RandString(6))
|
||||
|
||||
configV1 := fmt.Sprintf(testAccOkmsSecretDataSourceConfigV1, okmsID, path)
|
||||
configV2 := fmt.Sprintf(testAccOkmsSecretDataSourceConfigV2, okmsID, path)
|
||||
|
||||
resource.Test(t, resource.TestCase{
|
||||
PreCheck: func() { testAccPreCheckOkms(t) },
|
||||
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
|
||||
Steps: []resource.TestStep{
|
||||
{
|
||||
Config: configV1,
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
resource.TestCheckResourceAttr("data.ovh_okms_secret.latest", "version", "1"),
|
||||
resource.TestCheckResourceAttrSet("data.ovh_okms_secret.latest", "data"),
|
||||
),
|
||||
},
|
||||
{
|
||||
Config: configV2,
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
resource.TestCheckResourceAttr("data.ovh_okms_secret.latest", "version", "2"),
|
||||
resource.TestCheckResourceAttrSet("data.ovh_okms_secret.latest", "data"),
|
||||
resource.TestCheckResourceAttr("data.ovh_okms_secret.v1", "version", "1"),
|
||||
resource.TestCheckResourceAttrSet("data.ovh_okms_secret.v1", "data"),
|
||||
resource.TestCheckResourceAttr("data.ovh_okms_secret.v2", "version", "2"),
|
||||
resource.TestCheckResourceAttrSet("data.ovh_okms_secret.v2", "data"),
|
||||
),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
@ -250,6 +250,7 @@ func (p *OvhProvider) DataSources(_ context.Context) []func() datasource.DataSou
|
|||
NewOkmsServiceKeyDataSource,
|
||||
NewOkmsServiceKeyJwkDataSource,
|
||||
NewOkmsServiceKeyPemDataSource,
|
||||
NewOkmsSecretDataSource,
|
||||
NewOvhcloudConnectDatacentersDataSource,
|
||||
NewOvhcloudConnectConfigPopDatacenterExtrasDataSource,
|
||||
NewOvhcloudConnectConfigPopDatacentersDataSource,
|
||||
|
|
@ -296,6 +297,7 @@ func (p *OvhProvider) Resources(_ context.Context) []func() resource.Resource {
|
|||
NewOkmsCredentialResource,
|
||||
NewOkmsServiceKeyResource,
|
||||
NewOkmsServiceKeyJwkResource,
|
||||
NewOkmsSecretResource,
|
||||
NewOvhcloudConnectPopConfigResource,
|
||||
NewOvhcloudConnectPopDatacenterConfigResource,
|
||||
NewOvhcloudConnectPopDatacenterExtraConfigResource,
|
||||
|
|
|
|||
281
ovh/resource_okms_secret.go
Normal file
281
ovh/resource_okms_secret.go
Normal file
|
|
@ -0,0 +1,281 @@
|
|||
package ovh
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource"
|
||||
ovhtypes "github.com/ovh/terraform-provider-ovh/v2/ovh/types"
|
||||
)
|
||||
|
||||
var _ resource.ResourceWithConfigure = (*okmsSecretResource)(nil)
|
||||
|
||||
func NewOkmsSecretResource() resource.Resource {
|
||||
return &okmsSecretResource{}
|
||||
}
|
||||
|
||||
type okmsSecretResource struct {
|
||||
config *Config
|
||||
}
|
||||
|
||||
func (r *okmsSecretResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
|
||||
resp.TypeName = req.ProviderTypeName + "_okms_secret"
|
||||
}
|
||||
|
||||
func (d *okmsSecretResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
|
||||
if req.ProviderData == nil {
|
||||
return
|
||||
}
|
||||
|
||||
config, ok := req.ProviderData.(*Config)
|
||||
if !ok {
|
||||
resp.Diagnostics.AddError(
|
||||
"Unexpected Resource Configure Type",
|
||||
fmt.Sprintf("Expected *Config, got: %T. Please report this issue to the provider developers.", req.ProviderData),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
d.config = config
|
||||
}
|
||||
|
||||
func (d *okmsSecretResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
|
||||
resp.Schema = OkmsSecretResourceSchema(ctx)
|
||||
}
|
||||
|
||||
func (r *okmsSecretResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
|
||||
var data, responseData OkmsSecretModel
|
||||
|
||||
// Read Terraform plan data into the model
|
||||
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
if !data.Cas.IsNull() && !data.Cas.IsUnknown() {
|
||||
resp.Diagnostics.AddWarning(
|
||||
"CAS Ignored On Create",
|
||||
"The 'cas' attribute is only used on update operations and ignored during creation.",
|
||||
)
|
||||
}
|
||||
|
||||
endpoint := "/v2/okms/resource/" + url.PathEscape(data.OkmsId.ValueString()) + "/secret"
|
||||
createPayload := buildSecretPayload(&data, true)
|
||||
if err := r.config.OVHClient.Post(endpoint, createPayload, &responseData); err != nil {
|
||||
resp.Diagnostics.AddError(
|
||||
fmt.Sprintf("Error calling Post %s", endpoint),
|
||||
err.Error(),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
responseData.MergeWith(&data)
|
||||
if responseData.Version.Data.IsNull() || responseData.Version.Data.IsUnknown() {
|
||||
responseData.Version = data.Version
|
||||
}
|
||||
|
||||
populateVersionComputedFields(r, &responseData, data.OkmsId.ValueString(), data.Path.ValueString())
|
||||
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, &responseData)...)
|
||||
}
|
||||
|
||||
func (r *okmsSecretResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
|
||||
var data, responseData OkmsSecretModel
|
||||
|
||||
// Read Terraform prior state data into the model
|
||||
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
endpoint := "/v2/okms/resource/" + url.PathEscape(data.OkmsId.ValueString()) + "/secret/" + url.PathEscape(data.Path.ValueString()) + ""
|
||||
|
||||
if err := r.config.OVHClient.Get(endpoint, &responseData); err != nil {
|
||||
resp.Diagnostics.AddError(
|
||||
fmt.Sprintf("Error calling Get %s", endpoint),
|
||||
err.Error(),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
data.MergeWith(&responseData)
|
||||
|
||||
// Save updated data into Terraform state
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
|
||||
}
|
||||
|
||||
func (r *okmsSecretResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
|
||||
var data, planData, responseData OkmsSecretModel
|
||||
|
||||
// Read Terraform plan data into the model
|
||||
resp.Diagnostics.Append(req.Plan.Get(ctx, &planData)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
// Read Terraform prior state data into the model
|
||||
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
// Update resource
|
||||
endpoint := "/v2/okms/resource/" + url.PathEscape(data.OkmsId.ValueString()) + "/secret/" + url.PathEscape(data.Path.ValueString()) + ""
|
||||
|
||||
// Avoid creating a new secret version when only metadata (or other fields) changed and
|
||||
// the version data itself is unchanged. The API creates a new version whenever a
|
||||
// version payload is sent, even if the content is identical. We therefore don't include the
|
||||
// version field in the update payload when the user-specified data matches the
|
||||
// prior state value.
|
||||
planForPayload := planData // shallow copy
|
||||
if !planData.Version.Data.IsNull() && !planData.Version.Data.IsUnknown() &&
|
||||
!data.Version.Data.IsNull() && !data.Version.Data.IsUnknown() &&
|
||||
planData.Version.Data.ValueString() == data.Version.Data.ValueString() {
|
||||
// Mark version data null in the payload model so buildSecretPayload skips it.
|
||||
planForPayload.Version.Data = ovhtypes.NewTfStringNull()
|
||||
}
|
||||
updatePayload := buildSecretPayload(&planForPayload, false)
|
||||
// cas (check-and-set) must be passed as query parameter
|
||||
casQuery := ""
|
||||
if !planData.Cas.IsNull() && !planData.Cas.IsUnknown() {
|
||||
casQuery = "?cas=" + url.QueryEscape(fmt.Sprintf("%d", planData.Cas.ValueInt64()))
|
||||
}
|
||||
if err := r.config.OVHClient.Put(endpoint+casQuery, updatePayload, nil); err != nil {
|
||||
resp.Diagnostics.AddError(
|
||||
fmt.Sprintf("Error calling Put %s", endpoint+casQuery),
|
||||
err.Error(),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Read updated resource
|
||||
endpoint = "/v2/okms/resource/" + url.PathEscape(data.OkmsId.ValueString()) + "/secret/" + url.PathEscape(data.Path.ValueString()) + ""
|
||||
if err := r.config.OVHClient.Get(endpoint, &responseData); err != nil {
|
||||
resp.Diagnostics.AddError(
|
||||
fmt.Sprintf("Error calling Get %s", endpoint),
|
||||
err.Error(),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
responseData.MergeWith(&planData)
|
||||
|
||||
populateVersionComputedFields(r, &responseData, data.OkmsId.ValueString(), data.Path.ValueString())
|
||||
|
||||
// Save updated data into Terraform state
|
||||
resp.Diagnostics.Append(resp.State.Set(ctx, &responseData)...)
|
||||
}
|
||||
|
||||
func (r *okmsSecretResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
|
||||
var data OkmsSecretModel
|
||||
|
||||
// Read Terraform prior state data into the model
|
||||
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
|
||||
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
// Delete API call logic
|
||||
endpoint := "/v2/okms/resource/" + url.PathEscape(data.OkmsId.ValueString()) + "/secret/" + url.PathEscape(data.Path.ValueString()) + ""
|
||||
if err := r.config.OVHClient.Delete(endpoint, nil); err != nil {
|
||||
resp.Diagnostics.AddError(
|
||||
fmt.Sprintf("Error calling Delete %s", endpoint),
|
||||
err.Error(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// buildSecretPayload constructs the payload for create/update.
|
||||
// On create include path; on update path is immutable so omitted.
|
||||
func buildSecretPayload(m *OkmsSecretModel, isCreate bool) map[string]any {
|
||||
payload := map[string]any{}
|
||||
if isCreate && !m.Path.IsNull() && !m.Path.IsUnknown() {
|
||||
payload["path"] = m.Path.ValueString()
|
||||
}
|
||||
if !m.Version.Data.IsNull() && !m.Version.Data.IsUnknown() {
|
||||
if vp := buildVersionData(m.Version.Data.ValueString()); vp != nil {
|
||||
payload["version"] = vp
|
||||
}
|
||||
}
|
||||
if meta := buildMetadataPayload(&m.Metadata); meta != nil {
|
||||
payload["metadata"] = meta
|
||||
}
|
||||
return payload
|
||||
}
|
||||
|
||||
// buildVersionData attempts to JSON decode the user provided string; if structured returns structured form.
|
||||
func buildVersionData(raw string) map[string]any {
|
||||
versionPayload := map[string]any{}
|
||||
var decoded any
|
||||
if err := json.Unmarshal([]byte(raw), &decoded); err == nil {
|
||||
switch decoded.(type) {
|
||||
case map[string]any, []any:
|
||||
versionPayload["data"] = decoded
|
||||
default:
|
||||
versionPayload["data"] = raw
|
||||
}
|
||||
} else {
|
||||
versionPayload["data"] = raw
|
||||
}
|
||||
return versionPayload
|
||||
}
|
||||
|
||||
// buildMetadataPayload extracts settable metadata fields.
|
||||
func buildMetadataPayload(meta *MetadataValue) map[string]any {
|
||||
mp := map[string]any{}
|
||||
if !meta.CustomMetadata.IsNull() && !meta.CustomMetadata.IsUnknown() {
|
||||
mp["customMetadata"] = meta.CustomMetadata
|
||||
}
|
||||
if !meta.MaxVersions.IsNull() && !meta.MaxVersions.IsUnknown() {
|
||||
mp["maxVersions"] = meta.MaxVersions
|
||||
}
|
||||
if !meta.DeactivateVersionAfter.IsNull() && !meta.DeactivateVersionAfter.IsUnknown() {
|
||||
mp["deactivateVersionAfter"] = meta.DeactivateVersionAfter
|
||||
}
|
||||
if !meta.CasRequired.IsNull() && !meta.CasRequired.IsUnknown() {
|
||||
mp["casRequired"] = meta.CasRequired
|
||||
}
|
||||
if len(mp) == 0 {
|
||||
return nil
|
||||
}
|
||||
return mp
|
||||
}
|
||||
|
||||
// populateVersionComputedFields fills secret version attributes
|
||||
func populateVersionComputedFields(r *okmsSecretResource, model *OkmsSecretModel, okmsId, path string) {
|
||||
// If currentVersion unknown or zero, nothing to enrich
|
||||
if model.Metadata.CurrentVersion.IsNull() || model.Metadata.CurrentVersion.IsUnknown() || model.Metadata.CurrentVersion.ValueInt64() == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
current := model.Metadata.CurrentVersion.ValueInt64()
|
||||
|
||||
// First try the efficient direct version endpoint
|
||||
versionEndpoint := "/v2/okms/resource/" + url.PathEscape(okmsId) + "/secret/" + url.PathEscape(path) + "/version/" + fmt.Sprintf("%d", current)
|
||||
var ver struct {
|
||||
Id *int64 `json:"id"`
|
||||
CreatedAt *string `json:"createdAt"`
|
||||
State *string `json:"state"`
|
||||
DeactivatedAt *string `json:"deactivatedAt"`
|
||||
}
|
||||
if err := r.config.OVHClient.Get(versionEndpoint, &ver); err != nil || ver.Id == nil {
|
||||
// Best-effort enrichment; silently skip on error
|
||||
return
|
||||
}
|
||||
// Populate from direct call
|
||||
model.Version.Id = ovhtypes.NewTfInt64Value(*ver.Id)
|
||||
if ver.CreatedAt != nil {
|
||||
model.Version.CreatedAt = ovhtypes.NewTfStringValue(*ver.CreatedAt)
|
||||
}
|
||||
if ver.State != nil {
|
||||
model.Version.State = ovhtypes.NewTfStringValue(*ver.State)
|
||||
}
|
||||
if ver.DeactivatedAt != nil {
|
||||
model.Version.DeactivatedAt = ovhtypes.NewTfStringValue(*ver.DeactivatedAt)
|
||||
} else {
|
||||
model.Version.DeactivatedAt = ovhtypes.NewTfStringNull()
|
||||
}
|
||||
}
|
||||
295
ovh/resource_okms_secret_gen.go
Normal file
295
ovh/resource_okms_secret_gen.go
Normal file
|
|
@ -0,0 +1,295 @@
|
|||
// Code generated by terraform-plugin-framework-generator DO NOT EDIT.
|
||||
|
||||
package ovh
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-framework/attr"
|
||||
"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"
|
||||
ovhtypes "github.com/ovh/terraform-provider-ovh/v2/ovh/types"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
|
||||
)
|
||||
|
||||
func OkmsSecretResourceSchema(ctx context.Context) schema.Schema {
|
||||
attrs := map[string]schema.Attribute{
|
||||
"cas": schema.Int64Attribute{
|
||||
CustomType: ovhtypes.TfInt64Type{},
|
||||
Optional: true,
|
||||
Description: "Check-and-set guard. Only used on update operations: must equal the current secret version for the update to succeed. Ignored on create.",
|
||||
MarkdownDescription: "Check-and-set guard. Only used on update operations: must equal the current secret version for the update to succeed. Ignored on create.",
|
||||
},
|
||||
"iam": schema.SingleNestedAttribute{
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"display_name": schema.StringAttribute{
|
||||
CustomType: ovhtypes.TfStringType{},
|
||||
Computed: true,
|
||||
Description: "Resource display name",
|
||||
MarkdownDescription: "Resource display name",
|
||||
},
|
||||
"id": schema.StringAttribute{
|
||||
CustomType: ovhtypes.TfStringType{},
|
||||
Computed: true,
|
||||
Description: "Unique identifier of the resource",
|
||||
MarkdownDescription: "Unique identifier of the resource",
|
||||
},
|
||||
"tags": schema.MapAttribute{
|
||||
CustomType: ovhtypes.NewTfMapNestedType[ovhtypes.TfStringValue](ctx),
|
||||
Computed: true,
|
||||
Description: "Resource tags. Tags that were internally computed are prefixed with ovh:",
|
||||
MarkdownDescription: "Resource tags. Tags that were internally computed are prefixed with ovh:",
|
||||
},
|
||||
"urn": schema.StringAttribute{
|
||||
CustomType: ovhtypes.TfStringType{},
|
||||
Computed: true,
|
||||
Description: "Unique resource name used in policies",
|
||||
MarkdownDescription: "Unique resource name used in policies",
|
||||
},
|
||||
},
|
||||
CustomType: IamType{
|
||||
ObjectType: types.ObjectType{
|
||||
AttrTypes: IamValue{}.AttributeTypes(ctx),
|
||||
},
|
||||
},
|
||||
Computed: true,
|
||||
Description: "IAM resource metadata embedded in services models",
|
||||
MarkdownDescription: "IAM resource metadata embedded in services models",
|
||||
},
|
||||
"include_data": schema.BoolAttribute{
|
||||
CustomType: ovhtypes.TfBoolType{},
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
DeprecationMessage: "No effect; will be removed in a future version.",
|
||||
},
|
||||
"metadata": schema.SingleNestedAttribute{
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"cas_required": schema.BoolAttribute{
|
||||
CustomType: ovhtypes.TfBoolType{},
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
Description: "The “Cas” parameter will be required for each write request if set to true. When the “cas” (Check and set) is specified, the current version of the secret is verified before updating it.",
|
||||
MarkdownDescription: "The “Cas” parameter will be required for each write request if set to true. When the “cas” (Check and set) is specified, the current version of the secret is verified before updating it.",
|
||||
},
|
||||
"created_at": schema.StringAttribute{
|
||||
CustomType: ovhtypes.TfStringType{},
|
||||
Computed: true,
|
||||
Description: "Time of creation of the secret",
|
||||
MarkdownDescription: "Time of creation of the secret",
|
||||
},
|
||||
"current_version": schema.Int64Attribute{
|
||||
CustomType: ovhtypes.TfInt64Type{},
|
||||
Computed: true,
|
||||
Description: "The secret version",
|
||||
MarkdownDescription: "The secret version",
|
||||
},
|
||||
"custom_metadata": schema.MapAttribute{
|
||||
CustomType: ovhtypes.NewTfMapNestedType[ovhtypes.TfStringValue](ctx),
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
Description: "Custom metadata",
|
||||
MarkdownDescription: "Custom metadata",
|
||||
},
|
||||
"deactivate_version_after": schema.StringAttribute{
|
||||
CustomType: ovhtypes.TfStringType{},
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
Description: "Time duration before a version is deactivated",
|
||||
MarkdownDescription: "Time duration before a version is deactivated",
|
||||
},
|
||||
"max_versions": schema.Int64Attribute{
|
||||
CustomType: ovhtypes.TfInt64Type{},
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
Description: "The number of versions to keep (10 default)",
|
||||
MarkdownDescription: "The number of versions to keep (10 default)",
|
||||
},
|
||||
"oldest_version": schema.Int64Attribute{
|
||||
CustomType: ovhtypes.TfInt64Type{},
|
||||
Computed: true,
|
||||
Description: "The secret oldest version",
|
||||
MarkdownDescription: "The secret oldest version",
|
||||
},
|
||||
"updated_at": schema.StringAttribute{
|
||||
CustomType: ovhtypes.TfStringType{},
|
||||
Computed: true,
|
||||
Description: "Time of the last update of the secret",
|
||||
MarkdownDescription: "Time of the last update of the secret",
|
||||
},
|
||||
},
|
||||
CustomType: MetadataType{
|
||||
ObjectType: types.ObjectType{
|
||||
AttrTypes: MetadataValue{}.AttributeTypes(ctx),
|
||||
},
|
||||
},
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
Description: "Create a secret metadata",
|
||||
MarkdownDescription: "Create a secret metadata",
|
||||
},
|
||||
"okms_id": schema.StringAttribute{
|
||||
CustomType: ovhtypes.TfStringType{},
|
||||
Required: true,
|
||||
Description: "Okms ID",
|
||||
MarkdownDescription: "Okms ID",
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.RequiresReplace(),
|
||||
},
|
||||
},
|
||||
"path": schema.StringAttribute{
|
||||
CustomType: ovhtypes.TfStringType{},
|
||||
Required: true,
|
||||
Description: "Secret path",
|
||||
MarkdownDescription: "Secret path",
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.RequiresReplace(),
|
||||
},
|
||||
},
|
||||
"version": schema.SingleNestedAttribute{
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"created_at": schema.StringAttribute{
|
||||
CustomType: ovhtypes.TfStringType{},
|
||||
Computed: true,
|
||||
Description: "Time of creation of the secret version",
|
||||
MarkdownDescription: "Time of creation of the secret version",
|
||||
},
|
||||
"data": schema.StringAttribute{
|
||||
CustomType: ovhtypes.TfStringType{},
|
||||
Required: true,
|
||||
Sensitive: true,
|
||||
},
|
||||
"deactivated_at": schema.StringAttribute{
|
||||
CustomType: ovhtypes.TfStringType{},
|
||||
Computed: true,
|
||||
Description: "Time of deactivation of the secret version",
|
||||
MarkdownDescription: "Time of deactivation of the secret version",
|
||||
},
|
||||
"id": schema.Int64Attribute{
|
||||
CustomType: ovhtypes.TfInt64Type{},
|
||||
Computed: true,
|
||||
Description: "Secret version",
|
||||
MarkdownDescription: "Secret version",
|
||||
},
|
||||
"state": schema.StringAttribute{
|
||||
CustomType: ovhtypes.TfStringType{},
|
||||
Computed: true,
|
||||
Description: "State of the secret version",
|
||||
MarkdownDescription: "State of the secret version",
|
||||
},
|
||||
},
|
||||
Required: true,
|
||||
Description: "Create an OKMS secret version",
|
||||
MarkdownDescription: "Create an OKMS secret version",
|
||||
},
|
||||
}
|
||||
|
||||
return schema.Schema{
|
||||
Description: "",
|
||||
Attributes: attrs,
|
||||
}
|
||||
}
|
||||
|
||||
func (v MetadataValue) MarshalJSON() ([]byte, error) {
|
||||
toMarshal := map[string]any{}
|
||||
if !v.CasRequired.IsNull() && !v.CasRequired.IsUnknown() {
|
||||
toMarshal["casRequired"] = v.CasRequired
|
||||
}
|
||||
if !v.CustomMetadata.IsNull() && !v.CustomMetadata.IsUnknown() {
|
||||
toMarshal["customMetadata"] = v.CustomMetadata
|
||||
}
|
||||
if !v.DeactivateVersionAfter.IsNull() && !v.DeactivateVersionAfter.IsUnknown() {
|
||||
toMarshal["deactivateVersionAfter"] = v.DeactivateVersionAfter
|
||||
}
|
||||
if !v.MaxVersions.IsNull() && !v.MaxVersions.IsUnknown() {
|
||||
toMarshal["maxVersions"] = v.MaxVersions
|
||||
}
|
||||
|
||||
return json.Marshal(toMarshal)
|
||||
}
|
||||
|
||||
type SecretVersionValue struct {
|
||||
CreatedAt ovhtypes.TfStringValue `tfsdk:"created_at" json:"createdAt"`
|
||||
Data ovhtypes.TfStringValue `tfsdk:"data" json:"data"`
|
||||
DeactivatedAt ovhtypes.TfStringValue `tfsdk:"deactivated_at" json:"deactivatedAt"`
|
||||
Id ovhtypes.TfInt64Value `tfsdk:"id" json:"id"`
|
||||
State ovhtypes.TfStringValue `tfsdk:"state" json:"state"`
|
||||
state attr.ValueState
|
||||
}
|
||||
|
||||
func (v SecretVersionValue) MarshalJSON() ([]byte, error) {
|
||||
toMarshal := map[string]any{}
|
||||
if !v.CreatedAt.IsNull() && !v.CreatedAt.IsUnknown() {
|
||||
toMarshal["createdAt"] = v.CreatedAt
|
||||
}
|
||||
if !v.Data.IsNull() && !v.Data.IsUnknown() {
|
||||
toMarshal["data"] = v.Data
|
||||
}
|
||||
if !v.DeactivatedAt.IsNull() && !v.DeactivatedAt.IsUnknown() {
|
||||
toMarshal["deactivatedAt"] = v.DeactivatedAt
|
||||
}
|
||||
if !v.Id.IsNull() && !v.Id.IsUnknown() {
|
||||
toMarshal["id"] = v.Id
|
||||
}
|
||||
if !v.State.IsNull() && !v.State.IsUnknown() {
|
||||
toMarshal["state"] = v.State
|
||||
}
|
||||
|
||||
return json.Marshal(toMarshal)
|
||||
}
|
||||
|
||||
// Custom unmarshal to accept either a JSON string or any JSON value (object/array/number/bool)
|
||||
// for the secret version's data field. We always store it as its raw JSON string representation
|
||||
// in the TfStringValue so Terraform diffing works against the jsonencode(...) input provided
|
||||
// by the user.
|
||||
func (v *SecretVersionValue) UnmarshalJSON(b []byte) error {
|
||||
// Define an alias without method to avoid recursion
|
||||
type rawAlias struct {
|
||||
CreatedAt *string `json:"createdAt"`
|
||||
Data json.RawMessage `json:"data"`
|
||||
DeactivatedAt *string `json:"deactivatedAt"`
|
||||
Id *int64 `json:"id"`
|
||||
State *string `json:"state"`
|
||||
}
|
||||
var tmp rawAlias
|
||||
if err := json.Unmarshal(b, &tmp); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// CreatedAt
|
||||
if tmp.CreatedAt != nil {
|
||||
v.CreatedAt = ovhtypes.NewTfStringValue(*tmp.CreatedAt)
|
||||
} else {
|
||||
v.CreatedAt = ovhtypes.NewTfStringNull()
|
||||
}
|
||||
// Data: accept any JSON. If null -> null, else raw bytes as string
|
||||
if len(tmp.Data) == 0 || string(tmp.Data) == "null" {
|
||||
v.Data = ovhtypes.NewTfStringNull()
|
||||
} else {
|
||||
// Store exact raw JSON (no reformat) as string
|
||||
v.Data = ovhtypes.NewTfStringValue(string(tmp.Data))
|
||||
}
|
||||
// DeactivatedAt
|
||||
if tmp.DeactivatedAt != nil {
|
||||
v.DeactivatedAt = ovhtypes.NewTfStringValue(*tmp.DeactivatedAt)
|
||||
} else {
|
||||
v.DeactivatedAt = ovhtypes.NewTfStringNull()
|
||||
}
|
||||
// Id
|
||||
if tmp.Id != nil {
|
||||
v.Id = ovhtypes.NewTfInt64Value(*tmp.Id)
|
||||
} else {
|
||||
v.Id = ovhtypes.NewTfInt64ValueNull()
|
||||
}
|
||||
// State
|
||||
if tmp.State != nil {
|
||||
v.State = ovhtypes.NewTfStringValue(*tmp.State)
|
||||
} else {
|
||||
v.State = ovhtypes.NewTfStringNull()
|
||||
}
|
||||
|
||||
v.state = attr.ValueStateKnown
|
||||
return nil
|
||||
}
|
||||
293
ovh/resource_okms_secret_test.go
Normal file
293
ovh/resource_okms_secret_test.go
Normal file
|
|
@ -0,0 +1,293 @@
|
|||
package ovh
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-testing/helper/acctest"
|
||||
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
|
||||
)
|
||||
|
||||
// Step 1: create secret v1
|
||||
const testAccOkmsSecretResourceConfigV1 = `
|
||||
resource "ovh_okms_secret" "test" {
|
||||
okms_id = "%s"
|
||||
path = "%s"
|
||||
version = {
|
||||
data = jsonencode({ initial = "v1" })
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
// Step 2: update secret with cas=1 to create v2
|
||||
const testAccOkmsSecretResourceConfigV2 = `
|
||||
resource "ovh_okms_secret" "test" {
|
||||
okms_id = "%s"
|
||||
path = "%s"
|
||||
cas = 1
|
||||
version = {
|
||||
data = jsonencode({ initial = "v1", second = "v2" })
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
// Step 3: update again with cas=2 and change metadata (max_versions) to ensure metadata update and version 3 creation.
|
||||
const testAccOkmsSecretResourceConfigV3 = `
|
||||
resource "ovh_okms_secret" "test" {
|
||||
okms_id = "%s"
|
||||
path = "%s"
|
||||
cas = 2
|
||||
metadata = {
|
||||
max_versions = 5
|
||||
}
|
||||
version = {
|
||||
data = jsonencode({ initial = "v1", second = "v2", third = "v3" })
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
// Update attempt with an incorrect CAS value (expected to fail)
|
||||
const testAccOkmsSecretResourceConfigWrongCas = `
|
||||
resource "ovh_okms_secret" "test" {
|
||||
okms_id = "%s"
|
||||
path = "%s"
|
||||
cas = 9999
|
||||
version = {
|
||||
data = jsonencode({ initial = "v1", wrong = "cas" })
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
// Update to create v2 with cas=1 and set metadata.cas_required = true
|
||||
const testAccOkmsSecretResourceConfigV2CasRequired = `
|
||||
resource "ovh_okms_secret" "test" {
|
||||
okms_id = "%s"
|
||||
path = "%s"
|
||||
cas = 1
|
||||
metadata = {
|
||||
cas_required = true
|
||||
}
|
||||
version = {
|
||||
data = jsonencode({ initial = "v1", second = "v2" })
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
// Metadata-only update (same version data) should NOT create a new version (version.id stays constant)
|
||||
const testAccOkmsSecretResourceConfigMetadataOnlyNoNewVersion = `
|
||||
resource "ovh_okms_secret" "test" {
|
||||
okms_id = "%s"
|
||||
path = "%s"
|
||||
version = {
|
||||
data = jsonencode({ initial = "v1" })
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const testAccOkmsSecretResourceConfigMetadataOnlyNoNewVersionStep2 = `
|
||||
resource "ovh_okms_secret" "test" {
|
||||
okms_id = "%s"
|
||||
path = "%s"
|
||||
cas = 1
|
||||
metadata = {
|
||||
max_versions = 9
|
||||
}
|
||||
// SAME DATA -> should not create a new version
|
||||
version = {
|
||||
data = jsonencode({ initial = "v1" })
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
// Create secret with initial metadata to test MetadataValue.ToCreate implementation
|
||||
const testAccOkmsSecretResourceConfigCreateWithMetadata = `
|
||||
resource "ovh_okms_secret" "test" {
|
||||
okms_id = "%s"
|
||||
path = "%s"
|
||||
metadata = {
|
||||
cas_required = true
|
||||
max_versions = 7
|
||||
deactivate_version_after = "0s"
|
||||
custom_metadata = {
|
||||
env = "acc"
|
||||
}
|
||||
}
|
||||
version = {
|
||||
data = jsonencode({ initial = "v1" })
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
func TestAccOkmsSecretResource_basicLifecycle(t *testing.T) {
|
||||
okmsID := os.Getenv("OVH_OKMS")
|
||||
if okmsID == "" {
|
||||
checkEnvOrSkip(t, "OVH_OKMS")
|
||||
}
|
||||
path := fmt.Sprintf("tfacc-%s", acctest.RandString(6))
|
||||
|
||||
configV1 := fmt.Sprintf(testAccOkmsSecretResourceConfigV1, okmsID, path)
|
||||
configV2 := fmt.Sprintf(testAccOkmsSecretResourceConfigV2, okmsID, path)
|
||||
configV3 := fmt.Sprintf(testAccOkmsSecretResourceConfigV3, okmsID, path)
|
||||
|
||||
resource.Test(t, resource.TestCase{
|
||||
PreCheck: func() { testAccPreCheckOkms(t) },
|
||||
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
|
||||
Steps: []resource.TestStep{
|
||||
{
|
||||
Config: configV1,
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
resource.TestCheckResourceAttr("ovh_okms_secret.test", "version.id", "1"),
|
||||
resource.TestCheckResourceAttrSet("ovh_okms_secret.test", "version.data"),
|
||||
),
|
||||
},
|
||||
{
|
||||
Config: configV2,
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
resource.TestCheckResourceAttr("ovh_okms_secret.test", "version.id", "2"),
|
||||
resource.TestCheckResourceAttrSet("ovh_okms_secret.test", "version.data"),
|
||||
),
|
||||
},
|
||||
{
|
||||
Config: configV3,
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
resource.TestCheckResourceAttr("ovh_okms_secret.test", "version.id", "3"),
|
||||
resource.TestCheckResourceAttr("ovh_okms_secret.test", "metadata.max_versions", "5"),
|
||||
resource.TestCheckResourceAttrSet("ovh_okms_secret.test", "version.data"),
|
||||
),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Verify CAS mismatch returns an error when wrong cas is provided.
|
||||
func TestAccOkmsSecretResource_casMismatch(t *testing.T) {
|
||||
okmsID := os.Getenv("OVH_OKMS")
|
||||
if okmsID == "" {
|
||||
checkEnvOrSkip(t, "OVH_OKMS")
|
||||
}
|
||||
path := fmt.Sprintf("tfacc-%s", acctest.RandString(6))
|
||||
|
||||
// Create v1
|
||||
configCreate := fmt.Sprintf(testAccOkmsSecretResourceConfigV1, okmsID, path)
|
||||
// Attempt update with wrong cas (expect failure)
|
||||
wrongCasConfig := fmt.Sprintf(testAccOkmsSecretResourceConfigWrongCas, okmsID, path)
|
||||
|
||||
resource.Test(t, resource.TestCase{
|
||||
PreCheck: func() { testAccPreCheckOkms(t) },
|
||||
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
|
||||
Steps: []resource.TestStep{
|
||||
{Config: configCreate},
|
||||
{
|
||||
Config: wrongCasConfig,
|
||||
ExpectError: regexp.MustCompile(`(?i)cas`),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Verify updating only metadata (cas required) also creates a new version when version data changes
|
||||
// and that changing path forces recreation.
|
||||
func TestAccOkmsSecretResource_pathRecreateAndMetadata(t *testing.T) {
|
||||
okmsID := os.Getenv("OVH_OKMS")
|
||||
if okmsID == "" {
|
||||
checkEnvOrSkip(t, "OVH_OKMS")
|
||||
}
|
||||
path1 := fmt.Sprintf("tfacc-%s", acctest.RandString(6))
|
||||
path2 := path1 + "-b"
|
||||
|
||||
// initial create v1
|
||||
cfg1 := fmt.Sprintf(testAccOkmsSecretResourceConfigV1, okmsID, path1)
|
||||
// update to v2 with cas=1 and set cas_required true via metadata
|
||||
cfg2 := fmt.Sprintf(testAccOkmsSecretResourceConfigV2CasRequired, okmsID, path1)
|
||||
// change path should force recreation (id reset to 1 again) because RequiresReplace
|
||||
cfg3 := fmt.Sprintf(testAccOkmsSecretResourceConfigV1, okmsID, path2)
|
||||
|
||||
resource.Test(t, resource.TestCase{
|
||||
PreCheck: func() { testAccPreCheckOkms(t) },
|
||||
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
|
||||
Steps: []resource.TestStep{
|
||||
{
|
||||
Config: cfg1,
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
resource.TestCheckResourceAttr("ovh_okms_secret.test", "version.id", "1"),
|
||||
),
|
||||
},
|
||||
{
|
||||
Config: cfg2,
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
resource.TestCheckResourceAttr("ovh_okms_secret.test", "version.id", "2"),
|
||||
resource.TestCheckResourceAttr("ovh_okms_secret.test", "metadata.cas_required", "true"),
|
||||
),
|
||||
},
|
||||
{
|
||||
Config: cfg3,
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
resource.TestCheckResourceAttr("ovh_okms_secret.test", "version.id", "1"),
|
||||
),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Test metadata-only update does not create a new version
|
||||
func TestAccOkmsSecretResource_metadataOnlyNoNewVersion(t *testing.T) {
|
||||
okmsID := os.Getenv("OVH_OKMS")
|
||||
if okmsID == "" {
|
||||
checkEnvOrSkip(t, "OVH_OKMS")
|
||||
}
|
||||
path := fmt.Sprintf("tfacc-%s", acctest.RandString(6))
|
||||
|
||||
cfg1 := fmt.Sprintf(testAccOkmsSecretResourceConfigMetadataOnlyNoNewVersion, okmsID, path)
|
||||
cfg2 := fmt.Sprintf(testAccOkmsSecretResourceConfigMetadataOnlyNoNewVersionStep2, okmsID, path)
|
||||
|
||||
resource.Test(t, resource.TestCase{
|
||||
PreCheck: func() { testAccPreCheckOkms(t) },
|
||||
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
|
||||
Steps: []resource.TestStep{
|
||||
{
|
||||
Config: cfg1,
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
resource.TestCheckResourceAttr("ovh_okms_secret.test", "version.id", "1"),
|
||||
),
|
||||
},
|
||||
{
|
||||
Config: cfg2,
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
// version should remain 1 because data unchanged
|
||||
resource.TestCheckResourceAttr("ovh_okms_secret.test", "version.id", "1"),
|
||||
resource.TestCheckResourceAttr("ovh_okms_secret.test", "metadata.max_versions", "9"),
|
||||
),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Test creation with metadata supplied at creation time (no subsequent update needed)
|
||||
func TestAccOkmsSecretResource_createWithMetadata(t *testing.T) {
|
||||
okmsID := os.Getenv("OVH_OKMS")
|
||||
if okmsID == "" {
|
||||
checkEnvOrSkip(t, "OVH_OKMS")
|
||||
}
|
||||
path := fmt.Sprintf("tfacc-%s", acctest.RandString(6))
|
||||
|
||||
cfg := fmt.Sprintf(testAccOkmsSecretResourceConfigCreateWithMetadata, okmsID, path)
|
||||
|
||||
resource.Test(t, resource.TestCase{
|
||||
PreCheck: func() { testAccPreCheckOkms(t) },
|
||||
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
|
||||
Steps: []resource.TestStep{
|
||||
{
|
||||
Config: cfg,
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
resource.TestCheckResourceAttr("ovh_okms_secret.test", "version.id", "1"),
|
||||
resource.TestCheckResourceAttr("ovh_okms_secret.test", "metadata.cas_required", "true"),
|
||||
resource.TestCheckResourceAttr("ovh_okms_secret.test", "metadata.max_versions", "7"),
|
||||
resource.TestCheckResourceAttr("ovh_okms_secret.test", "metadata.deactivate_version_after", "0s"),
|
||||
resource.TestCheckResourceAttr("ovh_okms_secret.test", "metadata.custom_metadata.env", "acc"),
|
||||
),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
63
templates/data-sources/okms_secret.md.tmpl
Normal file
63
templates/data-sources/okms_secret.md.tmpl
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
---
|
||||
subcategory : "Key Management Service (KMS)"
|
||||
---
|
||||
|
||||
# ovh_okms_secret (Data Source)
|
||||
|
||||
Retrieves metadata (and optionally the payload) of a secret stored in OVHcloud KMS.
|
||||
|
||||
> WARNING: If `include_data = true` the secret value is stored in cleartext (JSON) in the Terraform state file. Marked **Sensitive** only hides it from CLI output. If you use this option it is recommended to protect your state with encryption and access controls.
|
||||
|
||||
## Example Usage
|
||||
|
||||
Get the latest secret version (metadata only):
|
||||
|
||||
{{tffile "examples/data-sources/okms_secret/example_2.tf"}}
|
||||
|
||||
Get the latest secret version including its data:
|
||||
|
||||
{{tffile "examples/data-sources/okms_secret/example_1.tf"}}
|
||||
|
||||
Get a specific version including its payload:
|
||||
|
||||
{{tffile "examples/data-sources/okms_secret/example_3.tf"}}
|
||||
|
||||
## Argument Reference
|
||||
|
||||
The following arguments are supported:
|
||||
|
||||
### Required
|
||||
|
||||
- `okms_id` (String) OKMS service ID that owns the secret.
|
||||
- `path` (String) Secret path (identifier within the OKMS instance).
|
||||
|
||||
### Optional
|
||||
|
||||
- `version` (Number) Specific version to retrieve. If omitted, the latest (current) version is selected.
|
||||
- `include_data` (Boolean) If true, retrieves the secret payload (`data` attribute). Defaults to false. When false only metadata is returned.
|
||||
|
||||
## Attributes Reference (Read-Only)
|
||||
|
||||
In addition to the arguments above, the following attributes are exported:
|
||||
|
||||
- `version` (Number) The resolved version number (requested or current latest).
|
||||
- `data` (String, Sensitive) Raw JSON secret payload (present only if `include_data` is true).
|
||||
- `metadata` (Block) Secret metadata:
|
||||
- `cas_required` (Boolean)
|
||||
- `created_at` (String)
|
||||
- `updated_at` (String)
|
||||
- `current_version` (Number)
|
||||
- `oldest_version` (Number)
|
||||
- `max_versions` (Number)
|
||||
- `deactivate_version_after` (String)
|
||||
- `custom_metadata` (Map of String)
|
||||
- `iam` (Block) IAM resource metadata:
|
||||
- `display_name` (String)
|
||||
- `id` (String)
|
||||
- `tags` (Map of String)
|
||||
- `urn` (String)
|
||||
|
||||
## Behavior & Notes
|
||||
|
||||
- The `data` attribute retains the raw JSON returned by the API. Use `jsondecode()` to work with individual keys.
|
||||
- Changing only `include_data` (true -> false) will cause the `data` attribute to become null in subsequent refreshes (state no longer holds the payload).
|
||||
81
templates/resources/okms_secret.md.tmpl
Normal file
81
templates/resources/okms_secret.md.tmpl
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
---
|
||||
subcategory : "Key Management Service (KMS)"
|
||||
---
|
||||
|
||||
# ovh_okms_secret (Resource)
|
||||
|
||||
Manages a secret stored in OVHcloud KMS.
|
||||
|
||||
> WARNING: `version.data` is marked **Sensitive** but still ends up in the state file. To mitigate that, it is recommended to protect your state with encryption and access controls. Avoid committing it to source control.
|
||||
|
||||
## Example Usage
|
||||
|
||||
Create a secret whose value is a JSON object. Use `jsonencode()` to produce a deterministic JSON string (ordering/whitespace) to minimize diffs.
|
||||
|
||||
{{tffile "examples/resources/okms_secret/example_1.tf"}}
|
||||
|
||||
# Reading a field from the secret version data:
|
||||
|
||||
{{tffile "examples/data-sources/okms_secret/example_1.tf"}}
|
||||
|
||||
Updating the secret (new version) while enforcing optimistic concurrency control using CAS:
|
||||
|
||||
{{tffile "examples/resources/okms_secret/example_2.tf"}}
|
||||
|
||||
## Argument Reference
|
||||
|
||||
The following arguments are supported:
|
||||
|
||||
### Required
|
||||
|
||||
- `okms_id` (String) ID of the OKMS service to create the secret in.
|
||||
- `path` (String) Secret path (acts as the secret identifier within the OKMS instance). Immutable after creation.
|
||||
- `version` (Block) Definition of the version to create/update. See Version Block below. (On updates providing a new `version.data` creates a new version.)
|
||||
|
||||
### Optional
|
||||
|
||||
- `cas` (Number) Check‑and‑set parameter used only on update (if `cas_required` metadata is set to true) to enforce optimistic concurrency control: its value must equal the current secret version (`metadata.current_version`) for the update to succeed. Ignored on create.
|
||||
- `metadata` (Block) Secret metadata configuration (subset of fields are user-settable). See Metadata Block below.
|
||||
|
||||
### Metadata Block
|
||||
|
||||
User configurable attributes inside `metadata`:
|
||||
|
||||
- `cas_required` (Boolean) If true, the server will enforce optimistic concurrency control by requiring the `cas` parameter to match the current version number on every write (update) request.
|
||||
- `custom_metadata` (Map of String) Arbitrary key/value metadata.
|
||||
- `deactivate_version_after` (String) Duration (e.g. `"24h"`) after which a version is deactivated. `"0s"` (default) means never automatically deactivate.
|
||||
- `max_versions` (Number) Number of versions to retain (default 10). Older versions beyond this limit are pruned.
|
||||
|
||||
Computed (read‑only) metadata attributes:
|
||||
|
||||
- `created_at` (String) Creation timestamp of the secret.
|
||||
- `updated_at` (String) Last update timestamp.
|
||||
- `current_version` (Number) Current (latest) version number.
|
||||
- `oldest_version` (Number) Oldest retained version number.
|
||||
|
||||
### Version Block
|
||||
|
||||
Required attribute:
|
||||
|
||||
- `data` (String, Sensitive) Secret payload. Commonly set with `jsonencode(...)` so that Terraform comparisons are stable. Any valid JSON (object, array, string, number, bool) is accepted. Changing `data` creates a new secret version.
|
||||
|
||||
Computed (read‑only) attributes:
|
||||
|
||||
- `id` (Number) Version number.
|
||||
- `created_at` (String) Version creation timestamp.
|
||||
- `deactivated_at` (String) Deactivation timestamp if the version was deactivated.
|
||||
- `state` (String) Version state (e.g. `ACTIVE`).
|
||||
|
||||
## Attributes Reference (Read-Only)
|
||||
|
||||
In addition to arguments above, the following attributes are exported:
|
||||
|
||||
- `iam` (Block) IAM metadata: `display_name`, `id`, `tags`, `urn`.
|
||||
- `metadata.*` computed fields as listed above.
|
||||
- `version.*` computed fields as listed above.
|
||||
|
||||
## Behavior & Notes
|
||||
|
||||
- Updating with a new `version.data` performs an API PUT that creates a new version; the previous version remains (subject to `max_versions`).
|
||||
- If `cas_required` is true, all write operations must include a correct `cas` query parameter (the expected current version number). Set `cas = ovh_okms_secret.example.metadata.current_version` to enforce optimistic concurrency. A mismatch causes the API to reject the update (preventing overwriting unseen changes).
|
||||
- `cas` is ignored on create (no existing version).
|
||||
Loading…
Add table
Reference in a new issue