From d80f4206fb5cc4d16e1b6b8c83720478eae23bc5 Mon Sep 17 00:00:00 2001 From: Jener Rasmussen Date: Mon, 2 Dec 2024 09:12:30 +0100 Subject: [PATCH] Merge terraform provider tidydns (#4) * Merge functionality from terraform-provider-tidydns upstream * Code cleanup * Cleanup comment * Close HTTP response bodies --- pkg/tidydns/tidydns.go | 254 ++++++++++++++++++++++++++++++++---- pkg/tidydns/tidydns_test.go | 201 ++++++++++++++++++++++++++++ pkg/tidydns/types.go | 28 ++++ 3 files changed, 458 insertions(+), 25 deletions(-) diff --git a/pkg/tidydns/tidydns.go b/pkg/tidydns/tidydns.go index bd9033b..4fe0861 100644 --- a/pkg/tidydns/tidydns.go +++ b/pkg/tidydns/tidydns.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "io" "net/http" "net/url" "strconv" @@ -11,6 +12,12 @@ import ( ) type TidyDNSClient interface { + GetSubnetIDs(ctx context.Context, subnetCIDR string) (*SubnetIDs, error) + GetFreeIP(ctx context.Context, subnetID int) (string, error) + CreateDHCPInterface(ctx context.Context, createInfo CreateInfo) (int, error) + ReadDHCPInterface(ctx context.Context, interfaceID int) (*InterfaceInfo, error) + UpdateDHCPInterfaceName(ctx context.Context, interfaceID int, interfaceName string) (int, error) + DeleteDHCPInterface(ctx context.Context, interfaceID int) error ListZones(ctx context.Context) ([]*ZoneInfo, error) FindZoneID(ctx context.Context, name string) (int, error) CreateRecord(ctx context.Context, zoneID int, info RecordInfo) (int, error) @@ -42,6 +49,7 @@ type CreateInfo struct { ZoneID int InterfaceIP string InterfaceName string + LocationID int } type RecordInfo struct { @@ -59,6 +67,7 @@ type LocationID int type RecordType int type RecordStatus int +//goland:noinspection GoUnusedConst const ( RecordStatusActive RecordStatus = 0 RecordStatusInactive RecordStatus = 1 @@ -93,6 +102,206 @@ func New(baseURL, username, password string) TidyDNSClient { } } +func (c *tidyDNSClient) GetSubnetIDs(ctx context.Context, subnetCIDR string) (*SubnetIDs, error) { + req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("%s/=/dhcp_subnet?subnet=%s", c.baseURL, subnetCIDR), nil) + if err != nil { + return nil, err + } + + req.SetBasicAuth(c.username, c.password) + + res, err := c.client.Do(req) + if err != nil { + return nil, err + } + defer func(Body io.ReadCloser) { + _ = Body.Close() + }(res.Body) + if res.StatusCode != http.StatusOK { + return nil, fmt.Errorf("error from tidyDNS server: %s", res.Status) + } + + var subnets []dhcpSubnet + err = json.NewDecoder(res.Body).Decode(&subnets) + if err != nil { + return nil, err + } + + if len(subnets) == 0 { + return nil, fmt.Errorf("subnet not found: %s", subnetCIDR) + } + + if len(subnets) > 1 { + return nil, fmt.Errorf("too many subnets found: %s", subnetCIDR) + } + + return &SubnetIDs{ + SubnetID: subnets[0].ID, + ZoneID: subnets[0].ZoneID, + VlanNo: subnets[0].VlanNo, + }, nil +} + +func (c *tidyDNSClient) GetFreeIP(ctx context.Context, subnetID int) (string, error) { + req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("%s/=/dhcp_subnet_free_ip/%d", c.baseURL, subnetID), nil) + if err != nil { + return "", err + } + + req.SetBasicAuth(c.username, c.password) + + res, err := c.client.Do(req) + if err != nil { + return "", err + } + defer func(Body io.ReadCloser) { + _ = Body.Close() + }(res.Body) + if res.StatusCode != http.StatusOK { + return "", fmt.Errorf("error from tidyDNS server: %s", res.Status) + } + + var freeIP dhcpFreeIP + err = json.NewDecoder(res.Body).Decode(&freeIP) + if err != nil { + return "", err + } + + return freeIP.Data.IPAddress, nil +} + +func (c *tidyDNSClient) CreateDHCPInterface(ctx context.Context, createInfo CreateInfo) (int, error) { + data := url.Values{ + "subnet_id": {strconv.Itoa(createInfo.SubnetID)}, + "zone_id": {strconv.Itoa(createInfo.ZoneID)}, + "name": {createInfo.InterfaceName}, + "destination": {createInfo.InterfaceIP}, + "location_id": {strconv.Itoa(createInfo.LocationID)}, + } + + var checkstring = fmt.Sprintf("Key (destination)=(%s) already exists", createInfo.InterfaceIP) + + req, err := http.NewRequestWithContext(ctx, "POST", fmt.Sprintf("%s/=/dhcp_interface//new", c.baseURL), strings.NewReader(data.Encode())) + if err != nil { + return 0, err + } + req.SetBasicAuth(c.username, c.password) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + res, err := c.client.Do(req) + if err != nil { + return 0, err + } + defer func(Body io.ReadCloser) { + _ = Body.Close() + }(res.Body) + + if res.StatusCode != http.StatusOK { + bodyBytes, err := io.ReadAll(res.Body) + bodyString := string(bodyBytes) + if err != nil { + return 0, err + } + if strings.Contains(bodyString, checkstring) { + return 1, fmt.Errorf("try again") + } + return 0, fmt.Errorf("error from tidyDNS server: %s", res.Status) + } + + var createResp interfaceCreate + err = json.NewDecoder(res.Body).Decode(&createResp) + if err != nil { + return 0, err + } + + return createResp.ID, nil +} + +func (c *tidyDNSClient) ReadDHCPInterface(ctx context.Context, interfaceID int) (*InterfaceInfo, error) { + req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("%s/=/dhcp_interface/?id=%d", c.baseURL, interfaceID), nil) + if err != nil { + return nil, err + } + + req.SetBasicAuth(c.username, c.password) + + res, err := c.client.Do(req) + if err != nil { + return nil, err + } + defer func(Body io.ReadCloser) { + _ = Body.Close() + }(res.Body) + if res.StatusCode != http.StatusOK { + return nil, fmt.Errorf("error from tidyDNS server: %s", res.Status) + } + + var interfaceRead interfaceRead + err = json.NewDecoder(res.Body).Decode(&interfaceRead) + if err != nil { + return nil, err + } + + return &InterfaceInfo{ + InterfaceIP: interfaceRead.Destination, + Interfacename: interfaceRead.Name, + }, nil +} + +func (c *tidyDNSClient) UpdateDHCPInterfaceName(ctx context.Context, interfaceID int, interfaceName string) (int, error) { + data := url.Values{ + "name": {interfaceName}, + } + + req, err := http.NewRequestWithContext(ctx, "POST", fmt.Sprintf("%s/=/dhcp_interface//%d", c.baseURL, interfaceID), strings.NewReader(data.Encode())) + if err != nil { + return 0, err + } + req.SetBasicAuth(c.username, c.password) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + res, err := c.client.Do(req) + if err != nil { + return 0, err + } + defer func(Body io.ReadCloser) { + _ = Body.Close() + }(res.Body) + if res.StatusCode != http.StatusOK { + return 0, fmt.Errorf("error from tidyDNS server: %s", res.Status) + } + + var createResp interfaceCreate + err = json.NewDecoder(res.Body).Decode(&createResp) + if err != nil { + return 0, err + } + + return createResp.ID, nil +} + +func (c *tidyDNSClient) DeleteDHCPInterface(ctx context.Context, interfaceID int) error { + req, err := http.NewRequestWithContext(ctx, "DELETE", fmt.Sprintf("%s/=/dhcp_interface/%d", c.baseURL, interfaceID), nil) + if err != nil { + return err + } + + req.SetBasicAuth(c.username, c.password) + + res, err := c.client.Do(req) + if err != nil { + return err + } + defer func(Body io.ReadCloser) { + _ = Body.Close() + }(res.Body) + if res.StatusCode != http.StatusOK { + return fmt.Errorf("error from tidyDNS server: %s", res.Status) + } + + return nil +} + func (c *tidyDNSClient) ListZones(ctx context.Context) ([]*ZoneInfo, error) { var zones []zoneInfo err := c.getData(ctx, fmt.Sprintf("%s/=/zone?type=json", c.baseURL), &zones) @@ -149,13 +358,12 @@ func (c *tidyDNSClient) CreateRecord(ctx context.Context, zoneID int, info Recor req.Header.Set("Content-Type", "application/x-www-form-urlencoded") res, err := c.client.Do(req) - if res != nil { - defer res.Body.Close() - } - - if err != nil { + if err != nil || res == nil { return 0, err } + defer func(Body io.ReadCloser) { + _ = Body.Close() + }(res.Body) if res.StatusCode != http.StatusOK { return 0, fmt.Errorf("error from tidyDNS server: %s", res.Status) } @@ -168,13 +376,12 @@ func (c *tidyDNSClient) CreateRecord(ctx context.Context, zoneID int, info Recor req.SetBasicAuth(c.username, c.password) res, err = c.client.Do(req) - if res != nil { - defer res.Body.Close() - } - - if err != nil { + if err != nil || res == nil { return 0, err } + defer func(Body io.ReadCloser) { + _ = Body.Close() + }(res.Body) if res.StatusCode != http.StatusOK { return 0, fmt.Errorf("error from tidyDNS server: %s", res.Status) } @@ -211,13 +418,12 @@ func (c *tidyDNSClient) UpdateRecord(ctx context.Context, zoneID int, recordID i req.Header.Set("Content-Type", "application/x-www-form-urlencoded") res, err := c.client.Do(req) - if res != nil { - defer res.Body.Close() - } - - if err != nil { + if err != nil || res == nil { return err } + defer func(Body io.ReadCloser) { + _ = Body.Close() + }(res.Body) if res.StatusCode != http.StatusOK { return fmt.Errorf("error from tidyDNS server: %s", res.Status) } @@ -299,13 +505,12 @@ func (c *tidyDNSClient) DeleteRecord(ctx context.Context, zoneID int, recordID i req.SetBasicAuth(c.username, c.password) res, err := c.client.Do(req) - if res != nil { - defer res.Body.Close() - } - - if err != nil { + if err != nil || res == nil { return err } + defer func(Body io.ReadCloser) { + _ = Body.Close() + }(res.Body) if res.StatusCode != http.StatusOK { return fmt.Errorf("error from tidyDNS server: %s", res.Status) } @@ -322,13 +527,12 @@ func (c *tidyDNSClient) getData(ctx context.Context, url string, value interface req.SetBasicAuth(c.username, c.password) res, err := c.client.Do(req) - if res != nil { - defer res.Body.Close() - } - - if err != nil { + if err != nil || res == nil { return err } + defer func(Body io.ReadCloser) { + _ = Body.Close() + }(res.Body) if res.StatusCode != http.StatusOK { return fmt.Errorf("error from tidyDNS server: %s", res.Status) } diff --git a/pkg/tidydns/tidydns_test.go b/pkg/tidydns/tidydns_test.go index 5c0e08b..c6a89fa 100644 --- a/pkg/tidydns/tidydns_test.go +++ b/pkg/tidydns/tidydns_test.go @@ -9,6 +9,207 @@ import ( "github.com/stretchr/testify/assert" ) +const subnetResponse = `[ + { + "vlan_name": "netic-shared-k8s-utility01", + "created_date": "2021-04-16 14:46:46", + "modified_by": "mef", + "vlan_id": 959, + "loc_name": "internal", + "subnet_nice_name": "10.68.0.128/26 - netic-shared-k8s-utility01-v4", + "dhcp_failover": 1, + "vlan": "534 netic-shared-k8s-utility01", + "status": 0, + "dhcp_active": 0, + "location_id": 1, + "description": null, + "id": 1185, + "modified_date": "2021-07-08 10:38:02", + "name": "netic-shared-k8s-utility01-v4", + "subnet": "10.68.0.128/26", + "zone": "k8s.netic.dk", + "customer_id": 0, + "family": 4, + "zone_id": 2861, + "vmps_active": 0, + "shared_network": null, + "icons": "", + "vlan_no": 534 + } +]` + +const freeIPResponse = `{ + "status": 0, + "data": { + "name_suggestion": "netic-shared-k8s-utility01-v4-134", + "last_octet": "134", + "ip_address": "10.68.0.134" + } +}` + +const createResponseV1 = `{ + "status": "0", + "id": 30641, + "subnet_id": 1185 +}` + +const createResponseV2 = `{ + "status": 0, + "id": 30641, + "subnet_id": 1185 +}` + +const readResponse = `{ + "brother_id": null, + "id": 30641, + "extra_ip": 0, + "address_id": 63573, + "subnet_id": 1185, + "modified_date": "2021-07-08", + "subnet": "10.68.0.128/26", + "mac_addr": null, + "destination": "10.68.0.134", + "fqdn_namehelper": null, + "vlan_id": 959, + "icons": "", + "description": "", + "aliases": "", + "type": 1, + "dhcp_active": 0, + "subnet_name": "netic-shared-k8s-utility01-v4", + "ip_family": 4, + "is_template": 0, + "zone": "k8s.netic.dk", + "name": "test-tal", + "modified_by": "tal", + "customer_id_inherited": 0, + "customer_id": null, + "vlan_name": "netic-shared-k8s-utility01", + "star": "", + "vmps_active": 0, + "vlan_no": 534, + "record_id": 30641, + "ip_address": "10.68.0.134", + "status": 0, + "fqdn_name": "test-tal.k8s.netic.dk", + "seen_date": null, + "fqdn": "test-tal.k8s.netic.dk", + "location_id": 1, + "dual_stack": "", + "zone_id": 2861, + "brother_destination": null +}` + +func TestGetSubnetIDs(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + assert.Equal(t, "10.68.0.128/26", req.URL.Query().Get("subnet")) + assert.Equal(t, "GET", req.Method) + rw.Write([]byte(subnetResponse)) + })) + defer server.Close() + + c := New(server.URL, "username", "password") + ids, err := c.GetSubnetIDs(context.Background(), "10.68.0.128/26") + assert.NoError(t, err) + assert.Equal(t, 1185, ids.SubnetID) + assert.Equal(t, 2861, ids.ZoneID) + assert.Equal(t, 534, ids.VlanNo) +} + +func TestGetFreeIP(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + assert.Contains(t, req.URL.Path, "1185") + assert.Equal(t, "GET", req.Method) + rw.Write([]byte(freeIPResponse)) + })) + defer server.Close() + + c := New(server.URL, "username", "password") + ip, err := c.GetFreeIP(context.Background(), 1185) + assert.NoError(t, err) + assert.Equal(t, "10.68.0.134", ip) +} + +func TestCreateDHCPInterfaceV1(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + assert.Equal(t, "POST", req.Method) + rw.Write([]byte(createResponseV1)) + })) + defer server.Close() + + c := New(server.URL, "username", "password") + createInfo := CreateInfo{ + SubnetID: 1185, + ZoneID: 2861, + InterfaceIP: "8.8.8.8", + InterfaceName: "unittest", + } + id, err := c.CreateDHCPInterface(context.Background(), createInfo) + assert.NoError(t, err) + assert.Equal(t, 30641, id) +} + +func TestCreateDHCPInterfaceV2(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + assert.Equal(t, "POST", req.Method) + rw.Write([]byte(createResponseV2)) + })) + defer server.Close() + + c := New(server.URL, "username", "password") + createInfo := CreateInfo{ + SubnetID: 1185, + ZoneID: 2861, + InterfaceIP: "8.8.8.8", + InterfaceName: "unittest", + } + id, err := c.CreateDHCPInterface(context.Background(), createInfo) + assert.NoError(t, err) + assert.Equal(t, 30641, id) +} + +func TestReadDHCPInterface(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + assert.Equal(t, "30641", req.URL.Query().Get("id")) + assert.Equal(t, "GET", req.Method) + rw.Write([]byte(readResponse)) + })) + defer server.Close() + + c := New(server.URL, "username", "password") + info, err := c.ReadDHCPInterface(context.Background(), 30641) + assert.NoError(t, err) + assert.Equal(t, "10.68.0.134", info.InterfaceIP) + assert.Equal(t, "test-tal", info.Interfacename) +} + +func TestUpdateDHCPInterfaceName(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + assert.Contains(t, req.URL.Path, "30641") + assert.Equal(t, "POST", req.Method) + rw.Write([]byte(createResponseV1)) + })) + defer server.Close() + + c := New(server.URL, "username", "password") + id, err := c.UpdateDHCPInterfaceName(context.Background(), 30641, "test-tal-update") + assert.NoError(t, err) + assert.Equal(t, 30641, id) +} + +func TestDeleteDHCPInterface(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + assert.Contains(t, req.URL.Path, "30641") + assert.Equal(t, "DELETE", req.Method) + rw.Write([]byte(createResponseV1)) + })) + defer server.Close() + + c := New(server.URL, "username", "password") + err := c.DeleteDHCPInterface(context.Background(), 30641) + assert.NoError(t, err) +} + func TestFindZoneID(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { assert.Contains(t, req.URL.Query().Get("name"), "hackerdays.trifork.dev") diff --git a/pkg/tidydns/types.go b/pkg/tidydns/types.go index e6bcc8f..a1ca23d 100644 --- a/pkg/tidydns/types.go +++ b/pkg/tidydns/types.go @@ -1,5 +1,33 @@ package tidydns +type dhcpSubnet struct { + ID int `json:"id"` + VlanId int `json:"vlan_id"` + VlanNo int `json:"vlan_no"` + ZoneID int `json:"zone_id"` + LocationID int `json:"location_id"` +} + +type dhcpFreeIP struct { + Status int `json:"status"` + Data dhcpFreeIPData `json:"data"` +} + +type dhcpFreeIPData struct { + IPAddress string `json:"ip_address"` +} + +type interfaceCreate struct { + Status interface{} `json:"status"` + ID int `json:"id"` + SubnetID int `json:"subnet_id"` +} + +type interfaceRead struct { + Name string `json:"name"` + Destination string `json:"destination"` +} + type zoneInfo struct { ID int `json:"id"` Name string `json:"name"`