5577851f22
- Image prune only removes images matching project image refs, skips active instances - Add ListImagesByRef and RemoveImage to Docker client - Fix 409 conflict: use listProjects instead of duplicate POST - Add "Deploy immediately" toggle to Quick Deploy (off by default) - Replace raw access list ID with EntityPicker on project edit form - Trigger proxy resync on access list change - Fix stage form layout: single responsive row - Fix empty port default on project creation - Improve inspect error message for remote Docker
159 lines
4.6 KiB
Go
159 lines
4.6 KiB
Go
package docker
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/moby/moby/api/types/registry"
|
|
"github.com/moby/moby/client"
|
|
)
|
|
|
|
// ExtractPort parses the first exposed port from Docker EXPOSE entries.
|
|
// Entries are in the form "8080/tcp" or "8080". Returns 0 if none found.
|
|
func ExtractPort(exposedPorts []string) int {
|
|
if len(exposedPorts) == 0 {
|
|
return 0
|
|
}
|
|
raw := exposedPorts[0]
|
|
if idx := strings.Index(raw, "/"); idx != -1 {
|
|
raw = raw[:idx]
|
|
}
|
|
port, _ := strconv.Atoi(raw)
|
|
return port
|
|
}
|
|
|
|
// 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 := client.ImagePullOptions{}
|
|
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)
|
|
}
|
|
|
|
// Wait for the pull to complete.
|
|
if err := reader.Wait(ctx); err != nil {
|
|
return fmt.Errorf("wait for pull of %s: %w", ref, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// InspectImage retrieves metadata from a local image.
|
|
func (c *Client) InspectImage(ctx context.Context, imageRef string) (ImageInfo, error) {
|
|
inspectResult, err := c.api.ImageInspect(ctx, imageRef)
|
|
if err != nil {
|
|
return ImageInfo{}, fmt.Errorf("inspect image %s: %w", imageRef, err)
|
|
}
|
|
|
|
info := ImageInfo{}
|
|
|
|
// Extract labels from Config if available.
|
|
if inspectResult.Config != nil {
|
|
info.Labels = inspectResult.Config.Labels
|
|
|
|
// Extract exposed ports from OCI config (map[string]struct{}).
|
|
for port := range inspectResult.Config.ExposedPorts {
|
|
info.ExposedPorts = append(info.ExposedPorts, port)
|
|
}
|
|
|
|
// Extract healthcheck command.
|
|
if inspectResult.Config.Healthcheck != nil && len(inspectResult.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(inspectResult.Config.Healthcheck.Test) > 1 {
|
|
info.Healthcheck = joinArgs(inspectResult.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
|
|
}
|
|
|
|
// LocalImage represents a Docker image on the local machine.
|
|
type LocalImage struct {
|
|
ID string `json:"id"`
|
|
Ref string `json:"ref"` // e.g., "registry/org/app:tag"
|
|
Size int64 `json:"size"` // bytes
|
|
}
|
|
|
|
// ListImagesByRef returns all local images matching a given image reference prefix.
|
|
// For example, "registry.example.com/org/app" matches all tags of that image.
|
|
func (c *Client) ListImagesByRef(ctx context.Context, imageBase string) ([]LocalImage, error) {
|
|
result, err := c.api.ImageList(ctx, client.ImageListOptions{})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("list images: %w", err)
|
|
}
|
|
|
|
var images []LocalImage
|
|
for _, img := range result.Items {
|
|
for _, tag := range img.RepoTags {
|
|
if strings.HasPrefix(tag, imageBase+":") || tag == imageBase {
|
|
images = append(images, LocalImage{
|
|
ID: img.ID,
|
|
Ref: tag,
|
|
Size: img.Size,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
return images, nil
|
|
}
|
|
|
|
// RemoveImage removes a single Docker image by reference (name:tag or ID).
|
|
func (c *Client) RemoveImage(ctx context.Context, imageRef string) error {
|
|
_, err := c.api.ImageRemove(ctx, imageRef, client.ImageRemoveOptions{PruneChildren: true})
|
|
if err != nil {
|
|
return fmt.Errorf("remove image %s: %w", imageRef, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// joinArgs joins string arguments with spaces.
|
|
func joinArgs(args []string) string {
|
|
return strings.Join(args, " ")
|
|
}
|