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) if method != "GET" { 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) } }