Public source validation / validate (push) Failing after 3m8s
112 lines
3.6 KiB
Go
112 lines
3.6 KiB
Go
package unraid
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/itworx/pulse/internal/container"
|
|
)
|
|
|
|
// ContainerSource adapts Unraid's documented docker.containers query into the
|
|
// bounded container raw contract. It is deliberately read-only and contains no
|
|
// Docker-socket path; all I/O stays behind Client's fixed GraphQL allowlist.
|
|
type ContainerSource struct {
|
|
Client *Client
|
|
Now func() time.Time
|
|
}
|
|
|
|
func (s ContainerSource) Snapshot(ctx context.Context) (container.RawSnapshot, error) {
|
|
if err := ctx.Err(); err != nil {
|
|
return container.RawSnapshot{}, err
|
|
}
|
|
if s.Client == nil {
|
|
return container.RawSnapshot{}, errors.New("Unraid client is required")
|
|
}
|
|
payload, err := s.Client.Query(ctx, "containers")
|
|
if err != nil {
|
|
return container.RawSnapshot{}, fmt.Errorf("query Unraid containers: %w", err)
|
|
}
|
|
var response struct {
|
|
Docker struct {
|
|
Containers []struct {
|
|
ID string `json:"id"`
|
|
Names json.RawMessage `json:"names"`
|
|
State string `json:"state"`
|
|
Status string `json:"status"`
|
|
AutoStart bool `json:"autoStart"`
|
|
} `json:"containers"`
|
|
} `json:"docker"`
|
|
}
|
|
if err := json.Unmarshal(payload, &response); err != nil {
|
|
return container.RawSnapshot{}, errors.New("decode Unraid container response")
|
|
}
|
|
if len(response.Docker.Containers) > container.DefaultMaxContainers {
|
|
return container.RawSnapshot{}, errors.New("Unraid container response exceeds bounds")
|
|
}
|
|
now := time.Now().UTC()
|
|
if s.Now != nil {
|
|
now = s.Now().UTC()
|
|
}
|
|
items := make([]container.RawContainer, 0, len(response.Docker.Containers))
|
|
for _, item := range response.Docker.Containers {
|
|
name, err := containerName(item.Names)
|
|
if err != nil || strings.TrimSpace(item.ID) == "" {
|
|
return container.RawSnapshot{}, errors.New("Unraid container identity is invalid")
|
|
}
|
|
runtimeState := containerRuntimeState(item.State, item.Status)
|
|
items = append(items, container.RawContainer{
|
|
ID: item.ID, Name: name, State: runtimeState, Health: containerHealth(item.Status),
|
|
IntentionalStop: !item.AutoStart && runtimeState == "exited",
|
|
})
|
|
}
|
|
return container.RawSnapshot{Source: container.Source{ID: "unraid", Type: "unraid"}, Containers: items, ObservedAt: now, ReceivedAt: now}, nil
|
|
}
|
|
|
|
func containerRuntimeState(state, status string) string {
|
|
status = strings.ToLower(strings.TrimSpace(status))
|
|
switch {
|
|
case strings.HasPrefix(status, "restarting"):
|
|
return "restarting"
|
|
case status == "created":
|
|
return "created"
|
|
default:
|
|
return strings.ToLower(strings.TrimSpace(state))
|
|
}
|
|
}
|
|
|
|
func containerHealth(status string) string {
|
|
status = strings.ToLower(strings.TrimSpace(status))
|
|
switch {
|
|
case status == "healthy", strings.Contains(status, "(healthy)"):
|
|
return "healthy"
|
|
case status == "unhealthy", strings.Contains(status, "(unhealthy)"):
|
|
return "unhealthy"
|
|
case status == "starting", strings.Contains(status, "(health: starting)"), strings.Contains(status, "(starting)"):
|
|
return "starting"
|
|
default:
|
|
return "unknown"
|
|
}
|
|
}
|
|
|
|
func containerName(raw json.RawMessage) (string, error) {
|
|
var name string
|
|
if err := json.Unmarshal(raw, &name); err == nil && strings.TrimSpace(name) != "" {
|
|
return strings.TrimPrefix(strings.TrimSpace(name), "/"), nil
|
|
}
|
|
var names []string
|
|
if err := json.Unmarshal(raw, &names); err != nil || len(names) == 0 {
|
|
return "", errors.New("Unraid container name is invalid")
|
|
}
|
|
for _, candidate := range names {
|
|
candidate = strings.TrimPrefix(strings.TrimSpace(candidate), "/")
|
|
if candidate != "" {
|
|
return candidate, nil
|
|
}
|
|
}
|
|
return "", errors.New("Unraid container name is invalid")
|
|
}
|