Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

function/rfc3339_parse: Add RFC3339 parsing function #280

Merged
merged 24 commits into from
Mar 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changes/unreleased/FEATURES-20240116-135142.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
kind: FEATURES
body: 'functions/rfc3339_parse: Added a new `rfc3339_parse` function that parses an
RFC3339 timestamp string and returns an object representation.'
time: 2024-01-16T13:51:42.329253-05:00
custom:
Issue: "280"
12 changes: 11 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,13 @@ jobs:
with:
version: latest

# TODO: Temporary addition to ensure plugin-docs uses the v1.8.0-beta1 version of Terraform. This should be reverted once Terraform v1.8.0 is released.
- name: Setup Terraform (supporting provider functions)
uses: hashicorp/setup-terraform@a1502cd9e758c50496cc9ac5308c4843bcd56d36 # v3.0.0
with:
terraform_version: v1.8.0-beta1
terraform_wrapper: false

- name: Generate
run: make generate

Expand Down Expand Up @@ -73,7 +80,10 @@ jobs:
- name: Setup Terraform ${{ matrix.terraform }}
uses: hashicorp/setup-terraform@a1502cd9e758c50496cc9ac5308c4843bcd56d36 # v3.0.0
with:
terraform_version: ${{ matrix.terraform }}.*
# TODO: Temporary change has been made to `vars.TF_VERSIONS_PROTOCOL_V5` to include the `.*` to enable us
# to utilize the v1.8.0-beta1 version of Terraform. This should be reverted once Terraform v1.8.0 is released.
# terraform_version: ${{ matrix.terraform }}.*
terraform_version: ${{ matrix.terraform }}
terraform_wrapper: false

- name: Run acceptance test
Expand Down
60 changes: 60 additions & 0 deletions docs/functions/rfc3339_parse.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
page_title: "rfc3339_parse function - terraform-provider-time"
subcategory: ""
description: |-
Parse an RFC3339 timestamp string into an object
---

# function: rfc3339_parse

Given an RFC3339 timestamp string, will parse and return an object representation of that date and time.

## Example Usage

```terraform
# Configuration using provider functions must include required_providers configuration.
terraform {
required_providers {
time = {
source = "hashicorp/time"
# Setting the provider version is a strongly recommended practice
# version = "..."
}
}
# Provider functions require Terraform 1.8 and later.
required_version = ">= 1.8.0"
}

output "example_output" {
value = provider::time::rfc3339_parse("2023-07-25T23:43:16Z")
}
```

## Signature

<!-- signature generated by tfplugindocs -->
```text
rfc3339_parse(timestamp string) object
```

## Arguments

<!-- arguments generated by tfplugindocs -->
1. `timestamp` (String) RFC3339 timestamp string to parse


## Return Type

The `object` returned from `rfc3339_parse` has the following attributes:
- `year` (Number) The year for the timestamp.
- `year_day` (Number) The day of the year for the timestamp, in the range [1, 365] for non-leap years, and [1, 366] in leap years.
- `day` (Number) The day of the month for the timestamp.
- `month` (Number) The month of the year for the timestamp.
- `month_name` (String) The name of the month for the timestamp (ex. "January").
- `weekday` (Number) The day of the week for the timestamp.
- `weekday_name` (String) The name of the day for the timestamp (ex. "Sunday").
- `hour` (Number) The hour within the day for the timestamp, in the range [0, 23].
- `minute` (Number) The minute offset within the hour for the timestamp, in the range [0, 59].
- `second` (Number) The second offset within the minute for the timestamp, in the range [0, 59].
- `unix` (Number) The number of seconds elapsed since January 1, 1970 UTC.
- `iso_year` (Number) The ISO 8601 year number.
- `iso_week` (Number) The ISO 8601 week number.
16 changes: 16 additions & 0 deletions examples/functions/rfc3339_parse/function.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Configuration using provider functions must include required_providers configuration.
terraform {
required_providers {
time = {
source = "hashicorp/time"
# Setting the provider version is a strongly recommended practice
# version = "..."
}
}
# Provider functions require Terraform 1.8 and later.
required_version = ">= 1.8.0"
}

output "example_output" {
value = provider::time::rfc3339_parse("2023-07-25T23:43:16Z")
}
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ go 1.21
toolchain go1.21.6

require (
github.com/hashicorp/go-version v1.6.0
github.com/hashicorp/terraform-plugin-framework v1.6.1
github.com/hashicorp/terraform-plugin-framework-timetypes v0.3.0
github.com/hashicorp/terraform-plugin-framework-validators v0.12.0
github.com/hashicorp/terraform-plugin-go v0.22.0
github.com/hashicorp/terraform-plugin-log v0.9.0
github.com/hashicorp/terraform-plugin-testing v1.7.0
)

Expand All @@ -28,13 +30,11 @@ require (
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/go-plugin v1.6.0 // indirect
github.com/hashicorp/go-uuid v1.0.3 // indirect
github.com/hashicorp/go-version v1.6.0 // indirect
github.com/hashicorp/hc-install v0.6.3 // indirect
github.com/hashicorp/hcl/v2 v2.20.0 // indirect
github.com/hashicorp/logutils v1.0.0 // indirect
github.com/hashicorp/terraform-exec v0.20.0 // indirect
github.com/hashicorp/terraform-json v0.21.0 // indirect
github.com/hashicorp/terraform-plugin-log v0.9.0 // indirect
github.com/hashicorp/terraform-plugin-sdk/v2 v2.33.0 // indirect
github.com/hashicorp/terraform-registry-address v0.2.3 // indirect
github.com/hashicorp/terraform-svchost v0.1.1 // indirect
Expand Down
107 changes: 107 additions & 0 deletions internal/provider/function_rfc3339_parse.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package provider

import (
"context"
"fmt"
"time"

"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/function"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-log/tflog"
)

var rfc3339ReturnAttrTypes = map[string]attr.Type{
"year": types.Int64Type,
"year_day": types.Int64Type,
"day": types.Int64Type,
"month": types.Int64Type,
"month_name": types.StringType,
"weekday": types.Int64Type,
"weekday_name": types.StringType,
"hour": types.Int64Type,
"minute": types.Int64Type,
"second": types.Int64Type,
"unix": types.Int64Type,
"iso_year": types.Int64Type,
"iso_week": types.Int64Type,
}

var _ function.Function = &RFC3339ParseFunction{}

type RFC3339ParseFunction struct{}

func NewRFC3339ParseFunction() function.Function {
return &RFC3339ParseFunction{}
}

func (f *RFC3339ParseFunction) Metadata(ctx context.Context, req function.MetadataRequest, resp *function.MetadataResponse) {
resp.Name = "rfc3339_parse"
}

func (f *RFC3339ParseFunction) Definition(ctx context.Context, req function.DefinitionRequest, resp *function.DefinitionResponse) {
resp.Definition = function.Definition{
Summary: "Parse an RFC3339 timestamp string into an object",
Description: "Given an RFC3339 timestamp string, will parse and return an object representation of that date and time.",

Parameters: []function.Parameter{
function.StringParameter{
Name: "timestamp",
Description: "RFC3339 timestamp string to parse",
},
},
Return: function.ObjectReturn{
AttributeTypes: rfc3339ReturnAttrTypes,
},
}
}

func (f *RFC3339ParseFunction) Run(ctx context.Context, req function.RunRequest, resp *function.RunResponse) {
var timestamp string

resp.Error = req.Arguments.Get(ctx, &timestamp)
if resp.Error != nil {
return
}

rfc3339, err := time.Parse(time.RFC3339, timestamp)
if err != nil {
// Intentionally not including the Go parse error in the return diagnostic, as the message is based on a Go-specific
// reference time that may be unfamiliar to practitioners
tflog.Error(ctx, fmt.Sprintf("failed to parse RFC3339 timestamp, underlying time.Time error: %s", err.Error()))

resp.Error = function.NewArgumentFuncError(0, fmt.Sprintf("Error parsing RFC3339 timestamp: %q is not a valid RFC3339 timestamp", timestamp))
return
}

isoYear, isoWeek := rfc3339.ISOWeek()
austinvalle marked this conversation as resolved.
Show resolved Hide resolved

rfc3339Obj, diags := types.ObjectValue(
rfc3339ReturnAttrTypes,
map[string]attr.Value{
"year": types.Int64Value(int64(rfc3339.Year())),
"year_day": types.Int64Value(int64(rfc3339.YearDay())),
"day": types.Int64Value(int64(rfc3339.Day())),
"month": types.Int64Value(int64(rfc3339.Month())),
"month_name": types.StringValue(rfc3339.Month().String()),
"weekday": types.Int64Value(int64(rfc3339.Weekday())),
"weekday_name": types.StringValue(rfc3339.Weekday().String()),
"hour": types.Int64Value(int64(rfc3339.Hour())),
"minute": types.Int64Value(int64(rfc3339.Minute())),
"second": types.Int64Value(int64(rfc3339.Second())),
"unix": types.Int64Value(rfc3339.Unix()),
"iso_year": types.Int64Value(int64(isoYear)),
"iso_week": types.Int64Value(int64(isoWeek)),
},
)

resp.Error = function.FuncErrorFromDiags(ctx, diags)
if resp.Error != nil {
return
}

resp.Error = resp.Result.Set(ctx, &rfc3339Obj)
}
139 changes: 139 additions & 0 deletions internal/provider/function_rfc3339_parse_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package provider

import (
"regexp"
"testing"

"github.com/hashicorp/go-version"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
"github.com/hashicorp/terraform-plugin-testing/knownvalue"
"github.com/hashicorp/terraform-plugin-testing/plancheck"
"github.com/hashicorp/terraform-plugin-testing/tfversion"
)

func TestRFC3339Parse_UTC(t *testing.T) {
resource.UnitTest(t, resource.TestCase{
TerraformVersionChecks: []tfversion.TerraformVersionCheck{
// TODO: Replace with the stable v1.8.0 release when available
tfversion.SkipBelow(version.Must(version.NewVersion("v1.8.0-beta1"))),
},
ProtoV5ProviderFactories: protoV5ProviderFactories(),
Steps: []resource.TestStep{
{
Config: `
output "test" {
value = provider::time::rfc3339_parse("2023-07-25T23:43:16Z")
}
`,
ConfigPlanChecks: resource.ConfigPlanChecks{
PreApply: []plancheck.PlanCheck{
plancheck.ExpectKnownOutputValue("test", knownvalue.ObjectExact(
map[string]knownvalue.Check{
"day": knownvalue.Int64Exact(25),
"hour": knownvalue.Int64Exact(23),
"iso_week": knownvalue.Int64Exact(30),
"iso_year": knownvalue.Int64Exact(2023),
"minute": knownvalue.Int64Exact(43),
"month": knownvalue.Int64Exact(7),
"month_name": knownvalue.StringExact("July"),
"second": knownvalue.Int64Exact(16),
"unix": knownvalue.Int64Exact(1690328596),
"weekday": knownvalue.Int64Exact(2),
"weekday_name": knownvalue.StringExact("Tuesday"),
"year": knownvalue.Int64Exact(2023),
"year_day": knownvalue.Int64Exact(206),
},
)),
},
},
},
{
Config: `
output "test" {
value = provider::time::rfc3339_parse("2023-07-25T23:43:16-00:00")
}
`,
ConfigPlanChecks: resource.ConfigPlanChecks{
PreApply: []plancheck.PlanCheck{
plancheck.ExpectEmptyPlan(),
},
},
},
{
Config: `
output "test" {
value = provider::time::rfc3339_parse("2023-07-25T23:43:16+00:00")
}
`,
ConfigPlanChecks: resource.ConfigPlanChecks{
PreApply: []plancheck.PlanCheck{
plancheck.ExpectEmptyPlan(),
},
},
},
},
})
}

func TestRFC3339Parse_offset(t *testing.T) {
resource.UnitTest(t, resource.TestCase{
TerraformVersionChecks: []tfversion.TerraformVersionCheck{
// TODO: Replace with the stable v1.8.0 release when available
tfversion.SkipBelow(version.Must(version.NewVersion("v1.8.0-beta1"))),
},
ProtoV5ProviderFactories: protoV5ProviderFactories(),
Steps: []resource.TestStep{
{
Config: `
output "test" {
value = provider::time::rfc3339_parse("1996-12-19T16:39:57-08:00")
}
`,
ConfigPlanChecks: resource.ConfigPlanChecks{
PreApply: []plancheck.PlanCheck{
plancheck.ExpectKnownOutputValue("test", knownvalue.ObjectExact(
map[string]knownvalue.Check{
"day": knownvalue.Int64Exact(19),
"hour": knownvalue.Int64Exact(16),
"iso_week": knownvalue.Int64Exact(51),
"iso_year": knownvalue.Int64Exact(1996),
"minute": knownvalue.Int64Exact(39),
"month": knownvalue.Int64Exact(12),
"month_name": knownvalue.StringExact("December"),
"second": knownvalue.Int64Exact(57),
"unix": knownvalue.Int64Exact(851042397),
"weekday": knownvalue.Int64Exact(4),
"weekday_name": knownvalue.StringExact("Thursday"),
"year": knownvalue.Int64Exact(1996),
"year_day": knownvalue.Int64Exact(354),
},
)),
},
},
},
},
})
}

func TestRFC3339Parse_invalid(t *testing.T) {
resource.UnitTest(t, resource.TestCase{
TerraformVersionChecks: []tfversion.TerraformVersionCheck{
// TODO: Replace with the stable v1.8.0 release when available
tfversion.SkipBelow(version.Must(version.NewVersion("v1.8.0-beta1"))),
},
ProtoV5ProviderFactories: protoV5ProviderFactories(),
Steps: []resource.TestStep{
{
Config: `
output "test" {
value = provider::time::rfc3339_parse("abcdef")
}
`,
ExpectError: regexp.MustCompile(`"abcdef" is not a valid RFC3339 timestamp.`),
},
},
})
}
Loading
Loading