206 lines
5.5 KiB
Go
206 lines
5.5 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/tls"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"strconv"
|
|
"time"
|
|
)
|
|
|
|
// -------------------------------
|
|
// Data structures
|
|
// -------------------------------
|
|
|
|
type Stack struct {
|
|
ID int `json:"Id"`
|
|
Name string `json:"Name"`
|
|
EndpointId int `json:"EndpointId"`
|
|
GitConfig GitConfig `json:"gitConfig"`
|
|
}
|
|
|
|
type GitConfig struct {
|
|
URL string `json:"url"`
|
|
ReferenceName string `json:"referenceName"`
|
|
ConfigFilePath string `json:"configFilePath"`
|
|
}
|
|
|
|
type EnvVar struct {
|
|
Name string `json:"name"`
|
|
Value string `json:"value"`
|
|
}
|
|
|
|
// -------------------------------
|
|
// Environment-driven configuration
|
|
// -------------------------------
|
|
|
|
var (
|
|
portainerURL = os.Getenv("PORTAINER_URL")
|
|
portainerToken = os.Getenv("PORTAINER_TOKEN")
|
|
endpointIDStr = os.Getenv("PORTAINER_ENDPOINT")
|
|
stackName = os.Getenv("STACK_NAME")
|
|
repoURL = os.Getenv("REPO_URL")
|
|
repoRef = os.Getenv("REPO_REF")
|
|
repoComposeFile = os.Getenv("REPO_COMPOSE_FILE")
|
|
repoUsername = os.Getenv("REPO_USERNAME")
|
|
repoPassword = os.Getenv("REPO_PASSWORD")
|
|
tlsSkipVerify = os.Getenv("TLS_SKIP_VERIFY")
|
|
envData = os.Getenv("ENV_DATA")
|
|
)
|
|
|
|
// -------------------------------
|
|
// Helpers
|
|
// -------------------------------
|
|
|
|
func request(client *http.Client, method, url string, body interface{}) (*http.Response, error) {
|
|
var buf io.Reader
|
|
if body != nil {
|
|
data, err := json.Marshal(body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to marshal request body: %w", err)
|
|
}
|
|
buf = bytes.NewBuffer(data)
|
|
}
|
|
|
|
req, err := http.NewRequest(method, url, buf)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create request: %w", err)
|
|
}
|
|
|
|
req.Header.Set("X-API-Key", portainerToken)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
return client.Do(req)
|
|
}
|
|
|
|
// -------------------------------
|
|
// Main logic
|
|
// -------------------------------
|
|
|
|
func main() {
|
|
// Validate required environment variables
|
|
if portainerURL == "" || portainerToken == "" || endpointIDStr == "" || repoURL == "" || repoRef == "" || repoComposeFile == "" || stackName == "" {
|
|
fmt.Println("❌ Missing required environment variables.")
|
|
os.Exit(1)
|
|
}
|
|
|
|
// Convert endpointID to integer
|
|
endpointID, err := strconv.Atoi(endpointIDStr)
|
|
if err != nil {
|
|
fmt.Printf("❌ Invalid PORTAINER_ENDPOINT value: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
// Configure HTTP client
|
|
client := &http.Client{
|
|
Timeout: 15 * time.Second,
|
|
}
|
|
if tlsSkipVerify == "true" {
|
|
client.Transport = &http.Transport{
|
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
|
}
|
|
}
|
|
|
|
// 1. Fetch existing stacks
|
|
resp, err := request(client, "GET", fmt.Sprintf("%s/stacks", portainerURL), nil)
|
|
if err != nil {
|
|
fmt.Printf("❌ Error fetching stacks: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
body, _ := io.ReadAll(resp.Body)
|
|
fmt.Printf("❌ Error fetching stacks: %s\n", string(body))
|
|
os.Exit(1)
|
|
}
|
|
|
|
var stacks []Stack
|
|
if err := json.NewDecoder(resp.Body).Decode(&stacks); err != nil {
|
|
fmt.Printf("❌ Failed to parse stacks response: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
// 2. Find existing stack
|
|
var existing *Stack
|
|
for _, s := range stacks {
|
|
if s.GitConfig.URL == repoURL && s.Name == stackName {
|
|
existing = &s
|
|
break
|
|
}
|
|
}
|
|
|
|
// 3. Parse environment variables
|
|
var envVars []EnvVar
|
|
if envData != "" {
|
|
if err := json.Unmarshal([]byte(envData), &envVars); err != nil {
|
|
fmt.Printf("❌ Error parsing ENV_DATA: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
if existing == nil {
|
|
// 4. Create new stack
|
|
payload := map[string]interface{}{
|
|
"composeFile": repoComposeFile,
|
|
"env": envVars,
|
|
"fromAppTemplate": false,
|
|
"name": stackName,
|
|
"repositoryAuthentication": repoUsername != "" && repoPassword != "",
|
|
"repositoryUsername": repoUsername,
|
|
"repositoryPassword": repoPassword,
|
|
"repositoryReferenceName": repoRef,
|
|
"repositoryURL": repoURL,
|
|
"tlsskipVerify": tlsSkipVerify == "true",
|
|
}
|
|
|
|
url := fmt.Sprintf("%s/stacks/create/standalone/repository?endpointId=%d", portainerURL, endpointID)
|
|
resp, err = request(client, "POST", url, payload)
|
|
if err != nil {
|
|
fmt.Printf("❌ Error creating stack: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
|
|
body, _ := io.ReadAll(resp.Body)
|
|
fmt.Printf("❌ Error creating stack: %s\n", string(body))
|
|
os.Exit(1)
|
|
}
|
|
|
|
fmt.Printf("✅ Stack %s deployed successfully.\n", stackName)
|
|
} else {
|
|
// 5. Redeploy existing stack
|
|
payload := map[string]interface{}{
|
|
"env": envVars,
|
|
"prune": true,
|
|
"pullImage": true,
|
|
"repositoryAuthentication": repoUsername != "" && repoPassword != "",
|
|
"repositoryAuthorizationType": 0,
|
|
"repositoryUsername": repoUsername,
|
|
"repositoryPassword": repoPassword,
|
|
"repositoryReferenceName": repoRef,
|
|
"stackName": stackName,
|
|
}
|
|
|
|
url := fmt.Sprintf("%s/stacks/%d/git/redeploy?endpointId=%d", portainerURL, existing.ID, endpointID)
|
|
resp, err = request(client, "PUT", url, payload)
|
|
if err != nil {
|
|
fmt.Printf("❌ Error redeploying stack: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
body, _ := io.ReadAll(resp.Body)
|
|
fmt.Printf("❌ Error redeploying stack: %s\n", string(body))
|
|
os.Exit(1)
|
|
}
|
|
|
|
fmt.Printf("♻️ Stack %s redeployed successfully.\n", stackName)
|
|
}
|
|
}
|