add service inspect command, introduce squirrel for sql query building

This commit is contained in:
Pavel Sviderski
2024-12-03 10:56:29 +10:00
parent 21d55dc69a
commit daada3bbe9
12 changed files with 205 additions and 65 deletions
+57
View File
@@ -0,0 +1,57 @@
package service
import (
"github.com/docker/docker/api/types"
"regexp"
)
const (
LabelServiceID = "uncloud.service.id"
LabelServiceName = "uncloud.service.name"
)
type Container struct {
types.Container
}
// ServiceID returns the service ID that the container is part of.
func (c *Container) ServiceID() string {
return c.Labels[LabelServiceID]
}
// ServiceName returns the service name that the container is part of.
func (c *Container) ServiceName() string {
return c.Labels[LabelServiceName]
}
// runningStatusRegex matches the status string of a running container.
// - "Up 3 minutes (healthy)" -> groups: ["Up 3 minutes (healthy)", "healthy"]
// - "Up 5 seconds" -> groups: ["Up 5 seconds", ""]
// - "Up 2 hours (unhealthy)" -> groups: ["Up 2 hours (unhealthy)", "unhealthy"]
// - "Up 1 minute (health: starting)" -> groups: ["Up 1 minute (health: starting)", "health: starting"]
// - "Restarting (0) 5 seconds ago" -> no match
// See https://github.com/moby/moby/blob/c130ce1f5d1e38b98a97044a39557de43bc0d58f/container/state.go#L77-L90
// for more details on how the status string for a running container is formatted.
var runningStatusRegex = regexp.MustCompile(`^Up [^(]+(?:\(([^)]+)\))?$`)
// Healthy determines if the container is running and healthy based on its status string.
// A running container with no health check configured is considered healthy.
func (c *Container) Healthy() bool {
if c.State != "running" {
return false
}
matches := runningStatusRegex.FindStringSubmatch(c.Status)
// Not "Up" or invalid format.
if matches == nil {
return false
}
// If there's no health status (no health check configured so no parentheses), container is considered healthy.
if matches[1] == "" {
return true
}
// If the health status in parentheses is "healthy", the container is considered healthy.
return matches[1] == types.Healthy
}
+92
View File
@@ -0,0 +1,92 @@
package service
import (
"github.com/docker/docker/api/types"
"github.com/stretchr/testify/assert"
"testing"
)
func TestContainer_Healthy(t *testing.T) {
t.Parallel()
t.Run("exited", func(t *testing.T) {
t.Parallel()
c := &Container{Container: types.Container{
State: "exited",
Status: "Exited (0) 2 minutes ago",
}}
assert.False(t, c.Healthy())
})
t.Run("running with no health check", func(t *testing.T) {
t.Parallel()
c := &Container{Container: types.Container{
State: "running",
Status: "Up 5 minutes",
}}
assert.True(t, c.Healthy())
})
t.Run("running and healthy", func(t *testing.T) {
t.Parallel()
c := &Container{Container: types.Container{
State: "running",
Status: "Up 3 minutes (healthy)",
}}
assert.True(t, c.Healthy())
})
t.Run("running but unhealthy", func(t *testing.T) {
t.Parallel()
c := &Container{Container: types.Container{
State: "running",
Status: "Up 2 hours (unhealthy)",
}}
assert.False(t, c.Healthy())
})
t.Run("running with health starting", func(t *testing.T) {
t.Parallel()
c := &Container{Container: types.Container{
State: "running",
Status: "Up 1 minute (health: starting)",
}}
assert.False(t, c.Healthy())
})
t.Run("invalid up format no time", func(t *testing.T) {
t.Parallel()
c := &Container{Container: types.Container{
State: "running",
Status: "Up",
}}
assert.False(t, c.Healthy())
})
t.Run("invalid up format empty parentheses", func(t *testing.T) {
t.Parallel()
c := &Container{Container: types.Container{
State: "running",
Status: "Up 5 minutes ()",
}}
assert.False(t, c.Healthy())
})
t.Run("malformed status", func(t *testing.T) {
t.Parallel()
c := &Container{Container: types.Container{
State: "running",
Status: "Invalid status",
}}
assert.False(t, c.Healthy())
})
t.Run("restarting", func(t *testing.T) {
t.Parallel()
c := &Container{Container: types.Container{
State: "running",
Status: "Restarting (0) 5 seconds ago",
}}
assert.False(t, c.Healthy())
})
}
+13 -14
View File
@@ -3,7 +3,6 @@ package service
import (
"encoding/json"
"fmt"
"github.com/docker/docker/api/types"
"uncloud/internal/machine/api/pb"
)
@@ -15,19 +14,19 @@ const (
type Service struct {
ID string
Name string
Containers []Container
Containers []MachineContainer
}
type Container struct {
type MachineContainer struct {
MachineID string
Container types.Container
Container Container
}
func FromProto(s *pb.Service) (Service, error) {
var err error
containers := make([]Container, len(s.Containers))
for i, c := range s.Containers {
containers[i], err = containerFromProto(c)
containers := make([]MachineContainer, len(s.Containers))
for i, sc := range s.Containers {
containers[i], err = machineContainerFromProto(sc)
if err != nil {
return Service{}, err
}
@@ -40,14 +39,14 @@ func FromProto(s *pb.Service) (Service, error) {
}, nil
}
func containerFromProto(c *pb.Service_Container) (Container, error) {
var dockerCtr types.Container
if err := json.Unmarshal(c.Container, &dockerCtr); err != nil {
return Container{}, fmt.Errorf("unmarshal container: %w", err)
func machineContainerFromProto(sc *pb.Service_Container) (MachineContainer, error) {
var c Container
if err := json.Unmarshal(sc.Container, &c); err != nil {
return MachineContainer{}, fmt.Errorf("unmarshal container: %w", err)
}
return Container{
MachineID: c.MachineId,
Container: dockerCtr,
return MachineContainer{
MachineID: sc.MachineId,
Container: c,
}, nil
}