From 02165fa3908319f6eabb5b660ec573f77c5a9a65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20L=C3=A9one?= Date: Wed, 2 Jul 2025 16:44:08 +0200 Subject: [PATCH] feat(key_manager); add support for key manager key --- internal/provider/provider.go | 4 + internal/services/keymanager/helpers_key.go | 35 +++ internal/services/keymanager/key.go | 209 ++++++++++++++++++ .../services/keymanager/key_data_source.go | 91 ++++++++ 4 files changed, 339 insertions(+) create mode 100644 internal/services/keymanager/helpers_key.go create mode 100644 internal/services/keymanager/key.go create mode 100644 internal/services/keymanager/key_data_source.go diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 980d585e2..c44130b57 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -4,6 +4,8 @@ import ( "context" "os" + "github.com/scaleway/terraform-provider-scaleway/v2/internal/services/keymanager" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/plugin" @@ -195,6 +197,7 @@ func Provider(config *Config) plugin.ProviderFunc { "scaleway_k8s_acl": k8s.ResourceACL(), "scaleway_k8s_cluster": k8s.ResourceCluster(), "scaleway_k8s_pool": k8s.ResourcePool(), + "scaleway_key_manager_key": keymanager.ResourceKey(), "scaleway_lb": lb.ResourceLb(), "scaleway_lb_acl": lb.ResourceACL(), "scaleway_lb_backend": lb.ResourceBackend(), @@ -297,6 +300,7 @@ func Provider(config *Config) plugin.ProviderFunc { "scaleway_k8s_cluster": k8s.DataSourceCluster(), "scaleway_k8s_pool": k8s.DataSourcePool(), "scaleway_k8s_version": k8s.DataSourceVersion(), + "scaleway_key_manager_key": keymanager.DataSourceKey(), "scaleway_lb": lb.DataSourceLb(), "scaleway_lb_acls": lb.DataSourceACLs(), "scaleway_lb_backend": lb.DataSourceBackend(), diff --git a/internal/services/keymanager/helpers_key.go b/internal/services/keymanager/helpers_key.go new file mode 100644 index 000000000..c0ca88881 --- /dev/null +++ b/internal/services/keymanager/helpers_key.go @@ -0,0 +1,35 @@ +package keymanager + +import ( + "time" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + key_manager "github.com/scaleway/scaleway-sdk-go/api/key_manager/v1alpha1" + "github.com/scaleway/scaleway-sdk-go/scw" + "github.com/scaleway/terraform-provider-scaleway/v2/internal/locality/regional" + "github.com/scaleway/terraform-provider-scaleway/v2/internal/meta" +) + +const defaultKeyTimeout = 5 * time.Minute + +func newKeyManagerAPI(d *schema.ResourceData, m any) (*key_manager.API, scw.Region, error) { + api := key_manager.NewAPI(meta.ExtractScwClient(m)) + + region, err := meta.ExtractRegion(d, m) + if err != nil { + return nil, "", err + } + + return api, region, nil +} + +func NewKeyManagerAPIWithRegionAndID(m any, regionalID string) (*key_manager.API, scw.Region, string, error) { + api := key_manager.NewAPI(meta.ExtractScwClient(m)) + + region, ID, err := regional.ParseID(regionalID) + if err != nil { + return nil, "", "", err + } + + return api, region, ID, nil +} diff --git a/internal/services/keymanager/key.go b/internal/services/keymanager/key.go new file mode 100644 index 000000000..8fe9b76bf --- /dev/null +++ b/internal/services/keymanager/key.go @@ -0,0 +1,209 @@ +package keymanager + +import ( + "context" + + "github.com/scaleway/terraform-provider-scaleway/v2/internal/types" + + key_manager "github.com/scaleway/scaleway-sdk-go/api/key_manager/v1alpha1" + "github.com/scaleway/scaleway-sdk-go/scw" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/scaleway/terraform-provider-scaleway/v2/internal/locality/regional" + "github.com/scaleway/terraform-provider-scaleway/v2/internal/services/account" +) + +func ResourceKey() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceKeyCreate, + ReadContext: resourceKeyRead, + UpdateContext: resourceKeyUpdate, + DeleteContext: resourceKeyDelete, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + Timeouts: &schema.ResourceTimeout{ + Read: schema.DefaultTimeout(defaultKeyTimeout), + Update: schema.DefaultTimeout(defaultKeyTimeout), + Delete: schema.DefaultTimeout(defaultKeyTimeout), + Default: schema.DefaultTimeout(defaultKeyTimeout), + }, + SchemaVersion: 0, + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Optional: true, + Description: "The name of the key.", + }, + "description": { + Type: schema.TypeString, + Optional: true, + Description: "The description of the key.", + }, + "state": { + Type: schema.TypeString, + Computed: true, + Description: "The state of the key (enabled, disabled, pending_key_material)", + }, + "locked": { + Type: schema.TypeBool, + Computed: true, + Description: "The locked state of the key.", + }, + "protected": { + Type: schema.TypeBool, + Computed: true, + Description: "Returns `true` if key protection is applied to the key", + }, + "rotation_count": { + Type: schema.TypeInt, + Computed: true, + Description: "The number of times the key has been rotated", + }, + "rotated_at": { + Type: schema.TypeString, + Computed: true, + Description: "The date and time of the last rotation of the key", + }, + "created_at": { + Type: schema.TypeString, + Computed: true, + Description: "The date and time of the creation of the key", + }, + "updated_at": { + Type: schema.TypeString, + Computed: true, + Description: "The date and time of the last update of the key", + }, + "tags": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Description: "List of tags [\"tag1\", \"tag2\", ...] attached to a key", + }, + "origin": { + Type: schema.TypeString, + Computed: true, + Description: "The origin of the key (scaleway_kms, external)", + }, + // Computed + "project_id": account.ProjectIDSchema(), + "region": regional.ComputedSchema(), + }, + } +} + +func resourceKeyCreate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + api, region, err := newKeyManagerAPI(d, meta) + if err != nil { + return diag.FromErr(err) + } + + keyCreateRequest := &key_manager.CreateKeyRequest{ + Region: region, + ProjectID: d.Get("project_id").(string), + Name: types.ExpandStringPtr(d.Get("name")), + Description: types.ExpandStringPtr(d.Get("description")), + Unprotected: d.Get("protected").(bool), + } + + rawTag, tagExist := d.GetOk("tags") + if tagExist { + keyCreateRequest.Tags = types.ExpandStrings(rawTag) + } + + rawDescription, descriptionExist := d.GetOk("description") + if descriptionExist { + keyCreateRequest.Description = types.ExpandStringPtr(rawDescription) + } + + keyResponse, err := api.CreateKey(keyCreateRequest, scw.WithContext(ctx)) + if err != nil { + return diag.FromErr(err) + } + + d.SetId(regional.NewIDString(region, keyResponse.ID)) + + return resourceKeyRead(ctx, d, meta) +} + +func resourceKeyRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + api, region, id, err := NewKeyManagerAPIWithRegionAndID(meta, d.Id()) + if err != nil { + return diag.FromErr(err) + } + + key, err := api.GetKey(&key_manager.GetKeyRequest{ + Region: region, + KeyID: id, + }, scw.WithContext(ctx)) + if err != nil { + return diag.FromErr(err) + } + + _ = d.Set("region", key.Region) + _ = d.Set("project_id", key.ProjectID) + _ = d.Set("description", key.Description) + _ = d.Set("name", key.Name) + _ = d.Set("state", key.State) + _ = d.Set("rotation_count", key.RotationCount) + _ = d.Set("created_at", types.FlattenTime(key.CreatedAt)) + _ = d.Set("updated_at", types.FlattenTime(key.UpdatedAt)) + _ = d.Set("origin", key.Origin) + _ = d.Set("rotated_at", types.FlattenTime(key.RotatedAt)) + _ = d.Set("locked", key.Locked) + _ = d.Set("protected", key.Protected) + + return nil +} + +func resourceKeyUpdate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + keyManagerAPI, region, ID, err := NewKeyManagerAPIWithRegionAndID(meta, d.Id()) + if err != nil { + return diag.FromErr(err) + } + + shouldUpdate := false + req := &key_manager.UpdateKeyRequest{ + Region: region, + KeyID: ID, + } + + if d.HasChange("name") { + req.Name = types.ExpandStringPtr(d.Get("name")) + shouldUpdate = true + } + + if d.HasChange("tags") { + req.Tags = types.ExpandUpdatedStringsPtr(d.Get("tags")) + } + + if shouldUpdate { + _, err = keyManagerAPI.UpdateKey(req, scw.WithContext(ctx)) + if err != nil { + return diag.FromErr(err) + } + } + + return resourceKeyRead(ctx, d, meta) +} + +func resourceKeyDelete(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + keyManagerAPI, region, ID, err := NewKeyManagerAPIWithRegionAndID(meta, d.Id()) + if err != nil { + return diag.FromErr(err) + } + + err = keyManagerAPI.DeleteKey(&key_manager.DeleteKeyRequest{ + KeyID: ID, + Region: region, + }, scw.WithContext(ctx)) + if err != nil { + return diag.FromErr(err) + } + + return nil +} diff --git a/internal/services/keymanager/key_data_source.go b/internal/services/keymanager/key_data_source.go new file mode 100644 index 000000000..1bb45b13d --- /dev/null +++ b/internal/services/keymanager/key_data_source.go @@ -0,0 +1,91 @@ +package keymanager + +import ( + "context" + "fmt" + + key_manager "github.com/scaleway/scaleway-sdk-go/api/key_manager/v1alpha1" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/scaleway/scaleway-sdk-go/scw" + "github.com/scaleway/terraform-provider-scaleway/v2/internal/datasource" + "github.com/scaleway/terraform-provider-scaleway/v2/internal/locality" + "github.com/scaleway/terraform-provider-scaleway/v2/internal/types" + "github.com/scaleway/terraform-provider-scaleway/v2/internal/verify" +) + +func DataSourceKey() *schema.Resource { + // Generate datasource schema from resource + dsSchema := datasource.SchemaFromResourceSchema(ResourceKey().Schema) + // Set 'Optional' schema elements + datasource.AddOptionalFieldsToSchema(dsSchema, "name", "project_id") + + dsSchema["name"].ConflictsWith = []string{"key_id"} + dsSchema["key_id"] = &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Description: "The ID of the Key", + ConflictsWith: []string{"name"}, + ValidateDiagFunc: verify.IsUUIDorUUIDWithLocality(), + } + + return &schema.Resource{ + ReadContext: DataSourceKeyRead, + Schema: dsSchema, + } +} + +func DataSourceKeyRead(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { + api, region, err := newKeyManagerAPI(d, m) + if err != nil { + return diag.FromErr(err) + } + + keyID, ok := d.GetOk("key_id") + if !ok { + keyName := d.Get("name").(string) + + res, err := api.ListKeys(&key_manager.ListKeysRequest{ + Region: region, + Name: types.ExpandStringPtr(keyName), + ProjectID: types.ExpandStringPtr(d.Get("project_id")), + }, scw.WithContext(ctx)) + if err != nil { + return diag.FromErr(err) + } + + foundKey, err := datasource.FindExact( + res.Keys, + func(s *key_manager.Key) bool { return s.Name == keyName }, + keyName, + ) + if err != nil { + return diag.FromErr(err) + } + + keyID = foundKey.ID + } + + regionalID := datasource.NewRegionalID(keyID, region) + d.SetId(regionalID) + + err = d.Set("key_id", regionalID) + if err != nil { + return diag.FromErr(err) + } + + // Check if key exist as Read will return nil if resource does not exist + // keyID may be regional if using name in data source + getReq := &key_manager.GetKeyRequest{ + Region: region, + KeyID: locality.ExpandID(keyID.(string)), + } + + _, err = api.GetKey(getReq, scw.WithContext(ctx)) + if err != nil { + return diag.FromErr(fmt.Errorf("no key found with the id %s", keyID)) + } + + return resourceKeyRead(ctx, d, m) +}