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" Tag string `json:"tag"` // just the tag part Size int64 `json:"size"` // bytes Created int64 `json:"created"` // unix timestamp } // 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 { tagPart := "" if idx := strings.LastIndex(tag, ":"); idx != -1 { tagPart = tag[idx+1:] } images = append(images, LocalImage{ ID: img.ID, Ref: tag, Tag: tagPart, Size: img.Size, Created: img.Created, }) } } } 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, " ") }