Skip to content

Commit c358c6f

Browse files
committed
Initial scaffold: Terraform provider for LogStruct with framework, data sources (struct, cloudwatch_filter), metadata loader, docs, goreleaser
0 parents  commit c358c6f

File tree

9 files changed

+483
-0
lines changed

9 files changed

+483
-0
lines changed

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
dist/
2+
bin/
3+
.terraform/
4+
.terraform.lock.hcl
5+
*.tfstate*
6+
**/.DS_Store

.goreleaser.yaml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
project_name: terraform-provider-logstruct
2+
before:
3+
hooks:
4+
- go mod tidy
5+
builds:
6+
- id: provider
7+
main: ./main.go
8+
binary: terraform-provider-logstruct
9+
env:
10+
- CGO_ENABLED=0
11+
goos: [linux, darwin, windows]
12+
goarch: [amd64, arm64]
13+
flags: ["-trimpath"]
14+
ldflags:
15+
- -s -w -X main.version={{.Version}}
16+
archives:
17+
- id: archive
18+
name_template: '{{ .ProjectName }}_v{{ .Version }}_{{ .Os }}_{{ .Arch }}'
19+
builds: [provider]
20+
format: zip
21+
checksum:
22+
name_template: '{{ .ProjectName }}_v{{ .Version }}_SHA256SUMS'
23+
signs:
24+
- artifacts: checksum
25+
release:
26+
draft: false
27+
prerelease: auto
28+
name_template: 'v{{ .Version }}'

README.md

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
# terraform-provider-logstruct
2+
3+
Terraform provider for LogStruct: type-safe CloudWatch filter patterns and LogStruct metadata validation at plan time.
4+
5+
## Features
6+
7+
- Validate that a `struct` and `event` combination is valid based on LogStruct's typed definitions.
8+
- Generate CloudWatch Logs filter patterns without stringly-typed values.
9+
- Fail fast during `terraform validate/plan` if LogStruct enums/keys drift.
10+
11+
## Data Sources
12+
13+
### `logstruct_struct`
14+
15+
Inputs:
16+
17+
- `struct` (string): e.g. `"ActionMailer"`
18+
19+
Outputs:
20+
21+
- `fixed_source` (string, null if not fixed)
22+
- `allowed_events` (list of strings)
23+
- `keys` (map): canonical key names, e.g. `evt`, `src`, etc.
24+
25+
### `logstruct_cloudwatch_filter`
26+
27+
Inputs:
28+
29+
- `struct` (string)
30+
- `event` (string, serialized value as emitted by LogStruct)
31+
- `predicates` (map(string => list(string)), optional): additional equality clauses
32+
33+
Outputs:
34+
35+
- `pattern` (string): CloudWatch filter pattern `{ $.src = "mailer" && $.evt = "delivered" ... }`
36+
37+
## Provider Configuration
38+
39+
```hcl
40+
provider "logstruct" {
41+
export_dir = "./site/lib/log-generation"
42+
}
43+
```
44+
45+
`export_dir` should contain:
46+
47+
- `sorbet-enums.json`
48+
- `sorbet-log-structs.json`
49+
- `log-keys.json`
50+
51+
These are generated by LogStruct's existing exporters.
52+
53+
## Example
54+
55+
```hcl
56+
data "logstruct_cloudwatch_filter" "email_delivered" {
57+
struct = "ActionMailer"
58+
event = "delivered"
59+
}
60+
61+
resource "aws_cloudwatch_log_metric_filter" "email_delivered_count" {
62+
name = "Email Delivered Count"
63+
log_group_name = var.log_group.docspring
64+
pattern = data.logstruct_cloudwatch_filter.email_delivered.pattern
65+
66+
metric_transformation {
67+
name = "docspring_email_delivered_count"
68+
namespace = var.namespace.logs
69+
value = "1"
70+
default_value = "0"
71+
unit = "Count"
72+
}
73+
}
74+
```
75+
76+
## Releasing
77+
78+
Use GoReleaser to build and publish GitHub releases with platform-specific zips and checksums. Tags must be semantic versions prefixed with `v` (e.g. `v0.1.0`).
79+

go.mod

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
module github.com/DocSpring/terraform-provider-logstruct
2+
3+
go 1.22.0
4+
5+
require (
6+
github.com/hashicorp/terraform-plugin-framework v1.11.0
7+
github.com/hashicorp/terraform-plugin-log/tflog v0.9.0
8+
)

main.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"flag"
6+
"log"
7+
8+
"github.com/hashicorp/terraform-plugin-framework/providerserver"
9+
"github.com/DocSpring/terraform-provider-logstruct/pkg/provider"
10+
)
11+
12+
// Set by goreleaser at build time
13+
var (
14+
version = "dev"
15+
)
16+
17+
func main() {
18+
var debug bool
19+
flag.BoolVar(&debug, "debug", false, "Enable debug mode.")
20+
flag.Parse()
21+
22+
if err := providerserver.Serve(context.Background(), provider.New(version), providerserver.ServeOpts{
23+
Address: "github.com/DocSpring/logstruct",
24+
Debug: debug,
25+
}); err != nil {
26+
log.Fatal(err)
27+
}
28+
}
29+
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package provider
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"sort"
7+
"strings"
8+
9+
"github.com/hashicorp/terraform-plugin-framework/datasource"
10+
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
11+
"github.com/hashicorp/terraform-plugin-framework/types"
12+
)
13+
14+
type cwFilterDataSource struct{}
15+
16+
func NewCloudWatchFilterDataSource() datasource.DataSource { return &cwFilterDataSource{} }
17+
18+
type cwFilterModel struct {
19+
Struct types.String `tfsdk:"struct"`
20+
Event types.String `tfsdk:"event"`
21+
Predicates map[string][]string `tfsdk:"predicates"`
22+
23+
Pattern types.String `tfsdk:"pattern"`
24+
}
25+
26+
func (d *cwFilterDataSource) Metadata(_ context.Context, _ datasource.MetadataRequest, resp *datasource.MetadataResponse) {
27+
resp.TypeName = "logstruct_cloudwatch_filter"
28+
}
29+
30+
func (d *cwFilterDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
31+
resp.Schema = schema.Schema{
32+
Attributes: map[string]schema.Attribute{
33+
"struct": schema.StringAttribute{Required: true, Description: "LogStruct struct name e.g. ActionMailer"},
34+
"event": schema.StringAttribute{Required: true, Description: "Event value (serialized), validated against struct"},
35+
"predicates": schema.MapAttribute{
36+
Optional: true,
37+
ElementType: types.ListType{ElemType: types.StringType},
38+
Description: "Additional equality predicates map[field] = [values...]",
39+
},
40+
"pattern": schema.StringAttribute{Computed: true, Description: "Generated CloudWatch Logs filter pattern"},
41+
},
42+
}
43+
}
44+
45+
func (d *cwFilterDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
46+
var data cwFilterModel
47+
diags := req.Config.Get(ctx, &data)
48+
resp.Diagnostics.Append(diags...)
49+
if resp.Diagnostics.HasError() { return }
50+
51+
client, ok := req.ProviderData.(*MetadataClient)
52+
if !ok || client == nil {
53+
resp.Diagnostics.AddError("Provider not configured", "Missing metadata client")
54+
return
55+
}
56+
structName := data.Struct.ValueString()
57+
event := data.Event.ValueString()
58+
allowed, _, err := client.AllowedEventsForStruct(structName)
59+
if err != nil { resp.Diagnostics.AddError("Lookup error", err.Error()); return }
60+
if !contains(allowed, event) {
61+
resp.Diagnostics.AddError("Invalid event for struct", fmt.Sprintf("event %q not allowed for %s (allowed: %v)", event, structName, allowed))
62+
return
63+
}
64+
var parts []string
65+
// add evt
66+
evtKey, ok := client.Keys["evt"]
67+
if !ok { resp.Diagnostics.AddError("Missing key", "evt key missing from exports"); return }
68+
parts = append(parts, fmt.Sprintf("$.%s = \"%s\"", evtKey, event))
69+
// add source
70+
if src, fixed, err := client.FixedSourceForStruct(structName); err != nil {
71+
resp.Diagnostics.AddError("Lookup error", err.Error()); return
72+
} else if fixed {
73+
srcKey, ok := client.Keys["src"]; if !ok { resp.Diagnostics.AddError("Missing key", "src key missing from exports"); return }
74+
parts = append(parts, fmt.Sprintf("$.%s = \"%s\"", srcKey, src))
75+
}
76+
// add extra predicates
77+
if data.Predicates != nil {
78+
keys := make([]string, 0, len(data.Predicates))
79+
for k := range data.Predicates { keys = append(keys, k) }
80+
sort.Strings(keys)
81+
for _, k := range keys {
82+
values := data.Predicates[k]
83+
if len(values) == 0 { continue }
84+
keyPath := fmt.Sprintf("$.%s", k)
85+
if len(values) == 1 {
86+
parts = append(parts, fmt.Sprintf("%s = \"%s\"", keyPath, escape(values[0])))
87+
} else {
88+
ors := make([]string, 0, len(values))
89+
for _, v := range values { ors = append(ors, fmt.Sprintf("%s = \"%s\"", keyPath, escape(v))) }
90+
parts = append(parts, fmt.Sprintf("(%s)", strings.Join(ors, " || ")))
91+
}
92+
}
93+
}
94+
pattern := fmt.Sprintf("{ %s }", strings.Join(parts, " && "))
95+
data.Pattern = types.StringValue(pattern)
96+
97+
diags = resp.State.Set(ctx, &data)
98+
resp.Diagnostics.Append(diags...)
99+
}
100+
101+
func contains(arr []string, s string) bool { for _, v := range arr { if v == s { return true } }; return false }
102+
func escape(s string) string { return strings.ReplaceAll(s, "\"", "\\\"") }
103+

pkg/provider/datasource_struct.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package provider
2+
3+
import (
4+
"context"
5+
6+
"github.com/hashicorp/terraform-plugin-framework/datasource"
7+
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
8+
"github.com/hashicorp/terraform-plugin-framework/types"
9+
)
10+
11+
type structDataSource struct{}
12+
13+
func NewStructDataSource() datasource.DataSource { return &structDataSource{} }
14+
15+
type structDataModel struct {
16+
Struct types.String `tfsdk:"struct"`
17+
FixedSource types.String `tfsdk:"fixed_source"`
18+
AllowedEvents []types.String `tfsdk:"allowed_events"`
19+
Keys types.Map `tfsdk:"keys"`
20+
}
21+
22+
func (d *structDataSource) Metadata(_ context.Context, _ datasource.MetadataRequest, resp *datasource.MetadataResponse) {
23+
resp.TypeName = "logstruct_struct"
24+
}
25+
26+
func (d *structDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
27+
resp.Schema = schema.Schema{
28+
Attributes: map[string]schema.Attribute{
29+
"struct": schema.StringAttribute{Required: true, Description: "LogStruct struct name e.g. ActionMailer"},
30+
"fixed_source": schema.StringAttribute{Computed: true},
31+
"allowed_events": schema.ListAttribute{Computed: true, ElementType: types.StringType},
32+
"keys": schema.MapAttribute{Computed: true, ElementType: types.StringType},
33+
},
34+
}
35+
}
36+
37+
func (d *structDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
38+
var data structDataModel
39+
diags := req.Config.Get(ctx, &data)
40+
resp.Diagnostics.Append(diags...)
41+
if resp.Diagnostics.HasError() { return }
42+
43+
client, ok := req.ProviderData.(*MetadataClient)
44+
if !ok || client == nil {
45+
resp.Diagnostics.AddError("Provider not configured", "Missing metadata client")
46+
return
47+
}
48+
allowed, single, err := client.AllowedEventsForStruct(data.Struct.ValueString())
49+
if err != nil { resp.Diagnostics.AddError("Lookup error", err.Error()); return }
50+
if src, fixed, err := client.FixedSourceForStruct(data.Struct.ValueString()); err != nil {
51+
resp.Diagnostics.AddError("Lookup error", err.Error()); return
52+
} else if fixed {
53+
data.FixedSource = types.StringValue(src)
54+
} else {
55+
data.FixedSource = types.StringNull()
56+
}
57+
// events
58+
data.AllowedEvents = []types.String{}
59+
for _, e := range allowed { data.AllowedEvents = append(data.AllowedEvents, types.StringValue(e)) }
60+
_ = single
61+
// keys map
62+
kv := map[string]types.Value{}
63+
for k, v := range client.Keys { kv[k] = types.StringValue(v) }
64+
m, md := types.MapValue(types.StringType, kv)
65+
resp.Diagnostics.Append(md...)
66+
data.Keys = m
67+
68+
diags = resp.State.Set(ctx, &data)
69+
resp.Diagnostics.Append(diags...)
70+
}
71+

0 commit comments

Comments
 (0)