feat(discovery): parse Compose-managed labels
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
package discovery
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ComposeLabels captures the four Compose-managed labels we need
|
||||
// to drive a `docker compose pull`/`up -d` against the right stack.
|
||||
type ComposeLabels struct {
|
||||
Project string
|
||||
Service string
|
||||
WorkingDir string
|
||||
ConfigFiles []string
|
||||
}
|
||||
|
||||
const (
|
||||
labelProject = "com.docker.compose.project"
|
||||
labelService = "com.docker.compose.service"
|
||||
labelWorkingDir = "com.docker.compose.project.working_dir"
|
||||
labelConfigFiles = "com.docker.compose.project.config_files"
|
||||
)
|
||||
|
||||
// ParseComposeLabels extracts the Compose labels we need. Returns an
|
||||
// error naming the missing label if any required field is absent —
|
||||
// in practice this should only happen if the container was not
|
||||
// started by Compose.
|
||||
func ParseComposeLabels(labels map[string]string) (ComposeLabels, error) {
|
||||
get := func(key string) (string, error) {
|
||||
v, ok := labels[key]
|
||||
if !ok || v == "" {
|
||||
return "", fmt.Errorf("missing required label: %s", key)
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
project, err := get(labelProject)
|
||||
if err != nil {
|
||||
return ComposeLabels{}, err
|
||||
}
|
||||
service, err := get(labelService)
|
||||
if err != nil {
|
||||
return ComposeLabels{}, err
|
||||
}
|
||||
workingDir, err := get(labelWorkingDir)
|
||||
if err != nil {
|
||||
return ComposeLabels{}, err
|
||||
}
|
||||
configFilesRaw, err := get(labelConfigFiles)
|
||||
if err != nil {
|
||||
return ComposeLabels{}, err
|
||||
}
|
||||
|
||||
files := strings.Split(configFilesRaw, ",")
|
||||
for i, f := range files {
|
||||
files[i] = strings.TrimSpace(f)
|
||||
}
|
||||
|
||||
return ComposeLabels{
|
||||
Project: project,
|
||||
Service: service,
|
||||
WorkingDir: workingDir,
|
||||
ConfigFiles: files,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// HasOptIn reports whether the labels include the opt-in marker with
|
||||
// value "true" (exact, case-sensitive — anything else is excluded).
|
||||
func HasOptIn(labels map[string]string, key string) bool {
|
||||
return labels[key] == "true"
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package discovery_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/shcizo/package-updater/internal/discovery"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestParseComposeLabels_Success(t *testing.T) {
|
||||
labels := map[string]string{
|
||||
"com.docker.compose.project": "myapp-prod",
|
||||
"com.docker.compose.service": "web",
|
||||
"com.docker.compose.project.working_dir": "/home/shcizo/self-hosted/myapp-prod",
|
||||
"com.docker.compose.project.config_files": "/home/shcizo/self-hosted/myapp-prod/docker-compose.yml",
|
||||
"se.shcizo.auto-update": "true",
|
||||
}
|
||||
got, err := discovery.ParseComposeLabels(labels)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "myapp-prod", got.Project)
|
||||
require.Equal(t, "web", got.Service)
|
||||
require.Equal(t, "/home/shcizo/self-hosted/myapp-prod", got.WorkingDir)
|
||||
require.Equal(t, []string{"/home/shcizo/self-hosted/myapp-prod/docker-compose.yml"}, got.ConfigFiles)
|
||||
}
|
||||
|
||||
func TestParseComposeLabels_MultipleConfigFiles(t *testing.T) {
|
||||
labels := map[string]string{
|
||||
"com.docker.compose.project": "myapp",
|
||||
"com.docker.compose.service": "web",
|
||||
"com.docker.compose.project.working_dir": "/srv/myapp",
|
||||
"com.docker.compose.project.config_files": "/srv/myapp/docker-compose.yml,/srv/myapp/docker-compose.prod.yml",
|
||||
}
|
||||
got, err := discovery.ParseComposeLabels(labels)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []string{
|
||||
"/srv/myapp/docker-compose.yml",
|
||||
"/srv/myapp/docker-compose.prod.yml",
|
||||
}, got.ConfigFiles)
|
||||
}
|
||||
|
||||
func TestParseComposeLabels_MissingFieldsError(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
missing string
|
||||
labels map[string]string
|
||||
}{
|
||||
{"project", "com.docker.compose.project", map[string]string{
|
||||
"com.docker.compose.service": "web",
|
||||
"com.docker.compose.project.working_dir": "/x",
|
||||
"com.docker.compose.project.config_files": "/x/y.yml",
|
||||
}},
|
||||
{"service", "com.docker.compose.service", map[string]string{
|
||||
"com.docker.compose.project": "p",
|
||||
"com.docker.compose.project.working_dir": "/x",
|
||||
"com.docker.compose.project.config_files": "/x/y.yml",
|
||||
}},
|
||||
{"working_dir", "com.docker.compose.project.working_dir", map[string]string{
|
||||
"com.docker.compose.project": "p",
|
||||
"com.docker.compose.service": "web",
|
||||
"com.docker.compose.project.config_files": "/x/y.yml",
|
||||
}},
|
||||
{"config_files", "com.docker.compose.project.config_files", map[string]string{
|
||||
"com.docker.compose.project": "p",
|
||||
"com.docker.compose.service": "web",
|
||||
"com.docker.compose.project.working_dir": "/x",
|
||||
}},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
_, err := discovery.ParseComposeLabels(c.labels)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), c.missing)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHasOptIn(t *testing.T) {
|
||||
require.True(t, discovery.HasOptIn(map[string]string{
|
||||
"se.shcizo.auto-update": "true",
|
||||
}, "se.shcizo.auto-update"))
|
||||
require.False(t, discovery.HasOptIn(map[string]string{
|
||||
"se.shcizo.auto-update": "false",
|
||||
}, "se.shcizo.auto-update"))
|
||||
require.False(t, discovery.HasOptIn(map[string]string{
|
||||
"se.shcizo.auto-update": "TRUE",
|
||||
}, "se.shcizo.auto-update"))
|
||||
require.False(t, discovery.HasOptIn(map[string]string{}, "se.shcizo.auto-update"))
|
||||
}
|
||||
Reference in New Issue
Block a user