diff --git a/docs/functions/semver_check.md b/docs/functions/semver_check.md new file mode 100644 index 0000000..7207301 --- /dev/null +++ b/docs/functions/semver_check.md @@ -0,0 +1,42 @@ +--- +page_title: "semver_check function - terraform-provider-assert" +subcategory: "SemVer Functions" +description: |- + Check if a semver matches a constraint +--- + +# function: semver_check + + + + + +## Variable Validation Example + +```terraform +variable "version" { + type = string + validation { + condition = provider::assert::semver_check("~> 1.0", var.version) + error_message = "The provided version is not supported" + } +} +``` + +## Signature + + +```text +semver_check(constraint string, semver string) bool +``` + +## Arguments + + +1. `constraint` (String) The constraint to check against +1. `semver` (String) The version to check + + +## Return Type + +The return type of `semver_check` is a boolean. diff --git a/docs/functions/semver_constraint.md b/docs/functions/semver_constraint.md new file mode 100644 index 0000000..85a7780 --- /dev/null +++ b/docs/functions/semver_constraint.md @@ -0,0 +1,41 @@ +--- +page_title: "semver_constraint function - terraform-provider-assert" +subcategory: "SemVer Functions" +description: |- + Check if a semver constraint is valid +--- + +# function: semver_constraint + + + + + +## Variable Validation Example + +```terraform +variable "version_constraint" { + type = string + validation { + condition = provider::assert::semver_constraint("~> 1.0", var.version_constraint) + error_message = "The provided version constraint is not valid" + } +} +``` + +## Signature + + +```text +semver_constraint(constraint string) bool +``` + +## Arguments + + +1. `constraint` (String) The constraint to validate + + +## Return Type + +The return type of `semver_constraint` is a boolean. diff --git a/docs/functions/semver_version.md b/docs/functions/semver_version.md new file mode 100644 index 0000000..c1f8dcf --- /dev/null +++ b/docs/functions/semver_version.md @@ -0,0 +1,41 @@ +--- +page_title: "semver_version function - terraform-provider-assert" +subcategory: "SemVer Functions" +description: |- + Check if a semver version is valid +--- + +# function: semver_version + + + + + +## Variable Validation Example + +```terraform +variable "version" { + type = string + validation { + condition = provider::assert::semver_version(var.version) + error_message = "The provided version is not a valid SemVer version" + } +} +``` + +## Signature + + +```text +semver_version(version string) bool +``` + +## Arguments + + +1. `version` (String) The version to validate + + +## Return Type + +The return type of `semver_version` is a boolean. diff --git a/examples/functions/semver_check/variable.tf b/examples/functions/semver_check/variable.tf new file mode 100644 index 0000000..b6b8bb9 --- /dev/null +++ b/examples/functions/semver_check/variable.tf @@ -0,0 +1,7 @@ +variable "version" { + type = string + validation { + condition = provider::assert::semver_check("~> 1.0", var.version) + error_message = "The provided version is not supported" + } +} diff --git a/examples/functions/semver_constraint/variable.tf b/examples/functions/semver_constraint/variable.tf new file mode 100644 index 0000000..2f7739d --- /dev/null +++ b/examples/functions/semver_constraint/variable.tf @@ -0,0 +1,7 @@ +variable "version_constraint" { + type = string + validation { + condition = provider::assert::semver_constraint("~> 1.0", var.version_constraint) + error_message = "The provided version constraint is not valid" + } +} diff --git a/examples/functions/semver_version/variable.tf b/examples/functions/semver_version/variable.tf new file mode 100644 index 0000000..441783f --- /dev/null +++ b/examples/functions/semver_version/variable.tf @@ -0,0 +1,7 @@ +variable "version" { + type = string + validation { + condition = provider::assert::semver_version(var.version) + error_message = "The provided version is not a valid SemVer version" + } +} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 5653064..3bf6595 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -85,6 +85,9 @@ func (p *AssertProvider) Functions(ctx context.Context) []func() function.Functi NewEmptyFunction, NewNotEmptyFunction, NewRegexMatchesFunction, + NewSemVerCheckFunction, + NewSemVerVersionFunction, + NewSemVerConstraintFunction, } } diff --git a/internal/provider/semver_check_function.go b/internal/provider/semver_check_function.go new file mode 100644 index 0000000..c2b0d82 --- /dev/null +++ b/internal/provider/semver_check_function.go @@ -0,0 +1,69 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package provider + +import ( + "context" + + "github.com/hashicorp/go-version" + "github.com/hashicorp/terraform-plugin-framework/function" +) + +var ( + _ function.Function = SemVerCheckFunction{} +) + +func NewSemVerCheckFunction() function.Function { + return SemVerCheckFunction{} +} + +type SemVerCheckFunction struct{} + +func (r SemVerCheckFunction) Metadata(_ context.Context, req function.MetadataRequest, resp *function.MetadataResponse) { + resp.Name = "semver_check" +} + +func (r SemVerCheckFunction) Definition(_ context.Context, _ function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + Summary: "Check if a semver matches a constraint", + Parameters: []function.Parameter{ + function.StringParameter{ + AllowNullValue: false, + AllowUnknownValues: false, + Description: "The constraint to check against", + Name: "constraint", + }, + function.StringParameter{ + AllowNullValue: false, + AllowUnknownValues: false, + Description: "The version to check", + Name: "semver", + }, + }, + Return: function.BoolReturn{}, + } +} + +func (r SemVerCheckFunction) Run(ctx context.Context, req function.RunRequest, resp *function.RunResponse) { + var c, v string + + resp.Error = function.ConcatFuncErrors(req.Arguments.Get(ctx, &c, &v)) + if resp.Error != nil { + return + } + + semver, err := version.NewVersion(v) + if err != nil { + resp.Error = function.ConcatFuncErrors(resp.Result.Set(ctx, false)) + return + } + + constraints, err := version.NewConstraint(c) + if err != nil { + resp.Error = function.NewFuncError(err.Error()) + return + } + + resp.Error = function.ConcatFuncErrors(resp.Result.Set(ctx, constraints.Check(semver))) +} diff --git a/internal/provider/semver_check_function_test.go b/internal/provider/semver_check_function_test.go new file mode 100644 index 0000000..9c2ded6 --- /dev/null +++ b/internal/provider/semver_check_function_test.go @@ -0,0 +1,78 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package provider + +import ( + "testing" + + "github.com/hashicorp/go-version" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func TestSemVerCheckFunction_trueCase(t *testing.T) { + t.Parallel() + resource.UnitTest(t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(version.Must(version.NewVersion(MinimalRequiredTerraformVersion))), + }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: ` +output "test" { + value = provider::assert::semver_check("~> 1.0", "1.1.5") +} + `, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckOutput("test", "true"), + ), + }, + }, + }) +} + +func TestSemVerCheckFunction_falseCase(t *testing.T) { + t.Parallel() + resource.UnitTest(t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(version.Must(version.NewVersion(MinimalRequiredTerraformVersion))), + }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: ` +output "test" { + value = provider::assert::semver_check("< 1.0, < 1.2", "1.1.5") +} + `, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckOutput("test", "false"), + ), + }, + }, + }) +} + +func TestSemVerCheckFunction_invalidSemverCase(t *testing.T) { + t.Parallel() + resource.UnitTest(t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(version.Must(version.NewVersion(MinimalRequiredTerraformVersion))), + }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: ` +output "test" { + value = provider::assert::semver_check("< 1.0, < 1.2", "foobar") +} + `, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckOutput("test", "false"), + ), + }, + }, + }) +} diff --git a/internal/provider/semver_constraint_function.go b/internal/provider/semver_constraint_function.go new file mode 100644 index 0000000..44b224d --- /dev/null +++ b/internal/provider/semver_constraint_function.go @@ -0,0 +1,52 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package provider + +import ( + "context" + + "github.com/hashicorp/go-version" + "github.com/hashicorp/terraform-plugin-framework/function" +) + +var ( + _ function.Function = SemVerConstraintFunction{} +) + +func NewSemVerConstraintFunction() function.Function { + return SemVerConstraintFunction{} +} + +type SemVerConstraintFunction struct{} + +func (r SemVerConstraintFunction) Metadata(_ context.Context, req function.MetadataRequest, resp *function.MetadataResponse) { + resp.Name = "semver_constraint" +} + +func (r SemVerConstraintFunction) Definition(_ context.Context, _ function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + Summary: "Check if a semver constraint is valid", + Parameters: []function.Parameter{ + function.StringParameter{ + AllowNullValue: false, + AllowUnknownValues: false, + Description: "The constraint to validate", + Name: "constraint", + }, + }, + Return: function.BoolReturn{}, + } +} + +func (r SemVerConstraintFunction) Run(ctx context.Context, req function.RunRequest, resp *function.RunResponse) { + var constraint string + + resp.Error = function.ConcatFuncErrors(req.Arguments.Get(ctx, &constraint)) + if resp.Error != nil { + return + } + + _, err := version.NewConstraint(constraint) + resp.Error = function.ConcatFuncErrors(resp.Result.Set(ctx, err == nil)) +} diff --git a/internal/provider/semver_constraint_function_test.go b/internal/provider/semver_constraint_function_test.go new file mode 100644 index 0000000..33a5813 --- /dev/null +++ b/internal/provider/semver_constraint_function_test.go @@ -0,0 +1,56 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package provider + +import ( + "testing" + + "github.com/hashicorp/go-version" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func TestSemVerConstraintFunction_trueCase(t *testing.T) { + t.Parallel() + resource.UnitTest(t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(version.Must(version.NewVersion(MinimalRequiredTerraformVersion))), + }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: ` +output "test" { + value = provider::assert::semver_constraint("~> 1.1.5, <= 1.1.10") +} + `, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckOutput("test", "true"), + ), + }, + }, + }) +} + +func TestSemVerConstraintFunction_falseCase(t *testing.T) { + t.Parallel() + resource.UnitTest(t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(version.Must(version.NewVersion(MinimalRequiredTerraformVersion))), + }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: ` +output "test" { + value = provider::assert::semver_constraint("foobar") +} + `, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckOutput("test", "false"), + ), + }, + }, + }) +} diff --git a/internal/provider/semver_version_function.go b/internal/provider/semver_version_function.go new file mode 100644 index 0000000..e9e56d5 --- /dev/null +++ b/internal/provider/semver_version_function.go @@ -0,0 +1,52 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package provider + +import ( + "context" + + "github.com/hashicorp/go-version" + "github.com/hashicorp/terraform-plugin-framework/function" +) + +var ( + _ function.Function = SemVerVersionFunction{} +) + +func NewSemVerVersionFunction() function.Function { + return SemVerVersionFunction{} +} + +type SemVerVersionFunction struct{} + +func (r SemVerVersionFunction) Metadata(_ context.Context, req function.MetadataRequest, resp *function.MetadataResponse) { + resp.Name = "semver_version" +} + +func (r SemVerVersionFunction) Definition(_ context.Context, _ function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + Summary: "Check if a semver version is valid", + Parameters: []function.Parameter{ + function.StringParameter{ + AllowNullValue: false, + AllowUnknownValues: false, + Description: "The version to validate", + Name: "version", + }, + }, + Return: function.BoolReturn{}, + } +} + +func (r SemVerVersionFunction) Run(ctx context.Context, req function.RunRequest, resp *function.RunResponse) { + var semver string + + resp.Error = function.ConcatFuncErrors(req.Arguments.Get(ctx, &semver)) + if resp.Error != nil { + return + } + + _, err := version.NewVersion(semver) + resp.Error = function.ConcatFuncErrors(resp.Result.Set(ctx, err == nil)) +} diff --git a/internal/provider/semver_version_function_test.go b/internal/provider/semver_version_function_test.go new file mode 100644 index 0000000..49218fe --- /dev/null +++ b/internal/provider/semver_version_function_test.go @@ -0,0 +1,56 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package provider + +import ( + "testing" + + "github.com/hashicorp/go-version" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +func TestSemVerVersionFunction_trueCase(t *testing.T) { + t.Parallel() + resource.UnitTest(t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(version.Must(version.NewVersion(MinimalRequiredTerraformVersion))), + }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: ` +output "test" { + value = provider::assert::semver_version("1.1.5") +} + `, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckOutput("test", "true"), + ), + }, + }, + }) +} + +func TestSemVerVersionFunction_falseCase(t *testing.T) { + t.Parallel() + resource.UnitTest(t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(version.Must(version.NewVersion(MinimalRequiredTerraformVersion))), + }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: ` +output "test" { + value = provider::assert::semver_version("foobar") +} + `, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckOutput("test", "false"), + ), + }, + }, + }) +} diff --git a/templates/functions/semver_check.md.tmpl b/templates/functions/semver_check.md.tmpl new file mode 100644 index 0000000..d1c1d9c --- /dev/null +++ b/templates/functions/semver_check.md.tmpl @@ -0,0 +1,35 @@ +--- +page_title: "{{.Name}} {{.Type}} - {{.ProviderName}}" +subcategory: "SemVer Functions" +description: |- +{{ .Summary | plainmarkdown | trimspace | prefixlines " " }} +--- + +# {{.Type}}: {{.Name}} + +{{ .Description | trimspace }} + +{{ if .HasExample -}} +## Terraform Test Example + +{{tffile .ExampleFile }} +{{- end }} + +## Variable Validation Example + +{{ tffile (printf "examples/functions/%s/variable.tf" .Name)}} + +## Signature + +{{ .FunctionSignatureMarkdown }} + +## Arguments + +{{ .FunctionArgumentsMarkdown }} +{{ if .HasVariadic -}} +{{ .FunctionVariadicArgumentMarkdown }} +{{- end }} + +## Return Type + +The return type of `{{.Name}}` is a boolean. diff --git a/templates/functions/semver_constraint.md.tmpl b/templates/functions/semver_constraint.md.tmpl new file mode 100644 index 0000000..d1c1d9c --- /dev/null +++ b/templates/functions/semver_constraint.md.tmpl @@ -0,0 +1,35 @@ +--- +page_title: "{{.Name}} {{.Type}} - {{.ProviderName}}" +subcategory: "SemVer Functions" +description: |- +{{ .Summary | plainmarkdown | trimspace | prefixlines " " }} +--- + +# {{.Type}}: {{.Name}} + +{{ .Description | trimspace }} + +{{ if .HasExample -}} +## Terraform Test Example + +{{tffile .ExampleFile }} +{{- end }} + +## Variable Validation Example + +{{ tffile (printf "examples/functions/%s/variable.tf" .Name)}} + +## Signature + +{{ .FunctionSignatureMarkdown }} + +## Arguments + +{{ .FunctionArgumentsMarkdown }} +{{ if .HasVariadic -}} +{{ .FunctionVariadicArgumentMarkdown }} +{{- end }} + +## Return Type + +The return type of `{{.Name}}` is a boolean. diff --git a/templates/functions/semver_version.md.tmpl b/templates/functions/semver_version.md.tmpl new file mode 100644 index 0000000..d1c1d9c --- /dev/null +++ b/templates/functions/semver_version.md.tmpl @@ -0,0 +1,35 @@ +--- +page_title: "{{.Name}} {{.Type}} - {{.ProviderName}}" +subcategory: "SemVer Functions" +description: |- +{{ .Summary | plainmarkdown | trimspace | prefixlines " " }} +--- + +# {{.Type}}: {{.Name}} + +{{ .Description | trimspace }} + +{{ if .HasExample -}} +## Terraform Test Example + +{{tffile .ExampleFile }} +{{- end }} + +## Variable Validation Example + +{{ tffile (printf "examples/functions/%s/variable.tf" .Name)}} + +## Signature + +{{ .FunctionSignatureMarkdown }} + +## Arguments + +{{ .FunctionArgumentsMarkdown }} +{{ if .HasVariadic -}} +{{ .FunctionVariadicArgumentMarkdown }} +{{- end }} + +## Return Type + +The return type of `{{.Name}}` is a boolean.