diff --git a/CHANGELOG.md b/CHANGELOG.md index 182d81f34..9c558c599 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ See updating [Changelog example here](https://keepachangelog.com/en/1.0.0/) ## [Unreleased] +### Added + +- upcloud_managed_object_storage_static_site: new resource for Managed Object Storage static website configuration + ## [5.36.0] - 2026-03-20 ### Added diff --git a/internal/service/managedobjectstorage/custom_domain.go b/internal/service/managedobjectstorage/custom_domain.go index a097f02c1..f49ace7e2 100644 --- a/internal/service/managedobjectstorage/custom_domain.go +++ b/internal/service/managedobjectstorage/custom_domain.go @@ -46,6 +46,7 @@ type customDomainModel struct { DomainName types.String `tfsdk:"domain_name"` ID types.String `tfsdk:"id"` ServiceUUID types.String `tfsdk:"service_uuid"` + Mode types.String `tfsdk:"mode"` Type types.String `tfsdk:"type"` } @@ -77,6 +78,15 @@ func (r *managedObjectStorageCustomDomainResource) Schema(_ context.Context, _ r stringvalidator.OneOf("public"), }, }, + "mode": schema.StringAttribute{ + MarkdownDescription: "Routing mode for the domain. Defaults to `api`.", + Optional: true, + Computed: true, + Default: stringdefault.StaticString("api"), + Validators: []validator.String{ + stringvalidator.OneOf("api", "static-website"), + }, + }, }, } } @@ -93,14 +103,22 @@ func (r *managedObjectStorageCustomDomainResource) Create(ctx context.Context, r serviceUUID, err := uuid.Parse(data.ServiceUUID.ValueString()) if err != nil { - resp.Diagnostics.AddError("Invalid service UUID", utils.ErrorDiagnosticDetail(err)) + resp.Diagnostics.AddError( + "Unable to parse service UUID", + utils.ErrorDiagnosticDetail(err), + ) return } - apiResp, err := r.client.AttachObjectStorageCustomDomainWithResponse(ctx, serviceUUID, v9.ObjectStorage2CustomDomainCreate{ - DomainName: data.DomainName.ValueString(), - Type: v9.ObjectStorage2CustomDomainCreateType(data.Type.ValueString()), - }) + apiResp, err := r.client.AttachObjectStorageCustomDomainWithResponse( + ctx, + serviceUUID, + v9.AttachObjectStorageCustomDomainJSONRequestBody{ + DomainName: data.DomainName.ValueString(), + Type: v9.ObjectStorage2CustomDomainCreateType(data.Type.ValueString()), + Mode: (*v9.ObjectStorage2CustomDomainCreateMode)(data.Mode.ValueStringPointer()), + }, + ) if err != nil { resp.Diagnostics.AddError( "Unable to create managed object storage custom domain", @@ -132,21 +150,28 @@ func (r *managedObjectStorageCustomDomainResource) Read(ctx context.Context, req return } - var serviceUUIDStr, domainName string - resp.Diagnostics.Append(utils.UnmarshalIDDiag(data.ID.ValueString(), &serviceUUIDStr, &domainName)...) + var serviceUUID, domainName string + resp.Diagnostics.Append(utils.UnmarshalIDDiag(data.ID.ValueString(), &serviceUUID, &domainName)...) if resp.Diagnostics.HasError() { return } - serviceUUID, err := uuid.Parse(serviceUUIDStr) + data.ServiceUUID = types.StringValue(serviceUUID) + parsedUUID, err := uuid.Parse(serviceUUID) if err != nil { - resp.Diagnostics.AddError("Invalid service UUID", utils.ErrorDiagnosticDetail(err)) + resp.Diagnostics.AddError( + "Unable to parse service UUID", + utils.ErrorDiagnosticDetail(err), + ) return } - data.ServiceUUID = types.StringValue(serviceUUIDStr) - apiResp, err := r.client.GetObjectStorageCustomDomainWithResponse(ctx, serviceUUID, domainName) + apiResp, err := r.client.GetObjectStorageCustomDomainWithResponse( + ctx, + parsedUUID, + domainName, + ) if err != nil { resp.Diagnostics.AddError( "Unable to read managed object storage custom domain details", @@ -173,6 +198,9 @@ func (r *managedObjectStorageCustomDomainResource) Read(ctx context.Context, req if customDomain.Type != nil { data.Type = types.StringValue(*customDomain.Type) } + if customDomain.Mode != nil { + data.Mode = types.StringValue(string(*customDomain.Mode)) + } resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } @@ -185,25 +213,36 @@ func (r *managedObjectStorageCustomDomainResource) Update(ctx context.Context, r return } - var serviceUUIDStr, domainName string - resp.Diagnostics.Append(utils.UnmarshalIDDiag(state.ID.ValueString(), &serviceUUIDStr, &domainName)...) + var serviceUUID, domainName string + resp.Diagnostics.Append(utils.UnmarshalIDDiag(state.ID.ValueString(), &serviceUUID, &domainName)...) if resp.Diagnostics.HasError() { return } - serviceUUID, err := uuid.Parse(serviceUUIDStr) + parsedUUID, err := uuid.Parse(serviceUUID) if err != nil { - resp.Diagnostics.AddError("Invalid service UUID", utils.ErrorDiagnosticDetail(err)) + resp.Diagnostics.AddError( + "Unable to parse service UUID", + utils.ErrorDiagnosticDetail(err), + ) return } - newDomainName := data.DomainName.ValueString() - newType := v9.ObjectStorage2CustomDomainModifyType(data.Type.ValueString()) - apiResp, err := r.client.ModifyObjectStorageCustomDomainWithResponse(ctx, serviceUUID, domainName, v9.ObjectStorage2CustomDomainModify{ - DomainName: &newDomainName, - Type: &newType, - }) + body := v9.ModifyObjectStorageCustomDomainJSONRequestBody{ + DomainName: data.DomainName.ValueStringPointer(), + } + + if data.Type.ValueString() != "" { + t := v9.ObjectStorage2CustomDomainModifyType(data.Type.ValueString()) + body.Type = &t + } + + apiResp, err := r.client.ModifyObjectStorageCustomDomainWithResponse(ctx, + parsedUUID, + domainName, + body, + ) if err != nil { resp.Diagnostics.AddError( "Unable to modify managed object storage custom domain", @@ -227,6 +266,9 @@ func (r *managedObjectStorageCustomDomainResource) Update(ctx context.Context, r if customDomain.Type != nil { data.Type = types.StringValue(*customDomain.Type) } + if customDomain.Mode != nil { + data.Mode = types.StringValue(string(*customDomain.Mode)) + } resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } @@ -234,26 +276,32 @@ func (r *managedObjectStorageCustomDomainResource) Delete(ctx context.Context, r var data customDomainModel resp.Diagnostics.Append(req.State.Get(ctx, &data)...) - var serviceUUIDStr, domainName string - resp.Diagnostics.Append(utils.UnmarshalIDDiag(data.ID.ValueString(), &serviceUUIDStr, &domainName)...) + var serviceUUID, domainName string + resp.Diagnostics.Append(utils.UnmarshalIDDiag(data.ID.ValueString(), &serviceUUID, &domainName)...) if resp.Diagnostics.HasError() { return } - serviceUUID, err := uuid.Parse(serviceUUIDStr) + parsedUUID, err := uuid.Parse(serviceUUID) if err != nil { - resp.Diagnostics.AddError("Invalid service UUID", utils.ErrorDiagnosticDetail(err)) + resp.Diagnostics.AddError( + "Unable to parse service UUID", + utils.ErrorDiagnosticDetail(err), + ) return } - apiResp, err := r.client.DeleteObjectStorageCustomDomainWithResponse(ctx, serviceUUID, domainName) + apiResp, err := r.client.DeleteObjectStorageCustomDomainWithResponse( + ctx, + parsedUUID, + domainName, + ) if err != nil { resp.Diagnostics.AddError( "Unable to delete managed object storage custom domain", utils.ErrorDiagnosticDetail(err), ) - return } if apiResp.StatusCode() != http.StatusNoContent { resp.Diagnostics.AddError( diff --git a/internal/service/managedobjectstorage/static_site.go b/internal/service/managedobjectstorage/static_site.go new file mode 100644 index 000000000..b601c428f --- /dev/null +++ b/internal/service/managedobjectstorage/static_site.go @@ -0,0 +1,488 @@ +package managedobjectstorage + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + + "github.com/UpCloudLtd/terraform-provider-upcloud/internal/utils" + v9 "github.com/UpCloudLtd/upcloud-go-api/v9/pkg/upcloud" + "github.com/google/uuid" + "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +var ( + _ resource.Resource = &managedObjectStorageStaticSiteResource{} + _ resource.ResourceWithConfigure = &managedObjectStorageStaticSiteResource{} + _ resource.ResourceWithImportState = &managedObjectStorageStaticSiteResource{} +) + +func NewStaticSiteResource() resource.Resource { + return &managedObjectStorageStaticSiteResource{} +} + +type managedObjectStorageStaticSiteResource struct { + client *v9.ClientWithResponses +} + +func (r *managedObjectStorageStaticSiteResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_managed_object_storage_static_site" +} + +// Configure adds the provider configured client to the resource. +func (r *managedObjectStorageStaticSiteResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + r.client, resp.Diagnostics = utils.GetV9ClientFromProviderData(req.ProviderData) +} + +type staticSiteModel struct { + DomainName types.String `tfsdk:"domain_name"` + ID types.String `tfsdk:"id"` + ServiceUUID types.String `tfsdk:"service_uuid"` + BucketName types.String `tfsdk:"bucket_name"` + BucketPrefix types.String `tfsdk:"bucket_prefix"` + IndexDocument types.String `tfsdk:"index_document"` + SpaMode types.Bool `tfsdk:"spa_mode"` + Enabled types.Bool `tfsdk:"enabled"` + ErrorPages types.List `tfsdk:"error_page"` +} + +type errorPageModel struct { + StatusCode types.Int64 `tfsdk:"status_code"` + StatusRangeStart types.Int64 `tfsdk:"status_range_start"` + StatusRangeEnd types.Int64 `tfsdk:"status_range_end"` + ErrorDocument types.String `tfsdk:"error_document"` +} + +func (r *managedObjectStorageStaticSiteResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "This resource represents an UpCloud Managed Object Storage static site.", + Attributes: map[string]schema.Attribute{ + "domain_name": schema.StringAttribute{ + MarkdownDescription: "A custom domain in `static-website` mode attached to the service.", + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "id": schema.StringAttribute{ + Description: "ID of the custom domain. ID is in {object storage UUID}/{domain name} format.", + Computed: true, + }, + "service_uuid": schema.StringAttribute{ + Description: "Managed Object Storage service UUID.", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "bucket_name": schema.StringAttribute{ + MarkdownDescription: "S3 bucket from which static content is served. Must have a public read policy.", + Required: true, + }, + "bucket_prefix": schema.StringAttribute{ + MarkdownDescription: "Path prefix within the bucket. For example, `dist/` serves content from the `dist/` folder. Defaults to empty string (bucket root).", + Optional: true, + Computed: true, + }, + "index_document": schema.StringAttribute{ + Description: "Name of the index document. Defaults to `index.html`.", + Optional: true, + Computed: true, + }, + "spa_mode": schema.BoolAttribute{ + Description: "Whether to enable single-page application mode. In SPA mode, the index document is served for all paths, which is useful for client-side routing.", + Optional: true, + Computed: true, + }, + "enabled": schema.BoolAttribute{ + Description: "Enable or disable serving content on this domain. Defaults to true.", + Optional: true, + Computed: true, + }, + }, + Blocks: map[string]schema.Block{ + "error_page": schema.ListNestedBlock{ + MarkdownDescription: "Custom error pages served when the storage backend returns an error status.", + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "status_code": schema.Int64Attribute{ + Description: "Exact HTTP status code to match. Mutually exclusive with status range.", + Optional: true, + }, + "status_range_start": schema.Int64Attribute{ + Description: "Start of the status code range (inclusive).", + Optional: true, + }, + "status_range_end": schema.Int64Attribute{ + Description: "End of the status code range (inclusive). Must be greater than start.", + Optional: true, + }, + "error_document": schema.StringAttribute{ + Description: "Path to the error page document within the bucket.", + Required: true, + }, + }, + }, + Validators: []validator.List{ + listvalidator.SizeBetween(0, 25), + }, + }, + }, + } +} + +func errorPageType() types.ObjectType { + return types.ObjectType{AttrTypes: map[string]attr.Type{ + "status_code": types.Int64Type, + "status_range_start": types.Int64Type, + "status_range_end": types.Int64Type, + "error_document": types.StringType, + }} +} + +func parseErrorPages(ctx context.Context, list types.List) ([]v9.ObjectStorage2StaticWebsiteErrorPage, error) { + if list.IsNull() || list.IsUnknown() { + return nil, nil + } + + var pages []errorPageModel + if diags := list.ElementsAs(ctx, &pages, false); diags.HasError() { + return nil, fmt.Errorf("failed to decode error_page block") + } + + result := make([]v9.ObjectStorage2StaticWebsiteErrorPage, 0, len(pages)) + for _, page := range pages { + p := v9.ObjectStorage2StaticWebsiteErrorPage{ + ErrorDocument: page.ErrorDocument.ValueString(), + } + + if !page.StatusCode.IsNull() && !page.StatusCode.IsUnknown() { + v := int(page.StatusCode.ValueInt64()) + p.StatusCode = &v + } + + if !page.StatusRangeStart.IsNull() && !page.StatusRangeStart.IsUnknown() && !page.StatusRangeEnd.IsNull() && !page.StatusRangeEnd.IsUnknown() { + p.StatusRange = &struct { + End int `json:"end"` + Start int `json:"start"` + }{ + Start: int(page.StatusRangeStart.ValueInt64()), + End: int(page.StatusRangeEnd.ValueInt64()), + } + } + + result = append(result, p) + } + + return result, nil +} + +func setStaticSiteValues(ctx context.Context, data *staticSiteModel, site *v9.ObjectStorage2StaticWebsiteConfig) diag.Diagnostics { + var diags diag.Diagnostics + + data.DomainName = types.StringValue(site.DomainName) + data.BucketName = types.StringValue(site.BucketName) + data.BucketPrefix = types.StringValue(site.BucketPrefix) + data.IndexDocument = types.StringValue(site.IndexDocument) + data.Enabled = types.BoolValue(site.Enabled) + data.SpaMode = types.BoolPointerValue(site.SpaMode) + + pages := make([]errorPageModel, 0, len(site.ErrorPages)) + for _, page := range site.ErrorPages { + item := errorPageModel{ + ErrorDocument: types.StringValue(page.ErrorDocument), + StatusCode: types.Int64Null(), + StatusRangeStart: types.Int64Null(), + StatusRangeEnd: types.Int64Null(), + } + + if page.StatusCode != nil { + item.StatusCode = types.Int64Value(int64(*page.StatusCode)) + } + + if page.StatusRange != nil { + item.StatusRangeStart = types.Int64Value(int64(page.StatusRange.Start)) + item.StatusRangeEnd = types.Int64Value(int64(page.StatusRange.End)) + } + + pages = append(pages, item) + } + + errorPages, pageDiags := types.ListValueFrom(ctx, errorPageType(), pages) + diags.Append(pageDiags...) + data.ErrorPages = errorPages + + return diags +} + +func parseServiceUUID(raw string) (uuid.UUID, error) { + return uuid.Parse(raw) +} + +func diagUnexpectedStatus(respDiags *diag.Diagnostics, action string, status int, body []byte) { + respDiags.AddError( + fmt.Sprintf("Unable to %s managed object storage static site", action), + fmt.Sprintf("Unexpected API status code %d: %s", status, string(body)), + ) +} + +func (r *managedObjectStorageStaticSiteResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data staticSiteModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + svcUUID, err := parseServiceUUID(data.ServiceUUID.ValueString()) + if err != nil { + resp.Diagnostics.AddError( + "Unable to parse service UUID", + utils.ErrorDiagnosticDetail(err), + ) + return + } + + errorPages, err := parseErrorPages(ctx, data.ErrorPages) + if err != nil { + resp.Diagnostics.AddError( + "Unable to parse static site error pages", + utils.ErrorDiagnosticDetail(err), + ) + return + } + + body := v9.CreateStaticWebsiteJSONRequestBody{ + BucketName: data.BucketName.ValueString(), + } + + if !data.DomainName.IsNull() && !data.DomainName.IsUnknown() && data.DomainName.ValueString() != "" { + body.DomainName = data.DomainName.ValueStringPointer() + } + if !data.BucketPrefix.IsNull() && !data.BucketPrefix.IsUnknown() { + body.BucketPrefix = data.BucketPrefix.ValueStringPointer() + } + if !data.IndexDocument.IsNull() && !data.IndexDocument.IsUnknown() { + body.IndexDocument = data.IndexDocument.ValueStringPointer() + } + if !data.SpaMode.IsNull() && !data.SpaMode.IsUnknown() { + body.SpaMode = data.SpaMode.ValueBoolPointer() + } + if !data.Enabled.IsNull() && !data.Enabled.IsUnknown() { + body.Enabled = data.Enabled.ValueBoolPointer() + } + if errorPages != nil { + body.ErrorPages = &errorPages + } + + created, err := r.client.CreateStaticWebsiteWithResponse(ctx, svcUUID, body) + if err != nil { + resp.Diagnostics.AddError( + "Unable to create managed object storage static site", + utils.ErrorDiagnosticDetail(err), + ) + return + } + + if created.StatusCode() != http.StatusCreated { + diagUnexpectedStatus(&resp.Diagnostics, "create", created.StatusCode(), created.Body) + return + } + + var dest v9.ObjectStorage2CreateStaticWebsite201 + err = json.Unmarshal(created.Body, &dest) + if err != nil { + resp.Diagnostics.AddError( + "Unable to read created managed object storage static site", + utils.ErrorDiagnosticDetail(err), + ) + return + } + created.JSON201 = &dest + + data.ID = types.StringValue(utils.MarshalID(data.ServiceUUID.ValueString(), created.JSON201.DomainName)) + resp.Diagnostics.Append(setStaticSiteValues(ctx, &data, created.JSON201)...) + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *managedObjectStorageStaticSiteResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data staticSiteModel + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + if data.ID.ValueString() == "" { + resp.State.RemoveResource(ctx) + return + } + + var serviceUUID, domainName string + resp.Diagnostics.Append(utils.UnmarshalIDDiag(data.ID.ValueString(), &serviceUUID, &domainName)...) + + if resp.Diagnostics.HasError() { + return + } + + data.ServiceUUID = types.StringValue(serviceUUID) + svcUUID, err := parseServiceUUID(serviceUUID) + if err != nil { + resp.Diagnostics.AddError( + "Unable to parse service UUID", + utils.ErrorDiagnosticDetail(err), + ) + return + } + + site, err := r.client.GetStaticWebsiteWithResponse(ctx, svcUUID, domainName) + if err != nil { + resp.Diagnostics.AddError( + "Unable to read managed object storage static site details", + utils.ErrorDiagnosticDetail(err), + ) + return + } + + if site.StatusCode() == http.StatusNotFound { + resp.State.RemoveResource(ctx) + return + } + + if site.StatusCode() != http.StatusOK || site.JSON200 == nil { + diagUnexpectedStatus(&resp.Diagnostics, "read", site.StatusCode(), site.Body) + return + } + + resp.Diagnostics.Append(setStaticSiteValues(ctx, &data, site.JSON200)...) + data.ID = types.StringValue(utils.MarshalID(serviceUUID, data.DomainName.ValueString())) + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *managedObjectStorageStaticSiteResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var data, state staticSiteModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + + if resp.Diagnostics.HasError() { + return + } + + var serviceUUID, domainName string + resp.Diagnostics.Append(utils.UnmarshalIDDiag(state.ID.ValueString(), &serviceUUID, &domainName)...) + + if resp.Diagnostics.HasError() { + return + } + + svcUUID, err := parseServiceUUID(serviceUUID) + if err != nil { + resp.Diagnostics.AddError( + "Unable to parse service UUID", + utils.ErrorDiagnosticDetail(err), + ) + return + } + + errorPages, err := parseErrorPages(ctx, data.ErrorPages) + if err != nil { + resp.Diagnostics.AddError( + "Unable to parse static site error pages", + utils.ErrorDiagnosticDetail(err), + ) + return + } + + body := v9.ModifyStaticWebsiteJSONRequestBody{} + if !data.BucketName.IsNull() && !data.BucketName.IsUnknown() { + body.BucketName = data.BucketName.ValueStringPointer() + } + if !data.BucketPrefix.IsNull() && !data.BucketPrefix.IsUnknown() { + body.BucketPrefix = data.BucketPrefix.ValueStringPointer() + } + if !data.IndexDocument.IsNull() && !data.IndexDocument.IsUnknown() { + body.IndexDocument = data.IndexDocument.ValueStringPointer() + } + if !data.SpaMode.IsNull() && !data.SpaMode.IsUnknown() { + body.SpaMode = data.SpaMode.ValueBoolPointer() + } + if !data.Enabled.IsNull() && !data.Enabled.IsUnknown() { + body.Enabled = data.Enabled.ValueBoolPointer() + } + if errorPages != nil { + body.ErrorPages = &errorPages + } + + updated, err := r.client.ModifyStaticWebsiteWithResponse(ctx, svcUUID, domainName, body) + if err != nil { + resp.Diagnostics.AddError( + "Unable to modify managed object storage static site", + utils.ErrorDiagnosticDetail(err), + ) + return + } + + if updated.StatusCode() == http.StatusNotFound { + resp.State.RemoveResource(ctx) + return + } + + if updated.StatusCode() != http.StatusOK || updated.JSON200 == nil { + diagUnexpectedStatus(&resp.Diagnostics, "modify", updated.StatusCode(), updated.Body) + return + } + + resp.Diagnostics.Append(setStaticSiteValues(ctx, &data, updated.JSON200)...) + data.ID = types.StringValue(utils.MarshalID(data.ServiceUUID.ValueString(), data.DomainName.ValueString())) + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *managedObjectStorageStaticSiteResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var data staticSiteModel + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + var serviceUUID, domainName string + resp.Diagnostics.Append(utils.UnmarshalIDDiag(data.ID.ValueString(), &serviceUUID, &domainName)...) + + if resp.Diagnostics.HasError() { + return + } + + svcUUID, err := parseServiceUUID(serviceUUID) + if err != nil { + resp.Diagnostics.AddError( + "Unable to parse service UUID", + utils.ErrorDiagnosticDetail(err), + ) + return + } + + deleted, err := r.client.DeleteStaticWebsiteWithResponse(ctx, svcUUID, domainName) + if err != nil { + resp.Diagnostics.AddError( + "Unable to delete managed object storage static site", + utils.ErrorDiagnosticDetail(err), + ) + return + } + + if deleted.StatusCode() != http.StatusNoContent && deleted.StatusCode() != http.StatusNotFound { + diagUnexpectedStatus(&resp.Diagnostics, "delete", deleted.StatusCode(), deleted.Body) + } +} + +func (r *managedObjectStorageStaticSiteResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} diff --git a/subcategories.json b/subcategories.json index 2f3c1b5ae..cef0a6f80 100644 --- a/subcategories.json +++ b/subcategories.json @@ -37,6 +37,7 @@ "managed_object_storage_bucket.md": "Object Storage", "managed_object_storage_custom_domain.md": "Object Storage", "managed_object_storage_policy.md": "Object Storage", + "managed_object_storage_static_site.md": "Object Storage", "managed_object_storage_user.md": "Object Storage", "managed_object_storage_user_access_key.md": "Object Storage", "managed_object_storage_user_policy.md": "Object Storage", diff --git a/upcloud/managedobjectstorage/resource_upcloud_managed_object_storage_static_site_test.go b/upcloud/managedobjectstorage/resource_upcloud_managed_object_storage_static_site_test.go new file mode 100644 index 000000000..0be6fb3f4 --- /dev/null +++ b/upcloud/managedobjectstorage/resource_upcloud_managed_object_storage_static_site_test.go @@ -0,0 +1,64 @@ +package managedobjectstoragetests + +import ( + "testing" + + "github.com/UpCloudLtd/terraform-provider-upcloud/internal/utils" + "github.com/UpCloudLtd/terraform-provider-upcloud/upcloud" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +const ( + objsto = "upcloud_managed_object_storage.this" + bucket = "upcloud_managed_object_storage_bucket.this" + staticSite = "upcloud_managed_object_storage_static_site.this" +) + +func TestAccUpcloudManagedObjectStorageStaticSite(t *testing.T) { + testDataS1 := utils.ReadTestDataFile(t, "../testdata/upcloud_managed_object_storage/managed_object_storage_static_site_s1.tf") + testDataS2 := utils.ReadTestDataFile(t, "../testdata/upcloud_managed_object_storage/managed_object_storage_static_site_s2.tf") + staticSiteDomainName := "" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { upcloud.TestAccPreCheck(t) }, + ProtoV6ProviderFactories: upcloud.TestAccProviderFactories, + Steps: []resource.TestStep{ + { + Config: testDataS1, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(objsto, "configured_status", "started"), + resource.TestCheckResourceAttr(bucket, "name", "website"), + resource.TestCheckResourceAttr(staticSite, "bucket_name", "website"), + resource.TestCheckResourceAttr(staticSite, "bucket_prefix", ""), + resource.TestCheckResourceAttr(staticSite, "index_document", "index.html"), + resource.TestCheckResourceAttr(staticSite, "spa_mode", "false"), + resource.TestCheckResourceAttr(staticSite, "enabled", "true"), + resource.TestCheckResourceAttrSet(staticSite, "domain_name"), + upcloud.CheckStringDoesNotChange(staticSite, "domain_name", &staticSiteDomainName), + ), + }, + { + Config: testDataS1, + PlanOnly: true, + }, + { + Config: testDataS1, + ResourceName: staticSite, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testDataS2, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(staticSite, "bucket_prefix", "public/"), + resource.TestCheckResourceAttr(staticSite, "spa_mode", "true"), + resource.TestCheckResourceAttr(staticSite, "enabled", "true"), + upcloud.CheckStringDoesNotChange(staticSite, "domain_name", &staticSiteDomainName), + resource.TestCheckResourceAttr(staticSite, "error_page.#", "1"), + resource.TestCheckResourceAttr(staticSite, "error_page.0.error_document", "errors/404.html"), + resource.TestCheckResourceAttr(staticSite, "error_page.0.status_code", "404"), + ), + }, + }, + }) +} diff --git a/upcloud/managedobjectstorage/resource_upcloud_managed_object_storage_test.go b/upcloud/managedobjectstorage/resource_upcloud_managed_object_storage_test.go index 09f8968f1..10b7ce58c 100644 --- a/upcloud/managedobjectstorage/resource_upcloud_managed_object_storage_test.go +++ b/upcloud/managedobjectstorage/resource_upcloud_managed_object_storage_test.go @@ -130,6 +130,7 @@ func TestAccUpcloudManagedObjectStorage_CustomDomain(t *testing.T) { Config: testDataS1, Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr(customDomain, "domain_name", "objects.example.com"), + resource.TestCheckResourceAttr(customDomain, "mode", "static-website"), ), }, { @@ -142,6 +143,7 @@ func TestAccUpcloudManagedObjectStorage_CustomDomain(t *testing.T) { Config: testDataS2, Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr(customDomain, "domain_name", "obj.example.com"), + resource.TestCheckResourceAttr(customDomain, "mode", "static-website"), ), ConfigPlanChecks: resource.ConfigPlanChecks{ PreApply: []plancheck.PlanCheck{ diff --git a/upcloud/provider.go b/upcloud/provider.go index c4eed8170..8aca19cdd 100644 --- a/upcloud/provider.go +++ b/upcloud/provider.go @@ -195,6 +195,7 @@ func (p *upcloudProvider) Resources(_ context.Context) []func() resource.Resourc managedobjectstorage.NewManagedObjectStorageResource, managedobjectstorage.NewBucketResource, managedobjectstorage.NewCustomDomainResource, + managedobjectstorage.NewStaticSiteResource, managedobjectstorage.NewPolicyResource, managedobjectstorage.NewUserResource, managedobjectstorage.NewUserAccessKeyResource, diff --git a/upcloud/testdata/upcloud_managed_object_storage/managed_object_storage_custom_domain_s1.tf b/upcloud/testdata/upcloud_managed_object_storage/managed_object_storage_custom_domain_s1.tf index 476fc8aa3..68c7734b0 100644 --- a/upcloud/testdata/upcloud_managed_object_storage/managed_object_storage_custom_domain_s1.tf +++ b/upcloud/testdata/upcloud_managed_object_storage/managed_object_storage_custom_domain_s1.tf @@ -28,4 +28,5 @@ resource "upcloud_managed_object_storage" "this" { resource "upcloud_managed_object_storage_custom_domain" "this" { service_uuid = upcloud_managed_object_storage.this.id domain_name = "objects.example.com" + mode = "static-website" } diff --git a/upcloud/testdata/upcloud_managed_object_storage/managed_object_storage_custom_domain_s2.tf b/upcloud/testdata/upcloud_managed_object_storage/managed_object_storage_custom_domain_s2.tf index fa26c5110..5e49da91f 100644 --- a/upcloud/testdata/upcloud_managed_object_storage/managed_object_storage_custom_domain_s2.tf +++ b/upcloud/testdata/upcloud_managed_object_storage/managed_object_storage_custom_domain_s2.tf @@ -29,4 +29,5 @@ resource "upcloud_managed_object_storage" "this" { resource "upcloud_managed_object_storage_custom_domain" "this" { service_uuid = upcloud_managed_object_storage.this.id domain_name = "obj.example.com" + mode = "static-website" } diff --git a/upcloud/testdata/upcloud_managed_object_storage/managed_object_storage_static_site_s1.tf b/upcloud/testdata/upcloud_managed_object_storage/managed_object_storage_static_site_s1.tf new file mode 100644 index 000000000..9ac55bdcb --- /dev/null +++ b/upcloud/testdata/upcloud_managed_object_storage/managed_object_storage_static_site_s1.tf @@ -0,0 +1,36 @@ +variable "prefix" { + default = "tf-acc-test-objstov2-static-site-" + type = string +} + +variable "region" { + default = "europe-3" + type = string +} + +resource "upcloud_managed_object_storage" "this" { + name = "${var.prefix}static" + region = var.region + configured_status = "started" + + network { + family = "IPv4" + name = "public" + type = "public" + } +} + +resource "upcloud_managed_object_storage_bucket" "this" { + service_uuid = upcloud_managed_object_storage.this.id + name = "website" +} + +resource "upcloud_managed_object_storage_static_site" "this" { + service_uuid = upcloud_managed_object_storage.this.id + bucket_name = upcloud_managed_object_storage_bucket.this.name + bucket_prefix = "" + index_document = "index.html" + spa_mode = false + enabled = true +} + diff --git a/upcloud/testdata/upcloud_managed_object_storage/managed_object_storage_static_site_s2.tf b/upcloud/testdata/upcloud_managed_object_storage/managed_object_storage_static_site_s2.tf new file mode 100644 index 000000000..ace1a62de --- /dev/null +++ b/upcloud/testdata/upcloud_managed_object_storage/managed_object_storage_static_site_s2.tf @@ -0,0 +1,41 @@ +variable "prefix" { + default = "tf-acc-test-objstov2-static-site-" + type = string +} + +variable "region" { + default = "europe-3" + type = string +} + +resource "upcloud_managed_object_storage" "this" { + name = "${var.prefix}static" + region = var.region + configured_status = "started" + + network { + family = "IPv4" + name = "public" + type = "public" + } +} + +resource "upcloud_managed_object_storage_bucket" "this" { + service_uuid = upcloud_managed_object_storage.this.id + name = "website" +} + +resource "upcloud_managed_object_storage_static_site" "this" { + service_uuid = upcloud_managed_object_storage.this.id + bucket_name = upcloud_managed_object_storage_bucket.this.name + bucket_prefix = "public/" + index_document = "index.html" + spa_mode = true + enabled = true + + error_page { + status_code = 404 + error_document = "errors/404.html" + } +} +