In my Portainer container: How can I export stack details (services) of stacks that are created outside of Portainer (type=swarm,control=limited)?

I have a portainer on a remote server with containers inside. With following code I can list stacks and the stack details of the containers without problem, when the control of the stacks are “total” and the type is “compose”. But for stacks that are built outside of Portainer (control=limited, type=swarm), I get them not listed. I am looking for a code that can list also these.

My current code:

public static async Task Export_Portainer_Main()
    {
        string baseUrl = "myurl"; // 
        string username = "myuser";
        string password = "mypassword";
        string exportPath = @"C:\temp\Portainer\Export"; // 
        string stacksFilePath = @"C:\temp\Portainer\Export\stacks.txt"; // 

        try
        {
            // Authenticate and get token
            string token = await Authenticate(baseUrl, username, password);

            // List endpoints and stacks
            await ListEndpointsAndStacks(baseUrl, token, stacksFilePath);

            // Export all stacks
            await ExportAllStacks(baseUrl, token, exportPath);
            Console.WriteLine("All stacks exported successfully.");
            Console.ReadKey();
            
        }
        catch (Exception ex)
        {
            Console.WriteLine($"An error occurred: {ex.Message}");
            Console.ReadKey();
        }
    }



    private static async Task<string> Authenticate(string baseUrl, string username, string password)
    {
        using (var client = new HttpClient())
        {
            var authData = new { Username = username, Password = password };
            var content = new StringContent(Newtonsoft.Json.JsonConvert.SerializeObject(authData));
            content.Headers.ContentType = new MediaTypeHeaderValue("application/json");

            var response = await client.PostAsync($"{baseUrl}/api/auth", content);
            response.EnsureSuccessStatusCode();

            var responseData = await response.Content.ReadAsStringAsync();
            return JObject.Parse(responseData)["jwt"].ToString(); // Extract JWT token
        }
    }

    private static async Task ListEndpointsAndStacks(string baseUrl, string token, string filePath)
    {
        using (var client = new HttpClient())
        {
            client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);

            var sb = new StringBuilder();

            // Fetch all endpoints
            var endpointsResponse = await client.GetAsync($"{baseUrl}/api/endpoints");
            endpointsResponse.EnsureSuccessStatusCode();

            var endpoints = JArray.Parse(await endpointsResponse.Content.ReadAsStringAsync());

            // Fetch all stacks globally
            var stacksResponse = await client.GetAsync($"{baseUrl}/api/stacks");
            stacksResponse.EnsureSuccessStatusCode();

            var stacks = JArray.Parse(await stacksResponse.Content.ReadAsStringAsync());

            sb.AppendLine("Endpoints and Stacks:");
            sb.AppendLine("=====================");

            foreach (var endpoint in endpoints)
            {
                string endpointId = endpoint["Id"].ToString();
                string endpointName = endpoint["Name"].ToString();

                sb.AppendLine($"Endpoint: {endpointName} (ID: {endpointId})");

                // Filter stacks for this endpoint
                var stacksForEndpoint = stacks.Where(stack => stack["EndpointId"].ToString() == endpointId);

                if (!stacksForEndpoint.Any())
                {
                    sb.AppendLine("  No stacks found.");
                }
                else
                {
                    foreach (var stack in stacksForEndpoint)
                    {
                        string stackName = stack["Name"].ToString();
                        sb.AppendLine($"  Stack: {stackName}");
                    }
                }

                sb.AppendLine(); // Blank line between endpoints
            }

            // Write all data to file
            File.WriteAllText(filePath, sb.ToString());

            Console.WriteLine($"Endpoints and stack names written to: {filePath}");
        }
    }




    private static async Task ExportAllStacks(string baseUrl, string token, string exportPath)
    {
        using (var client = new HttpClient())
        {
            client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);

            // Ensure the export directory exists
            Directory.CreateDirectory(exportPath);

            try
            {
                // Fetch all endpoints
                var endpointsResponse = await client.GetAsync($"{baseUrl}/api/endpoints");
                endpointsResponse.EnsureSuccessStatusCode();

                var endpoints = JArray.Parse(await endpointsResponse.Content.ReadAsStringAsync());

                // Fetch all stacks globally
                var stacksResponse = await client.GetAsync($"{baseUrl}/api/stacks");
                stacksResponse.EnsureSuccessStatusCode();

                var stacks = JArray.Parse(await stacksResponse.Content.ReadAsStringAsync());

                // Group stacks by endpoint
                foreach (var endpoint in endpoints)
                {
                    string endpointId = endpoint["Id"].ToString();
                    string endpointName = endpoint["Name"].ToString();

                    Console.WriteLine($"Processing Endpoint: {endpointName} (ID: {endpointId})");

                    var endpointStacks = stacks.Where(stack => stack["EndpointId"].ToString() == endpointId).ToList();

                    if (!endpointStacks.Any())
                    {
                        Console.WriteLine($"  No stacks found for endpoint '{endpointName}' (ID: {endpointId})");
                        continue;
                    }

                    foreach (var stack in endpointStacks)
                    {
                        string stackId = stack["Id"].ToString();
                        string stackName = stack["Name"].ToString();

                        try
                        {
                            Console.WriteLine($"Exporting Stack: {stackName} (ID: {stackId}) for Endpoint: {endpointName}");

                            // Fetch stack file content
                            var stackFileResponse = await client.GetAsync($"{baseUrl}/api/stacks/{stackId}/file");

                            if (!stackFileResponse.IsSuccessStatusCode)
                            {
                                Console.WriteLine($"  Failed to export stack '{stackName}' for endpoint '{endpointName}': {stackFileResponse.ReasonPhrase}");
                                continue; // Skip this stack and continue with the next one
                            }

                            string stackFileContent = await stackFileResponse.Content.ReadAsStringAsync();

                            // Save to a file, including the endpoint name for uniqueness
                            string sanitizedFileName = Path.Combine(exportPath, $"{endpointName}_{stackName.Replace(" ", "_")}.txt");
                            File.WriteAllText(sanitizedFileName, stackFileContent);

                            Console.WriteLine($"  Stack '{stackName}' exported to '{sanitizedFileName}'");
                        }
                        catch (Exception ex)
                        {
                            Console.WriteLine($"  Error exporting stack '{stackName}' for endpoint '{endpointName}': {ex.Message}");
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine($"An error occurred: {ex.Message}");
            }
        }
    }

Hello,
To list and handle stacks built outside of Portainer (those with control=limited and type=swarm), you’ll need to interact with the Docker Swarm API directly or extend your code to account for the specific Portainer API endpoints that manage these types of stacks. Here’s how you could approach this:

  1. Identify the Endpoints for Limited-Control Stacks: Portainer typically doesn’t list “limited” stacks in the same way as “total” stacks because it only has partial control over them. You may need to query the Docker Swarm API directly to gather information on these stacks.
  2. Include Docker Swarm Calls in Your Code: For stacks with type=swarm, use the Docker API for Swarm-related tasks. For example, you can query the services endpoint in Docker to retrieve information about these stacks.
  3. Modify Your Existing Code: You could extend your ListEndpointsAndStacks method to include a fallback mechanism for querying stacks via the Docker API when Portainer’s API doesn’t return them.

Here’s an example of what an extended approach might look like in C#:
public static async Task ListEndpointsAndStacks(string baseUrl, string token, string stacksFilePath)
{
try
{
// Portainer API to list stacks
var stacksUrl = $“{baseUrl}/api/stacks”;
using (var client = new HttpClient())
{
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(“Bearer”, token);
var response = await client.GetAsync(stacksUrl);
response.EnsureSuccessStatusCode();

        var stacksJson = await response.Content.ReadAsStringAsync();
        var stacks = JsonConvert.DeserializeObject<List<dynamic>>(stacksJson);

        using (StreamWriter writer = new StreamWriter(stacksFilePath))
        {
            foreach (var stack in stacks)
            {
                await writer.WriteLineAsync($"Stack Name: {stack.Name}, Control: {stack.Control}, Type: {stack.Type}");
                
                // Fallback for limited-control, swarm-type stacks
                if (stack.Control == "limited" && stack.Type == "swarm")
                {
                    Console.WriteLine($"Detected limited-control, swarm-type stack: {stack.Name}");
                    
                    // Query Docker API for swarm-specific details
                    var dockerServiceUrl = $"{baseUrl}/v1.41/services"; // Adjust version if necessary
                    var dockerResponse = await client.GetAsync(dockerServiceUrl);
                    dockerResponse.EnsureSuccessStatusCode();
                    
                    var servicesJson = await dockerResponse.Content.ReadAsStringAsync();
                    var services = JsonConvert.DeserializeObject<List<dynamic>>(servicesJson);

                    foreach (var service in services)
                    {
                        await writer.WriteLineAsync($"Service Name: {service.Spec.Name}, Image: {service.Spec.TaskTemplate.ContainerSpec.Image}");
                    }
                }
            }
        }
    }
}
catch (Exception ex)
{
    Console.WriteLine($"An error occurred while listing endpoints and stacks: {ex.Message}");
}

}

Key Notes:

  • Docker API Version: The example uses version 1.41. Adjust the version based on the Docker version installed on your server.
  • Authentication: Ensure Docker’s API is exposed on your server and secured using TLS or an authentication mechanism.
  • Fallback Logic: The code checks the stack’s Control and Type attributes, then queries the Docker API to list services for swarm stacks with limited control.

So in short, ypu are using Portainer and it looks like you are using Portainer’s API, so the question could be asked from a Portainer community. For anything that the Portainer API doesn’t return, you need the Docker API directly.

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.