feat: add Timeweb Cloud provider for Woodpecker CI autoscaler
- 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
This commit is contained in:
222
providers/timewebcloud/api/client.go
Normal file
222
providers/timewebcloud/api/client.go
Normal file
@@ -0,0 +1,222 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user