Skip to content

Commit

Permalink
chore!: Add tests for Mikrotik API client code (#91)
Browse files Browse the repository at this point in the history
BREAKING: This PR replaces the `MIKROTIK_HOST` and `MIKROTIK_PORT` environment variables with a single one called `MIKROTIK_BASEURL`
mircea-pavel-anton committed Oct 26, 2024
1 parent dc30baa commit e7314ac
Showing 4 changed files with 837 additions and 110 deletions.
2 changes: 1 addition & 1 deletion internal/dnsprovider/dnsprovider.go
Original file line number Diff line number Diff line change
@@ -43,7 +43,7 @@ func Init(config configuration.Config) (provider.Provider, error) {
}
log.Info(createMsg)

mikrotikConfig := mikrotik.Config{}
mikrotikConfig := mikrotik.MikrotikConnectionConfig{}
if err := env.Parse(&mikrotikConfig); err != nil {
return nil, fmt.Errorf("reading mikrotik configuration failed: %v", err)
}
71 changes: 36 additions & 35 deletions internal/mikrotik/client.go
Original file line number Diff line number Diff line change
@@ -17,24 +17,23 @@ import (
"sigs.k8s.io/external-dns/endpoint"
)

// Config holds the connection details for the API client
type Config struct {
Host string `env:"MIKROTIK_HOST,notEmpty"`
Port string `env:"MIKROTIK_PORT,notEmpty" envDefault:"443"`
// MikrotikConnectionConfig holds the connection details for the API client
type MikrotikConnectionConfig struct {
BaseUrl string `env:"MIKROTIK_BASEURL,notEmpty"`
Username string `env:"MIKROTIK_USERNAME,notEmpty"`
Password string `env:"MIKROTIK_PASSWORD,notEmpty"`
SkipTLSVerify bool `env:"MIKROTIK_SKIP_TLS_VERIFY" envDefault:"false"`
}

// MikrotikApiClient encapsulates the client configuration and HTTP client
type MikrotikApiClient struct {
*Config
*MikrotikConnectionConfig
*http.Client
}

// SystemInfo represents MikroTik system information
// MikrotikSystemInfo represents MikroTik system information
// https://help.mikrotik.com/docs/display/ROS/Resource
type SystemInfo struct {
type MikrotikSystemInfo struct {
ArchitectureName string `json:"architecture-name"`
BadBlocks string `json:"bad-blocks"`
BoardName string `json:"board-name"`
@@ -56,7 +55,7 @@ type SystemInfo struct {
}

// NewMikrotikClient creates a new instance of MikrotikApiClient
func NewMikrotikClient(config *Config) (*MikrotikApiClient, error) {
func NewMikrotikClient(config *MikrotikConnectionConfig) (*MikrotikApiClient, error) {
log.Infof("creating a new Mikrotik API Client")

jar, err := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List})
@@ -66,7 +65,7 @@ func NewMikrotikClient(config *Config) (*MikrotikApiClient, error) {
}

client := &MikrotikApiClient{
Config: config,
MikrotikConnectionConfig: config,
Client: &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
@@ -77,29 +76,23 @@ func NewMikrotikClient(config *Config) (*MikrotikApiClient, error) {
},
}

info, err := client.GetSystemInfo()
if err != nil {
log.Errorf("failed to connect to the MikroTik RouterOS API Endpoint: %v", err)
return nil, err
}

log.Infof("connected to board %s running RouterOS version %s (%s)", info.BoardName, info.Version, info.ArchitectureName)
return client, nil
}

// GetSystemInfo fetches system information from the MikroTik API
func (c *MikrotikApiClient) GetSystemInfo() (*SystemInfo, error) {
func (c *MikrotikApiClient) GetSystemInfo() (*MikrotikSystemInfo, error) {
log.Debugf("fetching system information.")

resp, err := c._doRequest(http.MethodGet, "system/resource", nil)
// Send the request
resp, err := c.doRequest(http.MethodGet, "system/resource", nil)
if err != nil {
log.Errorf("error getching system info: %v", err)
log.Errorf("error fetching system info: %v", err)
return nil, err
}

defer resp.Body.Close()

var info SystemInfo
// Parse the response
var info MikrotikSystemInfo
if err = json.NewDecoder(resp.Body).Decode(&info); err != nil {
log.Errorf("error decoding response body: %v", err)
return nil, err
@@ -113,26 +106,29 @@ func (c *MikrotikApiClient) GetSystemInfo() (*SystemInfo, error) {
func (c *MikrotikApiClient) CreateDNSRecord(endpoint *endpoint.Endpoint) (*DNSRecord, error) {
log.Infof("creating DNS record: %+v", endpoint)

// Convert ExternalDNS to Mikrotik DNS
record, err := NewDNSRecord(endpoint)
if err != nil {
log.Errorf("error converting ExternalDNS endpoint to Mikrotik DNS Record: %v", err)
return nil, err
}

// Serialize the data to JSON to be sent to the API
jsonBody, err := json.Marshal(record)
if err != nil {
log.Errorf("error marshalling DNS record: %v", err)
return nil, err
}

resp, err := c._doRequest(http.MethodPut, "ip/dns/static", bytes.NewReader(jsonBody))
// Send the request
resp, err := c.doRequest(http.MethodPut, "ip/dns/static", bytes.NewReader(jsonBody))
if err != nil {
log.Errorf("error creating DNS record: %v", err)
return nil, err
}

defer resp.Body.Close()

// Parse the response
if err = json.NewDecoder(resp.Body).Decode(&record); err != nil {
log.Errorf("Error decoding response body: %v", err)
return nil, err
@@ -146,14 +142,15 @@ func (c *MikrotikApiClient) CreateDNSRecord(endpoint *endpoint.Endpoint) (*DNSRe
func (c *MikrotikApiClient) GetAllDNSRecords() ([]DNSRecord, error) {
log.Infof("fetching all DNS records")

resp, err := c._doRequest(http.MethodGet, "ip/dns/static", nil)
// Send the request
resp, err := c.doRequest(http.MethodGet, "ip/dns/static", nil)
if err != nil {
log.Errorf("error fetching DNS records: %v", err)
return nil, err
}

defer resp.Body.Close()

// Parse the response
var records []DNSRecord
if err = json.NewDecoder(resp.Body).Decode(&records); err != nil {
log.Errorf("error decoding response body: %v", err)
@@ -168,24 +165,27 @@ func (c *MikrotikApiClient) GetAllDNSRecords() ([]DNSRecord, error) {
func (c *MikrotikApiClient) DeleteDNSRecord(endpoint *endpoint.Endpoint) error {
log.Infof("deleting DNS record: %+v", endpoint)

record, err := c._lookupDNSRecord(endpoint.DNSName, endpoint.RecordType)
// Send the request
record, err := c.lookupDNSRecord(endpoint.DNSName, endpoint.RecordType)
if err != nil {
log.Errorf("failed lookup for DNS record: %+v", err)
return err
}

_, err = c._doRequest(http.MethodDelete, fmt.Sprintf("ip/dns/static/%s", record.ID), nil)
// Parse the response
resp, err := c.doRequest(http.MethodDelete, fmt.Sprintf("ip/dns/static/%s", record.ID), nil)
if err != nil {
log.Errorf("error deleting DNS record: %+v", err)
return err
}
defer resp.Body.Close()
log.Infof("record deleted")

return nil
}

// _lookupDNSRecord searches for a DNS record by key and type
func (c *MikrotikApiClient) _lookupDNSRecord(key, recordType string) (*DNSRecord, error) {
// lookupDNSRecord searches for a DNS record by key and type
func (c *MikrotikApiClient) lookupDNSRecord(key, recordType string) (*DNSRecord, error) {
log.Infof("Searching for DNS record: Key: %s, RecordType: %s", key, recordType)

searchParams := fmt.Sprintf("name=%s", key)
@@ -194,13 +194,14 @@ func (c *MikrotikApiClient) _lookupDNSRecord(key, recordType string) (*DNSRecord
}
log.Debugf("Search params: %s", searchParams)

resp, err := c._doRequest(http.MethodGet, fmt.Sprintf("ip/dns/static?%s", searchParams), nil)
// Send the request
resp, err := c.doRequest(http.MethodGet, fmt.Sprintf("ip/dns/static?%s", searchParams), nil)
if err != nil {
return nil, err
}

defer resp.Body.Close()

// Parse the response
var record []DNSRecord
if err = json.NewDecoder(resp.Body).Decode(&record); err != nil {
log.Errorf("Error decoding response body: %v", err)
@@ -215,9 +216,9 @@ func (c *MikrotikApiClient) _lookupDNSRecord(key, recordType string) (*DNSRecord
return &record[0], nil
}

// _doRequest sends an HTTP request to the MikroTik API with credentials
func (c *MikrotikApiClient) _doRequest(method, path string, body io.Reader) (*http.Response, error) {
endpoint_url := fmt.Sprintf("https://%s:%s/rest/%s", c.Config.Host, c.Config.Port, path)
// doRequest sends an HTTP request to the MikroTik API with credentials
func (c *MikrotikApiClient) doRequest(method, path string, body io.Reader) (*http.Response, error) {
endpoint_url := fmt.Sprintf("%s/rest/%s", c.MikrotikConnectionConfig.BaseUrl, path)
log.Debugf("sending %s request to: %s", method, endpoint_url)

req, err := http.NewRequest(method, endpoint_url, body)
@@ -226,7 +227,7 @@ func (c *MikrotikApiClient) _doRequest(method, path string, body io.Reader) (*ht
return nil, err
}

req.SetBasicAuth(c.Config.Username, c.Config.Password)
req.SetBasicAuth(c.MikrotikConnectionConfig.Username, c.MikrotikConnectionConfig.Password)

resp, err := c.Client.Do(req)
if err != nil {
855 changes: 785 additions & 70 deletions internal/mikrotik/client_test.go

Large diffs are not rendered by default.

19 changes: 15 additions & 4 deletions internal/mikrotik/provider.go
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@ import (
"context"
"fmt"

log "github.com/sirupsen/logrus"
"sigs.k8s.io/external-dns/endpoint"
"sigs.k8s.io/external-dns/plan"
"sigs.k8s.io/external-dns/provider"
@@ -17,15 +18,25 @@ type MikrotikProvider struct {
domainFilter endpoint.DomainFilter
}

// NewMikrotikProvider initializes a new DNSProvider.
func NewMikrotikProvider(domainFilter endpoint.DomainFilter, config *Config) (provider.Provider, error) {
c, err := NewMikrotikClient(config)
// NewMikrotikProvider initializes a new DNSProvider, of the Mikrotik variety
func NewMikrotikProvider(domainFilter endpoint.DomainFilter, config *MikrotikConnectionConfig) (provider.Provider, error) {
// Create the Mikrotik API Client
client, err := NewMikrotikClient(config)
if err != nil {
return nil, fmt.Errorf("failed to create the MikroTik client: %w", err)
}

// Ensure the Client can connect to the API by fetching system info
info, err := client.GetSystemInfo()
if err != nil {
log.Errorf("failed to connect to the MikroTik RouterOS API Endpoint: %v", err)
return nil, err
}
log.Infof("connected to board %s running RouterOS version %s (%s)", info.BoardName, info.Version, info.ArchitectureName)

// If the client connects properly, create the DNS Provider
p := &MikrotikProvider{
client: c,
client: client,
domainFilter: domainFilter,
}

0 comments on commit e7314ac

Please sign in to comment.