Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) <TODO: YEAR AND NAME>
Copyright (c) 2025 philip bergman

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
121 changes: 103 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,29 +1,114 @@
DEVELOPER INSTRUCTIONS:
=======================
## Abstract Provider for `libdns`

This repo is a template for developers to use when creating new [libdns](https://github.com/libdns/libdns) provider implementations.
This package helps reduce duplicated and fragmented code across different DNS providers by implementing the core logic for the main `libdns` interfaces:

Be sure to update:
* `RecordGetter`
* `RecordAppender`
* `RecordSetter`
* `RecordDeleter`
* `ZoneLister`

- The package name
- The Go module name in go.mod
- The latest `libdns/libdns` version in go.mod
- All comments and documentation, including README below and godocs
- License (must be compatible with Apache/MIT)
- All "TODO:"s is in the code
- All methods that currently do nothing
As defined in the [libdns contracts](https://github.com/libdns/libdns/blob/master/libdns.go).

**Please be sure to conform to the semantics described at the [libdns godoc](https://github.com/libdns/libdns).**
It works on the principle that this *provider helper* fetches all records for a zone, generates a change list, and passes that list to the `client` to apply.

_Remove this section from the readme before publishing._
By doing so, the only thing you need to implement is a [`client`](client.go).
This approach allows faster development of new providers and ensures more consistent behavior, since all contract logic is handled and maintained in one central place.


---

## Client

A client implementation should follow this interface signature:

```go
GetDNSList(ctx context.Context, domain string) ([]libdns.Record, error)
SetDNSList(ctx context.Context, domain string, change ChangeList) ([]libdns.Record, error)
```

A simple implementation could look like this:

```go
func (c *client) create(ctx context.Context, domain string, record *libdns.RR) error {
// ...
return nil
}

func (c *client) remove(ctx context.Context, domain string, record *libdns.RR) error {
// ...
return nil
}

func (c *client) SetDNSList(ctx context.Context, domain string, change ChangeList) ([]libdns.Record, error) {

for record := range change.Iterate(provider.Delete) {
if err := c.remove(ctx, domain, record); err != nil {
return nil, err
}
}

for record := range change.Iterate(provider.Create) {
if err := c.create(ctx, domain, record); err != nil {
return nil, err
}
}

return nil, nil
}
```

---

\<PROVIDER NAME\> for [`libdns`](https://github.com/libdns/libdns)
=======================
## Provider

Because Go doesn’t support class-level abstraction, this package provides helper functions that your provider can call directly:

```go
type Provider struct {
client Client
mutex sync.RWMutex
}

func (p *Provider) getClient() Client {
// initialize client...
return p.client
}

func (p *Provider) GetRecords(ctx context.Context, zone string) ([]libdns.Record, error) {
return GetRecords(ctx, &p.mutex, p.getClient(), zone)
}

func (p *Provider) AppendRecords(ctx context.Context, zone string, recs []libdns.Record) ([]libdns.Record, error) {
return AppendRecords(ctx, &p.mutex, p.getClient(), zone, recs)
}

[![Go Reference](https://pkg.go.dev/badge/test.svg)](https://pkg.go.dev/github.com/libdns/TODO:PROVIDER_NAME)
func (p *Provider) SetRecords(ctx context.Context, zone string, recs []libdns.Record) ([]libdns.Record, error) {
return SetRecords(ctx, &p.mutex, p.getClient(), zone, recs)
}

This package implements the [libdns interfaces](https://github.com/libdns/libdns) for \<PROVIDER\>, allowing you to manage DNS records.
func (p *Provider) DeleteRecords(ctx context.Context, zone string, recs []libdns.Record) ([]libdns.Record, error) {
return DeleteRecords(ctx, &p.mutex, p.getClient(), zone, recs)
}

TODO: Show how to configure and use. Explain any caveats.
var (
_ libdns.RecordGetter = (*Provider)(nil)
_ libdns.RecordAppender = (*Provider)(nil)
_ libdns.RecordSetter = (*Provider)(nil)
_ libdns.RecordDeleter = (*Provider)(nil)
)
```

---

### Implemented Interfaces

| Interface | Implementation Function |
| ----------------------- | --------------------------------- |
| `libdns.RecordGetter` | [GetRecords](record_get.go) |
| `libdns.RecordAppender` | [AppendRecords](record_get.go) |
| `libdns.RecordSetter` | [SetRecords](record_set.go) |
| `libdns.RecordDeleter` | [DeleteRecords](record_delete.go) |
| `libdns.ZoneLister` | [ListZones](zone_list.go) |

---
113 changes: 113 additions & 0 deletions change_list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package provider

import (
"iter"

"github.com/libdns/libdns"
)

type ChangeState uint8

const (
NoChange ChangeState = 1 << iota
Delete
Create
)

type ChangeRecord struct {
record *libdns.RR
state ChangeState
}

type ChangeList interface {
// Iterate wil return an iterator that returns records that
// match the given state. For example, when called like
// `Iterate(Delete)` will only return records marked for
// removal. The ChangeState can be combined to iterate
// multiple states like `Iterate(Delete|Create)` which
// will return all records that are marked delete or
// as created.
Iterate(state ChangeState) iter.Seq[*libdns.RR]
// Creates will return a slice of records that are
// marked for creating
Creates() []*libdns.RR
// Deletes will return a slice of records that are
// marked for deleting
Deletes() []*libdns.RR
// GetList will return a slice of records that
// represents the new dns list which can be used
// to update the whole set for a zone
GetList() []*libdns.RR
// Has wil check if this list has records for
// given state
Has(state ChangeState) bool
// addRecord is not exported because the record
// list is immutable
addRecord(record *libdns.RR, state ChangeState)
}

type changes struct {
records []*ChangeRecord
state ChangeState
}

func NewChangeList(size ...int) ChangeList {

var records []*ChangeRecord

switch len(size) {
case 1:
records = make([]*ChangeRecord, size[0])
case 2:
records = make([]*ChangeRecord, size[0], size[1])
default:
records = make([]*ChangeRecord, 0)
}

return &changes{
records: records,
}
}

func (c *changes) addRecord(record *libdns.RR, state ChangeState) {
c.records = append(c.records, &ChangeRecord{record: record, state: state})
c.state |= state
}

func (c *changes) Has(state ChangeState) bool {
return 0 != (c.state & state)
}

func (c *changes) Iterate(state ChangeState) iter.Seq[*libdns.RR] {
return func(yield func(*libdns.RR) bool) {
for i, x := 0, len(c.records); i < x; i++ {
if c.records[i].state == (c.records[i].state & state) {
if false == yield(c.records[i].record) {
return
}
}
}
}
}

func (c *changes) Creates() []*libdns.RR {
return c.list(Create)
}

func (c *changes) Deletes() []*libdns.RR {
return c.list(Delete)
}

func (c *changes) GetList() []*libdns.RR {
return c.list(Create | NoChange)
}

func (c *changes) list(state ChangeState) []*libdns.RR {
var items = make([]*libdns.RR, 0)

for record := range c.Iterate(state) {
items = append(items, record)
}

return items
}
57 changes: 57 additions & 0 deletions client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package provider

import (
"context"

"github.com/libdns/libdns"
)

type Domain interface {
Name() string
}

type Client interface {
// GetDNSList returns all DNS records available for the given zone.
//
// The returned records can be of the opaque RR type. If the provider supports
// parsing, the records will be automatically parsed before being returned.
GetDNSList(ctx context.Context, domain string) ([]libdns.Record, error)

// SetDNSList processes a ChangeList and updates DNS records based on their state.
//
// This allows the client to focus only on handling the changes, while the provider
// logic for appending, setting, and deleting records is centralized.
//
// Example: iterating through individual changes
//
// // Remove records marked for deletion
// for remove := range change.Iterate(Delete) {
// // remove record
// }
//
// // Create records marked for creation
// for create := range change.Iterate(Create) {
// // create record
// }
//
// Example: updating the whole zone at once
//
// // Generate a filtered list of all changes
// updatedRecords := change.GetList()
//
// // Use this list to update the entire zone file in a single call
// client.UpdateZone(ctx, domain, updatedRecords)
//
// Notes:
// - If the client API supports full-zone updates and returns the new record set,
// this can be returned. The provider uses this to validate records and skip
// extra API calls.
// - For clients that do not support full-zone updates or handle records individually,
// returning nil is fine.
SetDNSList(ctx context.Context, domain string, change ChangeList) ([]libdns.Record, error)
}

type ZoneAwareClient interface {
Client
Domains(ctx context.Context) ([]Domain, error)
}
Loading