Skip to content

Commit 7b0d451

Browse files
committed
Move repo
0 parents  commit 7b0d451

File tree

13 files changed

+1977
-0
lines changed

13 files changed

+1977
-0
lines changed

.env.example

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Copy this file to .env and fill with real values for local testing.
2+
# Do NOT commit a populated .env file. Add .env to .gitignore.
3+
4+
LIBDNS_SPACESHIP_APIKEY=your_api_key_here
5+
LIBDNS_SPACESHIP_APISECRET=your_api_secret_here
6+
LIBDNS_SPACESHIP_ZONE=example.com
7+
LIBDNS_SPACESHIP_BASEURL=
8+
LIBDNS_SPACESHIP_PAGESIZE=100
9+
LIBDNS_SPACESHIP_TIMEOUT=30

.github/workflows/ci.yml

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [ master ]
6+
pull_request:
7+
branches: [ master ]
8+
9+
jobs:
10+
test:
11+
runs-on: ubuntu-latest
12+
env:
13+
LIBDNS_SPACESHIP_APIKEY: ${{ secrets.LIBDNS_SPACESHIP_APIKEY }}
14+
LIBDNS_SPACESHIP_APISECRET: ${{ secrets.LIBDNS_SPACESHIP_APISECRET }}
15+
LIBDNS_SPACESHIP_ZONE: ${{ secrets.LIBDNS_SPACESHIP_ZONE }}
16+
LIBDNS_SPACESHIP_BASEURL: ${{ secrets.LIBDNS_SPACESHIP_BASEURL }}
17+
LIBDNS_SPACESHIP_PAGESIZE: ${{ secrets.LIBDNS_SPACESHIP_PAGESIZE }}
18+
LIBDNS_SPACESHIP_TIMEOUT: ${{ secrets.LIBDNS_SPACESHIP_TIMEOUT }}
19+
20+
steps:
21+
- name: Check out
22+
uses: actions/checkout@v4
23+
24+
- name: Set up Go
25+
uses: actions/setup-go@v4
26+
with:
27+
go-version: '1.18'
28+
29+
- name: Cache Go modules
30+
uses: actions/cache@v3
31+
with:
32+
path: |
33+
~/.cache/go-build
34+
${{ github.workspace }}/pkg/mod
35+
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
36+
restore-keys: |
37+
${{ runner.os }}-go-
38+
39+
- name: Build
40+
run: go build ./...
41+
42+
- name: Run tests
43+
run: |
44+
if [[ -n "$LIBDNS_SPACESHIP_APIKEY" && -n "$LIBDNS_SPACESHIP_APISECRET" ]]; then
45+
echo "Running live tests with API credentials"
46+
LIBDNS_SPACESHIP_RUN_LIVE=1 go test -v ./...
47+
else
48+
echo "Running tests without API credentials"
49+
go test -v ./...
50+
fi

.gitignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
.env
2+
# Binaries
3+
bin/
4+
oryxBuildBinary
5+
# Vendor
6+
vendor/
7+
# Go workspace file
8+
go.work

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2025 Redth
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
Spaceship for [`libdns`](https://github.com/libdns/libdns)
2+
=======================
3+
4+
[![Go Reference](https://pkg.go.dev/badge/test.svg)](https://pkg.go.dev/github.com/Redth/libdns-spaceship)
5+
6+
This package implements the [libdns interfaces](https://github.com/libdns/libdns) for Spaceship, allowing you to manage DNS records.
7+
8+
## Configuration
9+
10+
To use this provider, you need a Spaceship API key and secret. Configure the provider as follows:
11+
12+
```go
13+
provider := &libdnsspaceship.Provider{
14+
APIKey: "your-spaceship-api-key",
15+
APISecret: "your-spaceship-api-secret",
16+
}
17+
```
18+
19+
Optionally, you can customize the API base URL (defaults to `https://spaceship.dev/api`):
20+
21+
```go
22+
provider := &libdnsspaceship.Provider{
23+
APIKey: "your-spaceship-api-key",
24+
APISecret: "your-spaceship-api-secret",
25+
BaseURL: "https://custom-api.spaceship.com",
26+
}
27+
```
28+
29+
### Environment Variables
30+
31+
Alternatively, you can use environment variables or the `NewProviderFromEnv()` constructor to configure the provider:
32+
33+
```go
34+
provider := libdnsspaceship.NewProviderFromEnv()
35+
```
36+
37+
The following environment variables are supported:
38+
39+
| Environment Variable | Description | Required | Default |
40+
|---------------------|-------------|----------|---------|
41+
| `LIBDNS_SPACESHIP_APIKEY` | Your Spaceship API key | Yes | - |
42+
| `LIBDNS_SPACESHIP_APISECRET` | Your Spaceship API secret | Yes | - |
43+
| `LIBDNS_SPACESHIP_BASEURL` | Custom API base URL | No | `https://spaceship.dev/api` |
44+
| `LIBDNS_SPACESHIP_PAGESIZE` | Page size for GetRecords pagination | No | 100 |
45+
| `LIBDNS_SPACESHIP_TIMEOUT` | HTTP client timeout in seconds | No | 30 |
46+
47+
### Example .env file
48+
49+
```bash
50+
LIBDNS_SPACESHIP_APIKEY=your_api_key_here
51+
LIBDNS_SPACESHIP_APISECRET=your_api_secret_here
52+
LIBDNS_SPACESHIP_BASEURL=https://spaceship.dev/api
53+
LIBDNS_SPACESHIP_PAGESIZE=100
54+
LIBDNS_SPACESHIP_TIMEOUT=30
55+
```
56+
57+
## Usage
58+
59+
```go
60+
package main
61+
62+
import (
63+
"context"
64+
"time"
65+
66+
"github.com/Redth/libdns-spaceship"
67+
"github.com/libdns/libdns"
68+
)
69+
70+
func main() {
71+
provider := &libdnsspaceship.Provider{
72+
APIKey: "your-spaceship-api-key",
73+
APISecret: "your-spaceship-api-secret",
74+
}
75+
76+
zone := "example.com."
77+
78+
// Get all records
79+
records, err := provider.GetRecords(context.TODO(), zone)
80+
if err != nil {
81+
panic(err)
82+
}
83+
84+
// Add a new A record
85+
newRecords := []libdns.Record{
86+
libdns.Address{
87+
Name: "test",
88+
TTL: 300 * time.Second,
89+
IP: netip.MustParseAddr("192.0.2.1"),
90+
},
91+
}
92+
93+
createdRecords, err := provider.AppendRecords(context.TODO(), zone, newRecords)
94+
if err != nil {
95+
panic(err)
96+
}
97+
}
98+
```
99+
100+
## Supported Record Types
101+
102+
This provider supports the following DNS record types:
103+
- A and AAAA records (`libdns.Address`)
104+
- TXT records (`libdns.TXT`)
105+
- CNAME records (`libdns.CNAME`)
106+
- MX records (`libdns.MX`)
107+
- SRV records (`libdns.SRV`)
108+
- NS records (`libdns.NS`)
109+
- CAA records (`libdns.CAA`)
110+
- HTTPS records (`libdns.ServiceBinding` with scheme "https")
111+
112+
Unsupported record types (such as PTR, TLSA, etc.) are filtered out and not returned by `GetRecords`.
113+
114+
## API Documentation
115+
116+
For more information about the Spaceship API, see the [official documentation](https://docs.spaceship.dev/).
117+
118+
## License
119+
120+
MIT License (See [LICENSE](./LICENSE) file)

conversions.go

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
package libdnsspaceship
2+
3+
import (
4+
"net/netip"
5+
"strconv"
6+
"strings"
7+
"time"
8+
9+
"github.com/libdns/libdns"
10+
)
11+
12+
// toLibdnsRR converts a spaceshipRecordUnion (API) to a libdns.Record
13+
func (p *Provider) toLibdnsRR(sr spaceshipRecordUnion, zone string) libdns.Record {
14+
// normalize name relative to zone
15+
name := strings.TrimSuffix(sr.Name, "."+zone)
16+
name = strings.TrimSuffix(name, ".")
17+
if name == zone || sr.Name == zone {
18+
name = ""
19+
}
20+
ttl := time.Duration(sr.TTL) * time.Second
21+
22+
switch strings.ToUpper(sr.Type) {
23+
case "A", "AAAA":
24+
if sr.Address != "" {
25+
if ip, err := netip.ParseAddr(sr.Address); err == nil {
26+
return libdns.Address{Name: name, TTL: ttl, IP: ip, ProviderData: sr}
27+
}
28+
}
29+
case "TXT":
30+
return libdns.TXT{Name: name, TTL: ttl, Text: sr.Value, ProviderData: sr}
31+
case "CNAME":
32+
return libdns.CNAME{Name: name, TTL: ttl, Target: sr.Cname, ProviderData: sr}
33+
case "MX":
34+
return libdns.MX{Name: name, TTL: ttl, Target: sr.Exchange, Preference: uint16(sr.Preference), ProviderData: sr}
35+
case "SRV":
36+
// extract service/transport from name if present
37+
service, transport := "", ""
38+
if sr.Name != "" {
39+
labels := strings.Split(sr.Name, ".")
40+
if len(labels) >= 2 {
41+
service = strings.TrimPrefix(labels[0], "_")
42+
transport = strings.TrimPrefix(labels[1], "_")
43+
}
44+
}
45+
port := sr.PortInt
46+
if port == 0 {
47+
switch pv := sr.Port.(type) {
48+
case string:
49+
if v, err := strconv.Atoi(strings.TrimPrefix(pv, "_")); err == nil {
50+
port = v
51+
}
52+
case float64:
53+
port = int(pv)
54+
case int:
55+
port = pv
56+
}
57+
}
58+
return libdns.SRV{Name: name, TTL: ttl, Service: service, Transport: transport, Priority: uint16(sr.Priority), Weight: uint16(sr.Weight), Port: uint16(port), Target: sr.Target, ProviderData: sr}
59+
case "NS":
60+
// Use libdns.NS for nameserver records
61+
return libdns.NS{Name: name, TTL: ttl, Target: sr.Nameserver, ProviderData: sr}
62+
case "CAA":
63+
// Use libdns.CAA as the typed representation
64+
// convert stored union fields into a libdns.CAA value
65+
flag := 0
66+
if sr.Flag != nil {
67+
flag = *sr.Flag
68+
}
69+
var f8 uint8
70+
if flag < 0 {
71+
f8 = 0
72+
} else if flag > 255 {
73+
f8 = 255
74+
} else {
75+
f8 = uint8(flag)
76+
}
77+
return libdns.CAA{Name: name, TTL: ttl, Flags: f8, Tag: sr.Tag, Value: sr.Value, ProviderData: sr}
78+
case "HTTPS":
79+
// Convert to libdns.ServiceBinding with scheme "https"
80+
var params libdns.SvcParams
81+
if sr.SvcParams != "" {
82+
if p, err := libdns.ParseSvcParams(sr.SvcParams); err == nil {
83+
params = p
84+
}
85+
}
86+
target := sr.SvcTarget
87+
if target == "" {
88+
target = sr.TargetName
89+
}
90+
return libdns.ServiceBinding{
91+
Name: name,
92+
TTL: ttl,
93+
Scheme: "https",
94+
Priority: uint16(sr.SvcPriority),
95+
Target: target,
96+
Params: params,
97+
ProviderData: sr,
98+
}
99+
}
100+
// Return nil for unsupported record types (including PTR) - they will be filtered out
101+
return nil
102+
}
103+
104+
// fromLibdnsRR converts a libdns.Record into a spaceshipRecordUnion suitable for create/update
105+
// Returns nil for unsupported record types
106+
func (p *Provider) fromLibdnsRR(lr libdns.Record, zone string) *spaceshipRecordUnion {
107+
rr := lr.RR()
108+
name := rr.Name
109+
if name == "" {
110+
name = "@"
111+
} else {
112+
name = libdns.AbsoluteName(rr.Name, zone)
113+
}
114+
rec := spaceshipRecordUnion{ResourceRecordBase: ResourceRecordBase{Name: name, Type: strings.ToUpper(rr.Type), TTL: int(rr.TTL.Seconds())}}
115+
116+
// MX handled specially
117+
if mx, ok := lr.(libdns.MX); ok {
118+
rec.Exchange = mx.Target
119+
rec.Preference = int(mx.Preference)
120+
return &rec
121+
}
122+
123+
// Handle SRV records
124+
if srv, ok := lr.(libdns.SRV); ok {
125+
// map libdns.SRV fields into the spaceship payload
126+
rec.Service = "_" + strings.TrimPrefix(srv.Service, "_")
127+
rec.Protocol = "_" + strings.TrimPrefix(srv.Transport, "_")
128+
rec.Priority = int(srv.Priority)
129+
rec.Weight = int(srv.Weight)
130+
rec.Target = srv.Target
131+
rec.PortInt = int(srv.Port)
132+
if rec.PortInt != 0 {
133+
rec.Port = rec.PortInt
134+
}
135+
return &rec
136+
}
137+
138+
// Handle NS records
139+
if ns, ok := lr.(libdns.NS); ok {
140+
rec.Nameserver = ns.Target
141+
return &rec
142+
}
143+
144+
// Handle CAA records
145+
if caa, ok := lr.(libdns.CAA); ok {
146+
tmpFlag := new(int)
147+
*tmpFlag = int(caa.Flags)
148+
rec.Flag = tmpFlag
149+
rec.Tag = caa.Tag
150+
rec.Value = caa.Value
151+
return &rec
152+
}
153+
154+
// Handle ServiceBinding (HTTPS) records
155+
if svc, ok := lr.(libdns.ServiceBinding); ok {
156+
// Only handle HTTPS records (ServiceBinding with scheme "https")
157+
if strings.ToLower(svc.Scheme) == "https" {
158+
rec.Type = "HTTPS"
159+
rec.SvcPriority = int(svc.Priority)
160+
rec.TargetName = svc.Target // Use TargetName for API compatibility
161+
rec.SvcParams = svc.Params.String()
162+
return &rec
163+
}
164+
// For non-HTTPS ServiceBinding records, return nil (unsupported)
165+
return nil
166+
}
167+
168+
switch v := lr.(type) {
169+
case libdns.Address:
170+
rec.Address = v.IP.String()
171+
case libdns.TXT:
172+
rec.Value = v.Text
173+
case libdns.CNAME:
174+
rec.Cname = v.Target
175+
case libdns.MX:
176+
// already handled
177+
default:
178+
// Unsupported record type (including libdns.RR)
179+
return nil
180+
}
181+
return &rec
182+
}
183+

0 commit comments

Comments
 (0)