- Implement timewebcloud provider with DeployAgent, RemoveAgent, ListDeployedAgentNames - Add minimal HTTP API client for Timeweb Cloud (create/list/delete servers) - Register provider in main.go with CLI flags - Add timeweb-list and timeweb-tester utilities - Include Dockerfile and docker-compose.yml for deployment - Update DEPLOY.md with verified OS/preset IDs
223 lines
5.9 KiB
Go
223 lines
5.9 KiB
Go
package api
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strconv"
|
|
"time"
|
|
)
|
|
|
|
const defaultBaseURL = "https://api.timeweb.cloud"
|
|
|
|
// Client is a minimal HTTP client for the Timeweb Cloud API.
|
|
type Client struct {
|
|
baseURL string
|
|
token string
|
|
client *http.Client
|
|
}
|
|
|
|
// NewClient creates a new Timeweb Cloud API client.
|
|
func NewClient(token string) *Client {
|
|
return &Client{
|
|
baseURL: defaultBaseURL,
|
|
token: token,
|
|
client: &http.Client{Timeout: 30 * time.Second},
|
|
}
|
|
}
|
|
|
|
func (c *Client) request(ctx context.Context, method, path string, body []byte) (*http.Response, error) {
|
|
url := c.baseURL + path
|
|
var bodyReader io.Reader
|
|
if body != nil {
|
|
bodyReader = bytes.NewReader(body)
|
|
}
|
|
|
|
req, err := http.NewRequestWithContext(ctx, method, url, bodyReader)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
req.Header.Set("Authorization", "Bearer "+c.token)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
return c.client.Do(req)
|
|
}
|
|
|
|
// CreateServerRequest is the payload for creating a server.
|
|
type CreateServerRequest struct {
|
|
Name string `json:"name"`
|
|
OsID int32 `json:"os_id,omitempty"`
|
|
ImageID string `json:"image_id,omitempty"`
|
|
PresetID int32 `json:"preset_id,omitempty"`
|
|
Bandwidth int32 `json:"bandwidth,omitempty"`
|
|
SSHKeysIds []int32 `json:"ssh_keys_ids,omitempty"`
|
|
CloudInit string `json:"cloud_init,omitempty"`
|
|
AvailabilityZone string `json:"availability_zone,omitempty"`
|
|
Hostname string `json:"hostname,omitempty"`
|
|
Comment string `json:"comment,omitempty"`
|
|
}
|
|
|
|
// CreateServerResponse is the response from creating a server.
|
|
type CreateServerResponse struct {
|
|
Server Server `json:"server"`
|
|
}
|
|
|
|
// Server represents a Timeweb Cloud VDS.
|
|
type Server struct {
|
|
ID int32 `json:"id"`
|
|
Name string `json:"name"`
|
|
Status string `json:"status"`
|
|
Location string `json:"location"`
|
|
}
|
|
|
|
// CreateServer creates a new cloud server.
|
|
func (c *Client) CreateServer(ctx context.Context, req CreateServerRequest) (*CreateServerResponse, error) {
|
|
payload, err := json.Marshal(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
resp, err := c.request(ctx, http.MethodPost, "/api/v1/servers", payload)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusCreated {
|
|
body, _ := io.ReadAll(resp.Body)
|
|
return nil, fmt.Errorf("unexpected status code: %d, body: %s", resp.StatusCode, string(body))
|
|
}
|
|
|
|
var result CreateServerResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &result, nil
|
|
}
|
|
|
|
// DeleteServer deletes a cloud server by ID.
|
|
func (c *Client) DeleteServer(ctx context.Context, id int32) error {
|
|
path := "/api/v1/servers/" + strconv.FormatInt(int64(id), 10)
|
|
resp, err := c.request(ctx, http.MethodDelete, path, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNoContent {
|
|
body, _ := io.ReadAll(resp.Body)
|
|
return fmt.Errorf("unexpected status code: %d, body: %s", resp.StatusCode, string(body))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetServersResponse is the response for listing servers.
|
|
type GetServersResponse struct {
|
|
Meta Meta `json:"meta"`
|
|
Servers []Server `json:"servers"`
|
|
}
|
|
|
|
// Meta contains pagination info.
|
|
type Meta struct {
|
|
Total int32 `json:"total"`
|
|
}
|
|
|
|
// GetServers lists all cloud servers.
|
|
func (c *Client) GetServers(ctx context.Context) (*GetServersResponse, error) {
|
|
resp, err := c.request(ctx, http.MethodGet, "/api/v1/servers", nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
body, _ := io.ReadAll(resp.Body)
|
|
return nil, fmt.Errorf("unexpected status code: %d, body: %s", resp.StatusCode, string(body))
|
|
}
|
|
|
|
var result GetServersResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &result, nil
|
|
}
|
|
|
|
type OS struct {
|
|
ID int32 `json:"id"`
|
|
Family string `json:"family"`
|
|
Name string `json:"name"`
|
|
Version string `json:"version"`
|
|
VersionCodename string `json:"version_codename"`
|
|
Description string `json:"description"`
|
|
}
|
|
|
|
type GetOSListResponse struct {
|
|
OsList []OS `json:"servers_os"`
|
|
}
|
|
|
|
func (c *Client) GetOSList(ctx context.Context) (*GetOSListResponse, error) {
|
|
resp, err := c.request(ctx, http.MethodGet, "/api/v1/os/servers", nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
body, _ := io.ReadAll(resp.Body)
|
|
return nil, fmt.Errorf("unexpected status code: %d, body: %s", resp.StatusCode, string(body))
|
|
}
|
|
|
|
var result GetOSListResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &result, nil
|
|
}
|
|
|
|
type Preset struct {
|
|
ID int32 `json:"id"`
|
|
Location string `json:"location"`
|
|
Price float64 `json:"price"`
|
|
CPU int32 `json:"cpu"`
|
|
CPUFrequency string `json:"cpu_frequency"`
|
|
RAM int32 `json:"ram"`
|
|
Disk int32 `json:"disk"`
|
|
DiskType string `json:"disk_type"`
|
|
Bandwidth int32 `json:"bandwidth"`
|
|
DescriptionShort string `json:"description_short"`
|
|
IsDedicatedCPU bool `json:"is_dedicated_cpu"`
|
|
Tags []string `json:"tags"`
|
|
}
|
|
|
|
type GetPresetsResponse struct {
|
|
Presets []Preset `json:"server_presets"`
|
|
}
|
|
|
|
func (c *Client) GetServerPresets(ctx context.Context) (*GetPresetsResponse, error) {
|
|
resp, err := c.request(ctx, http.MethodGet, "/api/v1/presets/servers", nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
body, _ := io.ReadAll(resp.Body)
|
|
return nil, fmt.Errorf("unexpected status code: %d, body: %s", resp.StatusCode, string(body))
|
|
}
|
|
|
|
var result GetPresetsResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &result, nil
|
|
}
|