Skip to content

Commit 7cc20fa

Browse files
authored
feat: add objsto_bucket_cors_configuration resource (#16)
Implements #6
1 parent 82cbd84 commit 7cc20fa

File tree

7 files changed

+402
-1
lines changed

7 files changed

+402
-1
lines changed

.github/workflows/test.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ jobs:
4747
- run: go mod download
4848
- env:
4949
TF_ACC: "1"
50+
TEST_TARGET: UpCloud
5051
OBJSTO_ACCESS_KEY: ${{ secrets.UPCLOUD_ACCESS_KEY }}
5152
OBJSTO_SECRET_KEY: ${{ secrets.UPCLOUD_SECRET_KEY }}
5253
OBJSTO_ENDPOINT: ${{ secrets.UPCLOUD_ENDPOINT }}
@@ -71,6 +72,7 @@ jobs:
7172
- run: go mod download
7273
- env:
7374
TF_ACC: "1"
75+
TEST_TARGET: Minio
7476
OBJSTO_ACCESS_KEY: access_key
7577
OBJSTO_SECRET_KEY: secret_key
7678
OBJSTO_ENDPOINT: http://localhost:9000
@@ -98,6 +100,7 @@ jobs:
98100
- run: go mod download
99101
- env:
100102
TF_ACC: "1"
103+
TEST_TARGET: moto
101104
OBJSTO_ACCESS_KEY: access_key
102105
OBJSTO_SECRET_KEY: secret_key
103106
OBJSTO_ENDPOINT: http://localhost:5000

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ See updating [Changelog example here](https://keepachangelog.com/en/1.0.0/)
55

66
## [Unreleased]
77

8+
### Added
9+
10+
- objsto_bucket_cors_configuration resource for configuring CORS settings for buckets.
11+
812
## [0.1.1]
913

1014
### Fixed
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
resource "objsto_bucket" "example" {
2+
bucket = "example"
3+
}
4+
5+
resource "objsto_bucket_cors_configuration" "this" {
6+
bucket = objsto_bucket.example.bucket
7+
8+
cors_rule {
9+
allowed_headers = ["*"]
10+
allowed_methods = ["GET", "HEAD", "DELETE", "PUT", "POST"]
11+
allowed_origins = ["*"]
12+
expose_headers = ["x-amz-server-side-encryption"]
13+
max_age_seconds = 3000
14+
}
15+
}
Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
package provider
2+
3+
import (
4+
"context"
5+
"errors"
6+
7+
awshttp "github.com/aws/aws-sdk-go-v2/aws/transport/http"
8+
"github.com/aws/aws-sdk-go-v2/service/s3"
9+
s3_types "github.com/aws/aws-sdk-go-v2/service/s3/types"
10+
"github.com/hashicorp/terraform-plugin-framework-validators/listvalidator"
11+
"github.com/hashicorp/terraform-plugin-framework/diag"
12+
"github.com/hashicorp/terraform-plugin-framework/path"
13+
"github.com/hashicorp/terraform-plugin-framework/resource"
14+
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
15+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
16+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
17+
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
18+
"github.com/hashicorp/terraform-plugin-framework/types"
19+
)
20+
21+
// Ensure provider defined types fully satisfy framework interfaces.
22+
var _ resource.Resource = &BucketCORSConfigurationResource{}
23+
var _ resource.ResourceWithImportState = &BucketCORSConfigurationResource{}
24+
25+
func NewBucketCORSConfigurationResource() resource.Resource {
26+
return &BucketCORSConfigurationResource{}
27+
}
28+
29+
// BucketCORSConfigurationResource defines the resource implementation.
30+
type BucketCORSConfigurationResource struct {
31+
client *s3.Client
32+
}
33+
34+
// BucketCORSConfigurationResourceModel describes the resource data model.
35+
type BucketCORSConfigurationResourceModel struct {
36+
Bucket types.String `tfsdk:"bucket"`
37+
Rules types.List `tfsdk:"cors_rule"`
38+
}
39+
40+
type CORSRule struct {
41+
AllowedHeaders types.Set `tfsdk:"allowed_headers"`
42+
AllowedMethods types.Set `tfsdk:"allowed_methods"`
43+
AllowedOrigins types.Set `tfsdk:"allowed_origins"`
44+
ExposeHeaders types.Set `tfsdk:"expose_headers"`
45+
ID types.String `tfsdk:"id"`
46+
MaxAgeSeconds types.Int32 `tfsdk:"max_age_seconds"`
47+
}
48+
49+
func (r *BucketCORSConfigurationResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
50+
resp.TypeName = req.ProviderTypeName + "_bucket_cors_configuration"
51+
}
52+
53+
func (r *BucketCORSConfigurationResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
54+
resp.Schema = schema.Schema{
55+
MarkdownDescription: "A bucket CORS configuration resource. Note that there can only be one CORS configuration per bucket.",
56+
Attributes: map[string]schema.Attribute{
57+
"bucket": schema.StringAttribute{
58+
Required: true,
59+
MarkdownDescription: "The name of the bucket for which to configure the CORS.",
60+
PlanModifiers: []planmodifier.String{
61+
stringplanmodifier.UseStateForUnknown(),
62+
stringplanmodifier.RequiresReplace(),
63+
},
64+
},
65+
},
66+
Blocks: map[string]schema.Block{
67+
"cors_rule": schema.ListNestedBlock{
68+
MarkdownDescription: "A CORS rule to apply to the bucket.",
69+
Validators: []validator.List{
70+
listvalidator.SizeAtLeast(1),
71+
},
72+
NestedObject: schema.NestedBlockObject{
73+
Attributes: map[string]schema.Attribute{
74+
"allowed_headers": schema.SetAttribute{
75+
Optional: true,
76+
MarkdownDescription: "The headers to include in `Access-Control-Request-Headers` header.",
77+
ElementType: types.StringType,
78+
},
79+
"allowed_methods": schema.SetAttribute{
80+
Required: true,
81+
MarkdownDescription: "The allowed HTTP methods for this rule.",
82+
ElementType: types.StringType,
83+
},
84+
"allowed_origins": schema.SetAttribute{
85+
Required: true,
86+
MarkdownDescription: "The allowed origins for this rule.",
87+
ElementType: types.StringType,
88+
},
89+
"expose_headers": schema.SetAttribute{
90+
Optional: true,
91+
MarkdownDescription: "The headers to include in the `Access-Control-Expose-Headers` header.",
92+
ElementType: types.StringType,
93+
},
94+
"id": schema.StringAttribute{
95+
Optional: true,
96+
MarkdownDescription: "The identifier of the rule.",
97+
PlanModifiers: []planmodifier.String{
98+
stringplanmodifier.UseStateForUnknown(),
99+
},
100+
},
101+
"max_age_seconds": schema.Int32Attribute{
102+
Optional: true,
103+
MarkdownDescription: "The cache time in seconds.",
104+
},
105+
},
106+
},
107+
},
108+
},
109+
}
110+
}
111+
112+
func (r *BucketCORSConfigurationResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
113+
r.client, resp.Diagnostics = getClientFromProviderData(req.ProviderData)
114+
}
115+
116+
func setCORSConfigurationValues(ctx context.Context, data *BucketCORSConfigurationResourceModel, output *s3.GetBucketCorsOutput) (diags diag.Diagnostics) {
117+
var d diag.Diagnostics
118+
119+
rulesData := []CORSRule{}
120+
for _, rule := range output.CORSRules {
121+
ruleData := CORSRule{
122+
ID: types.StringPointerValue(rule.ID),
123+
MaxAgeSeconds: types.Int32PointerValue(rule.MaxAgeSeconds),
124+
}
125+
126+
ruleData.AllowedHeaders, d = types.SetValueFrom(ctx, types.StringType, rule.AllowedHeaders)
127+
diags.Append(d...)
128+
129+
ruleData.AllowedMethods, d = types.SetValueFrom(ctx, types.StringType, rule.AllowedMethods)
130+
diags.Append(d...)
131+
132+
ruleData.AllowedOrigins, d = types.SetValueFrom(ctx, types.StringType, rule.AllowedOrigins)
133+
diags.Append(d...)
134+
135+
ruleData.ExposeHeaders, d = types.SetValueFrom(ctx, types.StringType, rule.ExposeHeaders)
136+
diags.Append(d...)
137+
138+
rulesData = append(rulesData, ruleData)
139+
}
140+
141+
data.Rules, d = types.ListValueFrom(ctx, data.Rules.ElementType(ctx), rulesData)
142+
diags.Append(d...)
143+
return
144+
}
145+
146+
func (r *BucketCORSConfigurationResource) put(ctx context.Context, data *BucketCORSConfigurationResourceModel) (diags diag.Diagnostics) {
147+
var rulesData []CORSRule
148+
diags.Append(data.Rules.ElementsAs(ctx, &rulesData, false)...)
149+
150+
rules := []s3_types.CORSRule{}
151+
for _, ruleData := range rulesData {
152+
rule := s3_types.CORSRule{
153+
ID: ruleData.ID.ValueStringPointer(),
154+
MaxAgeSeconds: ruleData.MaxAgeSeconds.ValueInt32Pointer(),
155+
}
156+
157+
diags.Append(ruleData.AllowedMethods.ElementsAs(ctx, &rule.AllowedMethods, false)...)
158+
diags.Append(ruleData.AllowedOrigins.ElementsAs(ctx, &rule.AllowedOrigins, false)...)
159+
diags.Append(ruleData.AllowedHeaders.ElementsAs(ctx, &rule.AllowedHeaders, false)...)
160+
diags.Append(ruleData.ExposeHeaders.ElementsAs(ctx, &rule.ExposeHeaders, false)...)
161+
162+
rules = append(rules, rule)
163+
}
164+
165+
_, err := r.client.PutBucketCors(ctx, &s3.PutBucketCorsInput{
166+
Bucket: data.Bucket.ValueStringPointer(),
167+
CORSConfiguration: &s3_types.CORSConfiguration{
168+
CORSRules: rules,
169+
},
170+
})
171+
if err != nil {
172+
diags.AddError("Unable to create bucket CORS configuration", err.Error())
173+
}
174+
return
175+
}
176+
177+
func (r *BucketCORSConfigurationResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
178+
var data BucketCORSConfigurationResourceModel
179+
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
180+
181+
if resp.Diagnostics.HasError() {
182+
return
183+
}
184+
185+
resp.Diagnostics.Append(r.put(ctx, &data)...)
186+
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
187+
}
188+
189+
func (r *BucketCORSConfigurationResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
190+
var data BucketCORSConfigurationResourceModel
191+
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
192+
193+
if resp.Diagnostics.HasError() {
194+
return
195+
}
196+
197+
output, err := r.client.GetBucketCors(ctx, &s3.GetBucketCorsInput{
198+
Bucket: data.Bucket.ValueStringPointer(),
199+
})
200+
if err != nil {
201+
var re *awshttp.ResponseError
202+
if errors.As(err, &re) && re.HTTPStatusCode() == 404 {
203+
resp.State.RemoveResource(ctx)
204+
return
205+
}
206+
resp.Diagnostics.AddError("Unable to read bucket CORS configuration", err.Error())
207+
return
208+
}
209+
210+
resp.Diagnostics.Append(setCORSConfigurationValues(ctx, &data, output)...)
211+
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
212+
}
213+
214+
func (r *BucketCORSConfigurationResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
215+
var data BucketCORSConfigurationResourceModel
216+
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
217+
218+
if resp.Diagnostics.HasError() {
219+
return
220+
}
221+
222+
resp.Diagnostics.Append(r.put(ctx, &data)...)
223+
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
224+
}
225+
226+
func (r *BucketCORSConfigurationResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
227+
var data BucketCORSConfigurationResourceModel
228+
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
229+
230+
if resp.Diagnostics.HasError() {
231+
return
232+
}
233+
234+
_, err := r.client.DeleteBucketCors(ctx, &s3.DeleteBucketCorsInput{
235+
Bucket: data.Bucket.ValueStringPointer(),
236+
})
237+
if err != nil {
238+
resp.Diagnostics.AddError("Unable to delete bucket CORS", err.Error())
239+
}
240+
}
241+
242+
func (r *BucketCORSConfigurationResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
243+
resource.ImportStatePassthroughID(ctx, path.Root("bucket"), req, resp)
244+
}

0 commit comments

Comments
 (0)