package hetzner import ( "fmt" "time" "github.com/go-resty/resty/v2" ) const ( hetznerAPIBaseURL = "https://api.hetzner.cloud/v1" ) type Client struct { apiKey string resty *resty.Client } type Server struct { ID int `json:"id"` Name string `json:"name"` Status string `json:"status"` PublicNet PublicNet `json:"public_net"` } type PublicNet struct { IPv4 IPv4Info `json:"ipv4"` } type IPv4Info struct { IP string `json:"ip"` } type ServerResponse struct { Server Server `json:"server"` } type ServersResponse struct { Servers []Server `json:"servers"` } type CreateServerRequest struct { Name string `json:"name"` ServerType string `json:"server_type"` Image string `json:"image"` Location string `json:"location,omitempty"` SSHKeys []int `json:"ssh_keys,omitempty"` UserData string `json:"user_data,omitempty"` Networks []int `json:"networks,omitempty"` Labels map[string]string `json:"labels,omitempty"` } func NewClient(apiKey string) *Client { client := resty.New() client.SetBaseURL(hetznerAPIBaseURL) client.SetHeader("Authorization", fmt.Sprintf("Bearer %s", apiKey)) client.SetTimeout(30 * time.Second) return &Client{ apiKey: apiKey, resty: client, } } func (c *Client) CreateServer(req CreateServerRequest) (*Server, error) { var resp ServerResponse httpResp, err := c.resty.R(). SetBody(req). SetResult(&resp). Post("/servers") if err != nil { return nil, fmt.Errorf("failed to create server: %w", err) } if httpResp.IsError() { return nil, fmt.Errorf("API error: %s - %s", httpResp.Status(), string(httpResp.Body())) } return &resp.Server, nil } func (c *Client) GetServer(serverID int) (*Server, error) { var resp ServerResponse httpResp, err := c.resty.R(). SetResult(&resp). Get(fmt.Sprintf("/servers/%d", serverID)) if err != nil { return nil, fmt.Errorf("failed to get server: %w", err) } if httpResp.IsError() { return nil, fmt.Errorf("API error: %s - %s", httpResp.Status(), string(httpResp.Body())) } return &resp.Server, nil } func (c *Client) DeleteServer(serverID int) error { httpResp, err := c.resty.R(). Delete(fmt.Sprintf("/servers/%d", serverID)) if err != nil { return fmt.Errorf("failed to delete server: %w", err) } if httpResp.IsError() { return fmt.Errorf("API error: %s - %s", httpResp.Status(), string(httpResp.Body())) } return nil } func (c *Client) WaitForServerReady(serverID int, timeout time.Duration) error { deadline := time.Now().Add(timeout) ticker := time.NewTicker(5 * time.Second) defer ticker.Stop() for { select { case <-ticker.C: server, err := c.GetServer(serverID) if err != nil { return fmt.Errorf("failed to check server status: %w", err) } if server.Status == "running" { return nil } if time.Now().After(deadline) { return fmt.Errorf("timeout waiting for server to be ready (current status: %s)", server.Status) } case <-time.After(timeout): return fmt.Errorf("timeout waiting for server to be ready") } } } func (c *Client) GetServerIP(serverID int) (string, error) { server, err := c.GetServer(serverID) if err != nil { return "", err } if server.PublicNet.IPv4.IP == "" { return "", fmt.Errorf("server has no public IP address") } return server.PublicNet.IPv4.IP, nil } func (c *Client) ListServers() ([]Server, error) { var resp ServersResponse httpResp, err := c.resty.R(). SetResult(&resp). Get("/servers") if err != nil { return nil, fmt.Errorf("failed to list servers: %w", err) } if httpResp.IsError() { return nil, fmt.Errorf("API error: %s - %s", httpResp.Status(), string(httpResp.Body())) } return resp.Servers, nil }