Files
terraform-provider-kea/internal/provider/conversions.go
T
unkinben 7c851b8df5
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
Add terraform-provider-kea
Terraform/OpenTofu provider wrapping the kea-operator KeaAPI, modelled on
terraform-provider-encapi.

- add kea_subnet and kea_clientclass resources with full CRUD over the
  PUT/GET/DELETE /api/v1/{subnets,clientclasses}/{name} contract
- add provider config (endpoint + bearer token, KEA_API_TOKEN fallback);
  404 on read removes the resource from state
- add unit tests against httptest mock servers (client, wire round-trip,
  type conversions, schemas)
- add Makefile (patch|minor|major + package) and .woodpecker CI mirroring
  terraform-provider-encapi; tag release PUTs the zip to the artifactapi
  terraform-unkin registry under unkin/kea

Claude-Session: https://claude.ai/code/session_01JUoARVdmhxKQHyyyp1pxeT
2026-08-02 19:40:49 +10:00

126 lines
3.8 KiB
Go

package provider
import (
"context"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
)
// optionDataSchema is the shared option_data nested-list attribute used by both
// resources. Each element is a DHCP option (name/code/space + required data).
func optionDataSchema(desc string) schema.ListNestedAttribute {
return schema.ListNestedAttribute{
Description: desc,
Optional: true,
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"name": schema.StringAttribute{Description: "Option name.", Optional: true},
"code": schema.Int64Attribute{Description: "Numeric option code.", Optional: true},
"space": schema.StringAttribute{Description: "Option space.", Optional: true},
"data": schema.StringAttribute{Description: "Option value.", Required: true},
},
},
}
}
// optionDataModel is the nested-attribute form of a DHCP option value.
type optionDataModel struct {
Name types.String `tfsdk:"name"`
Code types.Int64 `tfsdk:"code"`
Space types.String `tfsdk:"space"`
Data types.String `tfsdk:"data"`
}
func optionDataObjectType() types.ObjectType {
return types.ObjectType{AttrTypes: map[string]attr.Type{
"name": types.StringType,
"code": types.Int64Type,
"space": types.StringType,
"data": types.StringType,
}}
}
// stringOrNull maps an omitempty API string to a Terraform value: empty -> null.
func stringOrNull(s string) types.String {
if s == "" {
return types.StringNull()
}
return types.StringValue(s)
}
// int64OrNull maps an omitempty API int to a Terraform value: zero -> null.
func int64OrNull(i int) types.Int64 {
if i == 0 {
return types.Int64Null()
}
return types.Int64Value(int64(i))
}
// listToStrings reads a Terraform list attribute into a Go slice. A null or
// unknown list yields nil, so it round-trips against the API's omitempty fields.
func listToStrings(ctx context.Context, l types.List, diags *diag.Diagnostics) []string {
if l.IsNull() || l.IsUnknown() {
return nil
}
out := make([]string, 0, len(l.Elements()))
diags.Append(l.ElementsAs(ctx, &out, false)...)
return out
}
// stringsToList renders an API slice as a Terraform list. An empty/nil slice
// becomes a null list so it compares equal to an omitted config attribute.
func stringsToList(s []string) types.List {
if len(s) == 0 {
return types.ListNull(types.StringType)
}
elems := make([]attr.Value, 0, len(s))
for _, v := range s {
elems = append(elems, types.StringValue(v))
}
return types.ListValueMust(types.StringType, elems)
}
// optionDataToAPI reads the option_data nested list into wire structs.
func optionDataToAPI(ctx context.Context, l types.List, diags *diag.Diagnostics) []optionDataAPI {
if l.IsNull() || l.IsUnknown() {
return nil
}
var models []optionDataModel
diags.Append(l.ElementsAs(ctx, &models, false)...)
if diags.HasError() {
return nil
}
out := make([]optionDataAPI, 0, len(models))
for _, m := range models {
out = append(out, optionDataAPI{
Name: m.Name.ValueString(),
Code: int(m.Code.ValueInt64()),
Space: m.Space.ValueString(),
Data: m.Data.ValueString(),
})
}
return out
}
// optionDataToList renders wire structs back into a nested list. Empty -> null.
func optionDataToList(in []optionDataAPI) types.List {
t := optionDataObjectType()
if len(in) == 0 {
return types.ListNull(t)
}
elems := make([]attr.Value, 0, len(in))
for _, o := range in {
obj := types.ObjectValueMust(t.AttrTypes, map[string]attr.Value{
"name": stringOrNull(o.Name),
"code": int64OrNull(o.Code),
"space": stringOrNull(o.Space),
"data": types.StringValue(o.Data),
})
elems = append(elems, obj)
}
return types.ListValueMust(t, elems)
}