package main import ( "bytes" "encoding/json" "fmt" "io" "net/http" "os" ) // ------------------------------- // 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") // e.g. https://portainer.example.com/api portainerToken = os.Getenv("PORTAINER_TOKEN") // API access token endpointID = os.Getenv("PORTAINER_ENDPOINT") // Portainer endpoint ID stackName = os.Getenv("STACK_NAME") // Name of stack to create/redeploy repoURL = os.Getenv("REPO_URL") // Git repo URL repoRef = os.Getenv("REPO_REF") // e.g. refs/heads/main repoComposeFile = os.Getenv("REPO_COMPOSE_FILE") // e.g. docker-compose.yml repoUsername = os.Getenv("REPO_USERNAME") // Git username (if needed) repoPassword = os.Getenv("REPO_PASSWORD") // Git password/token (if needed) tlsSkipVerify = os.Getenv("TLS_SKIP_VERIFY") // "true" / "false" ) // ------------------------------- // Helpers // ------------------------------- func request(client *http.Client, method, url string, body interface{}) (*http.Response, error) { var buf io.Reader if body != nil { data, _ := json.Marshal(body) buf = bytes.NewBuffer(data) } req, err := http.NewRequest(method, url, buf) if err != nil { return nil, err } req.Header.Set("Authorization", "Bearer "+portainerToken) req.Header.Set("Content-Type", "application/json") return client.Do(req) } // ------------------------------- // Main logic // ------------------------------- func main() { if portainerURL == "" || portainerToken == "" || endpointID == "" || repoURL == "" || repoRef == "" || repoComposeFile == "" || stackName == "" { fmt.Println("Missing required environment variables.") os.Exit(1) } client := &http.Client{} // 1. Fetch existing stacks resp, err := request(client, "GET", fmt.Sprintf("%s/stacks", portainerURL), nil) if err != nil { panic(err) } 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 { panic(err) } // 2. Find existing stack from same repo + name var existing *Stack for _, s := range stacks { if s.GitConfig.URL == repoURL && s.Name == stackName { existing = &s break } } if existing == nil { // 3. Create new stack payload := map[string]interface{}{ "composeFile": repoComposeFile, "env": []EnvVar{}, "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=%s", portainerURL, endpointID) resp, err = request(client, "POST", url, payload) if err != nil { panic(err) } 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 { // 4. Redeploy stack payload := map[string]interface{}{ "env": []EnvVar{}, "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=%s", portainerURL, existing.ID, endpointID) resp, err = request(client, "PUT", url, payload) if err != nil { panic(err) } 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) } }