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
* rename internal functions

* Fetch system info as part of the provider initialization instead of client initialization

* yes yes... useful comments

* add test for `NewMikrotikClient`

* change from individual host+port to baseurl

* add tests for `GetSystemInfo`

* update tests

* fix lint

* add tests for `CreateDNSRecord`

* fix lint

* add tests for `DeleteDNSRecord`

* add tests for `GetAllDNSRecords`

* cleanup?

* fix

* cleanup and refactor
  • Loading branch information
mircea-pavel-anton authored Oct 26, 2024
1 parent dc30baa commit dc482cc
Show file tree
Hide file tree
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
Expand Up @@ -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)
}
Expand Down
71 changes: 36 additions & 35 deletions internal/mikrotik/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand All @@ -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})
Expand All @@ -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{
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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 {
Expand Down
Loading

0 comments on commit dc482cc

Please sign in to comment.