Некоторые проверки не удались
CI / test (push) Failing after 6s
Docker / Build and publish worker image (push) Failing after 8s
Add an internal/compose package that discovers Compose projects via `docker compose ls` + `docker ps` labels (grouped by com.docker.compose.project/service) and enriches each container with `docker inspect` ports/mounts and Traefik router labels. Management runs `docker compose` in each project's working directory for up/down/stop/restart/pull plus per-service variants and log tails. Wire it into the webapp: a 60s ComposeRefresher (constructed in New, started in Start, stopped in Close), a /compose list + detail + logs HTML surface, and /web/api/compose/* JSON endpoints (list, detail, logs, project/service lifecycle). Browser lifecycle POSTs are session+CSRF protected; the /web/api/* variants accept HTTP basic auth. WORKER_COMPOSE_ENABLED defaults on (false to disable). Tests cover discovery parsing/grouping/traefik/summary, the action allowlists, and the full handler surface (list/detail/logs HTML+API, CSRF enforcement, disabled/unknown-action rejection, audit writes, success+failure exec paths) via a stub Docker binary.
145 строки
4.1 KiB
Go
145 строки
4.1 KiB
Go
package compose
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
)
|
|
|
|
func TestParseLabels(t *testing.T) {
|
|
got := parseLabels("com.docker.compose.project=rsmon,com.docker.compose.service=web,foo=a=b")
|
|
if got[LabelComposeProject] != "rsmon" {
|
|
t.Fatalf("project = %q", got[LabelComposeProject])
|
|
}
|
|
if got[LabelComposeService] != "web" {
|
|
t.Fatalf("service = %q", got[LabelComposeService])
|
|
}
|
|
if got["foo"] != "a=b" { // value may contain '='
|
|
t.Fatalf("foo = %q", got["foo"])
|
|
}
|
|
if len(parseLabels("")) != 0 {
|
|
t.Fatal("empty labels should yield empty map")
|
|
}
|
|
}
|
|
|
|
func TestGroupContainers(t *testing.T) {
|
|
res := &DiscoveryResult{Projects: map[string]Project{
|
|
"rsmon": {Name: "rsmon", Status: "running(2)", ConfigFiles: "/compose/rsmon/docker-compose.yml", Services: map[string]Service{}},
|
|
}}
|
|
containers := []Container{
|
|
{
|
|
ID: "a",
|
|
Name: "rsmon-web",
|
|
Image: "nginx",
|
|
State: "running",
|
|
Labels: map[string]string{LabelComposeProject: "rsmon", LabelComposeService: "web", LabelComposeWorkingDir: "/compose/rsmon", LabelComposeConfigFiles: "/compose/rsmon/docker-compose.yml"},
|
|
},
|
|
{
|
|
ID: "b",
|
|
Name: "rsmon-backend",
|
|
Image: "rsmon",
|
|
State: "running",
|
|
Labels: map[string]string{LabelComposeProject: "rsmon", LabelComposeService: "backend"},
|
|
},
|
|
{
|
|
ID: "c",
|
|
Name: "loose",
|
|
State: "running",
|
|
Labels: map[string]string{}, // not a compose container
|
|
},
|
|
}
|
|
groupContainers(res, containers)
|
|
|
|
proj, ok := res.Projects["rsmon"]
|
|
if !ok {
|
|
t.Fatal("rsmon project missing")
|
|
}
|
|
if proj.WorkingDir != "/compose/rsmon" {
|
|
t.Fatalf("working dir = %q", proj.WorkingDir)
|
|
}
|
|
if len(proj.Services) != 2 {
|
|
t.Fatalf("service count = %d", len(proj.Services))
|
|
}
|
|
if got := proj.Services["web"].Containers[0].Service; got != "web" {
|
|
t.Fatalf("web service tag = %q", got)
|
|
}
|
|
if _, ok := res.Projects["loose"]; ok {
|
|
t.Fatal("non-compose container should not create a project")
|
|
}
|
|
}
|
|
|
|
func TestParseTraefikLabels(t *testing.T) {
|
|
routes := ParseTraefikLabels(map[string]string{
|
|
"traefik.http.routers.rsmon.rule": "Host(`rsmon.rscx.ru`)",
|
|
"traefik.http.routers.rsmon-api.rule": "PathPrefix(`/api`)",
|
|
"traefik.http.routers.rsmon.service": "rsmon", // non-rule label, ignored
|
|
"traefik.http.middlewares.redirect.rule": "not a router", // wrong prefix path
|
|
})
|
|
if len(routes) != 2 {
|
|
t.Fatalf("route count = %d", len(routes))
|
|
}
|
|
var hosts, prefixes []string
|
|
for _, r := range routes {
|
|
hosts = append(hosts, r.Hostnames...)
|
|
prefixes = append(prefixes, r.PathPrefixes...)
|
|
}
|
|
if !contains(hosts, "rsmon.rscx.ru") {
|
|
t.Fatalf("hosts = %v", hosts)
|
|
}
|
|
if !contains(prefixes, "/api") {
|
|
t.Fatalf("prefixes = %v", prefixes)
|
|
}
|
|
}
|
|
|
|
func contains(s []string, v string) bool {
|
|
for _, x := range s {
|
|
if x == v {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func TestBuildProjectSummary(t *testing.T) {
|
|
res := &DiscoveryResult{Projects: map[string]Project{
|
|
"p": {
|
|
Name: "p",
|
|
Services: map[string]Service{
|
|
"web": {Name: "web", Containers: []Container{
|
|
{Name: "web-1", State: "running", Image: "nginx", Ports: []PortBinding{{HostPort: "80"}}},
|
|
}},
|
|
"db": {Name: "db", Containers: []Container{
|
|
{Name: "db-1", State: "exited", Image: "postgres"},
|
|
}},
|
|
},
|
|
},
|
|
}}
|
|
snap := Summarize(res)
|
|
if len(snap.Projects) != 1 {
|
|
t.Fatalf("project count = %d", len(snap.Projects))
|
|
}
|
|
p := snap.Projects[0]
|
|
if p.ContainerCount != 2 || p.RunningCount != 1 || p.ServiceCount != 2 {
|
|
t.Fatalf("counts = svc=%d c=%d run=%d", p.ServiceCount, p.ContainerCount, p.RunningCount)
|
|
}
|
|
if p.Services[0].Name != "db" { // sorted alphabetically
|
|
t.Fatalf("first service = %q", p.Services[0].Name)
|
|
}
|
|
}
|
|
|
|
func TestValidProjectAction(t *testing.T) {
|
|
for _, a := range []string{"up", "down", "stop", "restart", "pull"} {
|
|
if _, ok := ValidProjectAction(a); !ok {
|
|
t.Errorf("%q should be valid", a)
|
|
}
|
|
}
|
|
if _, ok := ValidProjectAction("rm"); ok {
|
|
t.Error("rm should be rejected")
|
|
}
|
|
}
|
|
|
|
func TestManageProjectRejectsUnknownAction(t *testing.T) {
|
|
if _, err := ManageProject(context.Background(), ProjectRefs{Name: "x", WorkingDir: "/tmp"}, "bogus"); err == nil {
|
|
t.Fatal("expected error for unknown action")
|
|
}
|
|
}
|