241 lines
5.4 KiB
Go
241 lines
5.4 KiB
Go
package portainer
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
)
|
|
|
|
type Client struct {
|
|
baseURL string
|
|
token string
|
|
httpClient *http.Client
|
|
endpointID int
|
|
}
|
|
|
|
type Endpoint struct {
|
|
ID int `json:"Id"`
|
|
Name string `json:"Name"`
|
|
}
|
|
|
|
type EnvVar struct {
|
|
Name string `json:"name"`
|
|
Value string `json:"value"`
|
|
}
|
|
|
|
type Stack struct {
|
|
ID int `json:"Id"`
|
|
Name string `json:"Name"`
|
|
Status int `json:"Status"` // 1 = active, 2 = inactive
|
|
EndpointID int `json:"EndpointId"`
|
|
Env []EnvVar `json:"Env"`
|
|
}
|
|
|
|
type StackFile struct {
|
|
StackFileContent string `json:"StackFileContent"`
|
|
}
|
|
|
|
type Container struct {
|
|
ID string `json:"Id"`
|
|
Names []string `json:"Names"`
|
|
}
|
|
|
|
// FindContainer returns the container ID for a given name (e.g. "seerr").
|
|
// Docker container names have a leading slash, so "/seerr" matches "seerr".
|
|
func (c *Client) FindContainer(name string) (string, error) {
|
|
var containers []Container
|
|
if err := c.get(fmt.Sprintf("/api/endpoints/%d/docker/containers/json", c.endpointID), &containers); err != nil {
|
|
return "", err
|
|
}
|
|
want := "/" + name
|
|
for _, ct := range containers {
|
|
for _, n := range ct.Names {
|
|
if n == want {
|
|
return ct.ID, nil
|
|
}
|
|
}
|
|
}
|
|
return "", fmt.Errorf("container %q not found", name)
|
|
}
|
|
|
|
func New(baseURL, token, endpointName string) (*Client, error) {
|
|
c := &Client{
|
|
baseURL: strings.TrimRight(baseURL, "/"),
|
|
token: token,
|
|
httpClient: &http.Client{},
|
|
}
|
|
|
|
id, err := c.discoverEndpoint(endpointName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
c.endpointID = id
|
|
return c, nil
|
|
}
|
|
|
|
func (c *Client) EndpointID() int {
|
|
return c.endpointID
|
|
}
|
|
|
|
func (c *Client) discoverEndpoint(name string) (int, error) {
|
|
var endpoints []Endpoint
|
|
if err := c.get("/api/endpoints", &endpoints); err != nil {
|
|
return 0, fmt.Errorf("listing endpoints: %w", err)
|
|
}
|
|
if len(endpoints) == 0 {
|
|
return 0, fmt.Errorf("no endpoints found in Portainer")
|
|
}
|
|
// If a name is specified, match by name; otherwise use the first active one.
|
|
for _, e := range endpoints {
|
|
if name == "" || e.Name == name {
|
|
return e.ID, nil
|
|
}
|
|
}
|
|
return 0, fmt.Errorf("endpoint %q not found", name)
|
|
}
|
|
|
|
func (c *Client) ListStacks() ([]Stack, error) {
|
|
var stacks []Stack
|
|
if err := c.get("/api/stacks", &stacks); err != nil {
|
|
return nil, err
|
|
}
|
|
return stacks, nil
|
|
}
|
|
|
|
func (c *Client) GetStack(id int) (*Stack, error) {
|
|
var s Stack
|
|
if err := c.get(fmt.Sprintf("/api/stacks/%d", id), &s); err != nil {
|
|
return nil, err
|
|
}
|
|
return &s, nil
|
|
}
|
|
|
|
func (c *Client) GetStackByName(name string) (*Stack, error) {
|
|
stacks, err := c.ListStacks()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, s := range stacks {
|
|
if s.Name == name {
|
|
return &s, nil
|
|
}
|
|
}
|
|
return nil, nil // not found, not an error
|
|
}
|
|
|
|
func (c *Client) GetStackFile(id int) (string, error) {
|
|
var f StackFile
|
|
if err := c.get(fmt.Sprintf("/api/stacks/%d/file", id), &f); err != nil {
|
|
return "", err
|
|
}
|
|
return f.StackFileContent, nil
|
|
}
|
|
|
|
func (c *Client) CreateStack(name, composeContent string, env []EnvVar) (*Stack, error) {
|
|
body := map[string]any{
|
|
"name": name,
|
|
"stackFileContent": composeContent,
|
|
"env": env,
|
|
}
|
|
var s Stack
|
|
if err := c.post(fmt.Sprintf("/api/stacks/create/standalone/string?endpointId=%d", c.endpointID), body, &s); err != nil {
|
|
return nil, err
|
|
}
|
|
return &s, nil
|
|
}
|
|
|
|
func (c *Client) StopStack(id int) error {
|
|
resp, err := c.do("POST", fmt.Sprintf("/api/stacks/%d/stop?endpointId=%d", id, c.endpointID), nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
resp.Body.Close()
|
|
return nil
|
|
}
|
|
|
|
func (c *Client) StartStack(id int) error {
|
|
resp, err := c.do("POST", fmt.Sprintf("/api/stacks/%d/start?endpointId=%d", id, c.endpointID), nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
resp.Body.Close()
|
|
return nil
|
|
}
|
|
|
|
func (c *Client) UpdateStack(id int, composeContent string, env []EnvVar) (*Stack, error) {
|
|
body := map[string]any{
|
|
"stackFileContent": composeContent,
|
|
"env": env,
|
|
"prune": true,
|
|
"pullImage": false,
|
|
}
|
|
var s Stack
|
|
if err := c.put(fmt.Sprintf("/api/stacks/%d?endpointId=%d", id, c.endpointID), body, &s); err != nil {
|
|
return nil, err
|
|
}
|
|
return &s, nil
|
|
}
|
|
|
|
// request helpers
|
|
|
|
func (c *Client) get(path string, out any) error {
|
|
resp, err := c.do("GET", path, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
return json.NewDecoder(resp.Body).Decode(out)
|
|
}
|
|
|
|
func (c *Client) post(path string, body, out any) error {
|
|
resp, err := c.do("POST", path, body)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
return json.NewDecoder(resp.Body).Decode(out)
|
|
}
|
|
|
|
func (c *Client) put(path string, body, out any) error {
|
|
resp, err := c.do("PUT", path, body)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
return json.NewDecoder(resp.Body).Decode(out)
|
|
}
|
|
|
|
func (c *Client) do(method, path string, body any) (*http.Response, error) {
|
|
var bodyReader io.Reader
|
|
if body != nil {
|
|
b, err := json.Marshal(body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
bodyReader = bytes.NewReader(b)
|
|
}
|
|
|
|
req, err := http.NewRequest(method, c.baseURL+path, bodyReader)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Header.Set("X-API-Key", c.token)
|
|
if body != nil {
|
|
req.Header.Set("Content-Type", "application/json")
|
|
}
|
|
|
|
resp, err := c.httpClient.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if resp.StatusCode >= 400 {
|
|
b, _ := io.ReadAll(resp.Body)
|
|
resp.Body.Close()
|
|
return nil, fmt.Errorf("portainer API %s %s: %s — %s", method, path, resp.Status, string(b))
|
|
}
|
|
return resp, nil
|
|
}
|