From 453cdd49642e23d1e078f565832273ed01a39cc6 Mon Sep 17 00:00:00 2001 From: Cian Hatton Date: Tue, 2 Aug 2022 01:03:07 +0100 Subject: [PATCH] Add script to start portainer stacks (#1) --- scripts/portainer/client/client.go | 148 +++++++++++++++++++++++++++++ scripts/portainer/client/types.go | 34 +++++++ scripts/portainer/go.mod | 3 + scripts/portainer/main.go | 81 ++++++++++++++++ 4 files changed, 266 insertions(+) create mode 100644 scripts/portainer/client/client.go create mode 100644 scripts/portainer/client/types.go create mode 100644 scripts/portainer/go.mod create mode 100644 scripts/portainer/main.go diff --git a/scripts/portainer/client/client.go b/scripts/portainer/client/client.go new file mode 100644 index 0000000..26cc8c2 --- /dev/null +++ b/scripts/portainer/client/client.go @@ -0,0 +1,148 @@ +package client + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" +) + +const ( + applicationJson = "application/json" +) + +// api docs +// https://app.swaggerhub.com/apis/portainer/portainer-ce/2.9.3 + +type PortainerClient struct { + authToken string + baseUrl string +} + +type Credentials struct { + Username string `json:"username"` + Password string `json:"password"` + BaseUrl string `json:"baseUrl"` +} + +func NewPortainerClient(creds Credentials) (*PortainerClient, error) { + c := &PortainerClient{ + baseUrl: creds.BaseUrl, + } + return c, c.Login(creds.Username, creds.Password) +} + +func (c *PortainerClient) IsLoggedIn() bool { + return c.authToken != "" +} + +func (c *PortainerClient) Login(username, password string) error { + payload := map[string]string{"Username": username, "Password": password} + body, err := c.post("api/auth", payload) + if err != nil { + return err + } + type JwtToken struct { + Token string `json:"jwt"` + } + token := JwtToken{} + if err := json.Unmarshal(body, &token); err != nil { + return err + } + c.authToken = token.Token + return nil +} + +func (c *PortainerClient) GetAllStacks() ([]Stack, error) { + b, err := c.get("stacks") + if err != nil { + return nil, err + } + var stacks []Stack + if err := json.Unmarshal(b, &stacks); err != nil { + return nil, err + } + return stacks, nil +} + +func (c *PortainerClient) GetStackByName(name string) (*Stack, error) { + stacks, err := c.GetAllStacks() + if err != nil { + return nil, err + } + for _, s := range stacks { + if s.Name == name { + return &s, nil + } + } + return nil, nil +} + +type ResponseMessage struct { + Message string `json:"message"` + Details string `json:"details"` +} + +func (c *PortainerClient) StartStack(stackId int) (ResponseMessage, error) { + url := fmt.Sprintf("api/stacks/%d/start", stackId) + b, err := c.post(url, nil) + msg := ResponseMessage{} + if err := json.Unmarshal(b, &msg); err != nil { + return msg, err + } + return msg, err +} +func (c *PortainerClient) post(path string, payload interface{}) ([]byte, error) { + jsonBytes, err := json.Marshal(payload) + if err != nil { + return nil, err + } + url := fmt.Sprintf("%s/%s", c.baseUrl, path) + // Create a Bearer string by appending string access token + var bearer = "Bearer " + c.authToken + // Create a new request using http + req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonBytes)) + + // add authorization header to the req + req.Header.Add("Authorization", bearer) + + // Send req using http Client + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + return body, nil +} + +func (c *PortainerClient) get(path string) ([]byte, error) { + url := fmt.Sprintf("%s/api/%s", c.baseUrl, path) + + // Create a Bearer string by appending string access token + var bearer = "Bearer " + c.authToken + // Create a new request using http + req, err := http.NewRequest("GET", url, nil) + + // add authorization header to the req + req.Header.Add("Authorization", bearer) + + // Send req using http Client + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + return body, nil +} diff --git a/scripts/portainer/client/types.go b/scripts/portainer/client/types.go new file mode 100644 index 0000000..3848c5e --- /dev/null +++ b/scripts/portainer/client/types.go @@ -0,0 +1,34 @@ +package client + +type Stack struct { + ID int `json:"Id"` + Name string `json:"Name"` + Type int `json:"Type"` + EndpointID int `json:"EndpointId"` + SwarmID string `json:"SwarmId"` + EntryPoint string `json:"EntryPoint"` + Env []interface{} `json:"Env"` + ResourceControl struct { + ID int `json:"Id"` + ResourceID string `json:"ResourceId"` + SubResourceIds []interface{} `json:"SubResourceIds"` + Type int `json:"Type"` + UserAccesses []interface{} `json:"UserAccesses"` + TeamAccesses []interface{} `json:"TeamAccesses"` + Public bool `json:"Public"` + AdministratorsOnly bool `json:"AdministratorsOnly"` + System bool `json:"System"` + } `json:"ResourceControl"` + Status int `json:"Status"` + ProjectPath string `json:"ProjectPath"` + CreationDate int `json:"CreationDate"` + CreatedBy string `json:"CreatedBy"` + UpdateDate int `json:"UpdateDate"` + UpdatedBy string `json:"UpdatedBy"` + AdditionalFiles interface{} `json:"AdditionalFiles"` + AutoUpdate interface{} `json:"AutoUpdate"` + GitConfig interface{} `json:"GitConfig"` + FromAppTemplate bool `json:"FromAppTemplate"` + Namespace string `json:"Namespace"` + IsComposeFormat bool `json:"IsComposeFormat"` +} diff --git a/scripts/portainer/go.mod b/scripts/portainer/go.mod new file mode 100644 index 0000000..007ae31 --- /dev/null +++ b/scripts/portainer/go.mod @@ -0,0 +1,3 @@ +module github.com/chatton/portainer + +go 1.18 diff --git a/scripts/portainer/main.go b/scripts/portainer/main.go new file mode 100644 index 0000000..a81a38e --- /dev/null +++ b/scripts/portainer/main.go @@ -0,0 +1,81 @@ +package main + +import ( + "encoding/json" + "errors" + "fmt" + "log" + "os" + "os/user" + "strings" + + "github.com/chatton/portainer/client" +) + +func loadCreds() client.Credentials { + usr, _ := user.Current() + credPath := fmt.Sprintf("%s/.homelab/portainer-creds.json", usr.HomeDir) + + if _, err := os.Stat(credPath); errors.Is(err, os.ErrNotExist) { + log.Fatal(fmt.Errorf("there must be a credentials file under: %s", credPath)) + } + + fileBytes, err := os.ReadFile(credPath) + if err != nil { + log.Fatal(err) + } + + creds := client.Credentials{} + if err := json.Unmarshal(fileBytes, &creds); err != nil { + log.Fatal(err) + } + return creds +} + +type StackResult struct { + Name string `json:"name"` + Id int `json:"id"` +} + +func main() { + args := os.Args + if len(args) != 2 { + fmt.Println("must specify name of stack to start!") + os.Exit(1) + } + + stackName := args[1] + creds := loadCreds() + c, err := client.NewPortainerClient(creds) + if err != nil { + log.Fatal(err) + } + s, err := c.GetStackByName(stackName) + if err != nil { + log.Fatal(err) + } + if s == nil { + log.Fatalf("no stack found with name: %s\n", stackName) + } + + msg, err := c.StartStack(s.ID) + if err != nil { + log.Fatal(err) + } + + if msg.Details != "" && !strings.Contains(msg.Details, "is already running") { + log.Fatalf("problem starting stack: %s", msg.Details) + } + + sr := StackResult{ + Name: stackName, + Id: s.ID, + } + + bytes, err := json.Marshal(sr) + if err != nil { + log.Fatal(err) + } + // output details of the stack that was started (or is already started) + fmt.Println(string(bytes)) +}