152 lines
4.1 KiB
Go
152 lines
4.1 KiB
Go
package docker
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os/exec"
|
|
"strings"
|
|
)
|
|
|
|
// Volume is a Docker volume that belongs to a Homarr instance.
|
|
type Volume struct {
|
|
Name string
|
|
Driver string
|
|
Mountpoint string
|
|
Size string
|
|
InUse bool
|
|
}
|
|
|
|
// PRNumber returns the pull request a volume stores data for, or zero.
|
|
func (v Volume) PRNumber() int {
|
|
var number int
|
|
if _, err := fmt.Sscanf(v.Name, "homarr_pr_%d_data", &number); err == nil {
|
|
return number
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// Tag returns the local image tag a volume stores data for, or an empty string.
|
|
func (v Volume) Tag() string {
|
|
if v.PRNumber() > 0 {
|
|
return ""
|
|
}
|
|
name := strings.TrimPrefix(v.Name, "homarr_")
|
|
if name == v.Name {
|
|
return ""
|
|
}
|
|
return strings.TrimSuffix(name, "_data")
|
|
}
|
|
|
|
// ListVolumes returns the Homarr data volumes, optionally annotated with disk
|
|
// usage when includeSizes is true.
|
|
func ListVolumes(ctx context.Context, includeSizes bool) ([]Volume, error) {
|
|
out, err := exec.CommandContext(ctx, "docker", "volume", "ls", "--filter", "name=homarr", "--format", "{{json .}}").Output()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("list Homarr volumes: %w", err)
|
|
}
|
|
volumes, err := parseVolumeList(out)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var sizes map[string]string
|
|
if includeSizes {
|
|
sizes = volumeSizes(ctx)
|
|
}
|
|
inUse := volumesInUse(ctx)
|
|
for index := range volumes {
|
|
if sizes != nil {
|
|
if size, known := sizes[volumes[index].Name]; known {
|
|
volumes[index].Size = size
|
|
}
|
|
}
|
|
volumes[index].InUse = inUse[volumes[index].Name]
|
|
}
|
|
return volumes, nil
|
|
}
|
|
|
|
func parseVolumeList(output []byte) ([]Volume, error) {
|
|
volumes := make([]Volume, 0)
|
|
for _, line := range strings.Split(strings.TrimSpace(string(output)), "\n") {
|
|
if line == "" {
|
|
continue
|
|
}
|
|
var row struct {
|
|
Name string
|
|
Driver string
|
|
Mountpoint string
|
|
Size string
|
|
}
|
|
if err := json.Unmarshal([]byte(line), &row); err != nil {
|
|
return nil, fmt.Errorf("parse Docker volume: %w", err)
|
|
}
|
|
// buildx keeps its cache in volumes that match the same name filter.
|
|
if !strings.HasPrefix(row.Name, "homarr_") && row.Name != "homarr" {
|
|
continue
|
|
}
|
|
size := row.Size
|
|
if size == "N/A" {
|
|
size = ""
|
|
}
|
|
volumes = append(volumes, Volume{Name: row.Name, Driver: row.Driver, Mountpoint: row.Mountpoint, Size: size})
|
|
}
|
|
return volumes, nil
|
|
}
|
|
|
|
// volumeSizes reads reclaimable space per volume. `docker system df -v` is slow
|
|
// and optional, so failures degrade to unknown sizes rather than an error.
|
|
func volumeSizes(ctx context.Context) map[string]string {
|
|
sizes := make(map[string]string)
|
|
out, err := exec.CommandContext(ctx, "docker", "system", "df", "-v", "--format", "{{json .Volumes}}").Output()
|
|
if err != nil {
|
|
return sizes
|
|
}
|
|
var rows []struct {
|
|
Name string
|
|
Size string
|
|
}
|
|
if err := json.Unmarshal(out, &rows); err != nil {
|
|
return sizes
|
|
}
|
|
for _, row := range rows {
|
|
if row.Size != "" && row.Size != "N/A" {
|
|
sizes[row.Name] = row.Size
|
|
}
|
|
}
|
|
return sizes
|
|
}
|
|
|
|
func volumesInUse(ctx context.Context) map[string]bool {
|
|
inUse := make(map[string]bool)
|
|
out, err := exec.CommandContext(ctx, "docker", "ps", "-a", "--filter", "name=homarr", "--format", "{{.Mounts}}").Output()
|
|
if err != nil {
|
|
return inUse
|
|
}
|
|
for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") {
|
|
for _, mount := range strings.Split(line, ",") {
|
|
if mount = strings.TrimSpace(mount); mount != "" {
|
|
inUse[mount] = true
|
|
}
|
|
}
|
|
}
|
|
return inUse
|
|
}
|
|
|
|
// RemoveVolume deletes a volume and the instance data it holds.
|
|
func RemoveVolume(ctx context.Context, name string) error {
|
|
out, err := exec.CommandContext(ctx, "docker", "volume", "rm", name).CombinedOutput()
|
|
if err != nil {
|
|
return fmt.Errorf("remove volume %s: %w: %s", name, err, strings.TrimSpace(string(out)))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// RemoveImage force-deletes a local image, including any tags pointing at it.
|
|
func RemoveImage(ctx context.Context, reference string) error {
|
|
out, err := exec.CommandContext(ctx, "docker", "image", "rm", "-f", reference).CombinedOutput()
|
|
if err != nil {
|
|
return fmt.Errorf("remove image %s: %w: %s", reference, err, strings.TrimSpace(string(out)))
|
|
}
|
|
return nil
|
|
}
|