Skip to content

Commit

Permalink
add dns support
Browse files Browse the repository at this point in the history
  • Loading branch information
mycrEEpy committed Nov 21, 2020
1 parent 2a2f0e9 commit 7db77d4
Show file tree
Hide file tree
Showing 3 changed files with 173 additions and 28 deletions.
101 changes: 76 additions & 25 deletions control.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"context"
"errors"
"fmt"
"net/http"
Expand All @@ -19,6 +20,7 @@ const (
LabelService = "mnbr.eu/svc"
LabelTTL = "mnbr.eu/ttl"
LabelActiveBlueprint = "mnbr.eu/active-blueprint"
LabelDNSRecordID = "mnbr.eu/dns-record-id"
)

type Control struct {
Expand All @@ -28,13 +30,14 @@ type Control struct {
}

type ControlConfig struct {
location *hcloud.Location
networks []*hcloud.Network
sshKeys []*hcloud.SSHKey
location *hcloud.Location
networks []*hcloud.Network
sshKeys []*hcloud.SSHKey
dnsZoneID string
}

type APIError struct {
Error error
Error string
}

type CreateNewServerRequest struct {
Expand Down Expand Up @@ -85,7 +88,7 @@ func (control *Control) ListServers(ctx *gin.Context) {
servers, _, err := control.hclient.Server.List(ctx, hcloud.ServerListOpts{})
if err != nil {
ctx.AbortWithStatusJSON(http.StatusInternalServerError, APIError{
fmt.Errorf("failed to list servers: %s", err),
fmt.Errorf("failed to list servers: %s", err).Error(),
})
return
}
Expand All @@ -105,15 +108,15 @@ func (control *Control) NewServer(ctx *gin.Context) {
err := ctx.ShouldBindJSON(&req)
if err != nil {
ctx.AbortWithStatusJSON(http.StatusBadRequest, APIError{
fmt.Errorf("failed to bind request: %s", err),
fmt.Errorf("failed to bind request: %s", err).Error(),
})
return
}

allImages, _, err := control.hclient.Image.List(ctx, hcloud.ImageListOpts{})
if err != nil {
ctx.AbortWithStatusJSON(http.StatusInternalServerError, APIError{
fmt.Errorf("failed to list images: %s", err),
fmt.Errorf("failed to list images: %s", err).Error(),
})
return
}
Expand All @@ -126,15 +129,15 @@ func (control *Control) NewServer(ctx *gin.Context) {
}
if blueprintImage == nil {
ctx.AbortWithStatusJSON(http.StatusInternalServerError, APIError{
fmt.Errorf("unable to find active blueprint image for server %s", req.ServerName),
fmt.Errorf("unable to find active blueprint image for server %s", req.ServerName).Error(),
})
return
}

ttlDuration, err := time.ParseDuration(req.TTL)
if err != nil {
ctx.AbortWithStatusJSON(http.StatusBadRequest, APIError{
fmt.Errorf("failed to parse ttl duration: %s", err),
fmt.Errorf("failed to parse ttl duration: %s", err).Error(),
})
return
}
Expand All @@ -155,18 +158,27 @@ func (control *Control) NewServer(ctx *gin.Context) {
})
if err != nil {
ctx.AbortWithStatusJSON(http.StatusInternalServerError, APIError{
fmt.Errorf("failed to create server %s: %s", req.ServerName, err),
fmt.Errorf("failed to create server %s: %s", req.ServerName, err).Error(),
})
return
}

err = control.attachDNSRecordToServer(ctx, r.Server)
if err != nil {
ctx.AbortWithStatusJSON(http.StatusInternalServerError, APIError{
fmt.Errorf("failed to attach dns record to server %s: %s", req.ServerName, err).Error(),
})
return
}

ctx.AbortWithStatusJSON(http.StatusCreated, *r.Server)
}

func (control *Control) StartServer(ctx *gin.Context) {
serverName, ok := ctx.Params.Get("name")
if !ok {
ctx.AbortWithStatusJSON(http.StatusBadRequest, APIError{
errors.New("missing name parameter"),
errors.New("missing name parameter").Error(),
})
return
}
Expand All @@ -175,15 +187,15 @@ func (control *Control) StartServer(ctx *gin.Context) {
err := ctx.ShouldBindJSON(&req)
if err != nil {
ctx.AbortWithStatusJSON(http.StatusBadRequest, APIError{
fmt.Errorf("failed to bind request: %s", err),
fmt.Errorf("failed to bind request: %s", err).Error(),
})
return
}

allImages, _, err := control.hclient.Image.List(ctx, hcloud.ImageListOpts{})
if err != nil {
ctx.AbortWithStatusJSON(http.StatusInternalServerError, APIError{
fmt.Errorf("failed to list images: %s", err),
fmt.Errorf("failed to list images: %s", err).Error(),
})
return
}
Expand All @@ -202,15 +214,15 @@ func (control *Control) StartServer(ctx *gin.Context) {
}
if latestServiceImage == nil {
ctx.AbortWithStatusJSON(http.StatusInternalServerError, APIError{
fmt.Errorf("unable to find previous snapshot for server %s", serverName),
fmt.Errorf("unable to find previous snapshot for server %s", serverName).Error(),
})
return
}

ttlDuration, err := time.ParseDuration(req.TTL)
if err != nil {
ctx.AbortWithStatusJSON(http.StatusBadRequest, APIError{
fmt.Errorf("failed to parse ttl duration: %s", err),
fmt.Errorf("failed to parse ttl duration: %s", err).Error(),
})
return
}
Expand All @@ -231,34 +243,45 @@ func (control *Control) StartServer(ctx *gin.Context) {
})
if err != nil {
ctx.AbortWithStatusJSON(http.StatusInternalServerError, APIError{
fmt.Errorf("failed to create server %s: %s", serverName, err),
fmt.Errorf("failed to create server %s: %s", serverName, err).Error(),
})
return
}

if len(control.Config.dnsZoneID) > 0 {
err = control.attachDNSRecordToServer(ctx, r.Server)
if err != nil {
ctx.AbortWithStatusJSON(http.StatusInternalServerError, APIError{
fmt.Errorf("failed to attach dns record to server %s: %s", serverName, err).Error(),
})
return
}
}

ctx.AbortWithStatusJSON(http.StatusCreated, *r.Server)
}

func (control *Control) TerminateServer(ctx *gin.Context) {
serverName, ok := ctx.Params.Get("name")
if !ok {
ctx.AbortWithStatusJSON(http.StatusBadRequest, APIError{
errors.New("missing name parameter"),
errors.New("missing name parameter").Error(),
})
return
}

server, _, err := control.hclient.Server.Get(ctx, serverName)
if err != nil {
ctx.AbortWithStatusJSON(http.StatusInternalServerError, APIError{
fmt.Errorf("failed to get server %s by name: %s", serverName, err),
fmt.Errorf("failed to get server %s by name: %s", serverName, err).Error(),
})
return
}

shutdownAction, _, err := control.hclient.Server.Shutdown(ctx, server)
if err != nil {
ctx.AbortWithStatusJSON(http.StatusInternalServerError, APIError{
fmt.Errorf("failed to shutdown server %s: %s", serverName, err),
fmt.Errorf("failed to shutdown server %s: %s", serverName, err).Error(),
})
return
}
Expand All @@ -281,7 +304,7 @@ func (control *Control) TerminateServer(ctx *gin.Context) {
}()
if err != nil {
ctx.AbortWithStatusJSON(http.StatusInternalServerError, APIError{
err,
err.Error(),
})
return
}
Expand All @@ -296,7 +319,7 @@ func (control *Control) TerminateServer(ctx *gin.Context) {
})
if err != nil {
ctx.AbortWithStatusJSON(http.StatusInternalServerError, APIError{
fmt.Errorf("failed to create snapshot for server %s: %s", serverName, err),
fmt.Errorf("failed to create snapshot for server %s: %s", serverName, err).Error(),
})
return
}
Expand All @@ -320,7 +343,7 @@ func (control *Control) TerminateServer(ctx *gin.Context) {
}()
if err != nil {
ctx.AbortWithStatusJSON(http.StatusInternalServerError, APIError{
err,
err.Error(),
})
return
}
Expand All @@ -329,7 +352,7 @@ func (control *Control) TerminateServer(ctx *gin.Context) {
_, err := control.hclient.Image.Delete(ctx, server.Image)
if err != nil {
ctx.AbortWithStatusJSON(http.StatusInternalServerError, APIError{
fmt.Errorf("failed to delete image %s[%d]: %s", server.Image.Name, server.Image.ID, err),
fmt.Errorf("failed to delete image %s[%d]: %s", server.Image.Name, server.Image.ID, err).Error(),
})
return
}
Expand Down Expand Up @@ -359,19 +382,47 @@ func (control *Control) TerminateServer(ctx *gin.Context) {
}()
if err != nil {
ctx.AbortWithStatusJSON(http.StatusInternalServerError, APIError{
err,
err.Error(),
})
return
}

_, err = control.hclient.Server.Delete(ctx, server)
if err != nil {
ctx.AbortWithStatusJSON(http.StatusInternalServerError, APIError{
fmt.Errorf("failed to delete server %s: %s", serverName, err),
fmt.Errorf("failed to delete server %s: %s", serverName, err).Error(),
})
return
}
log.Infof("deleted server %s", serverName)

if recordID, ok := server.Labels[LabelDNSRecordID]; ok {
err = deleteDNSRecord(recordID)
if err != nil {
ctx.AbortWithStatusJSON(http.StatusInternalServerError, APIError{
fmt.Errorf("failed to delete dns record for server %s: %s", serverName, err).Error(),
})
return
}
}

ctx.Status(http.StatusOK)
}

func (control *Control) attachDNSRecordToServer(ctx context.Context, server *hcloud.Server) error {
dnsRecordID, err := createDNSRecord(control.Config.dnsZoneID, server.Name+".svc", server.PublicNet.IPv4.IP.String())
if err != nil {
return fmt.Errorf("failed to create dns: %s", err)
}
labels := server.Labels
labels[LabelDNSRecordID] = dnsRecordID
_, _, err = control.hclient.Server.Update(ctx, server, hcloud.ServerUpdateOpts{Labels: labels})
if err != nil {
return fmt.Errorf("failed to attach dns record id to labels: %s", err)
}
_, _, err = control.hclient.Server.ChangeDNSPtr(ctx, server, server.PublicNet.IPv4.IP.String(), hcloud.String(server.Name+".svc.mnbr.eu"))
if err != nil {
return fmt.Errorf("failed to change reverse dns pointer for server %s: %s", server.Name, err)
}
return nil
}
92 changes: 92 additions & 0 deletions dns.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package main

import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"os"
"time"
)

const (
DNSEndpoint = "https://dns.hetzner.com/api/v1"
)

type CreateDNSRecordRequest struct {
ZoneID string `json:"zone_id"`
Type string `json:"type"`
Name string `json:"name"`
Value string `json:"value"`
TTL int `json:"ttl"`
}

type CreateDNSRecordResponse struct {
Record struct {
ID string `json:"id"`
} `json:"record"`
}

func createDNSRecord(zoneID string, name string, value string) (string, error) {
createDNSRecordRequest := CreateDNSRecordRequest{
ZoneID: zoneID,
Type: "A",
Name: name,
Value: value,
TTL: 300,
}
body, err := json.Marshal(createDNSRecordRequest)
if err != nil {
return "", err
}

client := &http.Client{Timeout: 10 * time.Second}
url := fmt.Sprintf("%s/records", DNSEndpoint)
req, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(body))
if err != nil {
return "", err
}
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Auth-API-Token", os.Getenv("HCLOUD_DNS_TOKEN"))

resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("failed to create dns record %s: %s", name, resp.Status)
}

var createDNSRecordResponse CreateDNSRecordResponse
respBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", err
}
err = json.Unmarshal(respBody, &createDNSRecordResponse)
if err != nil {
return "", err
}
return createDNSRecordResponse.Record.ID, nil
}

func deleteDNSRecord(recordID string) error {
client := &http.Client{Timeout: 10 * time.Second}
url := fmt.Sprintf("%s/records/%s", DNSEndpoint, recordID)
req, err := http.NewRequest(http.MethodDelete, url, nil)
if err != nil {
return err
}
req.Header.Add("Auth-API-Token", os.Getenv("HCLOUD_DNS_TOKEN"))

resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("failed to delete dns record %s: %s", recordID, resp.Status)
}
return nil
}
8 changes: 5 additions & 3 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ var (
locationName = flag.String("locationName", "nbg1", "location name")
networkIds = flag.String("networkIds", "", "comma separated list of network ids")
sshKeyIds = flag.String("sshKeyIds", "", "comma separated list if ssh key ids")
dnsZoneId = flag.String("dnsZoneId", "", "dns zone id")
)

func init() {
Expand Down Expand Up @@ -54,9 +55,10 @@ func main() {
}

control, err := NewControl(&ControlConfig{
location: &hcloud.Location{Name: *locationName},
networks: networks,
sshKeys: sshKeys,
location: &hcloud.Location{Name: *locationName},
networks: networks,
sshKeys: sshKeys,
dnsZoneID: *dnsZoneId,
})
if err != nil {
logrus.Fatalf("failed to create control: %w", err)
Expand Down

0 comments on commit 7db77d4

Please sign in to comment.