Skip to content

Commit

Permalink
[ UP-3747 ] Validate custom URL port
Browse files Browse the repository at this point in the history
Uptime API requires that port value in the URL field should match
to the Port value in the resource. This change checks that if URL
host contains port definition it matches to Port value in the resource.

https://uptimedotcom.atlassian.net/browse/UP-3747
  • Loading branch information
gigovich committed Aug 8, 2024
1 parent adcb381 commit bb149fc
Show file tree
Hide file tree
Showing 8 changed files with 172 additions and 2 deletions.
2 changes: 1 addition & 1 deletion docs/resources/check_http.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ Monitor a URL for specific status code(s)
- `notes` (String)
- `num_retries` (Number) How many times the check should be retried before a location is considered down
- `password` (String, Sensitive)
- `port` (Number)
- `port` (Number) The `Port` value is mandatory if the address URL contains a custom, non-standard port. It should be set to the same value.
- `proxy` (String)
- `send_string` (String) String to post
- `sensitivity` (Number) How many locations should be down before an alert is sent
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ require (
github.com/hashicorp/logutils v1.0.0 // indirect
github.com/hashicorp/terraform-exec v0.21.0 // indirect
github.com/hashicorp/terraform-json v0.22.1 // indirect
github.com/hashicorp/terraform-plugin-framework-validators v0.13.0 // indirect
github.com/hashicorp/terraform-plugin-log v0.9.0 // indirect
github.com/hashicorp/terraform-plugin-sdk/v2 v2.34.0 // indirect
github.com/hashicorp/terraform-registry-address v0.2.3 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ github.com/hashicorp/terraform-json v0.22.1 h1:xft84GZR0QzjPVWs4lRUwvTcPnegqlyS7
github.com/hashicorp/terraform-json v0.22.1/go.mod h1:JbWSQCLFSXFFhg42T7l9iJwdGXBYV8fmmD6o/ML4p3A=
github.com/hashicorp/terraform-plugin-framework v1.10.0 h1:xXhICE2Fns1RYZxEQebwkB2+kXouLC932Li9qelozrc=
github.com/hashicorp/terraform-plugin-framework v1.10.0/go.mod h1:qBXLDn69kM97NNVi/MQ9qgd1uWWsVftGSnygYG1tImM=
github.com/hashicorp/terraform-plugin-framework-validators v0.13.0 h1:bxZfGo9DIUoLLtHMElsu+zwqI4IsMZQBRRy4iLzZJ8E=
github.com/hashicorp/terraform-plugin-framework-validators v0.13.0/go.mod h1:wGeI02gEhj9nPANU62F2jCaHjXulejm/X+af4PdZaNo=
github.com/hashicorp/terraform-plugin-go v0.23.0 h1:AALVuU1gD1kPb48aPQUjug9Ir/125t+AAurhqphJ2Co=
github.com/hashicorp/terraform-plugin-go v0.23.0/go.mod h1:1E3Cr9h2vMlahWMbsSEcNrOCxovCZhOOIXjFHbjc/lQ=
github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0=
Expand Down
10 changes: 9 additions & 1 deletion internal/provider/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ type APIModeler[M APIModel, A, R any] interface {

type APIResourceMetadata struct {
schema.Schema
TypeNameSuffix string
TypeNameSuffix string
ConfigValidators func(context.Context) []resource.ConfigValidator
}

type APIResource[M APIModel, A, R any] struct {
Expand All @@ -50,6 +51,13 @@ func (r APIResource[M, A, R]) Schema(_ context.Context, _ resource.SchemaRequest
rs.Schema = r.meta.Schema
}

func (r APIResource[M, A, R]) ConfigValidators(ctx context.Context) []resource.ConfigValidator {
if r.meta.ConfigValidators == nil {
return []resource.ConfigValidator{}
}
return r.meta.ConfigValidators(ctx)
}

const (
toAPIArgumentError = "To API Argument Conversion Failed"
fromAPIResultError = "From API Result Conversion Failed"
Expand Down
7 changes: 7 additions & 0 deletions internal/provider/resource_check_http.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (

"github.com/hashicorp/go-multierror"
"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/int64default"
Expand Down Expand Up @@ -43,6 +44,8 @@ func NewCheckHTTPResource(_ context.Context, p *providerImpl) resource.Resource
Computed: true,
Optional: true,
Default: int64default.StaticInt64(0),
Description: ("The `Port` value is mandatory if the address URL contains a custom, non-standard port. " +
"It should be set to the same value."),
},
"username": schema.StringAttribute{
Optional: true,
Expand Down Expand Up @@ -99,6 +102,10 @@ func NewCheckHTTPResource(_ context.Context, p *providerImpl) resource.Resource
},
},
},
ConfigValidators: func(context.Context) []resource.ConfigValidator {
return []resource.ConfigValidator{PortMatchConfigValidator(
path.Root("address"), path.Root("port"))}
},
},
}
}
Expand Down
30 changes: 30 additions & 0 deletions internal/provider/resource_check_http_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package provider

import (
"regexp"
"testing"

petname "github.com/dustinkirkland/golang-petname"
Expand Down Expand Up @@ -202,3 +203,32 @@ func TestAccCheckHTTPResource_Password(t *testing.T) {
},
}))
}

func TestAccCheckHTTPResource_PortValidation(t *testing.T) {
names := []string{
petname.Generate(3, "-"),
petname.Generate(3, "-"),
}
resource.Test(t, testCaseFromSteps(t, []resource.TestStep{
{
ConfigDirectory: config.StaticDirectory("testdata/resource_check_http/port_validation"),
ConfigVariables: config.Variables{
"name": config.StringVariable(names[0]),
"address": config.StringVariable("https://example.com:9383"),
"port": config.IntegerVariable(9383),
},
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("uptime_check_http.test", "address", "https://example.com:9383"),
),
},
{
// basic manifest doesn't contain posrt definition, so it must fail
ConfigDirectory: config.StaticDirectory("testdata/resource_check_http/_basic"),
ConfigVariables: config.Variables{
"name": config.StringVariable(names[1]),
"address": config.StringVariable("https://example.com:9383"),
},
ExpectError: regexp.MustCompile("Port value should match"),
},
}))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
variable name {
type = string
}

variable address {
type = string
default = "https://example.com:8383"
}

variable port {
type = number
}

resource uptime_check_http test {
name = var.name
address = var.address
port = var.port
}

103 changes: 103 additions & 0 deletions internal/provider/validators.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,17 @@ import (
"fmt"
"net/url"
"regexp"
"strconv"
"sync"

"github.com/hashicorp/terraform-plugin-framework-validators/helpers/validatordiag"
"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/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
"github.com/hashicorp/terraform-plugin-framework/types"
)

func OneOfStringValidator(s []string) validator.String {
Expand Down Expand Up @@ -110,3 +118,98 @@ func (urlValidator) ValidateString(_ context.Context, rq validator.StringRequest
)
}
}

// PortMatchConfigValidator resource validator checks that address custom port and Port property is same
//
// This validator failes if in the url address property host has explicit port definition
// and Port property in the resource doesn't match it.
func PortMatchConfigValidator(urlP, portP path.Path) *portMatchConfigValidator {
return &portMatchConfigValidator{
urlPath: urlP,
portPath: portP,
}
}

type portMatchConfigValidator struct {
urlPath path.Path
portPath path.Path
}

func (p portMatchConfigValidator) Description(context.Context) string {
return "Port value should match address Host custom port when last is defined"
}

func (p portMatchConfigValidator) MarkdownDescription(ctx context.Context) string {
return p.Description(ctx)
}

func (p portMatchConfigValidator) ValidateResource(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) {
resp.Diagnostics = p.Validate(ctx, req.Config)
}

func (p portMatchConfigValidator) Validate(ctx context.Context, config tfsdk.Config) (diags diag.Diagnostics) {
// get url address host value and extract port from it if it exists
var v attr.Value
getAttributeDiags := config.GetAttribute(ctx, p.urlPath, &v)
diags.Append(getAttributeDiags...)

if getAttributeDiags.HasError() || v.IsUnknown() || v.IsNull() {
return
}

urlType, ok := v.(types.String)
if !ok {
diags.Append(validatordiag.InvalidAttributeTypeDiagnostic(
p.urlPath, "path contains non string value", v.String(),
))
return
}

urlValue, err := url.Parse(urlType.ValueString())
if err != nil {
diags.Append(validatordiag.InvalidAttributeTypeDiagnostic(
p.urlPath, "broken URL", urlType.ValueString(),
))
return
}
urlPortValue := urlValue.Port()

// get Port value if it exists
getAttributeDiags = config.GetAttribute(ctx, p.portPath, &v)
if getAttributeDiags.HasError() {
diags.Append(getAttributeDiags...)
return
}

// if Port field is not defined in the resource, but address url host
// property contains custom port, it is not valid combination
if (v.IsUnknown() || v.IsNull()) && urlPortValue != "" {
diags.Append(validatordiag.InvalidAttributeCombinationDiagnostic(
p.portPath, p.Description(ctx),
))
return
}

portType, ok := v.(types.Int64)
if !ok {
diags.Append(validatordiag.InvalidAttributeTypeDiagnostic(
p.portPath, "path contains non number type", v.String(),
))
return
}

// No custom ports defined, ok
if urlPortValue == "" && portType.ValueInt64() == 0 {
return diags
}

// Custom port in the address url port should match with Port property in the resource
portValue := strconv.FormatInt(portType.ValueInt64(), 10)
if urlPortValue != portValue {
diags.Append(validatordiag.InvalidAttributeCombinationDiagnostic(
p.portPath, p.Description(ctx),
))
}

return diags
}

0 comments on commit bb149fc

Please sign in to comment.