Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Signed-off-by: Lukas Kämmerling <lukas.kaemmerling@hetzner-cloud.de>
- Loading branch information
1 parent
1cde0d7
commit 785896c
Showing
2 changed files
with
305 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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") | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
} |