Skip to content

Commit 457230b

Browse files
committed
Add Docker Compose project grouping functionality and UI integration
- Updated `list_running_containers` to retrieve all containers and include labels. - Introduced `get_grouped_docker_compose_projects` to group containers by their Docker Compose project labels. - Added new API endpoint `ListDockerComposeProjectsView` for fetching grouped projects. - Enhanced frontend with a new UI for selecting Docker Compose projects and their containers, including error handling and loading states. - Refactored project creation modal to reset Docker selection state when opened.
1 parent 17ce3df commit 457230b

4 files changed

Lines changed: 315 additions & 46 deletions

File tree

backend/core/docker_service.py

Lines changed: 67 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,15 @@ def list_running_containers():
99
"""
1010
try:
1111
client = docker.from_env()
12-
containers = client.containers.list()
12+
containers = client.containers.list(all=True) # Get all containers, not just running, to see labels of stopped ones too
1313
container_list = []
1414
for container in containers:
1515
container_list.append({
1616
"id": container.short_id,
1717
"name": container.name,
1818
"image": container.attrs['Config']['Image'],
19-
"status": container.status
19+
"status": container.status,
20+
"labels": container.labels # Add labels
2021
})
2122
if not container_list:
2223
return "No running containers found."
@@ -26,6 +27,37 @@ def list_running_containers():
2627
except Exception as e:
2728
return f"An unexpected error occurred: {str(e)}"
2829

30+
def get_grouped_docker_compose_projects():
31+
"""
32+
Lists containers and groups them by the 'com.docker.compose.project' label.
33+
Returns a dictionary where keys are compose project names and values are lists of container dicts.
34+
Containers without the project label are grouped under '__ungrouped__'.
35+
"""
36+
containers_or_error = list_running_containers() # This now includes labels
37+
if isinstance(containers_or_error, str):
38+
return containers_or_error # Return error message if fetching containers failed
39+
40+
grouped_projects = {}
41+
for container in containers_or_error:
42+
project_name = container.get("labels", {}).get("com.docker.compose.project")
43+
# Also consider com.docker.compose.project.name for some compose versions/setups
44+
if not project_name:
45+
project_name = container.get("labels", {}).get("com.docker.compose.project.name")
46+
47+
if project_name:
48+
if project_name not in grouped_projects:
49+
grouped_projects[project_name] = []
50+
grouped_projects[project_name].append(container)
51+
else:
52+
if '__ungrouped__' not in grouped_projects:
53+
grouped_projects['__ungrouped__'] = []
54+
grouped_projects['__ungrouped__'].append(container)
55+
56+
# Convert to a list of dicts for easier frontend consumption if preferred, or keep as dict
57+
# Example: [{ "projectName": "my_project", "containers": [...] }]
58+
# For now, returning the dictionary direkte
59+
return grouped_projects
60+
2961
def get_container_code_paths(container_id: str):
3062
"""
3163
Retrieves the host paths for volume mounts of a given container.
@@ -65,23 +97,37 @@ def get_container_code_paths(container_id: str):
6597

6698
if __name__ == '__main__':
6799
print("Attempting to list running containers...")
68-
running_containers = list_running_containers()
69-
if isinstance(running_containers, str): # Error message
70-
print(running_containers)
71-
elif running_containers:
72-
print("Running containers:")
73-
for cont in running_containers:
74-
print(f" ID: {cont['id']}, Name: {cont['name']}, Image: {cont['image']}, Status: {cont['status']}")
75-
print(f" Attempting to get code paths for {cont['id']} ({cont['name']})...")
76-
paths = get_container_code_paths(cont['id'])
77-
if isinstance(paths, str): # Error message
78-
print(f" Error/Info: {paths}")
79-
elif paths:
80-
print(" Found potential host paths:")
81-
for path in paths:
82-
print(f" - {path}")
83-
else: # Empty list but no error string
84-
print(f" No specific host paths identified for {cont['id']}.")
100+
# Test the new grouping function
101+
grouped_containers = get_grouped_docker_compose_projects()
102+
if isinstance(grouped_containers, str):
103+
print(f"Error getting grouped projects: {grouped_containers}")
104+
else:
105+
print("\nGrouped Docker Compose Projects:")
106+
for project_name, containers_in_project in grouped_containers.items():
107+
print(f" Project: {project_name}")
108+
for cont in containers_in_project:
109+
print(f" - ID: {cont['id']}, Name: {cont['name']}, Status: {cont['status']}, Image: {cont['image']}")
110+
# Optional: print labels for verification
111+
# print(f" Labels: {cont['labels']}")
85112

86-
else: # Empty list from list_running_containers, but not an error string.
87-
print("No running containers were found to inspect.")
113+
# Original test for list_running_containers and get_container_code_paths (can be kept for direct testing)
114+
# running_containers = list_running_containers()
115+
# if isinstance(running_containers, str): # Error message
116+
# print(running_containers)
117+
# elif running_containers:
118+
# print("\nIndividual Running containers (for path testing):")
119+
# for cont in running_containers:
120+
# if cont['status'] == 'running': # Only try to get paths for actually running containers for this test part
121+
# print(f" ID: {cont['id']}, Name: {cont['name']}")
122+
# print(f" Attempting to get code paths for {cont['id']} ({cont['name']})...")
123+
# paths = get_container_code_paths(cont['id'])
124+
# if isinstance(paths, str): # Error message
125+
# print(f" Error/Info: {paths}")
126+
# elif paths:
127+
# print(" Found potential host paths:")
128+
# for path in paths:
129+
# print(f" - {path}")
130+
# else: # Empty list but no error string
131+
# print(f" No specific host paths identified for {cont['id']}.")
132+
# else: # Empty list from list_running_containers, but not an error string.
133+
# print("No running containers were found to inspect for paths.")

backend/core/urls.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
SecurityToolViewSet, ScanConfigurationViewSet, UserProfileViewSet,
66
ApiKeyViewSet, ScanTriggerViewSet, ScanJobViewSet,
77
ProjectMembershipViewSet, UserViewSet, CIScanTriggerViewSet,
8-
ListDockerContainersView, GetDockerContainerPathsView
8+
ListDockerContainersView, GetDockerContainerPathsView, ListDockerComposeProjectsView
99
)
1010

1111
router = DefaultRouter()
@@ -26,4 +26,5 @@
2626
path('', include(router.urls)),
2727
path('docker/containers/', ListDockerContainersView.as_view(), name='docker-list-containers'),
2828
path('docker/containers/<str:container_id>/paths/', GetDockerContainerPathsView.as_view(), name='docker-container-paths'),
29+
path('docker/compose-projects/', ListDockerComposeProjectsView.as_view(), name='docker-list-compose-projects'),
2930
]

backend/core/views.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
from .authentication import ApiKeyAuthentication # Import the custom authentication class
2929
# Docker API Integration Imports
3030
from rest_framework.views import APIView
31-
from .docker_service import list_running_containers, get_container_code_paths # Assuming docker_service.py is in the same app directory
31+
from .docker_service import list_running_containers, get_container_code_paths, get_grouped_docker_compose_projects # Added get_grouped_docker_compose_projects
3232
# End Docker API Integration Imports
3333
User = get_user_model()
3434

@@ -622,3 +622,26 @@ def get(self, request, container_id, *args, **kwargs):
622622
if not paths_or_error: # Empty list, but valid response
623623
return Response([], status=status.HTTP_200_OK)
624624
return Response(paths_or_error, status=status.HTTP_200_OK)
625+
626+
class ListDockerComposeProjectsView(APIView):
627+
"""
628+
Lists Docker containers grouped by their 'com.docker.compose.project' label.
629+
Requires admin privileges.
630+
"""
631+
permission_classes = [IsAdminUser]
632+
633+
def get(self, request, *args, **kwargs):
634+
grouped_projects_or_error = get_grouped_docker_compose_projects()
635+
if isinstance(grouped_projects_or_error, str): # Error message returned from service
636+
return Response({"error": grouped_projects_or_error}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
637+
638+
# Transform the dictionary into a list of objects for a more common API response structure
639+
# e.g., [{"project_name": "my_project", "containers": [...]}, ...]
640+
response_data = []
641+
for project_name, containers in grouped_projects_or_error.items():
642+
response_data.append({
643+
"compose_project_name": project_name,
644+
"containers": containers
645+
})
646+
647+
return Response(response_data, status=status.HTTP_200_OK)

0 commit comments

Comments
 (0)