feat(docker-watcher): phases 3+4 - Docker client & NPM client
Phase 3: Docker Engine API wrapper — pull/inspect images, container lifecycle (create/start/stop/remove/restart), network management, label-based container tracking, deterministic naming. Phase 4: Nginx Proxy Manager API client — JWT auth with auto-refresh, CRUD for proxy hosts, domain-based host lookup.
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/docker/api/types/image"
|
||||
"github.com/docker/docker/api/types/registry"
|
||||
)
|
||||
|
||||
// ImageInfo holds metadata extracted from a Docker image inspection.
|
||||
type ImageInfo struct {
|
||||
// ExposedPorts lists the ports declared via EXPOSE in the Dockerfile (e.g. ["8080/tcp"]).
|
||||
ExposedPorts []string
|
||||
|
||||
// Healthcheck is the CMD string from the image's HEALTHCHECK instruction, if any.
|
||||
Healthcheck string
|
||||
|
||||
// Labels are the key-value pairs defined in the image metadata.
|
||||
Labels map[string]string
|
||||
}
|
||||
|
||||
// PullImage pulls an image from a registry. If authConfig is non-empty, it is
|
||||
// used as the base64-encoded JSON auth payload for private registries.
|
||||
// The image reference should be in the form "repository:tag".
|
||||
func (c *Client) PullImage(ctx context.Context, imageRef string, tag string, authConfig string) error {
|
||||
ref := imageRef
|
||||
if tag != "" {
|
||||
ref = imageRef + ":" + tag
|
||||
}
|
||||
|
||||
opts := image.PullOptions{}
|
||||
if authConfig != "" {
|
||||
opts.RegistryAuth = authConfig
|
||||
}
|
||||
|
||||
reader, err := c.api.ImagePull(ctx, ref, opts)
|
||||
if err != nil {
|
||||
return fmt.Errorf("pull image %s: %w", ref, err)
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
// Drain the pull progress stream to completion.
|
||||
if _, err := io.Copy(io.Discard, reader); err != nil {
|
||||
return fmt.Errorf("read pull response for %s: %w", ref, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// InspectImage retrieves metadata from a local image.
|
||||
func (c *Client) InspectImage(ctx context.Context, imageRef string) (ImageInfo, error) {
|
||||
inspect, _, err := c.api.ImageInspectWithRaw(ctx, imageRef)
|
||||
if err != nil {
|
||||
return ImageInfo{}, fmt.Errorf("inspect image %s: %w", imageRef, err)
|
||||
}
|
||||
|
||||
info := ImageInfo{
|
||||
Labels: inspect.Config.Labels,
|
||||
}
|
||||
|
||||
// Extract exposed ports.
|
||||
for port := range inspect.Config.ExposedPorts {
|
||||
info.ExposedPorts = append(info.ExposedPorts, string(port))
|
||||
}
|
||||
|
||||
// Extract healthcheck command.
|
||||
if inspect.Config.Healthcheck != nil && len(inspect.Config.Healthcheck.Test) > 0 {
|
||||
// The Test slice is ["CMD", "arg1", "arg2", ...] or ["CMD-SHELL", "cmd string"].
|
||||
// Join all parts after the first element for a readable representation.
|
||||
if len(inspect.Config.Healthcheck.Test) > 1 {
|
||||
info.Healthcheck = joinArgs(inspect.Config.Healthcheck.Test[1:])
|
||||
}
|
||||
}
|
||||
|
||||
return info, nil
|
||||
}
|
||||
|
||||
// EncodeRegistryAuth builds a base64-encoded JSON auth string suitable for
|
||||
// Docker API calls. Pass empty strings for anonymous access.
|
||||
func EncodeRegistryAuth(username, password, serverAddress string) (string, error) {
|
||||
cfg := registry.AuthConfig{
|
||||
Username: username,
|
||||
Password: password,
|
||||
ServerAddress: serverAddress,
|
||||
}
|
||||
|
||||
data, err := json.Marshal(cfg)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("encode registry auth: %w", err)
|
||||
}
|
||||
|
||||
return base64.URLEncoding.EncodeToString(data), nil
|
||||
}
|
||||
|
||||
// joinArgs joins string arguments with spaces.
|
||||
func joinArgs(args []string) string {
|
||||
return strings.Join(args, " ")
|
||||
}
|
||||
Reference in New Issue
Block a user