diff --git a/internal/service/loadbalancer/frontend_rule.go b/internal/service/loadbalancer/frontend_rule.go index 368a38b0..7ef1e41b 100644 --- a/internal/service/loadbalancer/frontend_rule.go +++ b/internal/service/loadbalancer/frontend_rule.go @@ -2,208 +2,637 @@ package loadbalancer import ( "context" + "fmt" "github.com/UpCloudLtd/terraform-provider-upcloud/internal/utils" + "github.com/UpCloudLtd/upcloud-go-api/v8/upcloud" "github.com/UpCloudLtd/upcloud-go-api/v8/upcloud/request" "github.com/UpCloudLtd/upcloud-go-api/v8/upcloud/service" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" + "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier" + "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" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" ) -func ResourceFrontendRule() *schema.Resource { - return &schema.Resource{ - Description: "This resource represents load balancer frontend rule", - CreateContext: resourceFrontendRuleCreate, - ReadContext: resourceFrontendRuleRead, - UpdateContext: resourceFrontendRuleUpdate, - DeleteContext: resourceFrontendRuleDelete, - Importer: &schema.ResourceImporter{ - StateContext: schema.ImportStatePassthroughContext, - }, - CustomizeDiff: customdiff.All( - // Validate http_redirect fields here, because ExactlyOneOf does not work when MaxItems > 1 - validateHTTPRedirectChange, - validateActionsNotEmpty, - ), - Schema: map[string]*schema.Schema{ - "frontend": { - Description: "ID of the load balancer frontend to which the rule is connected.", - Type: schema.TypeString, - Required: true, - ForceNew: true, +var ( + _ resource.Resource = &frontendRuleResource{} + _ resource.ResourceWithConfigure = &frontendRuleResource{} + _ resource.ResourceWithImportState = &frontendRuleResource{} +) + +func NewFrontendRuleResource() resource.Resource { + return &frontendRuleResource{} +} + +type frontendRuleResource struct { + client *service.Service +} + +func (r *frontendRuleResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_loadbalancer_frontend_rule" +} + +// Configure adds the provider configured client to the resource. +func (r *frontendRuleResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + r.client, resp.Diagnostics = utils.GetClientFromProviderData(req.ProviderData) +} + +type frontendRuleModel struct { + ID types.String `tfsdk:"id"` + Frontend types.String `tfsdk:"frontend"` + Name types.String `tfsdk:"name"` + Priority types.Int64 `tfsdk:"priority"` + Matchers types.List `tfsdk:"matchers"` + Actions types.List `tfsdk:"actions"` +} + +func (r *frontendRuleResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "This resource represents load balancer frontend rule.", + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + MarkdownDescription: "ID of the frontend rule. ID is in `{load balancer UUID}/{frontend name}/{frontend rule name}` format.", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "frontend": schema.StringAttribute{ + MarkdownDescription: "ID of the load balancer frontend to which the frontend rule is connected.", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, }, - "name": { - Description: "The name of the frontend rule must be unique within the load balancer service.", - Type: schema.TypeString, - Required: true, - ValidateDiagFunc: validateNameDiagFunc, + "name": schema.StringAttribute{ + MarkdownDescription: "The name of the frontend rule. Must be unique within the frontend.", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + nameValidator, + }, }, - "priority": { - Description: "Rule with the higher priority goes first. Rules with the same priority processed in alphabetical order.", - Type: schema.TypeInt, - Required: true, - ValidateDiagFunc: validation.ToDiagFunc(validation.IntBetween(0, 100)), + "priority": schema.Int64Attribute{ + MarkdownDescription: "Rule with the higher priority goes first. Rules with the same priority processed in alphabetical order.", + Required: true, + Validators: []validator.Int64{ + int64validator.Between(0, 100), + }, }, - "matchers": { - Description: "Set of rule matchers. if rule doesn't have matchers, then action applies to all incoming requests.", - Type: schema.TypeList, - Optional: true, - MaxItems: 1, - ForceNew: true, - Elem: &schema.Resource{ - Schema: frontendRuleMatchersSchema(), + }, + Blocks: map[string]schema.Block{ + "actions": schema.ListNestedBlock{ + MarkdownDescription: "Rule actions.", + NestedObject: schema.NestedBlockObject{ + Blocks: map[string]schema.Block{ + "http_redirect": schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "location": schema.StringAttribute{ + MarkdownDescription: "Target location.", + Optional: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + stringvalidator.ConflictsWith(path.MatchRelative().AtParent().AtName("scheme")), + }, + }, + "scheme": schema.StringAttribute{ + MarkdownDescription: "Target scheme.", + Optional: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.OneOf( + string(upcloud.LoadBalancerActionHTTPRedirectSchemeHTTP), + string(upcloud.LoadBalancerActionHTTPRedirectSchemeHTTPS), + ), + stringvalidator.ConflictsWith(path.MatchRelative().AtParent().AtName("location")), + }, + }, + }, + }, + }, + "http_return": schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "content_type": schema.StringAttribute{ + MarkdownDescription: "Content type.", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "status": schema.Int64Attribute{ + MarkdownDescription: "HTTP status code.", + Required: true, + PlanModifiers: []planmodifier.Int64{ + int64planmodifier.RequiresReplace(), + }, + Validators: []validator.Int64{ + int64validator.Between(100, 599), + }, + }, + "payload": schema.StringAttribute{ + MarkdownDescription: "The payload.", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.LengthBetween(1, 4096), + }, + }, + }, + }, + }, + "set_forwarded_headers": schema.ListNestedBlock{ + MarkdownDescription: "Adds 'X-Forwarded-For / -Proto / -Port' headers in your forwarded requests", + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "active": schema.BoolAttribute{ + Optional: true, + Computed: true, + Default: booldefault.StaticBool(true), + }, + }, + }, + PlanModifiers: []planmodifier.List{ + listplanmodifier.RequiresReplace(), + }, + Validators: []validator.List{ + listvalidator.SizeBetween(0, 100), + }, + }, + "tcp_reject": schema.ListNestedBlock{ + MarkdownDescription: "Terminates a connection.", + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "active": schema.BoolAttribute{ + MarkdownDescription: "Indicates if the rule is active.", + Optional: true, + Computed: true, + Default: booldefault.StaticBool(true), + PlanModifiers: []planmodifier.Bool{ + boolplanmodifier.RequiresReplace(), + }, + }, + }, + }, + PlanModifiers: []planmodifier.List{ + listplanmodifier.RequiresReplace(), + }, + Validators: []validator.List{ + listvalidator.SizeBetween(0, 100), + }, + }, + "use_backend": schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "backend_name": schema.StringAttribute{ + MarkdownDescription: "The name of the backend where traffic will be routed.", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + }, + }, + Validators: []validator.List{ + listvalidator.SizeBetween(0, 100), + }, + PlanModifiers: []planmodifier.List{ + listplanmodifier.RequiresReplace(), + }, + }, + }, + }, + PlanModifiers: []planmodifier.List{ + listplanmodifier.RequiresReplace(), + }, + Validators: []validator.List{ + listvalidator.SizeBetween(0, 1), }, }, - "actions": { - Description: "Set of rule actions.", - Type: schema.TypeList, - Optional: true, - MaxItems: 1, - ForceNew: true, - Elem: &schema.Resource{ - Schema: frontendRuleActionsSchema(), + "matchers": schema.ListNestedBlock{ + MarkdownDescription: "Set of rule matchers. If rule doesn't have matchers, then action applies to all incoming requests.", + NestedObject: schema.NestedBlockObject{ + Blocks: map[string]schema.Block{ + "body_size": schema.ListNestedBlock{ + MarkdownDescription: "Matches by HTTP request body size.", + NestedObject: frontendRuleMatcherIntegerSchema(), + PlanModifiers: []planmodifier.List{ + listplanmodifier.RequiresReplace(), + }, + Validators: []validator.List{ + listvalidator.SizeBetween(0, 100), + }, + }, + "body_size_range": schema.ListNestedBlock{ + MarkdownDescription: "Matches by range of HTTP request body sizes.", + NestedObject: frontendRuleMatcherRangeSchema(), + PlanModifiers: []planmodifier.List{ + listplanmodifier.RequiresReplace(), + }, + Validators: []validator.List{ + listvalidator.SizeBetween(0, 100), + }, + }, + "cookie": schema.ListNestedBlock{ + MarkdownDescription: "Matches by HTTP cookie value. Cookie name must be provided.", + NestedObject: frontendRuleMatcherStringWithArgumentSchema(), + PlanModifiers: []planmodifier.List{ + listplanmodifier.RequiresReplace(), + }, + Validators: []validator.List{ + listvalidator.SizeBetween(0, 100), + }, + }, + "header": schema.ListNestedBlock{ + MarkdownDescription: "Matches by HTTP header value. Header name must be provided.", + NestedObject: frontendRuleMatcherStringWithArgumentSchema(), + PlanModifiers: []planmodifier.List{ + listplanmodifier.RequiresReplace(), + }, + Validators: []validator.List{ + listvalidator.SizeBetween(0, 100), + }, + }, + "host": schema.ListNestedBlock{ + MarkdownDescription: "Matches by hostname. Header extracted from HTTP Headers or from TLS certificate in case of secured connection.", + NestedObject: frontendRuleMatcherHostSchema(), + PlanModifiers: []planmodifier.List{ + listplanmodifier.RequiresReplace(), + }, + Validators: []validator.List{ + listvalidator.SizeBetween(0, 100), + }, + }, + "http_method": schema.ListNestedBlock{ + MarkdownDescription: "Matches by HTTP method.", + NestedObject: frontendRuleMatcherHTTPMethodSchema(), + PlanModifiers: []planmodifier.List{ + listplanmodifier.RequiresReplace(), + }, + Validators: []validator.List{ + listvalidator.SizeBetween(0, 100), + }, + }, + "num_members_up": schema.ListNestedBlock{ + MarkdownDescription: "Matches by number of healthy backend members.", + NestedObject: frontendRuleMatcherBackendSchema(), + PlanModifiers: []planmodifier.List{ + listplanmodifier.RequiresReplace(), + }, + Validators: []validator.List{ + listvalidator.SizeBetween(0, 100), + }, + }, + "path": schema.ListNestedBlock{ + MarkdownDescription: "Matches by URL path.", + NestedObject: frontendRuleMatcherStringSchema(), + PlanModifiers: []planmodifier.List{ + listplanmodifier.RequiresReplace(), + }, + Validators: []validator.List{ + listvalidator.SizeBetween(0, 100), + }, + }, + "src_ip": schema.ListNestedBlock{ + MarkdownDescription: "Matches by source IP address.", + NestedObject: frontendRuleMatcherIPSchema(), + PlanModifiers: []planmodifier.List{ + listplanmodifier.RequiresReplace(), + }, + Validators: []validator.List{ + listvalidator.SizeBetween(0, 100), + }, + }, + "src_port": schema.ListNestedBlock{ + MarkdownDescription: "Matches by source port number.", + NestedObject: frontendRuleMatcherIntegerSchema(), + PlanModifiers: []planmodifier.List{ + listplanmodifier.RequiresReplace(), + }, + Validators: []validator.List{ + listvalidator.SizeBetween(0, 100), + }, + }, + "src_port_range": schema.ListNestedBlock{ + MarkdownDescription: "Matches by range of source port numbers.", + NestedObject: frontendRuleMatcherRangeSchema(), + PlanModifiers: []planmodifier.List{ + listplanmodifier.RequiresReplace(), + }, + Validators: []validator.List{ + listvalidator.SizeBetween(0, 100), + }, + }, + "url": schema.ListNestedBlock{ + MarkdownDescription: "Matches by URL without schema, e.g. `example.com/dashboard`.", + NestedObject: frontendRuleMatcherStringSchema(), + PlanModifiers: []planmodifier.List{ + listplanmodifier.RequiresReplace(), + }, + Validators: []validator.List{ + listvalidator.SizeBetween(0, 100), + }, + }, + "url_param": schema.ListNestedBlock{ + MarkdownDescription: "Matches by URL query parameter value. Query parameter name must be provided", + NestedObject: frontendRuleMatcherStringWithArgumentSchema(), + PlanModifiers: []planmodifier.List{ + listplanmodifier.RequiresReplace(), + }, + Validators: []validator.List{ + listvalidator.SizeBetween(0, 100), + }, + }, + "url_query": schema.ListNestedBlock{ + MarkdownDescription: "Matches by URL query string.", + NestedObject: frontendRuleMatcherStringSchema(), + PlanModifiers: []planmodifier.List{ + listplanmodifier.RequiresReplace(), + }, + Validators: []validator.List{ + listvalidator.SizeBetween(0, 100), + }, + }, + }, + }, + PlanModifiers: []planmodifier.List{ + listplanmodifier.RequiresReplace(), + }, + Validators: []validator.List{ + listvalidator.SizeBetween(0, 1), }, }, }, } } -func resourceFrontendRuleCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) (diags diag.Diagnostics) { - svc := meta.(*service.Service) - matchers, err := loadBalancerMatchersFromResourceData(d) +func setFrontendRuleValues(ctx context.Context, data *frontendRuleModel, frontendRule *upcloud.LoadBalancerFrontendRule, blocks map[string]schema.ListNestedBlock) diag.Diagnostics { + var respDiagnostics diag.Diagnostics + + isImport := data.Frontend.ValueString() == "" + + var loadBalancer, frontendName, name string + err := utils.UnmarshalID(data.ID.ValueString(), &loadBalancer, &frontendName, &name) if err != nil { - return diag.FromErr(err) + respDiagnostics.AddError( + "Unable to unmarshal loadbalancer frontend rule ID", + utils.ErrorDiagnosticDetail(err), + ) } - actions, err := loadBalancerActionsFromResourceData(d) - if err != nil { - return diag.FromErr(err) + data.Frontend = types.StringValue(utils.MarshalID(loadBalancer, frontendName)) + data.Name = types.StringValue(name) + data.Priority = types.Int64Value(int64(frontendRule.Priority)) + + if !data.Actions.IsNull() || isImport { + respDiagnostics.Append(setFrontendRuleActionsValues(ctx, data, frontendRule, blocks)...) + } + + if !data.Matchers.IsNull() || isImport { + respDiagnostics.Append(setFrontendRuleMatchersValues(ctx, data, frontendRule, blocks)...) } - var serviceID, feName string - if err := utils.UnmarshalID(d.Get("frontend").(string), &serviceID, &feName); err != nil { - return diag.FromErr(err) + return respDiagnostics +} + +func (r *frontendRuleResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data frontendRuleModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return } - rule, err := svc.CreateLoadBalancerFrontendRule(ctx, &request.CreateLoadBalancerFrontendRuleRequest{ - ServiceUUID: serviceID, - FrontendName: feName, + var loadbalancer, frontendName string + err := utils.UnmarshalID(data.Frontend.ValueString(), &loadbalancer, &frontendName) + if err != nil { + resp.Diagnostics.AddError( + "Unable to unmarshal loadbalancer frontend name", + utils.ErrorDiagnosticDetail(err), + ) + + return + } + + matchers, diags := buildFrontendRuleMatchers(ctx, data.Matchers) + resp.Diagnostics.Append(diags...) + + actions, diags := buildFrontendRuleActions(ctx, data.Actions) + resp.Diagnostics.Append(diags...) + + apiReq := request.CreateLoadBalancerFrontendRuleRequest{ + ServiceUUID: loadbalancer, + FrontendName: frontendName, Rule: request.LoadBalancerFrontendRule{ - Name: d.Get("name").(string), - Priority: d.Get("priority").(int), + Name: data.Name.ValueString(), + Priority: int(data.Priority.ValueInt64()), Matchers: matchers, Actions: actions, }, - }) + } + + frontendRule, err := r.client.CreateLoadBalancerFrontendRule(ctx, &apiReq) if err != nil { - return diag.FromErr(err) + resp.Diagnostics.AddError( + "Unable to create loadbalancer frontend rule", + utils.ErrorDiagnosticDetail(err), + ) + return } - d.SetId(utils.MarshalID(serviceID, feName, rule.Name)) + data.ID = types.StringValue(utils.MarshalID(data.Frontend.ValueString(), data.Name.ValueString())) - if diags = setFrontendRuleResourceData(d, rule); len(diags) > 0 { - return diags + blocks := make(map[string]schema.ListNestedBlock) + for k, v := range req.Config.Schema.GetBlocks() { + block, ok := v.(schema.ListNestedBlock) + if !ok { + continue + } + + blocks[k] = block } - tflog.Info(ctx, "frontend rule created", map[string]interface{}{"name": rule.Name, "service_uuid": serviceID, "fe_name": feName}) - return diags + resp.Diagnostics.Append(setFrontendRuleValues(ctx, &data, frontendRule, blocks)...) + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } -func resourceFrontendRuleRead(ctx context.Context, d *schema.ResourceData, meta interface{}) (diags diag.Diagnostics) { - svc := meta.(*service.Service) - var serviceID, feName, name string - if err := utils.UnmarshalID(d.Id(), &serviceID, &feName, &name); err != nil { - return diag.FromErr(err) +func (r *frontendRuleResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data frontendRuleModel + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + if data.ID.ValueString() == "" { + resp.State.RemoveResource(ctx) + + return + } + + var loadbalancer, frontendName, name string + err := utils.UnmarshalID(data.ID.ValueString(), &loadbalancer, &frontendName, &name) + if err != nil { + resp.Diagnostics.AddError( + "Unable to unmarshal loadbalancer frontend rule ID", + utils.ErrorDiagnosticDetail(err), + ) + return } - rule, err := svc.GetLoadBalancerFrontendRule(ctx, &request.GetLoadBalancerFrontendRuleRequest{ - ServiceUUID: serviceID, - FrontendName: feName, + + frontendRule, err := r.client.GetLoadBalancerFrontendRule(ctx, &request.GetLoadBalancerFrontendRuleRequest{ + FrontendName: frontendName, Name: name, + ServiceUUID: loadbalancer, }) if err != nil { - return utils.HandleResourceError(d.Get("name").(string), d, err) + if utils.IsNotFoundError(err) { + resp.State.RemoveResource(ctx) + } else { + resp.Diagnostics.AddError( + "Unable to read loadbalancer frontend rule details", + utils.ErrorDiagnosticDetail(err), + ) + } + return } - d.SetId(utils.MarshalID(serviceID, feName, rule.Name)) - - if err = d.Set("frontend", utils.MarshalID(serviceID, feName)); err != nil { - return diag.FromErr(err) - } + blocks := make(map[string]schema.ListNestedBlock) + for k, v := range req.State.Schema.GetBlocks() { + block, ok := v.(schema.ListNestedBlock) + if !ok { + continue + } - if diags = setFrontendRuleResourceData(d, rule); len(diags) > 0 { - return diags + blocks[k] = block } - return diags + resp.Diagnostics.Append(setFrontendRuleValues(ctx, &data, frontendRule, blocks)...) + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } -func resourceFrontendRuleUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) (diags diag.Diagnostics) { - svc := meta.(*service.Service) - var serviceID, feName, name string - if err := utils.UnmarshalID(d.Id(), &serviceID, &feName, &name); err != nil { - return diag.FromErr(err) +func (r *frontendRuleResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var data frontendRuleModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + + var loadBalancer, frontendName, name string + if err := utils.UnmarshalID(data.ID.ValueString(), &loadBalancer, &frontendName, &name); err != nil { + resp.Diagnostics.AddError( + "Unable to unmarshal loadbalancer frontend rule ID", + utils.ErrorDiagnosticDetail(err), + ) + return } - // name and priority fields doesn't force replacement and can be updated in-place - rule, err := svc.ModifyLoadBalancerFrontendRule(ctx, &request.ModifyLoadBalancerFrontendRuleRequest{ - ServiceUUID: serviceID, - FrontendName: feName, + + if resp.Diagnostics.HasError() { + return + } + + apiReq := request.ModifyLoadBalancerFrontendRuleRequest{ + ServiceUUID: loadBalancer, + FrontendName: frontendName, Name: name, Rule: request.ModifyLoadBalancerFrontendRule{ - Name: d.Get("name").(string), - Priority: upcloud.IntPtr(d.Get("priority").(int)), + Name: data.Name.ValueString(), + Priority: upcloud.IntPtr(int(data.Priority.ValueInt64())), }, - }, - ) + } + + frontendRule, err := r.client.ModifyLoadBalancerFrontendRule(ctx, &apiReq) if err != nil { - return diag.FromErr(err) + resp.Diagnostics.AddError( + "Unable to modify loadbalancer frontend rule", + utils.ErrorDiagnosticDetail(err), + ) + return } - d.SetId(utils.MarshalID(serviceID, feName, rule.Name)) + blocks := make(map[string]schema.ListNestedBlock) + for k, v := range req.Config.Schema.GetBlocks() { + block, ok := v.(schema.ListNestedBlock) + if !ok { + continue + } - if diags = setFrontendRuleResourceData(d, rule); len(diags) > 0 { - return diags + blocks[k] = block } - tflog.Info(ctx, "frontend rule updated", map[string]interface{}{"name": rule.Name, "service_uuid": serviceID, "fe_name": feName}) - return diags + resp.Diagnostics.Append(setFrontendRuleValues(ctx, &data, frontendRule, blocks)...) + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } -func resourceFrontendRuleDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) (diags diag.Diagnostics) { - svc := meta.(*service.Service) - var serviceID, feName, name string - if err := utils.UnmarshalID(d.Id(), &serviceID, &feName, &name); err != nil { - return diag.FromErr(err) +func (r *frontendRuleResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var data frontendRuleModel + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + var loadBalancer, frontendName, name string + if err := utils.UnmarshalID(data.ID.ValueString(), &loadBalancer, &frontendName, &name); err != nil { + resp.Diagnostics.AddError( + "Unable to unmarshal loadbalancer frontend rule ID", + utils.ErrorDiagnosticDetail(err), + ) + return } - tflog.Info(ctx, "deleting frontend rule", map[string]interface{}{"name": name, "service_uuid": serviceID, "fe_name": feName}) + if resp.Diagnostics.HasError() { + return + } - return diag.FromErr(svc.DeleteLoadBalancerFrontendRule(ctx, &request.DeleteLoadBalancerFrontendRuleRequest{ - ServiceUUID: serviceID, - FrontendName: feName, + if err := r.client.DeleteLoadBalancerFrontendRule(ctx, &request.DeleteLoadBalancerFrontendRuleRequest{ + ServiceUUID: loadBalancer, + FrontendName: frontendName, Name: name, - })) + }); err != nil { + resp.Diagnostics.AddError( + "Unable to delete loadbalancer frontend rule", + utils.ErrorDiagnosticDetail(err), + ) + } } -func setFrontendRuleResourceData(d *schema.ResourceData, rule *upcloud.LoadBalancerFrontendRule) (diags diag.Diagnostics) { - if err := d.Set("name", rule.Name); err != nil { - return diag.FromErr(err) - } +func (r *frontendRuleResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} - if err := d.Set("priority", rule.Priority); err != nil { - return diag.FromErr(err) +func elementTypesByKey(k string, blocks map[string]schema.ListNestedBlock) (map[string]basetypes.ObjectTypable, error) { + topLevel, ok := blocks[k] + if !ok { + return nil, fmt.Errorf("block by key %s not found", k) } - if err := setFrontendRuleMatchersResourceData(d, rule); err != nil { - return diag.FromErr(err) - } + elementTypes := make(map[string]basetypes.ObjectTypable) + + for blockName, b := range topLevel.NestedObject.Blocks { + block, ok := b.(schema.ListNestedBlock) + if !ok { + continue + } - if err := setFrontendRuleActionsResourceData(d, rule); err != nil { - return diag.FromErr(err) + elementTypes[blockName] = block.NestedObject.Type() } - return diags + return elementTypes, nil } diff --git a/internal/service/loadbalancer/frontend_rule_action.go b/internal/service/loadbalancer/frontend_rule_action.go new file mode 100644 index 00000000..128a425b --- /dev/null +++ b/internal/service/loadbalancer/frontend_rule_action.go @@ -0,0 +1,238 @@ +package loadbalancer + +import ( + "context" + + "github.com/UpCloudLtd/upcloud-go-api/v8/upcloud" + "github.com/UpCloudLtd/upcloud-go-api/v8/upcloud/request" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +type frontendRuleActionModel struct { + HTTPRedirect types.List `tfsdk:"http_redirect"` + HTTPReturn types.List `tfsdk:"http_return"` + SetForwardedHeaders types.List `tfsdk:"set_forwarded_headers"` + TCPReject types.List `tfsdk:"tcp_reject"` + UseBackend types.List `tfsdk:"use_backend"` +} + +type frontendRuleActionHTTPRedirectModel struct { + Location types.String `tfsdk:"location"` + Scheme types.String `tfsdk:"scheme"` +} + +type frontendRuleActionHTTPReturnModel struct { + ContentType types.String `tfsdk:"content_type"` + Status types.Int64 `tfsdk:"status"` + Payload types.String `tfsdk:"payload"` +} + +type frontendRuleActionSetForwardedHeadersModel struct { + Active types.Bool `tfsdk:"active"` +} + +type frontendRuleActionTCPRejectModel struct { + Active types.Bool `tfsdk:"active"` +} + +type frontendRuleActionUseBackendModel struct { + BackendName types.String `tfsdk:"backend_name"` +} + +func buildFrontendRuleActions(ctx context.Context, dataActions types.List) ([]upcloud.LoadBalancerAction, diag.Diagnostics) { + if dataActions.IsNull() { + return nil, nil + } + + var planActions []frontendRuleActionModel + diags := dataActions.ElementsAs(ctx, &planActions, false) + if diags.HasError() { + return nil, diags + } + + actions := make([]upcloud.LoadBalancerAction, 0) + for _, planAction := range planActions { + // Ensure set_forwarded_headers action is iterated first to maintain correct action order. + // Managed Load Balancer evaluates actions in the order they are set, but separate TF blocks can't guarantee this order. + // This isn't a major issue since all actions except set_forwarded_headers are "final" (i.e., they end the chain). + // The main use-case is having set_forwarded_headers first, followed by a "final" action. + // We work around the ordering problem by always setting set_forwarded_headers actions first. + var setForwardedHeaders []frontendRuleActionSetForwardedHeadersModel + diags = planAction.SetForwardedHeaders.ElementsAs(ctx, &setForwardedHeaders, false) + if diags.HasError() { + return nil, diags + } + + for range setForwardedHeaders { + action := request.NewLoadBalancerSetForwardedHeadersAction() + + actions = append(actions, action) + } + + var httpRedirects []frontendRuleActionHTTPRedirectModel + diags = planAction.HTTPRedirect.ElementsAs(ctx, &httpRedirects, false) + if diags.HasError() { + return nil, diags + } + + for _, httpRedirect := range httpRedirects { + var action upcloud.LoadBalancerAction + if httpRedirect.Scheme.ValueString() != "" { + action = request.NewLoadBalancerHTTPRedirectSchemeAction(upcloud.LoadBalancerActionHTTPRedirectScheme(httpRedirect.Scheme.ValueString())) + } else if httpRedirect.Location.ValueString() != "" { + action = request.NewLoadBalancerHTTPRedirectAction(httpRedirect.Location.ValueString()) + } + + actions = append(actions, action) + } + + var httpReturns []frontendRuleActionHTTPReturnModel + diags = planAction.HTTPReturn.ElementsAs(ctx, &httpReturns, false) + if diags.HasError() { + return nil, diags + } + + for _, httpReturn := range httpReturns { + action := request.NewLoadBalancerHTTPReturnAction( + int(httpReturn.Status.ValueInt64()), + httpReturn.ContentType.ValueString(), + httpReturn.Payload.ValueString(), + ) + + actions = append(actions, action) + } + + var tcpRejects []frontendRuleActionTCPRejectModel + diags = planAction.TCPReject.ElementsAs(ctx, &tcpRejects, false) + if diags.HasError() { + return nil, diags + } + + for range tcpRejects { + action := request.NewLoadBalancerTCPRejectAction() + + actions = append(actions, action) + } + + var useBackends []frontendRuleActionUseBackendModel + diags = planAction.UseBackend.ElementsAs(ctx, &useBackends, false) + if diags.HasError() { + return nil, diags + } + + for _, useBackend := range useBackends { + action := request.NewLoadBalancerUseBackendAction(useBackend.BackendName.ValueString()) + + actions = append(actions, action) + } + } + + return actions, diags +} + +func setFrontendRuleActionsValues(ctx context.Context, data *frontendRuleModel, frontendRule *upcloud.LoadBalancerFrontendRule, blocks map[string]schema.ListNestedBlock) diag.Diagnostics { + if frontendRule == nil || len(frontendRule.Actions) == 0 { + return nil + } + + var diags, respDiagnostics diag.Diagnostics + + elementTypes, err := elementTypesByKey("actions", blocks) + if err != nil { + respDiagnostics.AddError("cannot set frontend rule actions", err.Error()) + + return respDiagnostics + } + + httpRedirects := make([]frontendRuleActionHTTPRedirectModel, 0) + httpReturns := make([]frontendRuleActionHTTPReturnModel, 0) + setForwardedHeaders := make([]frontendRuleActionSetForwardedHeadersModel, 0) + tcpRejects := make([]frontendRuleActionTCPRejectModel, 0) + useBackends := make([]frontendRuleActionUseBackendModel, 0) + + for _, a := range frontendRule.Actions { + if a.HTTPRedirect != nil { + httpRedirect := frontendRuleActionHTTPRedirectModel{} + + if a.HTTPRedirect.Scheme != "" { + httpRedirect.Scheme = types.StringValue(string(a.HTTPRedirect.Scheme)) + } + + if a.HTTPRedirect.Location != "" { + httpRedirect.Location = types.StringValue(a.HTTPRedirect.Location) + } + + httpRedirects = append(httpRedirects, httpRedirect) + } + + if a.HTTPReturn != nil { + httpReturns = append(httpReturns, frontendRuleActionHTTPReturnModel{ + ContentType: types.StringValue(a.HTTPReturn.ContentType), + Status: types.Int64Value(int64(a.HTTPReturn.Status)), + Payload: types.StringValue(a.HTTPReturn.Payload), + }) + } + + if a.SetForwardedHeaders != nil { + setForwardedHeaders = append(setForwardedHeaders, frontendRuleActionSetForwardedHeadersModel{ + Active: types.BoolValue(true), + }) + } + + if a.TCPReject != nil { + tcpRejects = append(tcpRejects, frontendRuleActionTCPRejectModel{ + Active: types.BoolValue(true), + }) + } + + if a.UseBackend != nil { + useBackends = append(useBackends, frontendRuleActionUseBackendModel{ + BackendName: types.StringValue(a.UseBackend.Backend), + }) + } + } + + action := frontendRuleActionModel{} + + if elementType, ok := elementTypes["http_redirect"]; ok { + action.HTTPRedirect, diags = types.ListValueFrom(ctx, elementType, httpRedirects) + respDiagnostics.Append(diags...) + } else { + respDiagnostics.AddError("cannot set frontend rule actions", "http_redirect element type not found") + } + + if elementType, ok := elementTypes["http_return"]; ok { + action.HTTPReturn, diags = types.ListValueFrom(ctx, elementType, httpReturns) + respDiagnostics.Append(diags...) + } else { + respDiagnostics.AddError("cannot set frontend rule actions", "http_return element type not found") + } + + if elementType, ok := elementTypes["set_forwarded_headers"]; ok { + action.SetForwardedHeaders, diags = types.ListValueFrom(ctx, elementType, setForwardedHeaders) + respDiagnostics.Append(diags...) + } else { + respDiagnostics.AddError("cannot set frontend rule actions", "set_forwarded_headers element type not found") + } + + if elementType, ok := elementTypes["tcp_reject"]; ok { + action.TCPReject, diags = types.ListValueFrom(ctx, elementType, tcpRejects) + respDiagnostics.Append(diags...) + } else { + respDiagnostics.AddError("cannot set frontend rule actions", "tcp_reject element type not found") + } + + if elementType, ok := elementTypes["use_backend"]; ok { + action.UseBackend, diags = types.ListValueFrom(ctx, elementType, useBackends) + respDiagnostics.Append(diags...) + } else { + respDiagnostics.AddError("cannot set frontend rule actions", "use_backend element type not found") + } + + data.Actions, diags = types.ListValueFrom(ctx, data.Actions.ElementType(ctx), []frontendRuleActionModel{action}) + respDiagnostics.Append(diags...) + + return respDiagnostics +} diff --git a/internal/service/loadbalancer/frontend_rule_actions.go b/internal/service/loadbalancer/frontend_rule_actions.go deleted file mode 100644 index e3d89859..00000000 --- a/internal/service/loadbalancer/frontend_rule_actions.go +++ /dev/null @@ -1,264 +0,0 @@ -package loadbalancer - -import ( - "context" - "errors" - "fmt" - - "github.com/UpCloudLtd/upcloud-go-api/v8/upcloud" - "github.com/UpCloudLtd/upcloud-go-api/v8/upcloud/request" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" -) - -func frontendRuleActionsSchema() map[string]*schema.Schema { - return map[string]*schema.Schema{ - "use_backend": { - Description: "Routes traffic to specified `backend`.", - Type: schema.TypeList, - Optional: true, - MaxItems: 100, - ForceNew: true, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "backend_name": { - Description: "The name of the backend where traffic will be routed.", - Type: schema.TypeString, - Required: true, - ForceNew: true, - }, - }, - }, - }, - "http_redirect": { - Description: "Redirects HTTP requests to specified location or URL scheme. Only either location or scheme can be defined at a time.", - Type: schema.TypeList, - Optional: true, - MaxItems: 100, - ForceNew: true, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "location": { - Description: "Target location.", - Type: schema.TypeString, - Optional: true, - ForceNew: true, - ValidateDiagFunc: validation.ToDiagFunc(validation.StringIsNotEmpty), - }, - "scheme": { - Description: "Target scheme.", - Type: schema.TypeString, - Optional: true, - ForceNew: true, - ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{ - string(upcloud.LoadBalancerActionHTTPRedirectSchemeHTTP), - string(upcloud.LoadBalancerActionHTTPRedirectSchemeHTTPS), - }, false)), - }, - }, - }, - }, - "http_return": { - Description: "Returns HTTP response with specified HTTP status.", - Type: schema.TypeList, - Optional: true, - MaxItems: 100, - ForceNew: true, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "content_type": { - Description: "Content type.", - Type: schema.TypeString, - Required: true, - ForceNew: true, - }, - "status": { - Description: "HTTP status code.", - Type: schema.TypeInt, - Required: true, - ForceNew: true, - ValidateDiagFunc: validation.ToDiagFunc(validation.IntBetween(100, 599)), - }, - "payload": { - Description: "The payload.", - Type: schema.TypeString, - Required: true, - ForceNew: true, - ValidateDiagFunc: validation.ToDiagFunc(validation.StringLenBetween(1, 4096)), - }, - }, - }, - }, - "tcp_reject": { - Description: "Terminates a connection.", - Type: schema.TypeList, - Optional: true, - MaxItems: 100, - ForceNew: true, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "active": { - Type: schema.TypeBool, - Optional: true, - Default: true, - ForceNew: true, - }, - }, - }, - }, - "set_forwarded_headers": { - Description: "Adds 'X-Forwarded-For / -Proto / -Port' headers in your forwarded requests", - Type: schema.TypeList, - Optional: true, - MaxItems: 100, - ForceNew: true, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "active": { - Type: schema.TypeBool, - Optional: true, - Default: true, - ForceNew: true, - }, - }, - }, - }, - } -} - -func loadBalancerActionsFromResourceData(d *schema.ResourceData) ([]upcloud.LoadBalancerAction, error) { - a := make([]upcloud.LoadBalancerAction, 0) - if _, ok := d.GetOk("actions.0"); !ok { - return a, nil - } - - // set_forwarded_headers action has to be iterated over first to avoid issues with actions ordering. This is because Managed Load Balancer evaluates actions in the same order - // as they were set. But because each action has it's own, separate block in TF configuration, we cannot actually make sure they are ordered as the user intended. - // This is not a big issue right now because all the actions except set_forwarded_headers are "final" (i.e. they end the chain and the next action is not evaluated). - // So the only real use-case of having multiple actions is to have set_forwarded_headers action first, and then one of the "final" actions. - // Therefore we work around the ordering problem by just making sure set_forwarded_headers actions are always set first. - // TODO: Look for some more robust way of handling this when release a new major version - for range d.Get("actions.0.set_forwarded_headers").([]interface{}) { - a = append(a, request.NewLoadBalancerSetForwardedHeadersAction()) - } - - for _, v := range d.Get("actions.0.use_backend").([]interface{}) { - v := v.(map[string]interface{}) - a = append(a, request.NewLoadBalancerUseBackendAction(v["backend_name"].(string))) - } - - for _, v := range d.Get("actions.0.http_return").([]interface{}) { - v := v.(map[string]interface{}) - a = append(a, request.NewLoadBalancerHTTPReturnAction( - v["status"].(int), - v["content_type"].(string), - v["payload"].(string), - )) - } - - for i := range d.Get("actions.0.http_redirect").([]interface{}) { - key := fmt.Sprintf("actions.0.http_redirect.%d", i) - location, locationOK := d.GetOk(key + ".location") - scheme, schemeOK := d.GetOk(key + ".scheme") - if schemeOK && locationOK { - // This is also validated by CustomizeDiff in ResourceFrontendRule so execution should not enter this block - return nil, errors.New("http_redirect action can have either target location or target scheme not both") - } - if locationOK { - a = append(a, request.NewLoadBalancerHTTPRedirectAction(location.(string))) - } - - if schemeOK { - a = append(a, request.NewLoadBalancerHTTPRedirectSchemeAction(upcloud.LoadBalancerActionHTTPRedirectScheme(scheme.(string)))) - } - } - - for range d.Get("actions.0.tcp_reject").([]interface{}) { - a = append(a, request.NewLoadBalancerTCPRejectAction()) - } - - return a, nil -} - -func setFrontendRuleActionsResourceData(d *schema.ResourceData, rule *upcloud.LoadBalancerFrontendRule) error { - if len(rule.Actions) == 0 { - return d.Set("actions", nil) - } - - actions := make(map[string][]interface{}) - for _, a := range rule.Actions { - t := string(a.Type) - var v map[string]interface{} - switch a.Type { - case upcloud.LoadBalancerActionTypeUseBackend: - v = map[string]interface{}{ - "backend_name": a.UseBackend.Backend, - } - case upcloud.LoadBalancerActionTypeHTTPRedirect: - v = map[string]interface{}{ - "location": a.HTTPRedirect.Location, - "scheme": a.HTTPRedirect.Scheme, - } - case upcloud.LoadBalancerActionTypeHTTPReturn: - v = map[string]interface{}{ - "content_type": a.HTTPReturn.ContentType, - "status": a.HTTPReturn.Status, - "payload": a.HTTPReturn.Payload, - } - case upcloud.LoadBalancerActionTypeTCPReject: - v = map[string]interface{}{ - "active": true, - } - case upcloud.LoadBalancerActionTypeSetForwardedHeaders: - v = map[string]interface{}{ - "active": true, - } - default: - return fmt.Errorf("received unsupported action type '%s' %+v", a.Type, a) - } - - actions[t] = append(actions[t], v) - } - return d.Set("actions", []interface{}{actions}) -} - -func getString(m map[string]interface{}, key string) string { - raw := m[key] - val, ok := raw.(string) - if !ok { - return "" - } - return val -} - -func validateHTTPRedirectChange(_ context.Context, d *schema.ResourceDiff, _ interface{}) error { - for _, v := range d.Get("actions.0.http_redirect").([]interface{}) { - v, ok := v.(map[string]interface{}) - if !ok { - // block is likely empty and `v` thus nil - return fmt.Errorf("either location or scheme should be defined for http_redirect") - } - - location := getString(v, "location") - scheme := getString(v, "scheme") - - if location != "" && scheme != "" { - return fmt.Errorf("only either location or scheme should be defined at a time for http_redirect") - } - } - - return nil -} - -func validateActionsNotEmpty(_ context.Context, d *schema.ResourceDiff, _ interface{}) error { - actions, ok := d.Get("actions.0").(map[string]interface{}) - if !ok { - return fmt.Errorf("received actions in unknown format") - } - - if len(actions) == 0 { - return fmt.Errorf("actions block should contain at least one action") - } - - return nil -} diff --git a/internal/service/loadbalancer/frontend_rule_matcher.go b/internal/service/loadbalancer/frontend_rule_matcher.go new file mode 100644 index 00000000..b12d0c50 --- /dev/null +++ b/internal/service/loadbalancer/frontend_rule_matcher.go @@ -0,0 +1,872 @@ +package loadbalancer + +import ( + "context" + "fmt" + "strings" + + "github.com/UpCloudLtd/upcloud-go-api/v8/upcloud" + "github.com/UpCloudLtd/upcloud-go-api/v8/upcloud/request" + "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier" + "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" +) + +type frontendRuleMatcherModel struct { + BodySize types.List `tfsdk:"body_size"` + BodySizeRange types.List `tfsdk:"body_size_range"` + Cookie types.List `tfsdk:"cookie"` + Header types.List `tfsdk:"header"` + Host types.List `tfsdk:"host"` + HTTPMethod types.List `tfsdk:"http_method"` + NumMembersUp types.List `tfsdk:"num_members_up"` + Path types.List `tfsdk:"path"` + SrcIP types.List `tfsdk:"src_ip"` + SrcPort types.List `tfsdk:"src_port"` + SrcPortRange types.List `tfsdk:"src_port_range"` + URL types.List `tfsdk:"url"` + URLParam types.List `tfsdk:"url_param"` + URLQuery types.List `tfsdk:"url_query"` +} + +type frontendRuleMatcherHostModel struct { + Value types.String `tfsdk:"value"` + Inverse types.Bool `tfsdk:"inverse"` +} + +type frontendRuleMatcherHTTPMethodModel struct { + Value types.String `tfsdk:"value"` + Inverse types.Bool `tfsdk:"inverse"` +} + +type frontendRuleMatcherNumMembersUpModel struct { + Method types.String `tfsdk:"method"` + Value types.Int64 `tfsdk:"value"` + BackendName types.String `tfsdk:"backend_name"` + Inverse types.Bool `tfsdk:"inverse"` +} + +type frontendRuleMatcherSrcIPModel struct { + Value types.String `tfsdk:"value"` + Inverse types.Bool `tfsdk:"inverse"` +} + +type frontendRuleMatcherIntegerModel struct { + Method types.String `tfsdk:"method"` + Value types.Int64 `tfsdk:"value"` + Inverse types.Bool `tfsdk:"inverse"` +} + +type frontendRuleMatcherRangeModel struct { + RangeStart types.Int64 `tfsdk:"range_start"` + RangeEnd types.Int64 `tfsdk:"range_end"` + Inverse types.Bool `tfsdk:"inverse"` +} + +type frontendRuleMatcherStringWithArgumentModel struct { + Method types.String `tfsdk:"method"` + Name types.String `tfsdk:"name"` + Value types.String `tfsdk:"value"` + IgnoreCase types.Bool `tfsdk:"ignore_case"` + Inverse types.Bool `tfsdk:"inverse"` +} + +type frontendRuleMatcherStringModel struct { + Method types.String `tfsdk:"method"` + Value types.String `tfsdk:"value"` + IgnoreCase types.Bool `tfsdk:"ignore_case"` + Inverse types.Bool `tfsdk:"inverse"` +} + +func frontendRuleMatcherHTTPMethodSchema() schema.NestedBlockObject { + methods := []string{ + string(upcloud.LoadBalancerHTTPMatcherMethodGet), + string(upcloud.LoadBalancerHTTPMatcherMethodHead), + string(upcloud.LoadBalancerHTTPMatcherMethodPost), + string(upcloud.LoadBalancerHTTPMatcherMethodPut), + string(upcloud.LoadBalancerHTTPMatcherMethodPatch), + string(upcloud.LoadBalancerHTTPMatcherMethodDelete), + string(upcloud.LoadBalancerHTTPMatcherMethodConnect), + string(upcloud.LoadBalancerHTTPMatcherMethodOptions), + string(upcloud.LoadBalancerHTTPMatcherMethodTrace), + } + + return schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "value": schema.StringAttribute{ + MarkdownDescription: fmt.Sprintf("String value (`%s`).", strings.Join(methods, "`, `")), + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.OneOf(methods...), + }, + }, + "inverse": frontendRuleMatcherInverseSchema(), + }, + } +} + +func frontendRuleMatcherBackendSchema() schema.NestedBlockObject { + methods := []string{ + string(upcloud.LoadBalancerIntegerMatcherMethodEqual), + string(upcloud.LoadBalancerIntegerMatcherMethodGreater), + string(upcloud.LoadBalancerIntegerMatcherMethodGreaterOrEqual), + string(upcloud.LoadBalancerIntegerMatcherMethodLess), + string(upcloud.LoadBalancerIntegerMatcherMethodLessOrEqual), + } + + return schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "method": schema.StringAttribute{ + MarkdownDescription: fmt.Sprintf("Match method (`%s`).", strings.Join(methods, "`, `")), + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.OneOf(methods...), + }, + }, + "value": schema.Int64Attribute{ + MarkdownDescription: "Integer value.", + Required: true, + PlanModifiers: []planmodifier.Int64{ + int64planmodifier.RequiresReplace(), + }, + Validators: []validator.Int64{ + int64validator.Between(0, 100), + }, + }, + "backend_name": schema.StringAttribute{ + MarkdownDescription: "The name of the `backend`.", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.LengthBetween(1, 255), + }, + }, + "inverse": frontendRuleMatcherInverseSchema(), + }, + } +} + +func frontendRuleMatcherStringWithArgumentSchema() schema.NestedBlockObject { + methods := []string{ + string(upcloud.LoadBalancerStringMatcherMethodExact), + string(upcloud.LoadBalancerStringMatcherMethodSubstring), + string(upcloud.LoadBalancerStringMatcherMethodRegexp), + string(upcloud.LoadBalancerStringMatcherMethodStarts), + string(upcloud.LoadBalancerStringMatcherMethodEnds), + string(upcloud.LoadBalancerStringMatcherMethodDomain), + string(upcloud.LoadBalancerStringMatcherMethodIP), + string(upcloud.LoadBalancerStringMatcherMethodExists), + } + + return schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "method": schema.StringAttribute{ + MarkdownDescription: fmt.Sprintf("Match method (`%s`). Matcher with `exists` and `ip` methods must be used without `value` and `ignore_case` fields.", strings.Join(methods, "`, `")), + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.OneOf(methods...), + }, + }, + "name": schema.StringAttribute{ + MarkdownDescription: "Name of the argument.", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.LengthBetween(1, 255), + }, + }, + "value": schema.StringAttribute{ + MarkdownDescription: "String value.", + Optional: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.LengthBetween(1, 255), + }, + }, + "ignore_case": frontendRuleMatcherIgnoreCaseSchema(), + "inverse": frontendRuleMatcherInverseSchema(), + }, + } +} + +func frontendRuleMatcherIPSchema() schema.NestedBlockObject { + return schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "value": schema.StringAttribute{ + MarkdownDescription: "IP address. CIDR masks are supported, e.g. `192.168.0.0/24`.", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "inverse": frontendRuleMatcherInverseSchema(), + }, + } +} + +func frontendRuleMatcherStringSchema() schema.NestedBlockObject { + methods := []string{ + string(upcloud.LoadBalancerStringMatcherMethodExact), + string(upcloud.LoadBalancerStringMatcherMethodSubstring), + string(upcloud.LoadBalancerStringMatcherMethodRegexp), + string(upcloud.LoadBalancerStringMatcherMethodStarts), + string(upcloud.LoadBalancerStringMatcherMethodEnds), + string(upcloud.LoadBalancerStringMatcherMethodDomain), + string(upcloud.LoadBalancerStringMatcherMethodIP), + string(upcloud.LoadBalancerStringMatcherMethodExists), + } + + return schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "method": schema.StringAttribute{ + Description: fmt.Sprintf("Match method (`%s`). Matcher with `exists` and `ip` methods must be used without `value` and `ignore_case` fields.", strings.Join(methods, "`, `")), + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.OneOf(methods...), + }, + }, + "value": schema.StringAttribute{ + MarkdownDescription: "String value.", + Optional: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.LengthBetween(1, 255), + }, + }, + "ignore_case": frontendRuleMatcherIgnoreCaseSchema(), + "inverse": frontendRuleMatcherInverseSchema(), + }, + } +} + +func frontendRuleMatcherHostSchema() schema.NestedBlockObject { + return schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "value": schema.StringAttribute{ + MarkdownDescription: "String value.", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.LengthBetween(1, 255), + }, + }, + "inverse": frontendRuleMatcherInverseSchema(), + }, + } +} + +func frontendRuleMatcherIntegerSchema() schema.NestedBlockObject { + methods := []string{ + string(upcloud.LoadBalancerIntegerMatcherMethodEqual), + string(upcloud.LoadBalancerIntegerMatcherMethodGreater), + string(upcloud.LoadBalancerIntegerMatcherMethodGreaterOrEqual), + string(upcloud.LoadBalancerIntegerMatcherMethodLess), + string(upcloud.LoadBalancerIntegerMatcherMethodLessOrEqual), + } + + return schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "method": schema.StringAttribute{ + MarkdownDescription: fmt.Sprintf("Match method (`%s`).", strings.Join(methods, "`, `")), + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.OneOf(methods...), + }, + }, + "value": schema.Int64Attribute{ + MarkdownDescription: "Integer value.", + Required: true, + PlanModifiers: []planmodifier.Int64{ + int64planmodifier.RequiresReplace(), + }, + }, + "inverse": frontendRuleMatcherInverseSchema(), + }, + } +} + +func frontendRuleMatcherRangeSchema() schema.NestedBlockObject { + return schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "range_start": schema.Int64Attribute{ + MarkdownDescription: "Integer value.", + Required: true, + PlanModifiers: []planmodifier.Int64{ + int64planmodifier.RequiresReplace(), + }, + }, + "range_end": schema.Int64Attribute{ + MarkdownDescription: "Integer value.", + Required: true, + PlanModifiers: []planmodifier.Int64{ + int64planmodifier.RequiresReplace(), + }, + }, + "inverse": frontendRuleMatcherInverseSchema(), + }, + } +} + +func frontendRuleMatcherInverseSchema() schema.BoolAttribute { + return schema.BoolAttribute{ + MarkdownDescription: "Defines if the condition should be inverted. Works similarly to logical NOT operator.", + Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), + PlanModifiers: []planmodifier.Bool{ + boolplanmodifier.RequiresReplace(), + }, + } +} + +func frontendRuleMatcherIgnoreCaseSchema() schema.BoolAttribute { + return schema.BoolAttribute{ + MarkdownDescription: "Defines if case should be ignored. Defaults to `false`.", + Optional: true, + Computed: true, + Default: booldefault.StaticBool(false), + PlanModifiers: []planmodifier.Bool{ + boolplanmodifier.RequiresReplace(), + }, + } +} + +func buildFrontendRuleMatchers(ctx context.Context, dataMatchers types.List) ([]upcloud.LoadBalancerMatcher, diag.Diagnostics) { + if dataMatchers.IsNull() { + return nil, nil + } + + var planMatchers []frontendRuleMatcherModel + diags := dataMatchers.ElementsAs(ctx, &planMatchers, false) + if diags.HasError() { + return nil, diags + } + + matchers := make([]upcloud.LoadBalancerMatcher, 0) + for _, planMatcher := range planMatchers { + var bodySizes []frontendRuleMatcherIntegerModel + diags = planMatcher.BodySize.ElementsAs(ctx, &bodySizes, false) + if diags.HasError() { + return nil, diags + } + + for _, bodySize := range bodySizes { + matcher := request.NewLoadBalancerBodySizeMatcher( + upcloud.LoadBalancerIntegerMatcherMethod(bodySize.Method.ValueString()), + int(bodySize.Value.ValueInt64()), + ) + matchers = appendMatcher(matchers, matcher, bodySize.Inverse.ValueBool()) + } + + var bodySizeRanges []frontendRuleMatcherRangeModel + diags = planMatcher.BodySizeRange.ElementsAs(ctx, &bodySizeRanges, false) + if diags.HasError() { + return nil, diags + } + + for _, bodySizeRange := range bodySizeRanges { + matcher := request.NewLoadBalancerBodySizeRangeMatcher( + int(bodySizeRange.RangeStart.ValueInt64()), + int(bodySizeRange.RangeEnd.ValueInt64()), + ) + matchers = appendMatcher(matchers, matcher, bodySizeRange.Inverse.ValueBool()) + } + + var cookies []frontendRuleMatcherStringWithArgumentModel + diags = planMatcher.Cookie.ElementsAs(ctx, &cookies, false) + if diags.HasError() { + return nil, diags + } + + for _, cookie := range cookies { + matcher := request.NewLoadBalancerCookieMatcher( + upcloud.LoadBalancerStringMatcherMethod(cookie.Method.ValueString()), + cookie.Name.ValueString(), + cookie.Value.ValueString(), + cookie.IgnoreCase.ValueBoolPointer(), + ) + matchers = appendMatcher(matchers, matcher, cookie.Inverse.ValueBool()) + } + + var headers []frontendRuleMatcherStringWithArgumentModel + diags = planMatcher.Header.ElementsAs(ctx, &headers, false) + if diags.HasError() { + return nil, diags + } + + for _, header := range headers { + matcher := request.NewLoadBalancerHeaderMatcher( + upcloud.LoadBalancerStringMatcherMethod(header.Method.ValueString()), + header.Name.ValueString(), + header.Value.ValueString(), + header.IgnoreCase.ValueBoolPointer(), + ) + matchers = appendMatcher(matchers, matcher, header.Inverse.ValueBool()) + } + + var hosts []frontendRuleMatcherHostModel + diags = planMatcher.Host.ElementsAs(ctx, &hosts, false) + if diags.HasError() { + return nil, diags + } + + for _, host := range hosts { + matcher := request.NewLoadBalancerHostMatcher( + host.Value.ValueString(), + ) + matchers = appendMatcher(matchers, matcher, host.Inverse.ValueBool()) + } + + var httpMethods []frontendRuleMatcherHTTPMethodModel + diags = planMatcher.HTTPMethod.ElementsAs(ctx, &httpMethods, false) + if diags.HasError() { + return nil, diags + } + + for _, httpMethod := range httpMethods { + matcher := request.NewLoadBalancerHTTPMethodMatcher( + upcloud.LoadBalancerHTTPMatcherMethod(httpMethod.Value.ValueString()), + ) + matchers = appendMatcher(matchers, matcher, httpMethod.Inverse.ValueBool()) + } + + var numMembersUp []frontendRuleMatcherNumMembersUpModel + diags = planMatcher.NumMembersUp.ElementsAs(ctx, &numMembersUp, false) + if diags.HasError() { + return nil, diags + } + + for _, numMembers := range numMembersUp { + matcher := request.NewLoadBalancerNumMembersUpMatcher( + upcloud.LoadBalancerIntegerMatcherMethod(numMembers.Method.ValueString()), + int(numMembers.Value.ValueInt64()), + numMembers.BackendName.ValueString(), + ) + matchers = appendMatcher(matchers, matcher, numMembers.Inverse.ValueBool()) + } + + var paths []frontendRuleMatcherStringModel + diags = planMatcher.Path.ElementsAs(ctx, &paths, false) + if diags.HasError() { + return nil, diags + } + + for _, pathMatcher := range paths { + matcher := request.NewLoadBalancerPathMatcher( + upcloud.LoadBalancerStringMatcherMethod(pathMatcher.Method.ValueString()), + pathMatcher.Value.ValueString(), + pathMatcher.IgnoreCase.ValueBoolPointer(), + ) + matchers = appendMatcher(matchers, matcher, pathMatcher.Inverse.ValueBool()) + } + + var srcIPs []frontendRuleMatcherSrcIPModel + diags = planMatcher.SrcIP.ElementsAs(ctx, &srcIPs, false) + if diags.HasError() { + return nil, diags + } + + for _, srcIP := range srcIPs { + matcher := request.NewLoadBalancerSrcIPMatcher( + srcIP.Value.ValueString(), + ) + matchers = appendMatcher(matchers, matcher, srcIP.Inverse.ValueBool()) + } + + var srcPorts []frontendRuleMatcherIntegerModel + diags = planMatcher.SrcPort.ElementsAs(ctx, &srcPorts, false) + if diags.HasError() { + return nil, diags + } + + for _, srcPort := range srcPorts { + matcher := request.NewLoadBalancerSrcPortMatcher( + upcloud.LoadBalancerIntegerMatcherMethod(srcPort.Method.ValueString()), + int(srcPort.Value.ValueInt64()), + ) + matchers = appendMatcher(matchers, matcher, srcPort.Inverse.ValueBool()) + } + + var srcPortRanges []frontendRuleMatcherRangeModel + diags = planMatcher.SrcPortRange.ElementsAs(ctx, &srcPortRanges, false) + if diags.HasError() { + return nil, diags + } + + for _, srcPortRange := range srcPortRanges { + matcher := request.NewLoadBalancerSrcPortRangeMatcher( + int(srcPortRange.RangeStart.ValueInt64()), + int(srcPortRange.RangeEnd.ValueInt64()), + ) + matchers = appendMatcher(matchers, matcher, srcPortRange.Inverse.ValueBool()) + } + + var urls []frontendRuleMatcherStringModel + diags = planMatcher.URL.ElementsAs(ctx, &urls, false) + if diags.HasError() { + return nil, diags + } + + for _, url := range urls { + matcher := request.NewLoadBalancerURLMatcher( + upcloud.LoadBalancerStringMatcherMethod(url.Method.ValueString()), + url.Value.ValueString(), + url.IgnoreCase.ValueBoolPointer(), + ) + matchers = appendMatcher(matchers, matcher, url.Inverse.ValueBool()) + } + + var urlParams []frontendRuleMatcherStringWithArgumentModel + diags = planMatcher.URLParam.ElementsAs(ctx, &urlParams, false) + if diags.HasError() { + return nil, diags + } + + for _, urlParam := range urlParams { + matcher := request.NewLoadBalancerURLParamMatcher( + upcloud.LoadBalancerStringMatcherMethod(urlParam.Method.ValueString()), + urlParam.Name.ValueString(), + urlParam.Value.ValueString(), + urlParam.IgnoreCase.ValueBoolPointer(), + ) + matchers = appendMatcher(matchers, matcher, urlParam.Inverse.ValueBool()) + } + + var urlQueries []frontendRuleMatcherStringModel + diags = planMatcher.URLQuery.ElementsAs(ctx, &urlQueries, false) + if diags.HasError() { + return nil, diags + } + + for _, urlQuery := range urlQueries { + matcher := request.NewLoadBalancerURLQueryMatcher( + upcloud.LoadBalancerStringMatcherMethod(urlQuery.Method.ValueString()), + urlQuery.Value.ValueString(), + urlQuery.IgnoreCase.ValueBoolPointer(), + ) + matchers = appendMatcher(matchers, matcher, urlQuery.Inverse.ValueBool()) + } + } + + return matchers, diags +} + +func setFrontendRuleMatchersValues(ctx context.Context, data *frontendRuleModel, frontendRule *upcloud.LoadBalancerFrontendRule, blocks map[string]schema.ListNestedBlock) diag.Diagnostics { + var diags, respDiagnostics diag.Diagnostics + + elementTypes, err := elementTypesByKey("matchers", blocks) + if err != nil { + respDiagnostics.AddError("cannot set frontend rule matchers", err.Error()) + + return respDiagnostics + } + + bodySizes := make([]frontendRuleMatcherIntegerModel, 0) + bodySizeRanges := make([]frontendRuleMatcherRangeModel, 0) + cookies := make([]frontendRuleMatcherStringWithArgumentModel, 0) + headers := make([]frontendRuleMatcherStringWithArgumentModel, 0) + hosts := make([]frontendRuleMatcherHostModel, 0) + httpMethods := make([]frontendRuleMatcherHTTPMethodModel, 0) + numMembersUp := make([]frontendRuleMatcherNumMembersUpModel, 0) + paths := make([]frontendRuleMatcherStringModel, 0) + srcIPs := make([]frontendRuleMatcherSrcIPModel, 0) + srcPorts := make([]frontendRuleMatcherIntegerModel, 0) + srcPortRanges := make([]frontendRuleMatcherRangeModel, 0) + urls := make([]frontendRuleMatcherStringModel, 0) + urlParams := make([]frontendRuleMatcherStringWithArgumentModel, 0) + urlQueries := make([]frontendRuleMatcherStringModel, 0) + + for _, m := range frontendRule.Matchers { + if m.BodySize != nil { + if m.BodySize.Method == upcloud.LoadBalancerIntegerMatcherMethodRange { + bodySizeRanges = append(bodySizeRanges, frontendRuleMatcherRangeModel{ + Inverse: types.BoolValue(*m.Inverse), + RangeEnd: types.Int64Value(int64(m.BodySize.RangeEnd)), + RangeStart: types.Int64Value(int64(m.BodySize.RangeStart)), + }) + } else { + bodySizes = append(bodySizes, frontendRuleMatcherIntegerModel{ + Method: types.StringValue(string(m.BodySize.Method)), + Inverse: types.BoolValue(*m.Inverse), + Value: types.Int64Value(int64(m.BodySize.Value)), + }) + } + } + + if m.Cookie != nil { + var ignoreCase bool + if m.Cookie.IgnoreCase != nil { + ignoreCase = *m.Cookie.IgnoreCase + } + cookies = append(cookies, frontendRuleMatcherStringWithArgumentModel{ + IgnoreCase: types.BoolValue(ignoreCase), + Inverse: types.BoolValue(*m.Inverse), + Method: types.StringValue(string(m.Cookie.Method)), + Name: types.StringValue(m.Cookie.Name), + Value: types.StringValue(m.Cookie.Value), + }) + } + + if m.Header != nil { + var ignoreCase bool + if m.Header.IgnoreCase != nil { + ignoreCase = *m.Header.IgnoreCase + } + headers = append(headers, frontendRuleMatcherStringWithArgumentModel{ + IgnoreCase: types.BoolValue(ignoreCase), + Inverse: types.BoolValue(*m.Inverse), + Method: types.StringValue(string(m.Header.Method)), + Name: types.StringValue(m.Header.Name), + Value: types.StringValue(m.Header.Value), + }) + } + + if m.Host != nil { + hosts = append(hosts, frontendRuleMatcherHostModel{ + Inverse: types.BoolValue(*m.Inverse), + Value: types.StringValue(m.Host.Value), + }) + } + + if m.HTTPMethod != nil { + httpMethods = append(httpMethods, frontendRuleMatcherHTTPMethodModel{ + Inverse: types.BoolValue(*m.Inverse), + Value: types.StringValue(string(m.HTTPMethod.Value)), + }) + } + + if m.NumMembersUp != nil { + numMembersUp = append(numMembersUp, frontendRuleMatcherNumMembersUpModel{ + BackendName: types.StringValue(m.NumMembersUp.Backend), + Inverse: types.BoolValue(*m.Inverse), + Method: types.StringValue(string(m.NumMembersUp.Method)), + Value: types.Int64Value(int64(m.NumMembersUp.Value)), + }) + } + + if m.Path != nil { + var ignoreCase bool + if m.Path.IgnoreCase != nil { + ignoreCase = *m.Path.IgnoreCase + } + paths = append(paths, frontendRuleMatcherStringModel{ + IgnoreCase: types.BoolValue(ignoreCase), + Inverse: types.BoolValue(*m.Inverse), + Method: types.StringValue(string(m.Path.Method)), + Value: types.StringValue(m.Path.Value), + }) + } + + if m.SrcIP != nil { + srcIPs = append(srcIPs, frontendRuleMatcherSrcIPModel{ + Inverse: types.BoolValue(*m.Inverse), + Value: types.StringValue(m.SrcIP.Value), + }) + } + + if m.SrcPort != nil { + if m.SrcPort.Method == upcloud.LoadBalancerIntegerMatcherMethodRange { + srcPortRanges = append(srcPortRanges, frontendRuleMatcherRangeModel{ + Inverse: types.BoolValue(*m.Inverse), + RangeEnd: types.Int64Value(int64(m.SrcPort.RangeEnd)), + RangeStart: types.Int64Value(int64(m.SrcPort.RangeStart)), + }) + } else { + srcPorts = append(srcPorts, frontendRuleMatcherIntegerModel{ + Inverse: types.BoolValue(*m.Inverse), + Method: types.StringValue(string(m.SrcPort.Method)), + Value: types.Int64Value(int64(m.SrcPort.Value)), + }) + } + } + + if m.URL != nil { + var ignoreCase bool + if m.URL.IgnoreCase != nil { + ignoreCase = *m.URL.IgnoreCase + } + urls = append(urls, frontendRuleMatcherStringModel{ + IgnoreCase: types.BoolValue(ignoreCase), + Inverse: types.BoolValue(*m.Inverse), + Method: types.StringValue(string(m.URL.Method)), + Value: types.StringValue(m.URL.Value), + }) + } + + if m.URLParam != nil { + var ignoreCase bool + if m.URLParam.IgnoreCase != nil { + ignoreCase = *m.URLParam.IgnoreCase + } + urlParams = append(urlParams, frontendRuleMatcherStringWithArgumentModel{ + IgnoreCase: types.BoolValue(ignoreCase), + Inverse: types.BoolValue(*m.Inverse), + Method: types.StringValue(string(m.URLParam.Method)), + Name: types.StringValue(m.URLParam.Name), + Value: types.StringValue(m.URLParam.Value), + }) + } + + if m.URLQuery != nil { + var ignoreCase bool + if m.URLQuery.IgnoreCase != nil { + ignoreCase = *m.URLQuery.IgnoreCase + } + urlQueries = append(urlQueries, frontendRuleMatcherStringModel{ + IgnoreCase: types.BoolValue(ignoreCase), + Inverse: types.BoolValue(*m.Inverse), + Method: types.StringValue(string(m.URLQuery.Method)), + Value: types.StringValue(m.URLQuery.Value), + }) + } + } + + matcher := frontendRuleMatcherModel{} + + if elementType, ok := elementTypes["body_size"]; ok { + matcher.BodySize, diags = types.ListValueFrom(ctx, elementType, bodySizes) + respDiagnostics.Append(diags...) + } else { + respDiagnostics.AddError("cannot set frontend rule matcher", "body_size element type not found") + } + + if elementType, ok := elementTypes["body_size_range"]; ok { + matcher.BodySizeRange, diags = types.ListValueFrom(ctx, elementType, bodySizeRanges) + respDiagnostics.Append(diags...) + } else { + respDiagnostics.AddError("cannot set frontend rule matcher", "body_size_range element type not found") + } + + if elementType, ok := elementTypes["cookie"]; ok { + matcher.Cookie, diags = types.ListValueFrom(ctx, elementType, cookies) + respDiagnostics.Append(diags...) + } else { + respDiagnostics.AddError("cannot set frontend rule matcher", "cookie element type not found") + } + + if elementType, ok := elementTypes["header"]; ok { + matcher.Header, diags = types.ListValueFrom(ctx, elementType, headers) + respDiagnostics.Append(diags...) + } else { + respDiagnostics.AddError("cannot set frontend rule matcher", "header element type not found") + } + + if elementType, ok := elementTypes["host"]; ok { + matcher.Host, diags = types.ListValueFrom(ctx, elementType, hosts) + respDiagnostics.Append(diags...) + } else { + respDiagnostics.AddError("cannot set frontend rule matcher", "host element type not found") + } + + if elementType, ok := elementTypes["http_method"]; ok { + matcher.HTTPMethod, diags = types.ListValueFrom(ctx, elementType, httpMethods) + respDiagnostics.Append(diags...) + } else { + respDiagnostics.AddError("cannot set frontend rule matcher", "http_method element type not found") + } + + if elementType, ok := elementTypes["num_members_up"]; ok { + matcher.NumMembersUp, diags = types.ListValueFrom(ctx, elementType, numMembersUp) + respDiagnostics.Append(diags...) + } else { + respDiagnostics.AddError("cannot set frontend rule matcher", "num_members_up element type not found") + } + + if elementType, ok := elementTypes["path"]; ok { + matcher.Path, diags = types.ListValueFrom(ctx, elementType, paths) + respDiagnostics.Append(diags...) + } else { + respDiagnostics.AddError("cannot set frontend rule matcher", "path element type not found") + } + + if elementType, ok := elementTypes["src_ip"]; ok { + matcher.SrcIP, diags = types.ListValueFrom(ctx, elementType, srcIPs) + respDiagnostics.Append(diags...) + } else { + respDiagnostics.AddError("cannot set frontend rule matcher", "src_ip element type not found") + } + + if elementType, ok := elementTypes["src_port"]; ok { + matcher.SrcPort, diags = types.ListValueFrom(ctx, elementType, srcPorts) + respDiagnostics.Append(diags...) + } else { + respDiagnostics.AddError("cannot set frontend rule matcher", "src_port element type not found") + } + + if elementType, ok := elementTypes["src_port_range"]; ok { + matcher.SrcPortRange, diags = types.ListValueFrom(ctx, elementType, srcPortRanges) + respDiagnostics.Append(diags...) + } else { + respDiagnostics.AddError("cannot set frontend rule matcher", "src_port_range element type not found") + } + + if elementType, ok := elementTypes["url"]; ok { + matcher.URL, diags = types.ListValueFrom(ctx, elementType, urls) + respDiagnostics.Append(diags...) + } else { + respDiagnostics.AddError("cannot set frontend rule matcher", "url element type not found") + } + + if elementType, ok := elementTypes["url_param"]; ok { + matcher.URLParam, diags = types.ListValueFrom(ctx, elementType, urlParams) + respDiagnostics.Append(diags...) + } else { + respDiagnostics.AddError("cannot set frontend rule matcher", "url_param element type not found") + } + + if elementType, ok := elementTypes["url_query"]; ok { + matcher.URLQuery, diags = types.ListValueFrom(ctx, elementType, urlQueries) + respDiagnostics.Append(diags...) + } else { + respDiagnostics.AddError("cannot set frontend rule matcher", "url_query element type not found") + } + + data.Matchers, diags = types.ListValueFrom(ctx, data.Matchers.ElementType(ctx), []frontendRuleMatcherModel{matcher}) + respDiagnostics.Append(diags...) + + return respDiagnostics +} + +func appendMatcher( + matchers []upcloud.LoadBalancerMatcher, + newMatcher upcloud.LoadBalancerMatcher, + inverse interface{}, +) []upcloud.LoadBalancerMatcher { + if inverse.(bool) { + return append(matchers, request.NewLoadBalancerInverseMatcher(newMatcher)) + } + return append(matchers, newMatcher) +} diff --git a/internal/service/loadbalancer/frontend_rule_matchers.go b/internal/service/loadbalancer/frontend_rule_matchers.go deleted file mode 100644 index abb87da9..00000000 --- a/internal/service/loadbalancer/frontend_rule_matchers.go +++ /dev/null @@ -1,637 +0,0 @@ -package loadbalancer - -import ( - "fmt" - "strings" - - "github.com/UpCloudLtd/upcloud-go-api/v8/upcloud" - "github.com/UpCloudLtd/upcloud-go-api/v8/upcloud/request" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" -) - -const ( - customSrcPortRangeMatcherType = "src_port_range" - customBodySizeRangeMatcherType = "body_size_range" -) - -func frontendRuleMatchersSchema() map[string]*schema.Schema { - return map[string]*schema.Schema{ - "src_port": { - Description: "Matches by source port number.", - Type: schema.TypeList, - Optional: true, - MaxItems: 100, - ForceNew: true, - Elem: &schema.Resource{ - Schema: frontendRuleMatcherIntegerSchema(), - }, - }, - "src_port_range": { - Description: "Matches by range of source port numbers", - Type: schema.TypeList, - Optional: true, - MaxItems: 100, - ForceNew: true, - Elem: &schema.Resource{ - Schema: frontendRuleMatcherRangeSchema(), - }, - }, - "src_ip": { - Description: "Matches by source IP address.", - Type: schema.TypeList, - Optional: true, - MaxItems: 100, - ForceNew: true, - Elem: &schema.Resource{ - Schema: frontendRuleMatcherIPSchema(), - }, - }, - "body_size": { - Description: "Matches by HTTP request body size.", - Type: schema.TypeList, - Optional: true, - MaxItems: 100, - ForceNew: true, - Elem: &schema.Resource{ - Schema: frontendRuleMatcherIntegerSchema(), - }, - }, - "body_size_range": { - Description: "Matches by range of HTTP request body sizes", - Type: schema.TypeList, - Optional: true, - MaxItems: 100, - ForceNew: true, - Elem: &schema.Resource{ - Schema: frontendRuleMatcherRangeSchema(), - }, - }, - "path": { - Description: "Matches by URL path.", - Type: schema.TypeList, - Optional: true, - MaxItems: 100, - ForceNew: true, - Elem: &schema.Resource{ - Schema: frontendRuleMatcherStringSchema(), - }, - }, - "url": { - Description: "Matches by URL without schema, e.g. `example.com/dashboard`.", - Type: schema.TypeList, - Optional: true, - MaxItems: 100, - ForceNew: true, - Elem: &schema.Resource{ - Schema: frontendRuleMatcherStringSchema(), - }, - }, - "url_query": { - Description: "Matches by URL query string.", - Type: schema.TypeList, - Optional: true, - MaxItems: 100, - ForceNew: true, - Elem: &schema.Resource{ - Schema: frontendRuleMatcherStringSchema(), - }, - }, - "host": { - Description: "Matches by hostname. Header extracted from HTTP Headers or from TLS certificate in case of secured connection.", - Type: schema.TypeList, - Optional: true, - MaxItems: 100, - ForceNew: true, - Elem: &schema.Resource{ - Schema: frontendRuleMatcherHostSchema(), - }, - }, - "http_method": { - Description: "Matches by HTTP method.", - Type: schema.TypeList, - Optional: true, - MaxItems: 100, - ForceNew: true, - Elem: &schema.Resource{ - Schema: frontendRuleMatcherHTTPMethodSchema(), - }, - }, - "cookie": { - Description: "Matches by HTTP cookie value. Cookie name must be provided.", - Type: schema.TypeList, - Optional: true, - MaxItems: 100, - ForceNew: true, - Elem: &schema.Resource{ - Schema: frontendRuleMatcherStringWithArgumentSchema(), - }, - }, - "header": { - Description: "Matches by HTTP header value. Header name must be provided.", - Type: schema.TypeList, - Optional: true, - MaxItems: 100, - ForceNew: true, - Elem: &schema.Resource{ - Schema: frontendRuleMatcherStringWithArgumentSchema(), - }, - }, - "url_param": { - Description: "Matches by URL query parameter value. Query parameter name must be provided", - Type: schema.TypeList, - Optional: true, - MaxItems: 100, - ForceNew: true, - Elem: &schema.Resource{ - Schema: frontendRuleMatcherStringWithArgumentSchema(), - }, - }, - "num_members_up": { - Description: "Matches by number of healthy backend members.", - Type: schema.TypeList, - Optional: true, - MaxItems: 100, - ForceNew: true, - Elem: &schema.Resource{ - Schema: frontendRuleMatcherBackendSchema(), - }, - }, - } -} - -func inverseSchema() *schema.Schema { - return &schema.Schema{ - Description: "Sets if the condition should be inverted. Works similar to logical NOT operator.", - Type: schema.TypeBool, - Optional: true, - ForceNew: true, - Default: false, - } -} - -func frontendRuleMatcherBackendSchema() map[string]*schema.Schema { - methods := []string{ - string(upcloud.LoadBalancerIntegerMatcherMethodEqual), - string(upcloud.LoadBalancerIntegerMatcherMethodGreater), - string(upcloud.LoadBalancerIntegerMatcherMethodGreaterOrEqual), - string(upcloud.LoadBalancerIntegerMatcherMethodLess), - string(upcloud.LoadBalancerIntegerMatcherMethodLessOrEqual), - } - - return map[string]*schema.Schema{ - "method": { - Description: fmt.Sprintf("Match method (`%s`).", strings.Join(methods, "`, `")), - Type: schema.TypeString, - Required: true, - ForceNew: true, - ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice(methods, false)), - }, - "value": { - Description: "Integer value.", - Type: schema.TypeInt, - Required: true, - ForceNew: true, - ValidateDiagFunc: validation.ToDiagFunc(validation.IntBetween(0, 100)), - }, - "backend_name": { - Description: "The name of the `backend` which members will be monitored.", - Type: schema.TypeString, - Required: true, - ForceNew: true, - ValidateDiagFunc: validation.ToDiagFunc(validation.StringLenBetween(1, 255)), - }, - "inverse": inverseSchema(), - } -} - -func frontendRuleMatcherStringWithArgumentSchema() map[string]*schema.Schema { - methods := []string{ - string(upcloud.LoadBalancerStringMatcherMethodExact), - string(upcloud.LoadBalancerStringMatcherMethodSubstring), - string(upcloud.LoadBalancerStringMatcherMethodRegexp), - string(upcloud.LoadBalancerStringMatcherMethodStarts), - string(upcloud.LoadBalancerStringMatcherMethodEnds), - string(upcloud.LoadBalancerStringMatcherMethodDomain), - string(upcloud.LoadBalancerStringMatcherMethodIP), - string(upcloud.LoadBalancerStringMatcherMethodExists), - } - - return map[string]*schema.Schema{ - "method": { - Description: fmt.Sprintf("Match method (`%s`). Matcher with `exists` and `ip` methods must be used without `value` and `ignore_case` fields.", strings.Join(methods, "`, `")), - Type: schema.TypeString, - Required: true, - ForceNew: true, - ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice(methods, false)), - }, - "name": { - Description: "Name of the argument.", - Type: schema.TypeString, - Required: true, - ForceNew: true, - ValidateDiagFunc: validation.ToDiagFunc(validation.StringLenBetween(1, 255)), - }, - "value": { - Description: "String value.", - Type: schema.TypeString, - Optional: true, - ForceNew: true, - ValidateDiagFunc: validation.ToDiagFunc(validation.StringLenBetween(1, 255)), - }, - "ignore_case": { - Description: "Ignore case, default `false`.", - Type: schema.TypeBool, - Optional: true, - Default: false, - ForceNew: true, - }, - "inverse": inverseSchema(), - } -} - -func frontendRuleMatcherStringSchema() map[string]*schema.Schema { - methods := []string{ - string(upcloud.LoadBalancerStringMatcherMethodExact), - string(upcloud.LoadBalancerStringMatcherMethodSubstring), - string(upcloud.LoadBalancerStringMatcherMethodRegexp), - string(upcloud.LoadBalancerStringMatcherMethodStarts), - string(upcloud.LoadBalancerStringMatcherMethodEnds), - string(upcloud.LoadBalancerStringMatcherMethodDomain), - string(upcloud.LoadBalancerStringMatcherMethodIP), - string(upcloud.LoadBalancerStringMatcherMethodExists), - } - - return map[string]*schema.Schema{ - "method": { - Description: fmt.Sprintf("Match method (`%s`). Matcher with `exists` and `ip` methods must be used without `value` and `ignore_case` fields.", strings.Join(methods, "`, `")), - Type: schema.TypeString, - Required: true, - ForceNew: true, - ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice(methods, false)), - }, - "value": { - Description: "String value.", - Type: schema.TypeString, - Optional: true, - ForceNew: true, - ValidateDiagFunc: validation.ToDiagFunc(validation.StringLenBetween(1, 255)), - }, - "ignore_case": { - Description: "Ignore case, default `false`.", - Type: schema.TypeBool, - Optional: true, - ForceNew: true, - Default: false, - }, - "inverse": inverseSchema(), - } -} - -func frontendRuleMatcherHTTPMethodSchema() map[string]*schema.Schema { - methods := []string{ - string(upcloud.LoadBalancerHTTPMatcherMethodGet), - string(upcloud.LoadBalancerHTTPMatcherMethodHead), - string(upcloud.LoadBalancerHTTPMatcherMethodPost), - string(upcloud.LoadBalancerHTTPMatcherMethodPut), - string(upcloud.LoadBalancerHTTPMatcherMethodPatch), - string(upcloud.LoadBalancerHTTPMatcherMethodDelete), - string(upcloud.LoadBalancerHTTPMatcherMethodConnect), - string(upcloud.LoadBalancerHTTPMatcherMethodOptions), - string(upcloud.LoadBalancerHTTPMatcherMethodTrace), - } - - return map[string]*schema.Schema{ - "value": { - Description: fmt.Sprintf("String value (`%s`).", strings.Join(methods, "`, `")), - Type: schema.TypeString, - Required: true, - ForceNew: true, - ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice(methods, false)), - }, - "inverse": inverseSchema(), - } -} - -func frontendRuleMatcherHostSchema() map[string]*schema.Schema { - return map[string]*schema.Schema{ - "value": { - Description: "String value.", - Type: schema.TypeString, - Required: true, - ForceNew: true, - ValidateDiagFunc: validation.ToDiagFunc(validation.StringLenBetween(1, 255)), - }, - "inverse": inverseSchema(), - } -} - -func frontendRuleMatcherIntegerSchema() map[string]*schema.Schema { - methods := []string{ - string(upcloud.LoadBalancerIntegerMatcherMethodEqual), - string(upcloud.LoadBalancerIntegerMatcherMethodGreater), - string(upcloud.LoadBalancerIntegerMatcherMethodGreaterOrEqual), - string(upcloud.LoadBalancerIntegerMatcherMethodLess), - string(upcloud.LoadBalancerIntegerMatcherMethodLessOrEqual), - } - - return map[string]*schema.Schema{ - "method": { - Description: fmt.Sprintf("Match method (`%s`).", strings.Join(methods, "`, `")), - Type: schema.TypeString, - Required: true, - ForceNew: true, - ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice(methods, false)), - }, - "value": { - Description: "Integer value.", - Type: schema.TypeInt, - Required: true, - ForceNew: true, - }, - "inverse": inverseSchema(), - } -} - -func frontendRuleMatcherRangeSchema() map[string]*schema.Schema { - return map[string]*schema.Schema{ - "range_start": { - Description: "Integer value.", - Type: schema.TypeInt, - Required: true, - ForceNew: true, - }, - "range_end": { - Description: "Integer value.", - Type: schema.TypeInt, - Required: true, - ForceNew: true, - }, - "inverse": inverseSchema(), - } -} - -func frontendRuleMatcherIPSchema() map[string]*schema.Schema { - return map[string]*schema.Schema{ - "value": { - Description: "IP address. CIDR masks are supported, e.g. `192.168.0.0/24`.", - Type: schema.TypeString, - Required: true, - ForceNew: true, - }, - "inverse": inverseSchema(), - } -} - -func appendMatcher( - matchers []upcloud.LoadBalancerMatcher, - newMatcher upcloud.LoadBalancerMatcher, - inverse interface{}, -) []upcloud.LoadBalancerMatcher { - if inverse.(bool) { - return append(matchers, request.NewLoadBalancerInverseMatcher(newMatcher)) - } - return append(matchers, newMatcher) -} - -func loadBalancerMatchersFromResourceData(d *schema.ResourceData) ([]upcloud.LoadBalancerMatcher, error) { - m := make([]upcloud.LoadBalancerMatcher, 0) - if _, ok := d.GetOk("matchers.0"); !ok { - return m, nil - } - for _, v := range d.Get("matchers.0.src_port").([]interface{}) { - v := v.(map[string]interface{}) - m = appendMatcher(m, request.NewLoadBalancerSrcPortMatcher( - upcloud.LoadBalancerIntegerMatcherMethod(v["method"].(string)), - v["value"].(int), - ), v["inverse"]) - } - - for _, v := range d.Get("matchers.0.src_port_range").([]interface{}) { - v := v.(map[string]interface{}) - m = appendMatcher(m, request.NewLoadBalancerSrcPortRangeMatcher( - v["range_start"].(int), - v["range_end"].(int), - ), v["inverse"]) - } - - for _, v := range d.Get("matchers.0.src_ip").([]interface{}) { - v := v.(map[string]interface{}) - m = appendMatcher(m, request.NewLoadBalancerSrcIPMatcher(v["value"].(string)), v["inverse"]) - } - - for _, v := range d.Get("matchers.0.body_size").([]interface{}) { - v := v.(map[string]interface{}) - m = appendMatcher(m, request.NewLoadBalancerBodySizeMatcher( - upcloud.LoadBalancerIntegerMatcherMethod(v["method"].(string)), - v["value"].(int), - ), v["inverse"]) - } - - for _, v := range d.Get("matchers.0.body_size_range").([]interface{}) { - v := v.(map[string]interface{}) - m = appendMatcher(m, request.NewLoadBalancerBodySizeRangeMatcher( - v["range_start"].(int), - v["range_end"].(int), - ), v["inverse"]) - } - - for _, v := range d.Get("matchers.0.path").([]interface{}) { - v := v.(map[string]interface{}) - m = appendMatcher(m, request.NewLoadBalancerPathMatcher( - upcloud.LoadBalancerStringMatcherMethod(v["method"].(string)), - v["value"].(string), - upcloud.BoolPtr(v["ignore_case"].(bool)), - ), v["inverse"]) - } - - for _, v := range d.Get("matchers.0.url").([]interface{}) { - v := v.(map[string]interface{}) - m = appendMatcher(m, request.NewLoadBalancerURLMatcher( - upcloud.LoadBalancerStringMatcherMethod(v["method"].(string)), - v["value"].(string), - upcloud.BoolPtr(v["ignore_case"].(bool)), - ), v["inverse"]) - } - - for _, v := range d.Get("matchers.0.url_query").([]interface{}) { - v := v.(map[string]interface{}) - m = appendMatcher(m, request.NewLoadBalancerURLQueryMatcher( - upcloud.LoadBalancerStringMatcherMethod(v["method"].(string)), - v["value"].(string), - upcloud.BoolPtr(v["ignore_case"].(bool)), - ), v["inverse"]) - } - - for _, v := range d.Get("matchers.0.host").([]interface{}) { - v := v.(map[string]interface{}) - m = appendMatcher(m, request.NewLoadBalancerHostMatcher(v["value"].(string)), v["inverse"]) - } - - for _, v := range d.Get("matchers.0.http_method").([]interface{}) { - v := v.(map[string]interface{}) - m = appendMatcher(m, request.NewLoadBalancerHTTPMethodMatcher( - upcloud.LoadBalancerHTTPMatcherMethod(v["value"].(string)), - ), v["inverse"]) - } - - for _, v := range d.Get("matchers.0.cookie").([]interface{}) { - v := v.(map[string]interface{}) - m = appendMatcher(m, request.NewLoadBalancerCookieMatcher( - upcloud.LoadBalancerStringMatcherMethod(v["method"].(string)), - v["name"].(string), - v["value"].(string), - upcloud.BoolPtr(v["ignore_case"].(bool)), - ), v["inverse"]) - } - - for _, v := range d.Get("matchers.0.header").([]interface{}) { - v := v.(map[string]interface{}) - m = appendMatcher(m, request.NewLoadBalancerHeaderMatcher( - upcloud.LoadBalancerStringMatcherMethod(v["method"].(string)), - v["name"].(string), - v["value"].(string), - upcloud.BoolPtr(v["ignore_case"].(bool)), - ), v["inverse"]) - } - - for _, v := range d.Get("matchers.0.url_param").([]interface{}) { - v := v.(map[string]interface{}) - m = appendMatcher(m, request.NewLoadBalancerURLParamMatcher( - upcloud.LoadBalancerStringMatcherMethod(v["method"].(string)), - v["name"].(string), - v["value"].(string), - upcloud.BoolPtr(v["ignore_case"].(bool)), - ), v["inverse"]) - } - - for _, v := range d.Get("matchers.0.num_members_up").([]interface{}) { - v := v.(map[string]interface{}) - m = appendMatcher(m, request.NewLoadBalancerNumMembersUpMatcher( - upcloud.LoadBalancerIntegerMatcherMethod(v["method"].(string)), - v["value"].(int), - v["backend_name"].(string), - ), v["inverse"]) - } - - return m, nil -} - -func setFrontendRuleMatchersResourceData(d *schema.ResourceData, rule *upcloud.LoadBalancerFrontendRule) error { - if len(rule.Matchers) == 0 { - return d.Set("matchers", nil) - } - - matchers := make(map[string][]interface{}) - for _, m := range rule.Matchers { - t := string(m.Type) - var v map[string]interface{} - switch m.Type { - case upcloud.LoadBalancerMatcherTypeSrcIP: - v = map[string]interface{}{ - "value": m.SrcIP.Value, - "inverse": m.Inverse, - } - case upcloud.LoadBalancerMatcherTypeSrcPort: - if m.SrcPort.Method == upcloud.LoadBalancerIntegerMatcherMethodRange { - t = customSrcPortRangeMatcherType - - v = map[string]interface{}{ - "range_start": m.SrcPort.RangeStart, - "range_end": m.SrcPort.RangeEnd, - "inverse": m.Inverse, - } - } else { - v = map[string]interface{}{ - "method": m.SrcPort.Method, - "value": m.SrcPort.Value, - "inverse": m.Inverse, - } - } - case upcloud.LoadBalancerMatcherTypeBodySize: - if m.BodySize.Method == upcloud.LoadBalancerIntegerMatcherMethodRange { - t = customBodySizeRangeMatcherType - - v = map[string]interface{}{ - "range_start": m.BodySize.RangeStart, - "range_end": m.BodySize.RangeEnd, - "inverse": m.Inverse, - } - } else { - v = map[string]interface{}{ - "method": m.BodySize.Method, - "value": m.BodySize.Value, - "inverse": m.Inverse, - } - } - case upcloud.LoadBalancerMatcherTypePath: - v = map[string]interface{}{ - "value": m.Path.Value, - "ignore_case": m.Path.IgnoreCase, - "method": m.Path.Method, - "inverse": m.Inverse, - } - case upcloud.LoadBalancerMatcherTypeURL: - v = map[string]interface{}{ - "value": m.URL.Value, - "ignore_case": m.URL.IgnoreCase, - "method": m.URL.Method, - "inverse": m.Inverse, - } - case upcloud.LoadBalancerMatcherTypeURLParam: - v = map[string]interface{}{ - "value": m.URLParam.Value, - "ignore_case": m.URLParam.IgnoreCase, - "name": m.URLParam.Name, - "method": m.URLParam.Method, - "inverse": m.Inverse, - } - case upcloud.LoadBalancerMatcherTypeURLQuery: - v = map[string]interface{}{ - "value": m.URLQuery.Value, - "ignore_case": m.URLQuery.IgnoreCase, - "method": m.URLQuery.Method, - "inverse": m.Inverse, - } - case upcloud.LoadBalancerMatcherTypeHost: - v = map[string]interface{}{ - "value": m.Host.Value, - "inverse": m.Inverse, - } - case upcloud.LoadBalancerMatcherTypeHTTPMethod: - v = map[string]interface{}{ - "value": m.HTTPMethod.Value, - "inverse": m.Inverse, - } - case upcloud.LoadBalancerMatcherTypeCookie: - v = map[string]interface{}{ - "value": m.Cookie.Value, - "name": m.Cookie.Name, - "method": m.Cookie.Method, - "ignore_case": m.Cookie.IgnoreCase, - "inverse": m.Inverse, - } - case upcloud.LoadBalancerMatcherTypeHeader: - v = map[string]interface{}{ - "value": m.Header.Value, - "name": m.Header.Name, - "method": m.Header.Method, - "ignore_case": m.Header.IgnoreCase, - "inverse": m.Inverse, - } - case upcloud.LoadBalancerMatcherTypeNumMembersUp: - v = map[string]interface{}{ - "value": m.NumMembersUp.Value, - "method": m.NumMembersUp.Method, - "backend_name": m.NumMembersUp.Backend, - "inverse": m.Inverse, - } - default: - return fmt.Errorf("received unsupported matcher type '%s' %+v", m.Type, m) - } - - matchers[t] = append(matchers[t], v) - } - - return d.Set("matchers", []interface{}{matchers}) -} diff --git a/upcloud/provider.go b/upcloud/provider.go index 844b5374..6739d2f5 100644 --- a/upcloud/provider.go +++ b/upcloud/provider.go @@ -151,6 +151,7 @@ func (p *upcloudProvider) Resources(_ context.Context) []func() resource.Resourc loadbalancer.NewDynamicCertificateBundleResource, loadbalancer.NewBackendResource, loadbalancer.NewFrontendResource, + loadbalancer.NewFrontendRuleResource, loadbalancer.NewFrontendTLSConfigResource, loadbalancer.NewManualCertificateBundleResource, managedobjectstorage.NewManagedObjectStorageCustomDomainResource, diff --git a/upcloud/sdkv2_provider.go b/upcloud/sdkv2_provider.go index d4185c1d..2e7636c5 100644 --- a/upcloud/sdkv2_provider.go +++ b/upcloud/sdkv2_provider.go @@ -86,7 +86,6 @@ func Provider() *schema.Provider { "upcloud_managed_object_storage_user_policy": managedobjectstorage.ResourceManagedObjectStorageUserPolicy(), "upcloud_loadbalancer": loadbalancer.ResourceLoadBalancer(), "upcloud_loadbalancer_resolver": loadbalancer.ResourceResolver(), - "upcloud_loadbalancer_frontend_rule": loadbalancer.ResourceFrontendRule(), }, DataSourcesMap: map[string]*schema.Resource{