Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,7 @@
"prettier.documentSelectors": ["**/*.svelte"],
"tasks.statusbar.default.hide": true,
"tasks.statusbar.limit": 8,
"github.copilot.chat.summarizeAgentConversationHistory.enabled": false
"github.copilot.chat.summarizeAgentConversationHistory.enabled": false,
"snyk.advanced.organization": "a7adead1-b200-490f-91c5-8fa0e410d62c",
"snyk.advanced.autoSelectOrganization": true
}
2 changes: 1 addition & 1 deletion backend/internal/bootstrap/services_bootstrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ func initializeServices(ctx context.Context, db *database.DB, cfg *config.Config
svcs.BuildWorkspace = services.NewBuildWorkspaceService(svcs.Settings)
svcs.Project = services.NewProjectService(db, svcs.Settings, svcs.Event, svcs.Image, svcs.Docker, svcs.Build, cfg)
svcs.Container = services.NewContainerService(db, svcs.Event, svcs.Docker, svcs.Image, svcs.Settings, svcs.Project)
svcs.Dashboard = services.NewDashboardService(db, svcs.Docker, svcs.Container, svcs.Settings, svcs.Vulnerability)
svcs.Dashboard = services.NewDashboardService(db, svcs.Docker, svcs.Container, svcs.Project, svcs.Settings, svcs.Vulnerability)
svcs.Volume = services.NewVolumeService(db, svcs.Docker, svcs.Event, svcs.Settings, svcs.Container, svcs.Image, cfg.BackupVolumeName)
svcs.Network = services.NewNetworkService(db, svcs.Docker, svcs.Event)
svcs.Port = services.NewPortService(svcs.Docker)
Expand Down
4 changes: 4 additions & 0 deletions backend/internal/huma/handlers/containers.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ type ListContainersInput struct {
GroupBy string `query:"groupBy" doc:"Optional grouping mode (for example: project)"`
IncludeInternal bool `query:"includeInternal" default:"false" doc:"Include internal containers"`
Updates string `query:"updates" doc:"Filter by update status (has_update, up_to_date, error, unknown)"`
Standalone string `query:"standalone" doc:"Filter standalone containers only (true/false)"`
}

type ListContainersOutput struct {
Expand Down Expand Up @@ -244,6 +245,9 @@ func (h *ContainerHandler) ListContainers(ctx context.Context, input *ListContai
if input.Updates != "" {
filters["updates"] = input.Updates
}
if input.Standalone != "" {
filters["standalone"] = input.Standalone
}

params := pagination.QueryParams{
SearchQuery: pagination.SearchQuery{Search: input.Search},
Expand Down
2 changes: 1 addition & 1 deletion backend/internal/huma/handlers/dashboard_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ func TestDashboardHandlerGetDashboardReturnsSnapshot(t *testing.T) {

dockerSvc := newDashboardHandlerTestDockerService(t, settingsSvc, containers, images)
handler := &DashboardHandler{
dashboardService: services.NewDashboardService(db, dockerSvc, nil, settingsSvc, nil),
dashboardService: services.NewDashboardService(db, dockerSvc, nil, nil, settingsSvc, nil),
}

output, err := handler.GetDashboard(context.Background(), &GetDashboardInput{EnvironmentID: "0"})
Expand Down
75 changes: 73 additions & 2 deletions backend/internal/huma/handlers/image_updates.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,19 @@ package handlers
import (
"context"
"net/http"
"strings"

"github.com/danielgtaylor/huma/v2"
"github.com/getarcaneapp/arcane/backend/internal/common"
"github.com/getarcaneapp/arcane/backend/internal/services"
"github.com/getarcaneapp/arcane/types/base"
imagetypes "github.com/getarcaneapp/arcane/types/image"
"github.com/getarcaneapp/arcane/types/imageupdate"
)

type ImageUpdateHandler struct {
imageUpdateService *services.ImageUpdateService
imageService *services.ImageService
}

type CheckImageUpdateInput struct {
Expand Down Expand Up @@ -51,6 +54,15 @@ type CheckAllImagesOutput struct {
Body base.ApiResponse[imageupdate.BatchResponse]
}

type GetUpdateInfoByRefsInput struct {
EnvironmentID string `path:"id" doc:"Environment ID"`
ImageRefs string `query:"imageRefs" doc:"Comma-separated image references"`
}

type GetUpdateInfoByRefsOutput struct {
Body base.ApiResponse[map[string]*imagetypes.UpdateInfo]
}

type GetUpdateSummaryInput struct {
EnvironmentID string `path:"id" doc:"Environment ID"`
}
Expand All @@ -60,8 +72,11 @@ type GetUpdateSummaryOutput struct {
}

// RegisterImageUpdates registers image update endpoints.
func RegisterImageUpdates(api huma.API, imageUpdateSvc *services.ImageUpdateService) {
h := &ImageUpdateHandler{imageUpdateService: imageUpdateSvc}
func RegisterImageUpdates(api huma.API, imageUpdateSvc *services.ImageUpdateService, imageSvc *services.ImageService) {
h := &ImageUpdateHandler{
imageUpdateService: imageUpdateSvc,
imageService: imageSvc,
}

huma.Register(api, huma.Operation{
OperationID: "check-image-update",
Expand Down Expand Up @@ -108,6 +123,15 @@ func RegisterImageUpdates(api huma.API, imageUpdateSvc *services.ImageUpdateServ
Security: []map[string][]string{{"BearerAuth": {}}, {"ApiKeyAuth": {}}},
}, h.CheckAllImages)

huma.Register(api, huma.Operation{
OperationID: "get-update-info-by-refs",
Method: http.MethodGet,
Path: "/environments/{id}/image-updates/by-refs",
Summary: "Get persisted update info for image references",
Tags: []string{"Image Updates"},
Security: []map[string][]string{{"BearerAuth": {}}, {"ApiKeyAuth": {}}},
}, h.GetUpdateInfoByRefs)

huma.Register(api, huma.Operation{
OperationID: "get-update-summary",
Method: http.MethodGet,
Expand Down Expand Up @@ -192,6 +216,30 @@ func (h *ImageUpdateHandler) CheckAllImages(ctx context.Context, input *CheckAll
}, nil
}

func (h *ImageUpdateHandler) GetUpdateInfoByRefs(ctx context.Context, input *GetUpdateInfoByRefsInput) (*GetUpdateInfoByRefsOutput, error) {
imageRefs := parseImageRefsQueryInternal(input.ImageRefs)
if len(imageRefs) == 0 {
return &GetUpdateInfoByRefsOutput{
Body: base.ApiResponse[map[string]*imagetypes.UpdateInfo]{
Success: true,
Data: map[string]*imagetypes.UpdateInfo{},
},
}, nil
}

result, err := h.imageService.GetUpdateInfoByImageRefs(ctx, imageRefs)
if err != nil {
return nil, huma.Error500InternalServerError((&common.BatchImageUpdateCheckError{Err: err}).Error())
}

return &GetUpdateInfoByRefsOutput{
Body: base.ApiResponse[map[string]*imagetypes.UpdateInfo]{
Success: true,
Data: result,
},
}, nil
}

func (h *ImageUpdateHandler) GetUpdateSummary(ctx context.Context, input *GetUpdateSummaryInput) (*GetUpdateSummaryOutput, error) {
summary, err := h.imageUpdateService.GetUpdateSummary(ctx)
if err != nil {
Expand All @@ -205,3 +253,26 @@ func (h *ImageUpdateHandler) GetUpdateSummary(ctx context.Context, input *GetUpd
},
}, nil
}

func parseImageRefsQueryInternal(raw string) []string {
if strings.TrimSpace(raw) == "" {
return nil
}

parts := strings.Split(raw, ",")
result := make([]string, 0, len(parts))
seen := make(map[string]struct{}, len(parts))
for _, part := range parts {
ref := strings.TrimSpace(part)
if ref == "" {
continue
}
if _, exists := seen[ref]; exists {
continue
}
seen[ref] = struct{}{}
result = append(result, ref)
}

return result
}
13 changes: 10 additions & 3 deletions backend/internal/huma/handlers/projects.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ type ListProjectsInput struct {
Start int `query:"start" default:"0" doc:"Start index for pagination"`
Limit int `query:"limit" default:"20" doc:"Number of items per page"`
Status string `query:"status" doc:"Filter by status (comma-separated: running,stopped,partially running)"`
Updates string `query:"updates" doc:"Filter by update status (has_update, up_to_date, error, unknown)"`
}

type ListProjectsOutput struct {
Expand Down Expand Up @@ -375,6 +376,14 @@ func (h *ProjectHandler) ListProjects(ctx context.Context, input *ListProjectsIn
return nil, huma.Error500InternalServerError("service not available")
}

filters := map[string]string{}
if input.Status != "" {
filters["status"] = input.Status
}
if input.Updates != "" {
filters["updates"] = input.Updates
}

params := pagination.QueryParams{
SearchQuery: pagination.SearchQuery{
Search: input.Search,
Expand All @@ -387,9 +396,7 @@ func (h *ProjectHandler) ListProjects(ctx context.Context, input *ListProjectsIn
Start: input.Start,
Limit: input.Limit,
},
Filters: map[string]string{
"status": input.Status,
},
Filters: filters,
}

projects, paginationResp, err := h.projectService.ListProjects(ctx, params)
Expand Down
2 changes: 1 addition & 1 deletion backend/internal/huma/huma.go
Original file line number Diff line number Diff line change
Expand Up @@ -394,7 +394,7 @@ func registerHandlers(api huma.API, svc *Services) {
handlers.RegisterTemplates(api, templateSvc, environmentSvc)
handlers.RegisterImages(api, dockerSvc, imageSvc, imageUpdateSvc, settingsSvc, buildSvc)
handlers.RegisterBuildWorkspaces(api, buildWorkspaceSvc)
handlers.RegisterImageUpdates(api, imageUpdateSvc)
handlers.RegisterImageUpdates(api, imageUpdateSvc, imageSvc)
handlers.RegisterSettings(api, settingsSvc, settingsSearchSvc, environmentSvc, cfg)
handlers.RegisterJobSchedules(api, jobScheduleSvc, environmentSvc)
handlers.RegisterVolumes(api, dockerSvc, volumeSvc)
Expand Down
14 changes: 14 additions & 0 deletions backend/internal/services/container_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -1287,6 +1287,20 @@ func (s *ContainerService) buildContainerFilterAccessors() []pagination.FilterAc
}
},
},
{
Key: "standalone",
Fn: func(c containertypes.Summary, filterValue string) bool {
isStandalone := strings.TrimSpace(c.Labels["com.docker.compose.project"]) == ""
switch filterValue {
case "true", "1":
return isStandalone
case "false", "0":
return !isStandalone
default:
return true
}
},
},
}
}

Expand Down
18 changes: 18 additions & 0 deletions backend/internal/services/container_service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,24 @@ func TestGroupContainersByProjectUsesNoProjectBucket(t *testing.T) {
require.Equal(t, containerNoProjectGroup, getContainerProjectNameInternal(groups[1].Items[1]))
}

func TestBuildContainerFilterAccessors_FiltersStandaloneContainers(t *testing.T) {
service := &ContainerService{}
items := []containertypes.Summary{
{ID: "standalone", Labels: map[string]string{}},
{ID: "compose", Labels: map[string]string{"com.docker.compose.project": "alpha"}},
}

result := pagination.SearchOrderAndPaginate(
items,
pagination.QueryParams{Filters: map[string]string{"standalone": "true"}},
pagination.Config[containertypes.Summary]{FilterAccessors: service.buildContainerFilterAccessors()},
)

require.Len(t, result.Items, 1)
require.Equal(t, "standalone", result.Items[0].ID)
require.Equal(t, int64(1), result.TotalCount)
}

func TestBuildCleanNetworkingConfigInternalPreservesEndpointSettings(t *testing.T) {
containerInspect := container.InspectResponse{
NetworkSettings: &container.NetworkSettings{
Expand Down
Loading
Loading