This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
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")
|
||||
}
|
||||
Reference in New Issue
Block a user