diff --git a/docs/resources/bucket.md b/docs/resources/bucket.md new file mode 100644 index 0000000..5c644dd --- /dev/null +++ b/docs/resources/bucket.md @@ -0,0 +1,62 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "garage_bucket Resource - terraform-provider-garage" +subcategory: "" +description: |- + This resource can be used to manage Garage buckets. +--- + +# garage_bucket (Resource) + +This resource can be used to manage Garage buckets. + +## Example Usage + +```terraform +resource "garage_bucket" "bucket" {} + +resource "garage_bucket" "bucket-with-website" { + website_access_enabled = true + website_config_index_document = "index.html" + website_config_error_document = "error.html" +} + +resource "garage_bucket" "bucket-with-quota" { + quota_max_size = 1024 + quota_max_objects = 100 +} +``` + + +## Schema + +### Optional + +- `quota_max_objects` (Number) +- `quota_max_size` (Number) +- `website_access_enabled` (Boolean) +- `website_config_error_document` (String) +- `website_config_index_document` (String) + +### Read-Only + +- `bytes` (Number) +- `global_aliases` (List of String) +- `id` (String) The ID of this resource. +- `keys` (Set of Object) (see [below for nested schema](#nestedatt--keys)) +- `objects` (Number) +- `unfinished_uploads` (Number) + + +### Nested Schema for `keys` + +Read-Only: + +- `access_key_id` (String) +- `local_aliases` (List of String) +- `name` (String) +- `permissions_owner` (Boolean) +- `permissions_read` (Boolean) +- `permissions_write` (Boolean) + + diff --git a/docs/resources/bucket_global_alias.md b/docs/resources/bucket_global_alias.md new file mode 100644 index 0000000..af7e70f --- /dev/null +++ b/docs/resources/bucket_global_alias.md @@ -0,0 +1,41 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "garage_bucket_global_alias Resource - terraform-provider-garage" +subcategory: "" +description: |- + This resource can be used to manage Garage bucket global aliases. +--- + +# garage_bucket_global_alias (Resource) + +This resource can be used to manage Garage bucket global aliases. + +## Example Usage + +```terraform +resource "garage_bucket" "website" {} + +resource "garage_bucket_global_alias" "website" { + bucket_id = garage_bucket.website.id + alias = "website" +} + +resource "garage_bucket_global_alias" "www" { + bucket_id = garage_bucket.website.id + alias = "www" +} +``` + + +## Schema + +### Required + +- `alias` (String) +- `bucket_id` (String) + +### Read-Only + +- `id` (String) The ID of this resource. + + diff --git a/docs/resources/bucket_key.md b/docs/resources/bucket_key.md new file mode 100644 index 0000000..27d83d0 --- /dev/null +++ b/docs/resources/bucket_key.md @@ -0,0 +1,47 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "garage_bucket_key Resource - terraform-provider-garage" +subcategory: "" +description: |- + This resource can be used to manage Garage bucket global aliases. +--- + +# garage_bucket_key (Resource) + +This resource can be used to manage Garage bucket global aliases. + +## Example Usage + +```terraform +resource "garage_key" "key" { + name = "my_key" +} + +resource "garage_bucket" "bucket" {} + +resource "garage_bucket_key" "bucket_key_read-only" { + bucket_id = garage_bucket.bucket.id + access_key_id = garage_key.key.acccess_key_id + read = true +} +``` + + +## Schema + +### Required + +- `access_key_id` (String) +- `bucket_id` (String) + +### Optional + +- `owner` (Boolean) +- `read` (Boolean) +- `write` (Boolean) + +### Read-Only + +- `id` (String) The ID of this resource. + + diff --git a/docs/resources/bucket_local_alias.md b/docs/resources/bucket_local_alias.md new file mode 100644 index 0000000..2286557 --- /dev/null +++ b/docs/resources/bucket_local_alias.md @@ -0,0 +1,42 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "garage_bucket_local_alias Resource - terraform-provider-garage" +subcategory: "" +description: |- + This resource can be used to manage Garage bucket global aliases. +--- + +# garage_bucket_local_alias (Resource) + +This resource can be used to manage Garage bucket global aliases. + +## Example Usage + +```terraform +resource "garage_key" "key" { + name = "key" +} + +resource "garage_bucket" "bucket" {} + +resource "garage_bucket_local_alias" "bucket_key_private-files" { + bucket_id = garage_bucket.bucket.id + access_key_id = garage_key.key.access_key_id + alias = "private-files" +} +``` + + +## Schema + +### Required + +- `access_key_id` (String) +- `alias` (String) +- `bucket_id` (String) + +### Read-Only + +- `id` (String) The ID of this resource. + + diff --git a/docs/resources/key.md b/docs/resources/key.md new file mode 100644 index 0000000..c2a746a --- /dev/null +++ b/docs/resources/key.md @@ -0,0 +1,38 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "garage_key Resource - terraform-provider-garage" +subcategory: "" +description: |- + This resource can be used to manage Garage keys. +--- + +# garage_key (Resource) + +This resource can be used to manage Garage keys. + +## Example Usage + +```terraform +resource "garage_key" "key" { + name = "key" + permissions = { + create_bucket = true // defaults to false + } +} +``` + + +## Schema + +### Optional + +- `access_key_id` (String) +- `name` (String) The name of the key. +- `permissions` (Map of Boolean) +- `secret_access_key` (String, Sensitive) + +### Read-Only + +- `id` (String) The ID of this resource. + + diff --git a/examples/data-sources/.keep b/examples/data-sources/.keep new file mode 100644 index 0000000..e69de29 diff --git a/examples/resources/garage_bucket/resource.tf b/examples/resources/garage_bucket/resource.tf new file mode 100644 index 0000000..3d546d4 --- /dev/null +++ b/examples/resources/garage_bucket/resource.tf @@ -0,0 +1,12 @@ +resource "garage_bucket" "bucket" {} + +resource "garage_bucket" "bucket-with-website" { + website_access_enabled = true + website_config_index_document = "index.html" + website_config_error_document = "error.html" +} + +resource "garage_bucket" "bucket-with-quota" { + quota_max_size = 1024 + quota_max_objects = 100 +} diff --git a/examples/resources/garage_bucket_global_alias/resource.tf b/examples/resources/garage_bucket_global_alias/resource.tf new file mode 100644 index 0000000..6258c84 --- /dev/null +++ b/examples/resources/garage_bucket_global_alias/resource.tf @@ -0,0 +1,11 @@ +resource "garage_bucket" "website" {} + +resource "garage_bucket_global_alias" "website" { + bucket_id = garage_bucket.website.id + alias = "website" +} + +resource "garage_bucket_global_alias" "www" { + bucket_id = garage_bucket.website.id + alias = "www" +} diff --git a/examples/resources/garage_bucket_key/resource.tf b/examples/resources/garage_bucket_key/resource.tf new file mode 100644 index 0000000..089e124 --- /dev/null +++ b/examples/resources/garage_bucket_key/resource.tf @@ -0,0 +1,11 @@ +resource "garage_key" "key" { + name = "my_key" +} + +resource "garage_bucket" "bucket" {} + +resource "garage_bucket_key" "bucket_key_read-only" { + bucket_id = garage_bucket.bucket.id + access_key_id = garage_key.key.acccess_key_id + read = true +} diff --git a/examples/resources/garage_bucket_local_alias/resource.tf b/examples/resources/garage_bucket_local_alias/resource.tf new file mode 100644 index 0000000..91ad22f --- /dev/null +++ b/examples/resources/garage_bucket_local_alias/resource.tf @@ -0,0 +1,11 @@ +resource "garage_key" "key" { + name = "key" +} + +resource "garage_bucket" "bucket" {} + +resource "garage_bucket_local_alias" "bucket_key_private-files" { + bucket_id = garage_bucket.bucket.id + access_key_id = garage_key.key.access_key_id + alias = "private-files" +} diff --git a/examples/resources/garage_key/resource.tf b/examples/resources/garage_key/resource.tf new file mode 100644 index 0000000..f35654d --- /dev/null +++ b/examples/resources/garage_key/resource.tf @@ -0,0 +1,6 @@ +resource "garage_key" "key" { + name = "key" + permissions = { + create_bucket = true // defaults to false + } +} diff --git a/garage/provider.go b/garage/provider.go index 1202d7e..6136c7e 100644 --- a/garage/provider.go +++ b/garage/provider.go @@ -8,6 +8,15 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) +type garageProvider struct { + client *garage.APIClient + ctx context.Context +} + +func updateContext(tfCtx context.Context, p *garageProvider) context.Context { + return context.WithValue(tfCtx, garage.ContextAccessToken, p.ctx.Value(garage.ContextAccessToken)) +} + // Provider - func Provider() *schema.Provider { return &schema.Provider{ @@ -29,7 +38,13 @@ func Provider() *schema.Provider { DefaultFunc: schema.EnvDefaultFunc("GARAGE_TOKEN", nil), }, }, - ResourcesMap: map[string]*schema.Resource{}, + ResourcesMap: map[string]*schema.Resource{ + "garage_bucket": resourceBucket(), + "garage_bucket_global_alias": resourceBucketGlobalAlias(), + "garage_bucket_key": resourceBucketKey(), + "garage_bucket_local_alias": resourceBucketLocalAlias(), + "garage_key": resourceKey(), + }, DataSourcesMap: map[string]*schema.Resource{}, ConfigureContextFunc: providerConfigure, } @@ -60,5 +75,8 @@ func providerConfigure(ctx context.Context, d *schema.ResourceData) (interface{} ctx = context.WithValue(ctx, garage.ContextAccessToken, token) - return client, diags + return &garageProvider{ + client: client, + ctx: ctx, + }, diags } diff --git a/garage/resource_bucket.go b/garage/resource_bucket.go new file mode 100644 index 0000000..120c847 --- /dev/null +++ b/garage/resource_bucket.go @@ -0,0 +1,255 @@ +package garage + +import ( + "context" + + garage "git.deuxfleurs.fr/garage-sdk/garage-admin-sdk-golang" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/thoas/go-funk" +) + +func schemaBucket() map[string]*schema.Schema { + return map[string]*schema.Schema{ + "website_access_enabled": { + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + "website_config_index_document": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "website_config_error_document": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "quota_max_size": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + }, + "quota_max_objects": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + }, + // Computed + "global_aliases": { + Type: schema.TypeList, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Computed: true, + }, + "keys": { + Type: schema.TypeSet, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "access_key_id": { + Type: schema.TypeString, + Computed: true, + }, + "name": { + Type: schema.TypeString, + Computed: true, + }, + "permissions_read": { + Type: schema.TypeBool, + Computed: true, + }, + "permissions_write": { + Type: schema.TypeBool, + Computed: true, + }, + "permissions_owner": { + Type: schema.TypeBool, + Computed: true, + }, + "local_aliases": { + Type: schema.TypeList, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Computed: true, + }, + }, + }, + }, + "objects": { + Type: schema.TypeInt, + Computed: true, + }, + "bytes": { + Type: schema.TypeInt, + Computed: true, + }, + "unfinished_uploads": { + Type: schema.TypeInt, + Computed: true, + }, + } +} + +func resourceBucket() *schema.Resource { + return &schema.Resource{ + Description: "This resource can be used to manage Garage buckets.", + CreateContext: resourceBucketCreate, + ReadContext: resourceBucketRead, + UpdateContext: resourceBucketUpdate, + DeleteContext: resourceBucketDelete, + Schema: schemaBucket(), + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + } +} + +func flattenBucketKey(bucketKey garage.BucketKeyInfo) interface{} { + return map[string]interface{}{ + "access_key_id": *bucketKey.AccessKeyId, + "name": *bucketKey.Name, + "permissions_read": *bucketKey.Permissions.Read, + "permissions_write": *bucketKey.Permissions.Write, + "permissions_owner": *bucketKey.Permissions.Owner, + } +} + +func flattenBucketInfo(bucket *garage.BucketInfo) interface{} { + b := map[string]interface{}{} + b["global_aliases"] = bucket.GlobalAliases + + if bucket.HasWebsiteAccess() { + b["website_access_enabled"] = bucket.GetWebsiteAccess() + } + + if bucket.HasWebsiteConfig() { + b["website_config_index_document"] = bucket.GetWebsiteConfig().IndexDocument + b["website_config_error_document"] = bucket.GetWebsiteConfig().ErrorDocument + } + + if bucket.HasQuotas() { + if bucket.Quotas.MaxSize.IsSet() { + b["quota_max_size"] = bucket.GetQuotas().MaxSize.Get() + } + if bucket.Quotas.MaxObjects.IsSet() { + b["quota_max_objects"] = bucket.GetQuotas().MaxObjects.Get() + } + } + + b["keys"] = funk.Map(bucket.GetKeys(), flattenBucketKey) + + b["objects"] = *bucket.Objects + b["bytes"] = *bucket.Bytes + b["unfinished_uploads"] = *bucket.UnfinishedUploads + + return b +} + +func resourceBucketCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + p := m.(*garageProvider) + var diags diag.Diagnostics + + bucketInfo, _, err := p.client.BucketApi.CreateBucket(updateContext(ctx, p)).CreateBucketRequest(garage.CreateBucketRequest{}).Execute() + if err != nil { + return diag.FromErr(err) + } + + d.SetId(*bucketInfo.Id) + + diags = resourceBucketUpdate(ctx, d, m) + + return diags +} + +func resourceBucketRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + p := m.(*garageProvider) + var diags diag.Diagnostics + + bucketID := d.Id() + + bucketInfo, _, err := p.client.BucketApi.GetBucketInfo(updateContext(ctx, p), bucketID).Execute() + if err != nil { + return diag.FromErr(err) + } + + for key, value := range flattenBucketInfo(bucketInfo).(map[string]interface{}) { + err := d.Set(key, value) + if err != nil { + return diag.FromErr(err) + } + } + + return diags +} + +func resourceBucketUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + p := m.(*garageProvider) + var diags diag.Diagnostics + + webAccessEnabled := false + var webConfigIndexDoc *string + webConfigIndexDoc = nil + var webConfigErrorDoc *string + webConfigErrorDoc = nil + if webAccessEnabledVal, ok := d.GetOk("website_access_enabled"); ok { + webAccessEnabled = webAccessEnabledVal.(bool) + } + if webConfigIndexDocVal, ok := d.GetOk("website_config_index_document"); ok { + webConfigIndexDocVal := webConfigIndexDocVal.(string) + webConfigIndexDoc = &webConfigIndexDocVal + } + if webConfigErrorDocVal, ok := d.GetOk("website_config_error_document"); ok { + webConfigErrorDocVal := webConfigErrorDocVal.(string) + webConfigErrorDoc = &webConfigErrorDocVal + } + + var quotaMaxSize *int32 + quotaMaxSize = nil + var quotaMaxObjects *int32 + quotaMaxObjects = nil + if quotaMaxSizeVal, ok := d.GetOk("quota_max_size"); ok { + quotaMaxSizeVal := int32(quotaMaxSizeVal.(int)) + quotaMaxSize = "aMaxSizeVal + } + if quotaMaxObjectsVal, ok := d.GetOk("quota_max_objects"); ok { + quotaMaxObjectsVal := int32(quotaMaxObjectsVal.(int)) + quotaMaxObjects = "aMaxObjectsVal + } + + updateBucketRequest := garage.UpdateBucketRequest{ + WebsiteAccess: &garage.UpdateBucketRequestWebsiteAccess{ + Enabled: &webAccessEnabled, + IndexDocument: webConfigIndexDoc, + ErrorDocument: webConfigErrorDoc, + }, + Quotas: &garage.UpdateBucketRequestQuotas{ + MaxSize: *garage.NewNullableInt32(quotaMaxSize), + MaxObjects: *garage.NewNullableInt32(quotaMaxObjects), + }, + } + + _, _, err := p.client.BucketApi.UpdateBucket(updateContext(ctx, p), d.Id()).UpdateBucketRequest(updateBucketRequest).Execute() + if err != nil { + return diag.FromErr(err) + } + + diags = resourceBucketRead(ctx, d, m) + + return diags +} + +func resourceBucketDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + p := m.(*garageProvider) + var diags diag.Diagnostics + + _, err := p.client.BucketApi.DeleteBucket(updateContext(ctx, p), d.Id()).Execute() + if err != nil { + return diag.FromErr(err) + } + + return diags +} diff --git a/garage/resource_bucket_global_alias.go b/garage/resource_bucket_global_alias.go new file mode 100644 index 0000000..fc5f5a2 --- /dev/null +++ b/garage/resource_bucket_global_alias.go @@ -0,0 +1,72 @@ +package garage + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func schemaBucketGlobalAlias() map[string]*schema.Schema { + return map[string]*schema.Schema{ + "bucket_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "alias": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + } +} + +func resourceBucketGlobalAlias() *schema.Resource { + return &schema.Resource{ + Description: "This resource can be used to manage Garage bucket global aliases.", + CreateContext: resourceBucketGlobalAliasCreate, + ReadContext: resourceBucketGlobalAliasRead, + DeleteContext: resourceBucketGlobalAliasDelete, + Schema: schemaBucketGlobalAlias(), + } +} + +func resourceBucketGlobalAliasCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + p := m.(*garageProvider) + var diags diag.Diagnostics + + bucketID := d.Get("bucket_id").(string) + alias := d.Get("alias").(string) + + _, _, err := p.client.BucketApi.PutBucketGlobalAlias(updateContext(ctx, p)).Id(bucketID).Alias(alias).Execute() + if err != nil { + return diag.FromErr(err) + } + + d.SetId(fmt.Sprintf("%s/%s", bucketID, alias)) + + return diags +} + +func resourceBucketGlobalAliasRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + var diags diag.Diagnostics + // Noop + return diags +} + +func resourceBucketGlobalAliasDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + p := m.(*garageProvider) + var diags diag.Diagnostics + + bucketID := d.Get("bucket_id").(string) + alias := d.Get("alias").(string) + + _, _, err := p.client.BucketApi.DeleteBucketGlobalAlias(updateContext(ctx, p)).Id(bucketID).Alias(alias).Execute() + if err != nil { + return diag.FromErr(err) + } + + return diags +} diff --git a/garage/resource_bucket_key.go b/garage/resource_bucket_key.go new file mode 100644 index 0000000..da045d9 --- /dev/null +++ b/garage/resource_bucket_key.go @@ -0,0 +1,125 @@ +package garage + +import ( + "context" + "fmt" + + garage "git.deuxfleurs.fr/garage-sdk/garage-admin-sdk-golang" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func schemaBucketKey() map[string]*schema.Schema { + return map[string]*schema.Schema{ + "bucket_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "access_key_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "read": { + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + "write": { + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + "owner": { + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + } +} + +func resourceBucketKey() *schema.Resource { + return &schema.Resource{ + Description: "This resource can be used to manage Garage bucket global aliases.", + CreateContext: resourceBucketKeyCreateOrUpdate, + ReadContext: resourceBucketKeyRead, + UpdateContext: resourceBucketKeyCreateOrUpdate, + DeleteContext: resourceBucketKeyDelete, + Schema: schemaBucketKey(), + } +} + +func resourceBucketKeyCreateOrUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + p := m.(*garageProvider) + var diags diag.Diagnostics + + bucketID := d.Get("bucket_id").(string) + accessKeyID := d.Get("access_key_id").(string) + read := d.Get("read").(bool) + write := d.Get("write").(bool) + owner := d.Get("owner").(bool) + + allowBucketKeyRequest := garage.AllowBucketKeyRequest{ + BucketId: bucketID, + AccessKeyId: accessKeyID, + Permissions: garage.AllowBucketKeyRequestPermissions{ + Read: read, + Write: write, + Owner: owner, + }, + } + denyBucketKeyRequest := garage.AllowBucketKeyRequest{ + BucketId: bucketID, + AccessKeyId: accessKeyID, + Permissions: garage.AllowBucketKeyRequestPermissions{ + Read: !read, + Write: !write, + Owner: !owner, + }, + } + + _, _, err := p.client.BucketApi.AllowBucketKey(updateContext(ctx, p)).AllowBucketKeyRequest(allowBucketKeyRequest).Execute() + if err != nil { + return diag.FromErr(err) + } + _, _, err = p.client.BucketApi.DenyBucketKey(updateContext(ctx, p)).AllowBucketKeyRequest(denyBucketKeyRequest).Execute() + if err != nil { + return diag.FromErr(err) + } + + d.SetId(fmt.Sprintf("%s/%s", bucketID, accessKeyID)) + + return diags +} + +func resourceBucketKeyRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + var diags diag.Diagnostics + // Noop + return diags +} + +func resourceBucketKeyDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + p := m.(*garageProvider) + var diags diag.Diagnostics + + bucketID := d.Get("bucket_id").(string) + accessKeyID := d.Get("access_key_id").(string) + + denyBucketKeyRequest := garage.AllowBucketKeyRequest{ + BucketId: bucketID, + AccessKeyId: accessKeyID, + Permissions: garage.AllowBucketKeyRequestPermissions{ + Read: true, + Write: true, + Owner: true, + }, + } + + _, _, err := p.client.BucketApi.DenyBucketKey(updateContext(ctx, p)).AllowBucketKeyRequest(denyBucketKeyRequest).Execute() + if err != nil { + return diag.FromErr(err) + } + + return diags +} diff --git a/garage/resource_bucket_local_alias.go b/garage/resource_bucket_local_alias.go new file mode 100644 index 0000000..cb1acb4 --- /dev/null +++ b/garage/resource_bucket_local_alias.go @@ -0,0 +1,79 @@ +package garage + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func schemaBucketLocalAlias() map[string]*schema.Schema { + return map[string]*schema.Schema{ + "bucket_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "access_key_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "alias": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + } +} + +func resourceBucketLocalAlias() *schema.Resource { + return &schema.Resource{ + Description: "This resource can be used to manage Garage bucket global aliases.", + CreateContext: resourceBucketLocalAliasCreate, + ReadContext: resourceBucketLocalAliasRead, + DeleteContext: resourceBucketLocalAliasDelete, + Schema: schemaBucketLocalAlias(), + } +} + +func resourceBucketLocalAliasCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + p := m.(*garageProvider) + var diags diag.Diagnostics + + bucketID := d.Get("bucket_id").(string) + accessKeyID := d.Get("access_key_id").(string) + alias := d.Get("alias").(string) + + _, _, err := p.client.BucketApi.PutBucketLocalAlias(updateContext(ctx, p)).Id(bucketID).AccessKeyId(accessKeyID).Alias(alias).Execute() + if err != nil { + return diag.FromErr(err) + } + + d.SetId(fmt.Sprintf("%s/%s/%s", bucketID, accessKeyID, alias)) + + return diags +} + +func resourceBucketLocalAliasRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + var diags diag.Diagnostics + // Noop + return diags +} + +func resourceBucketLocalAliasDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + p := m.(*garageProvider) + var diags diag.Diagnostics + + bucketID := d.Get("bucket_id").(string) + accessKeyID := d.Get("access_key_id").(string) + alias := d.Get("alias").(string) + + _, _, err := p.client.BucketApi.DeleteBucketLocalAlias(updateContext(ctx, p)).Id(bucketID).AccessKeyId(accessKeyID).Alias(alias).Execute() + if err != nil { + return diag.FromErr(err) + } + + return diags +} diff --git a/garage/resource_key.go b/garage/resource_key.go new file mode 100644 index 0000000..1457a65 --- /dev/null +++ b/garage/resource_key.go @@ -0,0 +1,224 @@ +package garage + +import ( + "context" + + garage "git.deuxfleurs.fr/garage-sdk/garage-admin-sdk-golang" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func schemaKey() map[string]*schema.Schema { + return map[string]*schema.Schema{ + // Properties + "name": { + Description: "The name of the key.", + Type: schema.TypeString, + Computed: true, + Optional: true, + }, + "access_key_id": { + Type: schema.TypeString, + Computed: true, + Optional: true, + ForceNew: true, + }, + "secret_access_key": { + Type: schema.TypeString, + Computed: true, + Optional: true, + Sensitive: true, + ForceNew: true, + }, + "permissions": { + Type: schema.TypeMap, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeBool, + }, + Default: map[string]bool{ + "create_bucket": false, + }, + }, + // Computed + // TODO: buckets + } +} + +func resourceKey() *schema.Resource { + return &schema.Resource{ + Description: "This resource can be used to manage Garage keys.", + CreateContext: resourceKeyCreate, + ReadContext: resourceKeyRead, + UpdateContext: resourceKeyUpdate, + DeleteContext: resourceKeyDelete, + Schema: schemaKey(), + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + } +} + +func flattenKeyInfo(keyInfo *garage.KeyInfo) interface{} { + return map[string]interface{}{ + "name": keyInfo.Name, + "access_key_id": keyInfo.AccessKeyId, + "secret_access_key": keyInfo.SecretAccessKey, + "permissions": map[string]interface{}{ + "create_bucket": keyInfo.Permissions.CreateBucket, + }, + } +} + +func resourceKeyCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + p := m.(*garageProvider) + var diags diag.Diagnostics + + var name *string + name = nil + accessKeyID := "" + secretAccessKey := "" + + if nameVal, ok := d.GetOk("name"); ok { + nameVal := nameVal.(string) + name = &nameVal + } + + if accessKeyIDVal, ok := d.GetOk("access_key_id"); ok { + accessKeyID = accessKeyIDVal.(string) + } + + if secretAccessKeyVal, ok := d.GetOk("access_key_id"); ok { + secretAccessKey = secretAccessKeyVal.(string) + } + + var keyInfo *garage.KeyInfo + + if accessKeyID != "" || secretAccessKey != "" { + importKeyRequest := *garage.NewImportKeyRequest(*name, accessKeyID, secretAccessKey) + resp, _, err := p.client.KeyApi.ImportKey(updateContext(ctx, p)).ImportKeyRequest(importKeyRequest).Execute() + if err != nil { + return diag.FromErr(err) + } + keyInfo = resp + } else { + addKeyRequest := *garage.NewAddKeyRequest() + addKeyRequest.Name = name + resp, _, err := p.client.KeyApi.AddKey(updateContext(ctx, p)).AddKeyRequest(addKeyRequest).Execute() + if err != nil { + return diag.FromErr(err) + } + keyInfo = resp + } + + d.SetId(*keyInfo.AccessKeyId) + + if permissions, ok := d.GetOk("permissions"); ok { + permissions := permissions.(map[string]interface{}) + + allowCreateBucket := permissions["create_bucket"].(bool) + denyCreateBucket := !permissions["create_bucket"].(bool) + + allow := garage.UpdateKeyRequestAllow{ + CreateBucket: &allowCreateBucket, + } + deny := garage.UpdateKeyRequestDeny{ + CreateBucket: &denyCreateBucket, + } + + updateKeyRequest := garage.UpdateKeyRequest{ + Allow: &allow, + Deny: &deny, + } + + _, _, err := p.client.KeyApi.UpdateKey(updateContext(ctx, p), d.Id()).UpdateKeyRequest(updateKeyRequest).Execute() + if err != nil { + return diag.FromErr(err) + } + } + + diags = resourceKeyRead(ctx, d, m) + + return diags +} + +func resourceKeyRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + p := m.(*garageProvider) + var diags diag.Diagnostics + + accessKeyID := d.Id() + + keyInfo, _, err := p.client.KeyApi.GetKey(updateContext(ctx, p), accessKeyID).Execute() + if err != nil { + return diag.FromErr(err) + } + + for key, value := range flattenKeyInfo(keyInfo).(map[string]interface{}) { + err := d.Set(key, value) + if err != nil { + return diag.FromErr(err) + } + } + + return diags +} + +func resourceKeyUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + p := m.(*garageProvider) + var diags diag.Diagnostics + + var name *string + name = nil + var allow *garage.UpdateKeyRequestAllow + allow = nil + var deny *garage.UpdateKeyRequestDeny + deny = nil + + if nameVal, ok := d.GetOk("name"); ok { + nameVal := nameVal.(string) + name = &nameVal + } + + if permissions, ok := d.GetOk("permissions"); ok { + permissions := permissions.(map[string]interface{}) + + allowCreateBucket := permissions["create_bucket"].(bool) + denyCreateBucket := !permissions["create_bucket"].(bool) + + allow = &garage.UpdateKeyRequestAllow{ + CreateBucket: &allowCreateBucket, + } + deny = &garage.UpdateKeyRequestDeny{ + CreateBucket: &denyCreateBucket, + } + } + + updateKeyRequest := garage.UpdateKeyRequest{ + Name: name, + Allow: allow, + Deny: deny, + } + + _, _, err := p.client.KeyApi.UpdateKey(updateContext(ctx, p), d.Id()).UpdateKeyRequest(updateKeyRequest).Execute() + if err != nil { + return diag.FromErr(err) + } + + diags = resourceKeyRead(ctx, d, m) + + return diags +} + +func resourceKeyDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + p := m.(*garageProvider) + var diags diag.Diagnostics + + accessKeyID := d.Id() + + _, err := p.client.KeyApi.DeleteKey(updateContext(ctx, p), accessKeyID).Execute() + if err != nil { + return diag.FromErr(err) + } + + return diags +} diff --git a/go.mod b/go.mod index 408c347..63354ab 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.19 require ( git.deuxfleurs.fr/garage-sdk/garage-admin-sdk-golang v0.0.0-20221113145120-d012cff7c554 github.com/hashicorp/terraform-plugin-sdk/v2 v2.24.1 + github.com/thoas/go-funk v0.9.2 ) require ( diff --git a/go.sum b/go.sum index 915d8a1..1c7fff6 100644 --- a/go.sum +++ b/go.sum @@ -190,6 +190,8 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/thoas/go-funk v0.9.2 h1:oKlNYv0AY5nyf9g+/GhMgS/UO2ces0QRdPKwkhY3VCk= +github.com/thoas/go-funk v0.9.2/go.mod h1:+IWnUfUmFO1+WVYQWQtIJHeRRdaIyyYglZN7xzUPe4Q= github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI= github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk=