dnscontrol/providers/fortigate/fortigateProvider.go
Tom Limoncelli 7ab7d147fb
CHORE: Move non-provider code out of /providers (#3916)
# Issue

Fixes https://github.com/StackExchange/dnscontrol/issues/3912

# Resolution

```
#!/bin/sh

# Reset

git fetch origin main
git reset --hard origin/main
git checkout main
git branch -D tlim_moveproviders
git checkout -b tlim_moveproviders
find . -name \*.bak -delete

# Move the *.go files out of providers/

mkdir -p pkg/providers
git mv providers/*.go pkg/providers

# move the _all file out of providers/

git mv providers/_all pkg/providers/_all

# Update the imports (in go.* and the affected files)

sed -i.bak -e 's@"github.com/StackExchange/dnscontrol/v4/providers"@"github.com/StackExchange/dnscontrol/v4/pkg/providers"@g' go.* $(fgrep -lr --include '*.go' '"github.com/StackExchange/dnscontrol/v4/providers"' *)
sed -i.bak -e 's@"../../providers"@"../../pkg/providers"@g' pkg/normalize/capabilities_test.go
sed -i.bak -e 's@"github.com/StackExchange/dnscontrol/v4/providers/_all"@"github.com/StackExchange/dnscontrol/v4/pkg/providers/_all"@g' go.* $(fgrep -lr --include '*.go' '"github.com/StackExchange/dnscontrol/v4/providers/_all"' *)

# Fix the docs

sed -i.bak -e 's@StackExchange/dnscontrol/blob/main/providers/_all/all.go@StackExchange/dnscontrol/blob/main/pkg/providers/_all/all.go@g' documentation/advanced-features/writing-providers.md
sed -i.bak -e 's@StackExchange/dnscontrol/providers@StackExchange/dnscontrol/pkg/providers@g' documentation/advanced-features/writing-providers.md
sed -i.bak -e 's@StackExchange/dnscontrol/v4/providers@StackExchange/dnscontrol/v4/pkg/providers@g' documentation/advanced-features/writing-providers.md
sed -i.bak -e 's@dnscontrol/providers/providers.go@dnscontrol/pkg/providers/providers.go@g' documentation/advanced-features/writing-providers.md
sed -i.bak -e 's@providers/_all/all.go@pkg/providers/_all/all.go@g' documentation/advanced-features/writing-providers.md
#sed -i.bak -e 's@@@g' documentation/advanced-features/writing-providers.md
#sed -i.bak -e 's@@@g' documentation/advanced-features/writing-providers.md

find . -name \*.bak -delete

go fmt ./...

git status

echo git commit -a -m'CHORE: Move Non-provider files in providers to pkg/providers'



```
2025-12-15 12:53:52 -05:00

235 lines
6 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package fortigate
import (
"encoding/json"
"errors"
"fmt"
"net"
"strings"
"github.com/StackExchange/dnscontrol/v4/models"
"github.com/StackExchange/dnscontrol/v4/pkg/diff2"
"github.com/StackExchange/dnscontrol/v4/pkg/providers"
)
// Feature Declaration
var features = providers.DocumentationNotes{
providers.CanGetZones: providers.Can(),
providers.CanUsePTR: providers.Cannot(), // FortiGate does not really support ARPA Zones and handles PTR records really weired
providers.CanUseLOC: providers.Cannot(),
providers.CanConcur: providers.Unimplemented(),
providers.DocCreateDomains: providers.Can(),
providers.DocOfficiallySupported: providers.Cannot(), // unofficial integration
}
// Provider Registration
func init() {
const providerName = "FORTIGATE"
const providerMaintainer = "@KlettIT"
providers.RegisterDomainServiceProviderType(providerName, providers.DspFuncs{
Initializer: NewFortiGate,
RecordAuditor: AuditRecords,
}, features)
providers.RegisterMaintainer(providerName, providerMaintainer)
}
// Provider Struct
type fortigateProvider struct {
vdom string
host string
apiKey string
insecure bool
client *apiClient
}
// NewFortiGate creates a new instance of the FortiGate DNS provider.
func NewFortiGate(m map[string]string, _ json.RawMessage) (providers.DNSServiceProvider, error) {
host, vdom, apiKey := m["host"], m["vdom"], m["apiKey"]
var missing []string
if host == "" {
missing = append(missing, "host")
}
if vdom == "" {
missing = append(missing, "vdom")
}
if apiKey == "" {
missing = append(missing, "apiKey")
}
if len(missing) > 0 {
return nil, errors.New("[FORTIGATE] Missing required field(s): " + strings.Join(missing, ", "))
}
insecure := strings.EqualFold(m["insecure_tls"], "true")
debug := strings.EqualFold(m["debug_http"], "true")
p := &fortigateProvider{
host: host,
vdom: vdom,
apiKey: apiKey,
insecure: insecure,
}
p.client = newClient(host, vdom, apiKey, insecure, debug)
return p, nil
}
// Record Fetching
func (p *fortigateProvider) GetZoneRecords(domain string, meta map[string]string) (models.Records, error) {
records := models.Records{}
// Request the zone object from FortiGate
path := fmt.Sprintf("system/dns-database/%s", strings.TrimSuffix(domain, "."))
// According to the API, "results" is an array of objects
var resp struct {
Results []struct {
DNSEntry []fgDNSRecord `json:"dns-entry"`
} `json:"results"`
}
err := p.client.do("GET", path, nil, nil, &resp)
if err != nil {
if strings.Contains(err.Error(), "404") {
// Zone does not exist yet return empty record list
return records, nil
}
return nil, fmt.Errorf("[FORTIGATE] Fetching zone %q failed: %w", domain, err)
}
if len(resp.Results) == 0 {
// Zone exists but no dns-entry data found
return records, nil
}
// Convert native records to dnscontrol Records
for _, n := range resp.Results[0].DNSEntry {
rc, err := nativeToRecord(domain, n)
if err != nil {
return nil, err
}
records = append(records, rc)
}
return records, nil
}
// Correction Planning
func (p *fortigateProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, existingRecords models.Records) ([]*models.Correction, int, error) {
domain := strings.TrimSuffix(dc.Name, ".")
var corrections []*models.Correction
result, err := diff2.ByZone(existingRecords, dc, nil)
if err != nil {
return nil, 0, err
}
msgs, changed, actualChangeCount := result.Msgs, result.HasChanges, result.ActualChangeCount
if changed {
msgs = append(msgs, "[FORTIGATE] Zone update for "+domain)
msg := strings.Join(msgs, "\n")
resourceRecords, errs := recordsToNative(result.DesiredPlus)
if len(errs) > 0 {
return nil, 0, fmt.Errorf("[FORTIGATE] Failed to convert records: %v", errs)
}
if resourceRecords == nil {
resourceRecords = []*fgDNSRecord{}
}
payload, err := buildZonePayload(dc, resourceRecords)
if err != nil {
return nil, 0, err
}
corrections = append(corrections,
&models.Correction{
Msg: msg,
F: func() error {
if err := p.EnsureZoneExists(dc.Name, dc.Metadata); err != nil {
return err
}
return p.client.do("PUT", "system/dns-database/"+dc.Name, nil, payload, nil)
},
})
}
return corrections, actualChangeCount, nil
}
// Zone Existence Check & Creation
func (p *fortigateProvider) EnsureZoneExists(domain string, metadata map[string]string) error {
var probe struct{ Results []any }
err := p.client.do("GET", "system/dns-database/"+domain, nil, nil, &probe)
switch {
case err == nil && len(probe.Results) > 0:
return nil // already exists
case isNotFound(err):
body := map[string]any{"name": domain, "domain": domain, "forwarder": nil}
return p.client.do("POST", "system/dns-database", nil, body, nil)
default:
return err
}
}
// Misc DNSControl Plumbing
func (p *fortigateProvider) GetNameservers(string) ([]*models.Nameserver, error) {
return []*models.Nameserver{}, nil // FortiGate is authoritative only internally
}
func (p *fortigateProvider) ListZones() ([]string, error) {
var resp struct {
Results []struct{ Name string } `json:"results"`
}
if err := p.client.do("GET", "system/dns-database", nil, nil, &resp); err != nil {
return nil, err
}
zones := make([]string, len(resp.Results))
for i, z := range resp.Results {
zones[i] = z.Name
}
return zones, nil
}
// Helper
func buildZonePayload(dc *models.DomainConfig, resourceRecords []*fgDNSRecord) (map[string]any, error) {
payload := map[string]any{
"dns-entry": resourceRecords,
}
// default values
payload["forwarder"] = nil
payload["authoritative"] = "enable"
if v, ok := dc.Metadata["forwarder"]; ok {
ip := net.ParseIP(v)
if ip == nil || ip.To4() == nil {
return nil, fmt.Errorf("[FORTIGATE] Invalid forwarder IP: %q", v)
}
payload["forwarder"] = []string{v}
}
if v, ok := dc.Metadata["authoritative"]; ok {
if strings.ToLower(v) == "false" {
payload["authoritative"] = "disable"
}
}
return payload, nil
}