Split common package into common, client, and util packages

This commit is contained in:
Juan Carlos Mejías Rodríguez
2019-08-02 20:25:22 -04:00
parent a3b58499b8
commit 9c79379191
14 changed files with 400 additions and 369 deletions

View File

@ -1,299 +1,18 @@
package common
import (
"bytes"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"strings"
"github.com/greenled/portainer-stack-utils/client"
"github.com/spf13/viper"
)
var cachedClient PortainerClient
type ClientConfig struct {
Url string
User string
Password string
Token string
DoNotUseToken bool
}
type PortainerClient interface {
Authenticate() (token string, err error)
GetEndpoints() ([]EndpointSubset, error)
GetStacks(swarmId string, endpointId uint32) ([]Stack, error)
CreateSwarmStack(stackName string, environmentVariables []StackEnv, stackFileContent string, swarmClusterId string, endpointId string) error
CreateComposeStack(stackName string, environmentVariables []StackEnv, stackFileContent string, endpointId string) error
UpdateStack(stack Stack, environmentVariables []StackEnv, stackFileContent string, prune bool, endpointId string) error
DeleteStack(stackId uint32) error
GetStackFileContent(stackId uint32) (content string, err error)
GetEndpointDockerInfo(endpointId string) (info map[string]interface{}, err error)
GetStatus() (Status, error)
}
type PortainerClientImp struct {
httpClient *http.Client
url *url.URL
user string
password string
token string
doNotUseToken bool
}
// Check if an http.Response object has errors
func checkResponseForErrors(resp *http.Response) error {
if 300 <= resp.StatusCode {
// Guess it's a GenericError
respBody := GenericError{}
err := json.NewDecoder(resp.Body).Decode(&respBody)
if err != nil {
// It's not a GenericError
bodyBytes, err := ioutil.ReadAll(resp.Body)
defer resp.Body.Close()
if err != nil {
return err
}
resp.Body = ioutil.NopCloser(bytes.NewReader(bodyBytes))
return errors.New(string(bodyBytes))
}
return &respBody
}
return nil
}
// Do an http request
func (n *PortainerClientImp) do(uri, method string, request io.Reader, requestType string, headers http.Header) (resp *http.Response, err error) {
requestUrl, err := n.url.Parse(uri)
if err != nil {
return
}
req, err := http.NewRequest(method, requestUrl.String(), request)
if err != nil {
return
}
if headers != nil {
req.Header = headers
}
if request != nil {
req.Header.Set("Content-Type", requestType)
}
if !n.doNotUseToken {
if n.token == "" {
n.token, err = n.Authenticate()
if err != nil {
return
}
PrintDebug(fmt.Sprintf("Auth token: %s", n.token))
}
req.Header.Set("Authorization", "Bearer "+n.token)
}
PrintDebugRequest("Request", req)
resp, err = n.httpClient.Do(req)
if err != nil {
return
}
err = checkResponseForErrors(resp)
if err != nil {
return
}
PrintDebugResponse("Response", resp)
return
}
// Do a JSON http request
func (n *PortainerClientImp) doJSON(uri, method string, request interface{}, response interface{}) error {
var body io.Reader
if request != nil {
reqBodyBytes, err := json.Marshal(request)
if err != nil {
return err
}
body = bytes.NewReader(reqBodyBytes)
}
resp, err := n.do(uri, method, body, "application/json", nil)
if err != nil {
return err
}
if response != nil {
d := json.NewDecoder(resp.Body)
err := d.Decode(response)
if err != nil {
return err
}
}
return nil
}
// Authenticate a user to get an auth token
func (n *PortainerClientImp) Authenticate() (token string, err error) {
PrintVerbose("Getting auth token...")
reqBody := AuthenticateUserRequest{
Username: n.user,
Password: n.password,
}
respBody := AuthenticateUserResponse{}
previousDoNotUseTokenValue := n.doNotUseToken
n.doNotUseToken = true
err = n.doJSON("auth", http.MethodPost, &reqBody, &respBody)
if err != nil {
return
}
n.doNotUseToken = previousDoNotUseTokenValue
token = respBody.Jwt
return
}
// Get endpoints
func (n *PortainerClientImp) GetEndpoints() (endpoints []EndpointSubset, err error) {
PrintVerbose("Getting endpoints...")
err = n.doJSON("endpoints", http.MethodGet, nil, &endpoints)
return
}
// Get stacks, optionally filtered by swarmId and endpointId
func (n *PortainerClientImp) GetStacks(swarmId string, endpointId uint32) (stacks []Stack, err error) {
PrintVerbose("Getting stacks...")
filter := StackListFilter{
SwarmId: swarmId,
EndpointId: endpointId,
}
filterJsonBytes, _ := json.Marshal(filter)
filterJsonString := string(filterJsonBytes)
err = n.doJSON(fmt.Sprintf("stacks?filters=%s", filterJsonString), http.MethodGet, nil, &stacks)
return
}
// Create swarm stack
func (n *PortainerClientImp) CreateSwarmStack(stackName string, environmentVariables []StackEnv, stackFileContent string, swarmClusterId string, endpointId string) (err error) {
PrintVerbose("Deploying stack...")
reqBody := StackCreateRequest{
Name: stackName,
Env: environmentVariables,
SwarmID: swarmClusterId,
StackFileContent: stackFileContent,
}
err = n.doJSON(fmt.Sprintf("stacks?type=%v&method=%s&endpointId=%s", 1, "string", endpointId), http.MethodPost, &reqBody, nil)
return
}
// Create compose stack
func (n *PortainerClientImp) CreateComposeStack(stackName string, environmentVariables []StackEnv, stackFileContent string, endpointId string) (err error) {
PrintVerbose("Deploying stack...")
reqBody := StackCreateRequest{
Name: stackName,
Env: environmentVariables,
StackFileContent: stackFileContent,
}
err = n.doJSON(fmt.Sprintf("stacks?type=%v&method=%s&endpointId=%s", 2, "string", endpointId), http.MethodPost, &reqBody, nil)
return
}
// Update stack
func (n *PortainerClientImp) UpdateStack(stack Stack, environmentVariables []StackEnv, stackFileContent string, prune bool, endpointId string) (err error) {
PrintVerbose("Updating stack...")
reqBody := StackUpdateRequest{
Env: environmentVariables,
StackFileContent: stackFileContent,
Prune: prune,
}
err = n.doJSON(fmt.Sprintf("stacks/%v?endpointId=%s", stack.Id, endpointId), http.MethodPut, &reqBody, nil)
return
}
// Delete stack
func (n *PortainerClientImp) DeleteStack(stackId uint32) (err error) {
PrintVerbose("Deleting stack...")
err = n.doJSON(fmt.Sprintf("stacks/%d", stackId), http.MethodDelete, nil, nil)
return
}
// Get stack file content
func (n *PortainerClientImp) GetStackFileContent(stackId uint32) (content string, err error) {
PrintVerbose("Getting stack file content...")
var respBody StackFileInspectResponse
err = n.doJSON(fmt.Sprintf("stacks/%v/file", stackId), http.MethodGet, nil, &respBody)
if err != nil {
return
}
content = respBody.StackFileContent
return
}
// Get endpoint Docker info
func (n *PortainerClientImp) GetEndpointDockerInfo(endpointId string) (info map[string]interface{}, err error) {
PrintVerbose("Getting endpoint Docker info...")
err = n.doJSON(fmt.Sprintf("endpoints/%v/docker/info", endpointId), http.MethodGet, nil, &info)
return
}
// Get Portainer status info
func (n *PortainerClientImp) GetStatus() (status Status, err error) {
err = n.doJSON("status", http.MethodGet, nil, &status)
return
}
// Create a new client
func NewClient(httpClient *http.Client, config ClientConfig) (c PortainerClient, err error) {
apiUrl, err := url.Parse(strings.TrimRight(config.Url, "/") + "/api/")
if err != nil {
return
}
c = &PortainerClientImp{
httpClient: httpClient,
url: apiUrl,
user: config.User,
password: config.Password,
token: config.Token,
}
return
}
var cachedClient client.PortainerClient
// Get the cached client or a new one
func GetClient() (c PortainerClient, err error) {
func GetClient() (c client.PortainerClient, err error) {
if cachedClient == nil {
cachedClient, err = GetDefaultClient()
if err != nil {
@ -304,13 +23,13 @@ func GetClient() (c PortainerClient, err error) {
}
// Get the default client
func GetDefaultClient() (c PortainerClient, err error) {
return NewClient(GetDefaultHttpClient(), GetDefaultClientConfig())
func GetDefaultClient() (c client.PortainerClient, err error) {
return client.NewClient(GetDefaultHttpClient(), GetDefaultClientConfig())
}
// Get the default config for a client
func GetDefaultClientConfig() ClientConfig {
return ClientConfig{
func GetDefaultClientConfig() client.ClientConfig {
return client.ClientConfig{
Url: viper.GetString("url"),
User: viper.GetString("user"),
Password: viper.GetString("password"),

View File

@ -1,13 +0,0 @@
package common
import (
"fmt"
"log"
)
// CheckError checks if an error occurred (it's not nil)
func CheckError(err error) {
if err != nil {
log.Fatalln(fmt.Sprintf("Error: %s", err.Error()))
}
}

View File

@ -1,83 +0,0 @@
package common
import (
"bytes"
"fmt"
"github.com/spf13/viper"
"io/ioutil"
"log"
"net/http"
"os"
"strings"
"text/tabwriter"
)
func PrintVerbose(a ...interface{}) {
if viper.GetBool("verbose") {
log.Println(a)
}
}
func PrintDebug(a ...interface{}) {
if viper.GetBool("debug") {
log.Println(a)
}
}
func NewTabWriter(headers []string) (*tabwriter.Writer, error) {
writer := tabwriter.NewWriter(os.Stdout, 20, 2, 3, ' ', 0)
_, err := fmt.Fprintln(writer, strings.Join(headers, "\t"))
if err != nil {
return &tabwriter.Writer{}, err
}
return writer, nil
}
func PrintDebugRequest(title string, req *http.Request) error {
if viper.GetBool("debug") {
var bodyString string
if req.Body != nil {
bodyBytes, err := ioutil.ReadAll(req.Body)
defer req.Body.Close()
if err != nil {
return err
}
bodyString = string(bodyBytes)
req.Body = ioutil.NopCloser(bytes.NewReader(bodyBytes))
}
PrintDebug(fmt.Sprintf(`%s
---
Method: %s
URL: %s
Body:
%s
---`, title, req.Method, req.URL.String(), string(bodyString)))
}
return nil
}
func PrintDebugResponse(title string, resp *http.Response) error {
if viper.GetBool("debug") {
var bodyString string
if resp.Body != nil {
bodyBytes, err := ioutil.ReadAll(resp.Body)
defer resp.Body.Close()
if err != nil {
return err
}
bodyString = string(bodyBytes)
resp.Body = ioutil.NopCloser(bytes.NewReader(bodyBytes))
}
PrintDebug(fmt.Sprintf(`%s
---
Status: %s
Body:
%s
---`, title, resp.Status, bodyString))
}
return nil
}

View File

@ -1,87 +0,0 @@
package common
import "fmt"
type Stack struct {
// In the API documentation this field is a String,
// but it's returned as a number
Id uint32
Name string
Type uint8 // 1 for a Swarm stack, 2 for a Compose stack
EndpointID uint
EntryPoint string
SwarmID string
ProjectPath string
Env []StackEnv
}
func (s *Stack) GetTranslatedStackType() string {
switch s.Type {
case 1:
return "swarm"
case 2:
return "compose"
default:
return ""
}
}
type StackEnv struct {
Name string `json:"name"`
Value string `json:"value"`
}
type EndpointSubset struct {
Id uint32
Name string
Type uint8
URL string
PublicURL string
GroupID uint32
}
type StackCreateRequest struct {
Name string
SwarmID string
StackFileContent string
Env []StackEnv `json:",omitempty"`
}
type StackUpdateRequest struct {
StackFileContent string
Env []StackEnv `json:",omitempty"`
Prune bool
}
type StackFileInspectResponse struct {
StackFileContent string
}
type Status struct {
Authentication bool
EndpointManagement bool
Analytics bool
Version string
}
type GenericError struct {
Err string
Details string
}
func (e *GenericError) Error() string {
if e.Details != "" {
return fmt.Sprintf("%s: %s", e.Err, e.Details)
} else {
return fmt.Sprintf("%s", e.Err)
}
}
type AuthenticateUserRequest struct {
Username string
Password string
}
type AuthenticateUserResponse struct {
Jwt string
}

View File

@ -2,9 +2,13 @@ package common
import (
"fmt"
"github.com/greenled/portainer-stack-utils/util"
"github.com/greenled/portainer-stack-utils/client"
)
func GetStackByName(name string) (stack Stack, err error) {
func GetStackByName(name string) (stack client.Stack, err error) {
client, err := GetClient()
if err != nil {
return
@ -15,7 +19,7 @@ func GetStackByName(name string) (stack Stack, err error) {
return
}
PrintVerbose(fmt.Sprintf("Getting stack %s...", name))
util.PrintVerbose(fmt.Sprintf("Getting stack %s...", name))
for _, stack := range stacks {
if stack.Name == name {
return stack, nil
@ -27,11 +31,6 @@ func GetStackByName(name string) (stack Stack, err error) {
return
}
type StackListFilter struct {
SwarmId string `json:",omitempty"`
EndpointId uint32 `json:",omitempty"`
}
// Custom customerrors
type StackNotFoundError struct {
StackName string