Skip to content

Commit

Permalink
Add Metadata Client (#184)
Browse files Browse the repository at this point in the history
Signed-off-by: Lukas Kämmerling <lukas.kaemmerling@hetzner-cloud.de>
  • Loading branch information
LKaemmerling committed Aug 23, 2021
1 parent 1cde0d7 commit 785896c
Show file tree
Hide file tree
Showing 2 changed files with 305 additions and 0 deletions.
116 changes: 116 additions & 0 deletions hcloud/metadata/client.go
@@ -0,0 +1,116 @@
package metadata

import (
"io/ioutil"
"net"
"net/http"
"strconv"
"strings"
)

const Endpoint = "http://169.254.169.254/hetzner/v1/metadata"

// Client is a client for the Hetzner Cloud Server Metadata Endpoints.
type Client struct {
endpoint string

httpClient *http.Client
}

// A ClientOption is used to configure a Client.
type ClientOption func(*Client)

// WithEndpoint configures a Client to use the specified Metadata API endpoint.
func WithEndpoint(endpoint string) ClientOption {
return func(client *Client) {
client.endpoint = strings.TrimRight(endpoint, "/")
}
}

// WithHTTPClient configures a Client to perform HTTP requests with httpClient.
func WithHTTPClient(httpClient *http.Client) ClientOption {
return func(client *Client) {
client.httpClient = httpClient
}
}

// NewClient creates a new client.
func NewClient(options ...ClientOption) *Client {
client := &Client{
endpoint: Endpoint,
httpClient: &http.Client{},
}

for _, option := range options {
option(client)
}
return client
}

// NewRequest creates an HTTP request against the API. The returned request
// is assigned with ctx and has all necessary headers set (auth, user agent, etc.).
func (c *Client) get(path string) (string, error) {
url := c.endpoint + path
resp, err := c.httpClient.Get(url)
if err != nil {
return "", err
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", err
}
resp.Body.Close()
return string(body), nil
}

// IsHcloudServer checks if the currently called server is a hcloud server by calling a metadata endpoint
// if the endpoint answers with a non-empty value this method returns true, otherwise false
func (c *Client) IsHcloudServer() bool {
hostname, err := c.Hostname()
if err != nil {
return false
}
if len(hostname) > 0 {
return true
}
return false
}

// Hostname returns the hostname of the server that did the request to the Metadata server
func (c *Client) Hostname() (string, error) {
return c.get("/hostname")
}

// InstanceID returns the ID of the server that did the request to the Metadata server
func (c *Client) InstanceID() (int, error) {
resp, err := c.get("/instance-id")
if err != nil {
return 0, err
}
return strconv.Atoi(resp)
}

// PublicIPv4 returns the Public IPv4 of the server that did the request to the Metadata server
func (c *Client) PublicIPv4() (net.IP, error) {
resp, err := c.get("/public-ipv4")
if err != nil {
return nil, err
}
return net.ParseIP(resp), nil
}

// Region returns the Network Zone of the server that did the request to the Metadata server
func (c *Client) Region() (string, error) {
return c.get("/region")
}

// AvailabilityZone returns the datacenter of the server that did the request to the Metadata server
func (c *Client) AvailabilityZone() (string, error) {
return c.get("/availability-zone")
}

// PrivateNetworks returns details about the private networks the server is attached to
// Returns YAML (unparsed)
func (c *Client) PrivateNetworks() (string, error) {
return c.get("/private-networks")
}
189 changes: 189 additions & 0 deletions hcloud/metadata/client_test.go
@@ -0,0 +1,189 @@
package metadata

import (
"net/http"
"net/http/httptest"
"testing"

"github.com/stretchr/testify/assert"
)

type testEnv struct {
Server *httptest.Server
Mux *http.ServeMux
Client *Client
}

func (env *testEnv) Teardown() {
env.Server.Close()
env.Server = nil
env.Mux = nil
env.Client = nil
}

func newTestEnv() testEnv {
mux := http.NewServeMux()
server := httptest.NewServer(mux)
client := NewClient(
WithEndpoint(server.URL),
)
return testEnv{
Server: server,
Mux: mux,
Client: client,
}
}

func TestClient_IsHcloudServer(t *testing.T) {
env := newTestEnv()
env.Mux.HandleFunc("/hostname", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("my-server"))
})

isHcloudServer := env.Client.IsHcloudServer()

assert.True(t, isHcloudServer)
}

func TestClient_IsHcloudServer_EmptyReturn(t *testing.T) {
env := newTestEnv()
env.Mux.HandleFunc("/hostname", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(""))
})

isHcloudServer := env.Client.IsHcloudServer()

assert.False(t, isHcloudServer)
}

func TestClient_IsHcloudServer_HttpError(t *testing.T) {
env := newTestEnv()
env.Mux.HandleFunc("/hostname", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(404)
})
isHcloudServer := env.Client.IsHcloudServer()

assert.False(t, isHcloudServer)
}
func TestClient_Hostname(t *testing.T) {
env := newTestEnv()
env.Mux.HandleFunc("/hostname", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("my-server"))
})

hostname, err := env.Client.Hostname()
if err != nil {
t.Fatal(err)
}
if hostname != "my-server" {
t.Fatalf("Unexpected hostname %s", hostname)
}
}

func TestClient_InstanceID(t *testing.T) {
env := newTestEnv()
env.Mux.HandleFunc("/instance-id", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("123456"))
})

instanceID, err := env.Client.InstanceID()
if err != nil {
t.Fatal(err)
}
if instanceID != 123456 {
t.Fatalf("Unexpected instanceID %d", instanceID)
}
}

func TestClient_PublicIPv4(t *testing.T) {
env := newTestEnv()
env.Mux.HandleFunc("/public-ipv4", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("127.0.0.1"))
})

publicIPv4, err := env.Client.PublicIPv4()
if err != nil {
t.Fatal(err)
}
if publicIPv4.String() != "127.0.0.1" {
t.Fatalf("Unexpected PublicIPv4 %s", publicIPv4.String())
}
}

func TestClient_Region(t *testing.T) {
env := newTestEnv()
env.Mux.HandleFunc("/region", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("eu-central"))
})

region, err := env.Client.Region()
if err != nil {
t.Fatal(err)
}
if region != "eu-central" {
t.Fatalf("Unexpected region %s", region)
}
}

func TestClient_AvailabilityZone(t *testing.T) {
env := newTestEnv()
env.Mux.HandleFunc("/availability-zone", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("fsn1-dc14"))
})

availabilityZone, err := env.Client.AvailabilityZone()
if err != nil {
t.Fatal(err)
}
if availabilityZone != "fsn1-dc14" {
t.Fatalf("Unexpected availabilityZone %s", availabilityZone)
}
}

func TestClient_PrivateNetworks(t *testing.T) {
env := newTestEnv()
env.Mux.HandleFunc("/private-networks", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(`- ip: 10.0.0.2
alias_ips: [10.0.0.3, 10.0.0.4]
interface_num: 1
mac_address: 86:00:00:2a:7d:e0
network_id: 1234
network_name: nw-test1
network: 10.0.0.0/8
subnet: 10.0.0.0/24
gateway: 10.0.0.1
- ip: 192.168.0.2
alias_ips: []
interface_num: 2
mac_address: 86:00:00:2a:7d:e1
network_id: 4321
network_name: nw-test2
network: 192.168.0.0/16
subnet: 192.168.0.0/24
gateway: 192.168.0.1`))
})

privateNetworks, err := env.Client.PrivateNetworks()
if err != nil {
t.Fatal(err)
}
expectedNetworks := `- ip: 10.0.0.2
alias_ips: [10.0.0.3, 10.0.0.4]
interface_num: 1
mac_address: 86:00:00:2a:7d:e0
network_id: 1234
network_name: nw-test1
network: 10.0.0.0/8
subnet: 10.0.0.0/24
gateway: 10.0.0.1
- ip: 192.168.0.2
alias_ips: []
interface_num: 2
mac_address: 86:00:00:2a:7d:e1
network_id: 4321
network_name: nw-test2
network: 192.168.0.0/16
subnet: 192.168.0.0/24
gateway: 192.168.0.1`
assert.Equal(t, privateNetworks, expectedNetworks)
}

0 comments on commit 785896c

Please sign in to comment.