2473 lines
73 KiB
Go
2473 lines
73 KiB
Go
package dock
|
|
|
|
import (
|
|
"bufio"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"log"
|
|
"os"
|
|
"os/signal"
|
|
"regexp"
|
|
"runtime"
|
|
"strings"
|
|
"time"
|
|
|
|
"context"
|
|
"github.com/docker/docker/api/types"
|
|
"github.com/docker/docker/api/types/container"
|
|
"github.com/docker/docker/api/types/filters"
|
|
"github.com/docker/docker/api/types/image"
|
|
"github.com/docker/docker/api/types/network"
|
|
"github.com/docker/docker/client"
|
|
"github.com/docker/docker/pkg/jsonmessage"
|
|
"github.com/docker/go-connections/nat"
|
|
"github.com/moby/term"
|
|
"golang.org/x/crypto/ssh/terminal"
|
|
|
|
common "penthertz/rfswift/common"
|
|
rfutils "penthertz/rfswift/rfutils"
|
|
)
|
|
|
|
type HostConfigFull struct {
|
|
Binds []string `json:"Binds"`
|
|
ContainerIDFile string `json:"ContainerIDFile"`
|
|
LogConfig LogConfig `json:"LogConfig"`
|
|
NetworkMode string `json:"NetworkMode"`
|
|
PortBindings map[string][]PortBinding `json:"PortBindings"`
|
|
RestartPolicy RestartPolicy `json:"RestartPolicy"`
|
|
AutoRemove bool `json:"AutoRemove"`
|
|
VolumeDriver string `json:"VolumeDriver"`
|
|
VolumesFrom []string `json:"VolumesFrom"`
|
|
ConsoleSize []int `json:"ConsoleSize"`
|
|
CapAdd []string `json:"CapAdd"`
|
|
CapDrop []string `json:"CapDrop"`
|
|
CgroupnsMode string `json:"CgroupnsMode"`
|
|
Dns []string `json:"Dns"`
|
|
DnsOptions []string `json:"DnsOptions"`
|
|
DnsSearch []string `json:"DnsSearch"`
|
|
ExtraHosts []string `json:"ExtraHosts"`
|
|
GroupAdd []string `json:"GroupAdd"`
|
|
IpcMode string `json:"IpcMode"`
|
|
Cgroup string `json:"Cgroup"`
|
|
Links []string `json:"Links"`
|
|
OomScoreAdj int `json:"OomScoreAdj"`
|
|
PidMode string `json:"PidMode"`
|
|
Privileged bool `json:"Privileged"`
|
|
PublishAllPorts bool `json:"PublishAllPorts"`
|
|
ReadonlyRootfs bool `json:"ReadonlyRootfs"`
|
|
SecurityOpt []string `json:"SecurityOpt"`
|
|
UTSMode string `json:"UTSMode"`
|
|
UsernsMode string `json:"UsernsMode"`
|
|
ShmSize int64 `json:"ShmSize"`
|
|
Runtime string `json:"Runtime"`
|
|
Isolation string `json:"Isolation"`
|
|
CpuShares int64 `json:"CpuShares"`
|
|
Memory int64 `json:"Memory"`
|
|
NanoCpus int64 `json:"NanoCpus"`
|
|
CgroupParent string `json:"CgroupParent"`
|
|
BlkioWeight uint16 `json:"BlkioWeight"`
|
|
BlkioWeightDevice []ThrottleDevice `json:"BlkioWeightDevice"`
|
|
BlkioDeviceReadBps []ThrottleDevice `json:"BlkioDeviceReadBps"`
|
|
BlkioDeviceWriteBps []ThrottleDevice `json:"BlkioDeviceWriteBps"`
|
|
BlkioDeviceReadIOps []ThrottleDevice `json:"BlkioDeviceReadIOps"`
|
|
BlkioDeviceWriteIOps []ThrottleDevice `json:"BlkioDeviceWriteIOps"`
|
|
CpuPeriod int64 `json:"CpuPeriod"`
|
|
CpuQuota int64 `json:"CpuQuota"`
|
|
CpuRealtimePeriod int64 `json:"CpuRealtimePeriod"`
|
|
CpuRealtimeRuntime int64 `json:"CpuRealtimeRuntime"`
|
|
CpusetCpus string `json:"CpusetCpus"`
|
|
CpusetMems string `json:"CpusetMems"`
|
|
Devices []DeviceMapping `json:"Devices"`
|
|
DeviceCgroupRules []string `json:"DeviceCgroupRules"`
|
|
DeviceRequests []DeviceRequest `json:"DeviceRequests"`
|
|
MemoryReservation int64 `json:"MemoryReservation"`
|
|
MemorySwap int64 `json:"MemorySwap"`
|
|
MemorySwappiness *int `json:"MemorySwappiness"`
|
|
OomKillDisable *bool `json:"OomKillDisable"`
|
|
PidsLimit *int64 `json:"PidsLimit"`
|
|
Ulimits []Ulimit `json:"Ulimits"`
|
|
CpuCount int64 `json:"CpuCount"`
|
|
CpuPercent int64 `json:"CpuPercent"`
|
|
IOMaximumIOps int64 `json:"IOMaximumIOps"`
|
|
IOMaximumBandwidth int64 `json:"IOMaximumBandwidth"`
|
|
MaskedPaths []string `json:"MaskedPaths"`
|
|
ReadonlyPaths []string `json:"ReadonlyPaths"`
|
|
}
|
|
|
|
// Supporting structs
|
|
type LogConfig struct {
|
|
Type string `json:"Type"`
|
|
Config map[string]string `json:"Config"`
|
|
}
|
|
|
|
type RestartPolicy struct {
|
|
Name string `json:"Name"`
|
|
MaximumRetryCount int `json:"MaximumRetryCount"`
|
|
}
|
|
|
|
type PortBinding struct {
|
|
HostIP string `json:"HostIp"`
|
|
HostPort string `json:"HostPort"`
|
|
}
|
|
|
|
type ThrottleDevice struct {
|
|
Path string `json:"Path"`
|
|
Rate uint64 `json:"Rate"`
|
|
}
|
|
|
|
type DeviceMapping struct {
|
|
PathOnHost string `json:"PathOnHost"`
|
|
PathInContainer string `json:"PathInContainer"`
|
|
CgroupPermissions string `json:"CgroupPermissions"`
|
|
}
|
|
|
|
type DeviceRequest struct {
|
|
Driver string `json:"Driver"`
|
|
Count int `json:"Count"`
|
|
DeviceIDs []string `json:"DeviceIDs"`
|
|
Capabilities [][]string `json:"Capabilities"`
|
|
Options map[string]string `json:"Options"`
|
|
}
|
|
|
|
type Ulimit struct {
|
|
Name string `json:"Name"`
|
|
Hard int64 `json:"Hard"`
|
|
Soft int64 `json:"Soft"`
|
|
}
|
|
|
|
var inout chan []byte
|
|
|
|
type DockerInst struct {
|
|
net string
|
|
privileged bool
|
|
xdisplay string
|
|
x11forward string
|
|
usbforward string
|
|
usbdevice string
|
|
shell string
|
|
imagename string
|
|
repotag string
|
|
extrabinding string
|
|
entrypoint string
|
|
extrahosts string
|
|
extraenv string
|
|
pulse_server string
|
|
network_mode string
|
|
exposed_ports string
|
|
binded_ports string
|
|
devices string
|
|
caps string
|
|
seccomp string
|
|
cgroups string
|
|
}
|
|
|
|
var dockerObj = DockerInst{net: "host",
|
|
privileged: false,
|
|
xdisplay: "DISPLAY=:0",
|
|
entrypoint: "/bin/bash",
|
|
x11forward: "/tmp/.X11-unix:/tmp/.X11-unix",
|
|
usbforward: "",
|
|
extrabinding: "/run/dbus/system_bus_socket:/run/dbus/system_bus_socket", // Some more if needed /run/dbus/system_bus_socket:/run/dbus/system_bus_socket,/dev/snd:/dev/snd,/dev/dri:/dev/dri
|
|
imagename: "myrfswift:latest",
|
|
repotag: "penthertz/rfswift",
|
|
extrahosts: "",
|
|
extraenv: "",
|
|
network_mode: "host",
|
|
exposed_ports: "",
|
|
binded_ports: "",
|
|
pulse_server: "tcp:localhost:34567",
|
|
devices: "/dev/snd:/dev/snd,/dev/dri:/dev/dri,/dev/input:/dev/input",
|
|
caps: "SYS_RAWIO,NET_ADMIN,SYS_TTY_CONFIG,SYS_ADMIN",
|
|
seccomp: "unconfined",
|
|
cgroups: "c *:* rmw",
|
|
shell: "/bin/bash"} // Instance with default values
|
|
|
|
func init() {
|
|
updateDockerObjFromConfig()
|
|
}
|
|
|
|
func updateDockerObjFromConfig() {
|
|
config, err := rfutils.ReadOrCreateConfig(common.ConfigFileByPlatform())
|
|
if err != nil {
|
|
log.Printf("Error reading config: %v. Using default values.", err)
|
|
return
|
|
}
|
|
|
|
// Update dockerObj with values from config
|
|
dockerObj.imagename = config.General.ImageName
|
|
dockerObj.repotag = config.General.RepoTag
|
|
dockerObj.shell = config.Container.Shell
|
|
dockerObj.network_mode = config.Container.Network
|
|
dockerObj.exposed_ports = config.Container.ExposedPorts
|
|
dockerObj.binded_ports = config.Container.PortBindings
|
|
dockerObj.x11forward = config.Container.X11Forward
|
|
dockerObj.xdisplay = config.Container.XDisplay
|
|
dockerObj.extrahosts = config.Container.ExtraHost
|
|
dockerObj.extraenv = config.Container.ExtraEnv
|
|
dockerObj.devices = config.Container.Devices
|
|
dockerObj.pulse_server = config.Audio.PulseServer
|
|
dockerObj.privileged = strings.ToLower(config.Container.Privileged) == "true"
|
|
dockerObj.caps = config.Container.Caps
|
|
dockerObj.seccomp = config.Container.Seccomp
|
|
dockerObj.cgroups = config.Container.Cgroups
|
|
|
|
// Handle bindings
|
|
var bindings []string
|
|
for _, binding := range config.Container.Bindings {
|
|
if strings.Contains(binding, "/dev/bus/usb") {
|
|
dockerObj.usbforward = binding
|
|
//bindings = append(bindings, binding)
|
|
} else if strings.Contains(binding, ".X11-unix") {
|
|
dockerObj.x11forward = binding
|
|
} else {
|
|
bindings = append(bindings, binding)
|
|
}
|
|
}
|
|
dockerObj.extrabinding = strings.Join(bindings, ",")
|
|
}
|
|
|
|
func resizeTty(ctx context.Context, cli *client.Client, contid string, fd int) {
|
|
for {
|
|
width, height, err := getTerminalSize(fd)
|
|
if err != nil {
|
|
log.Printf("Error getting terminal size: %v", err)
|
|
time.Sleep(1 * time.Second)
|
|
continue
|
|
}
|
|
|
|
err = cli.ContainerResize(ctx, contid, container.ResizeOptions{
|
|
Height: uint(height),
|
|
Width: uint(width),
|
|
})
|
|
if err != nil {
|
|
log.Printf("Error resizing container TTY: %v", err)
|
|
}
|
|
|
|
time.Sleep(1 * time.Second)
|
|
}
|
|
}
|
|
|
|
func checkIfImageIsUpToDate(repo, tag string) (bool, error) {
|
|
architecture := getArchitecture()
|
|
tags, err := getLatestDockerHubTags(repo, architecture)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
for _, latestTag := range tags {
|
|
if latestTag.Name == tag {
|
|
return true, nil
|
|
}
|
|
}
|
|
|
|
return false, nil
|
|
}
|
|
|
|
func parseImageName(imageName string) (string, string) {
|
|
parts := strings.Split(imageName, ":")
|
|
repo := parts[0]
|
|
tag := "latest"
|
|
if len(parts) > 1 {
|
|
tag = parts[1]
|
|
}
|
|
return repo, tag
|
|
}
|
|
|
|
func getLocalImageCreationDate(ctx context.Context, cli *client.Client, imageName string) (time.Time, error) {
|
|
localImage, _, err := cli.ImageInspectWithRaw(ctx, imageName)
|
|
if err != nil {
|
|
return time.Time{}, err
|
|
}
|
|
localImageTime, err := time.Parse(time.RFC3339, localImage.Created)
|
|
if err != nil {
|
|
return time.Time{}, err
|
|
}
|
|
return localImageTime, nil
|
|
}
|
|
|
|
func checkImageStatus(ctx context.Context, cli *client.Client, repo, tag string) (bool, bool, error) {
|
|
const DefaultMessage = "test"
|
|
if common.Disconnected {
|
|
return false, true, nil
|
|
}
|
|
architecture := getArchitecture()
|
|
|
|
// Get the local image creation date
|
|
localImageTime, err := getLocalImageCreationDate(ctx, cli, fmt.Sprintf("%s:%s", repo, tag))
|
|
if err != nil {
|
|
return false, true, err
|
|
}
|
|
|
|
// Get the remote image creation date
|
|
remoteImageTime, err := getRemoteImageCreationDate(repo, tag, architecture)
|
|
if err != nil {
|
|
if err.Error() == "tag not found" {
|
|
return false, true, nil // Custom image if tag not found
|
|
}
|
|
return false, true, err
|
|
}
|
|
|
|
// Adjust the remote image creation time by an offset of 2 hours
|
|
remoteImageTimeAdjusted := remoteImageTime.Add(-2 * time.Hour)
|
|
|
|
// Compare local and adjusted remote image times
|
|
if localImageTime.Before(remoteImageTimeAdjusted) {
|
|
return false, false, nil // Obsolete
|
|
}
|
|
return true, false, nil // Up-to-date
|
|
}
|
|
|
|
func printContainerProperties(ctx context.Context, cli *client.Client, containerName string, props map[string]string, size string) {
|
|
white := "\033[37m"
|
|
blue := "\033[34m"
|
|
green := "\033[32m"
|
|
red := "\033[31m"
|
|
yellow := "\033[33m"
|
|
reset := "\033[0m"
|
|
|
|
// Determine if the image is up-to-date, obsolete, or custom
|
|
repo, tag := parseImageName(props["ImageName"])
|
|
isUpToDate, isCustom, err := checkImageStatus(ctx, cli, repo, tag)
|
|
if err != nil {
|
|
if err.Error() != "tag not found" {
|
|
log.Printf("Error checking image status: %v", err)
|
|
}
|
|
}
|
|
|
|
imageStatus := fmt.Sprintf("%s (Custom)", props["ImageName"])
|
|
if common.Disconnected {
|
|
imageStatus = fmt.Sprintf("%s (No network)", props["ImageName"])
|
|
}
|
|
imageStatusColor := yellow
|
|
if !isCustom {
|
|
if isUpToDate {
|
|
imageStatus = fmt.Sprintf("%s (Up to date)", props["ImageName"])
|
|
imageStatusColor = green
|
|
} else {
|
|
imageStatus = fmt.Sprintf("%s (Obsolete)", props["ImageName"])
|
|
imageStatusColor = red
|
|
}
|
|
}
|
|
|
|
seccompValue := props["Seccomp"]
|
|
if seccompValue == "" {
|
|
seccompValue = "(Default)"
|
|
}
|
|
|
|
properties := [][]string{
|
|
{"Container Name", containerName},
|
|
{"X Display", props["XDisplay"]},
|
|
{"Shell", props["Shell"]},
|
|
{"Privileged Mode", props["Privileged"]},
|
|
{"Network Mode", props["NetworkMode"]},
|
|
{"Exposed Ports", props["ExposedPorts"]},
|
|
{"Port Bindings", props["PortBindings"]},
|
|
{"Image Name", imageStatus},
|
|
{"Size on Disk", size},
|
|
{"Bindings", props["Bindings"]},
|
|
{"Extra Hosts", props["ExtraHosts"]},
|
|
{"Devices", props["Devices"]},
|
|
{"Capabilities", props["Caps"]},
|
|
{"Seccomp profile", seccompValue},
|
|
{"Cgroup rules", props["Cgroups"]},
|
|
}
|
|
|
|
width, _, err := terminal.GetSize(int(os.Stdout.Fd()))
|
|
if err != nil {
|
|
width = 80 // default width if terminal size cannot be determined
|
|
}
|
|
|
|
// Adjust width for table borders and padding
|
|
maxContentWidth := width - 4
|
|
if maxContentWidth < 20 {
|
|
maxContentWidth = 20 // Minimum content width
|
|
}
|
|
|
|
maxKeyLen := 0
|
|
for _, property := range properties {
|
|
if len(property[0]) > maxKeyLen {
|
|
maxKeyLen = len(property[0])
|
|
}
|
|
}
|
|
|
|
maxValueLen := maxContentWidth - maxKeyLen - 7 // 7 for borders and spaces
|
|
if maxValueLen < 10 {
|
|
maxValueLen = 10 // Minimum value length
|
|
}
|
|
|
|
totalWidth := maxKeyLen + maxValueLen + 7
|
|
|
|
// Print the title in blue, aligned to the left with some padding
|
|
title := "🧊 Container Summary"
|
|
leftPadding := 2 // You can adjust this value for more or less left padding
|
|
fmt.Printf("%s%s%s%s%s\n", blue, strings.Repeat(" ", leftPadding), title, strings.Repeat(" ", totalWidth-leftPadding-len(title)), reset)
|
|
|
|
fmt.Printf("%s", white) // Switch to white color for the box
|
|
fmt.Printf("╭%s╮\n", strings.Repeat("─", totalWidth-2))
|
|
|
|
for i, property := range properties {
|
|
key := property[0]
|
|
value := property[1]
|
|
valueColor := white
|
|
|
|
if key == "Image Name" {
|
|
valueColor = imageStatusColor
|
|
}
|
|
|
|
// Wrap long values
|
|
wrappedValue := wrapText(value, maxValueLen)
|
|
valueLines := strings.Split(wrappedValue, "\n")
|
|
|
|
for j, line := range valueLines {
|
|
if j == 0 {
|
|
fmt.Printf("│ %-*s │ %s%-*s%s │\n", maxKeyLen, key, valueColor, maxValueLen, line, reset)
|
|
} else {
|
|
fmt.Printf("│ %-*s │ %s%-*s%s │\n", maxKeyLen, "", valueColor, maxValueLen, line, reset)
|
|
}
|
|
|
|
if j < len(valueLines)-1 {
|
|
fmt.Printf("│%s│%s│\n", strings.Repeat(" ", maxKeyLen+2), strings.Repeat(" ", maxValueLen+2))
|
|
}
|
|
}
|
|
|
|
if i < len(properties)-1 {
|
|
fmt.Printf("├%s┼%s┤\n", strings.Repeat("─", maxKeyLen+2), strings.Repeat("─", maxValueLen+2))
|
|
}
|
|
}
|
|
|
|
fmt.Printf("╰%s╯\n", strings.Repeat("─", totalWidth-2))
|
|
fmt.Printf("%s", reset)
|
|
fmt.Println() // Ensure we end with a newline for clarity
|
|
}
|
|
|
|
func wrapText(text string, maxWidth int) string {
|
|
var result strings.Builder
|
|
currentLineWidth := 0
|
|
|
|
words := strings.Fields(text)
|
|
for i, word := range words {
|
|
if currentLineWidth+len(word) > maxWidth {
|
|
if currentLineWidth > 0 {
|
|
result.WriteString("\n")
|
|
currentLineWidth = 0
|
|
}
|
|
if len(word) > maxWidth {
|
|
for len(word) > maxWidth {
|
|
result.WriteString(word[:maxWidth] + "\n")
|
|
word = word[maxWidth:]
|
|
}
|
|
}
|
|
}
|
|
result.WriteString(word)
|
|
currentLineWidth += len(word)
|
|
if i < len(words)-1 && currentLineWidth+1+len(words[i+1]) <= maxWidth {
|
|
result.WriteString(" ")
|
|
currentLineWidth++
|
|
}
|
|
}
|
|
|
|
return result.String()
|
|
}
|
|
|
|
func DockerLast(ifilter string, labelKey string, labelValue string) {
|
|
ctx := context.Background()
|
|
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
defer cli.Close()
|
|
|
|
// Set up container filters for labels only
|
|
// We'll handle image ancestor and name/ID filtering manually
|
|
containerFilters := filters.NewArgs()
|
|
if labelKey != "" && labelValue != "" {
|
|
containerFilters.Add("label", fmt.Sprintf("%s=%s", labelKey, labelValue))
|
|
}
|
|
|
|
// Get container list
|
|
containers, err := cli.ContainerList(ctx, container.ListOptions{
|
|
All: true,
|
|
Limit: 15,
|
|
Filters: containerFilters,
|
|
})
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
// Create maps to store image mappings
|
|
imageIDToNames := make(map[string][]string)
|
|
hashToNames := make(map[string][]string)
|
|
|
|
// Get all images to build a mapping of image IDs to all their tags
|
|
images, err := cli.ImageList(ctx, image.ListOptions{All: true})
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
// Build image ID to names mapping
|
|
for _, img := range images {
|
|
shortID := img.ID[7:19] // Get a shortened version of the SHA256 hash
|
|
fullHash := img.ID[7:] // Remove "sha256:" prefix but keep full hash
|
|
|
|
// Store mappings if image has tags
|
|
if len(img.RepoTags) > 0 {
|
|
imageIDToNames[img.ID] = img.RepoTags
|
|
imageIDToNames[shortID] = img.RepoTags
|
|
hashToNames[fullHash] = img.RepoTags
|
|
}
|
|
}
|
|
|
|
//rfutils.ClearScreen()
|
|
tableData := [][]string{}
|
|
|
|
// Filter containers by image, name or ID (if ifilter is provided)
|
|
filteredContainers := []types.Container{}
|
|
|
|
if ifilter != "" {
|
|
lowerFilter := strings.ToLower(ifilter)
|
|
for _, container := range containers {
|
|
// Check if image name contains the filter (original behavior)
|
|
if strings.Contains(strings.ToLower(container.Image), lowerFilter) {
|
|
filteredContainers = append(filteredContainers, container)
|
|
continue
|
|
}
|
|
|
|
// Check if container ID (full or short) contains the filter
|
|
if strings.Contains(strings.ToLower(container.ID), lowerFilter) ||
|
|
strings.Contains(strings.ToLower(container.ID[:12]), lowerFilter) {
|
|
filteredContainers = append(filteredContainers, container)
|
|
continue
|
|
}
|
|
|
|
// Check if any container name contains the filter
|
|
for _, name := range container.Names {
|
|
// Remove leading slash from name if it exists
|
|
cleanName := name
|
|
if len(name) > 0 && name[0] == '/' {
|
|
cleanName = name[1:]
|
|
}
|
|
|
|
if strings.Contains(strings.ToLower(cleanName), lowerFilter) {
|
|
filteredContainers = append(filteredContainers, container)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
filteredContainers = containers
|
|
}
|
|
|
|
for _, container := range filteredContainers {
|
|
created := time.Unix(container.Created, 0).Format(time.RFC3339)
|
|
|
|
// Get the container image ID and associate with tags
|
|
containerImageID := container.ImageID
|
|
shortImageID := containerImageID[7:19] // shortened SHA256
|
|
|
|
// Get the display image name
|
|
imageTag := container.Image
|
|
|
|
// Check if this is a SHA256 hash
|
|
isSHA256 := strings.HasPrefix(imageTag, "sha256:")
|
|
|
|
// If this is a SHA256 hash, try to find a friendly name
|
|
if isSHA256 {
|
|
hashPart := imageTag[7:] // Remove "sha256:" prefix
|
|
// Check if we have a friendly name for this hash
|
|
if tags, ok := hashToNames[hashPart]; ok && len(tags) > 0 {
|
|
imageTag = tags[0] // Use the first tag
|
|
} else if tags, ok := imageIDToNames[containerImageID]; ok && len(tags) > 0 {
|
|
imageTag = tags[0] // Fallback to container image ID mapping
|
|
}
|
|
}
|
|
|
|
// Check if this is a renamed image (date format: -DDMMYYYY)
|
|
isRenamed := false
|
|
if len(imageTag) > 9 { // Make sure string is long enough before checking suffix
|
|
suffix := imageTag[len(imageTag)-9:]
|
|
if len(suffix) > 0 && suffix[0] == '-' {
|
|
// Check if the rest is a date format
|
|
datePattern := true
|
|
for i := 1; i < 9; i++ {
|
|
if i < 9 && (suffix[i] < '0' || suffix[i] > '9') {
|
|
datePattern = false
|
|
break
|
|
}
|
|
}
|
|
isRenamed = datePattern
|
|
}
|
|
}
|
|
|
|
// Prepare the display string
|
|
imageDisplay := imageTag
|
|
|
|
// For SHA256 or renamed images, show hash for clarity
|
|
if isSHA256 || isRenamed {
|
|
imageDisplay = fmt.Sprintf("%s (%s)", imageTag, shortImageID)
|
|
}
|
|
|
|
containerName := container.Names[0]
|
|
if containerName[0] == '/' {
|
|
containerName = containerName[1:]
|
|
}
|
|
containerID := container.ID[:12]
|
|
command := container.Command
|
|
|
|
// Truncate command if too long
|
|
if len(command) > 30 {
|
|
command = command[:27] + "..."
|
|
}
|
|
|
|
tableData = append(tableData, []string{
|
|
created,
|
|
imageDisplay,
|
|
containerName,
|
|
containerID,
|
|
command,
|
|
})
|
|
}
|
|
|
|
headers := []string{"Created", "Image Tag (ID)", "Container Name", "Container ID", "Command"}
|
|
width, _, err := terminal.GetSize(int(os.Stdout.Fd()))
|
|
if err != nil {
|
|
width = 80
|
|
}
|
|
|
|
// Calculate column widths
|
|
columnWidths := make([]int, len(headers))
|
|
for i, header := range headers {
|
|
columnWidths[i] = len(header)
|
|
}
|
|
for _, row := range tableData {
|
|
for i, col := range row {
|
|
if len(col) > columnWidths[i] {
|
|
columnWidths[i] = len(col)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Adjust column widths to fit terminal
|
|
totalWidth := len(headers) + 1
|
|
for _, w := range columnWidths {
|
|
totalWidth += w + 2
|
|
}
|
|
if totalWidth > width {
|
|
excess := totalWidth - width
|
|
for i := range columnWidths {
|
|
reduction := excess / len(columnWidths)
|
|
if columnWidths[i] > reduction {
|
|
columnWidths[i] -= reduction
|
|
excess -= reduction
|
|
}
|
|
}
|
|
totalWidth = width
|
|
}
|
|
|
|
// Print fancy table
|
|
pink := "\033[35m"
|
|
white := "\033[37m"
|
|
reset := "\033[0m"
|
|
title := "🤖 Last Run Containers"
|
|
fmt.Printf("%s%s%s%s%s\n", pink, strings.Repeat(" ", 2), title, strings.Repeat(" ", totalWidth-2-len(title)), reset)
|
|
fmt.Print(white)
|
|
printHorizontalBorder(columnWidths, "┌", "┬", "┐")
|
|
printRow(headers, columnWidths, "│")
|
|
printHorizontalBorder(columnWidths, "├", "┼", "┤")
|
|
for i, row := range tableData {
|
|
printRow(row, columnWidths, "│")
|
|
if i < len(tableData)-1 {
|
|
printHorizontalBorder(columnWidths, "├", "┼", "┤")
|
|
}
|
|
}
|
|
printHorizontalBorder(columnWidths, "└", "┴", "┘")
|
|
fmt.Print(reset)
|
|
fmt.Println()
|
|
}
|
|
|
|
func printHorizontalBorder(columnWidths []int, left, middle, right string) {
|
|
fmt.Print(left)
|
|
for i, width := range columnWidths {
|
|
fmt.Print(strings.Repeat("─", width+2))
|
|
if i < len(columnWidths)-1 {
|
|
fmt.Print(middle)
|
|
}
|
|
}
|
|
fmt.Println(right)
|
|
}
|
|
|
|
func printRow(row []string, columnWidths []int, separator string) {
|
|
fmt.Print(separator)
|
|
for i, col := range row {
|
|
fmt.Printf(" %-*s ", columnWidths[i], col)
|
|
fmt.Print(separator)
|
|
}
|
|
fmt.Println()
|
|
}
|
|
|
|
func distributeColumnWidths(availableWidth int, columnWidths []int) []int {
|
|
totalCurrentWidth := 0
|
|
for _, width := range columnWidths {
|
|
totalCurrentWidth += width
|
|
}
|
|
for i := range columnWidths {
|
|
columnWidths[i] = int(float64(columnWidths[i]) / float64(totalCurrentWidth) * float64(availableWidth))
|
|
if columnWidths[i] < 1 {
|
|
columnWidths[i] = 1
|
|
}
|
|
}
|
|
return columnWidths
|
|
}
|
|
|
|
func truncateString(s string, maxLength int) string {
|
|
if len(s) <= maxLength {
|
|
return s
|
|
}
|
|
return s[:maxLength-3] + "..."
|
|
}
|
|
|
|
func latestDockerID(labelKey string, labelValue string) string {
|
|
/* Get latest Docker container ID by image label
|
|
in(1): string label key
|
|
in(2): string label value
|
|
out: string container ID
|
|
*/
|
|
ctx := context.Background()
|
|
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
defer cli.Close()
|
|
|
|
// Filter containers by the specified image label
|
|
containerFilters := filters.NewArgs()
|
|
containerFilters.Add("label", fmt.Sprintf("%s=%s", labelKey, labelValue))
|
|
|
|
containers, err := cli.ContainerList(ctx, container.ListOptions{
|
|
All: true,
|
|
Filters: containerFilters,
|
|
})
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
var latestContainer types.Container
|
|
for _, container := range containers {
|
|
if latestContainer.ID == "" || container.Created > latestContainer.Created {
|
|
latestContainer = container
|
|
}
|
|
}
|
|
|
|
if latestContainer.ID == "" {
|
|
fmt.Println("No container found with the specified image label.")
|
|
return ""
|
|
}
|
|
|
|
return latestContainer.ID
|
|
}
|
|
|
|
func convertPortBindingsToString(portBindings nat.PortMap) string {
|
|
var result []string
|
|
|
|
for port, bindings := range portBindings {
|
|
for _, binding := range bindings {
|
|
// Format: HostIP:HostPort -> ContainerPort/Protocol
|
|
entry := fmt.Sprintf("%s:%s -> %s", binding.HostIP, binding.HostPort, port)
|
|
result = append(result, entry)
|
|
}
|
|
}
|
|
|
|
return strings.Join(result, ", ")
|
|
}
|
|
|
|
func convertExposedPortsToString(exposedPorts nat.PortSet) string {
|
|
var result []string
|
|
|
|
// Iterate through the PortSet (a map where keys are the exposed ports)
|
|
for port := range exposedPorts {
|
|
result = append(result, string(port)) // Convert the nat.Port to string
|
|
}
|
|
|
|
return strings.Join(result, ", ")
|
|
}
|
|
|
|
func convertDevicesToString(devices []container.DeviceMapping) string {
|
|
deviceStrings := make([]string, len(devices))
|
|
for i, device := range devices {
|
|
deviceStrings[i] = fmt.Sprintf("%s:%s", device.PathOnHost, device.PathInContainer)
|
|
}
|
|
return strings.Join(deviceStrings, ",")
|
|
}
|
|
|
|
func convertCapsToString(caps []string) string {
|
|
if len(caps) == 0 {
|
|
return ""
|
|
}
|
|
return strings.Join(caps, ",")
|
|
}
|
|
|
|
func convertSecurityOptToString(securityOpts []string) string {
|
|
if len(securityOpts) == 0 {
|
|
return ""
|
|
}
|
|
|
|
// Look specifically for seccomp profile
|
|
for _, opt := range securityOpts {
|
|
if strings.HasPrefix(opt, "seccomp=") {
|
|
// Extract just the profile value after "seccomp="
|
|
return strings.TrimPrefix(opt, "seccomp=")
|
|
}
|
|
}
|
|
|
|
// If no seccomp profile found, return empty string or join all options
|
|
return ""
|
|
}
|
|
|
|
func getContainerProperties(ctx context.Context, cli *client.Client, containerID string) (map[string]string, error) {
|
|
containerJSON, err := cli.ContainerInspect(ctx, containerID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Extract the DISPLAY environment variable value
|
|
var xdisplay string
|
|
for _, env := range containerJSON.Config.Env {
|
|
if strings.HasPrefix(env, "DISPLAY=") {
|
|
xdisplay = strings.TrimPrefix(env, "DISPLAY=")
|
|
break
|
|
}
|
|
}
|
|
|
|
// Get the image details to find the size
|
|
imageInfo, _, err := cli.ImageInspectWithRaw(ctx, containerJSON.Image)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
imageSize := fmt.Sprintf("%.2f MB", float64(imageInfo.Size)/1024/1024)
|
|
|
|
props := map[string]string{
|
|
"XDisplay": xdisplay,
|
|
"Shell": containerJSON.Path,
|
|
"Privileged": fmt.Sprintf("%v", containerJSON.HostConfig.Privileged),
|
|
"NetworkMode": string(containerJSON.HostConfig.NetworkMode),
|
|
"ExposedPorts": convertExposedPortsToString(containerJSON.Config.ExposedPorts),
|
|
"PortBindings": convertPortBindingsToString(containerJSON.HostConfig.PortBindings),
|
|
"ImageName": containerJSON.Config.Image,
|
|
"ImageHash": imageInfo.ID,
|
|
"Bindings": strings.Join(containerJSON.HostConfig.Binds, ","),
|
|
"ExtraHosts": strings.Join(containerJSON.HostConfig.ExtraHosts, ","),
|
|
"Size": imageSize,
|
|
"Devices": convertDevicesToString(containerJSON.HostConfig.Devices),
|
|
"Caps": convertCapsToString(containerJSON.HostConfig.CapAdd),
|
|
"Seccomp": convertSecurityOptToString(containerJSON.HostConfig.SecurityOpt),
|
|
"Cgroups": strings.Join(containerJSON.HostConfig.DeviceCgroupRules, ","),
|
|
}
|
|
|
|
return props, nil
|
|
}
|
|
|
|
func DockerExec(containerIdentifier string, WorkingDir string) {
|
|
ctx := context.Background()
|
|
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
|
|
if err != nil {
|
|
common.PrintErrorMessage(err)
|
|
return
|
|
}
|
|
defer cli.Close()
|
|
|
|
if containerIdentifier == "" {
|
|
labelKey := "org.container.project"
|
|
labelValue := "rfswift"
|
|
containerIdentifier = latestDockerID(labelKey, labelValue)
|
|
}
|
|
|
|
if err := cli.ContainerStart(ctx, containerIdentifier, container.StartOptions{}); err != nil {
|
|
common.PrintErrorMessage(err)
|
|
return
|
|
}
|
|
|
|
common.PrintSuccessMessage(fmt.Sprintf("Container '%s' started successfully", containerIdentifier))
|
|
|
|
// Get container properties and name
|
|
props, err := getContainerProperties(ctx, cli, containerIdentifier)
|
|
if err != nil {
|
|
common.PrintErrorMessage(err)
|
|
return
|
|
}
|
|
|
|
containerJSON, err := cli.ContainerInspect(ctx, containerIdentifier)
|
|
if err != nil {
|
|
common.PrintErrorMessage(err)
|
|
return
|
|
}
|
|
containerName := strings.TrimPrefix(containerJSON.Name, "/")
|
|
|
|
size := props["Size"]
|
|
printContainerProperties(ctx, cli, containerName, props, size)
|
|
|
|
// Create exec configuration
|
|
execConfig := container.ExecOptions{
|
|
AttachStdin: true,
|
|
AttachStdout: true,
|
|
AttachStderr: true,
|
|
Tty: true,
|
|
Cmd: []string{dockerObj.shell},
|
|
WorkingDir: WorkingDir,
|
|
}
|
|
|
|
// Create exec instance
|
|
execID, err := cli.ContainerExecCreate(ctx, containerIdentifier, execConfig)
|
|
if err != nil {
|
|
common.PrintErrorMessage(fmt.Errorf("failed to create exec instance: %v", err))
|
|
return
|
|
}
|
|
|
|
// Attach to the exec instance
|
|
attachResp, err := cli.ContainerExecAttach(ctx, execID.ID, container.ExecStartOptions{Tty: true})
|
|
if err != nil {
|
|
common.PrintErrorMessage(fmt.Errorf("failed to attach to exec instance: %v", err))
|
|
return
|
|
}
|
|
defer attachResp.Close()
|
|
|
|
// Setup raw terminal
|
|
inFd, inIsTerminal := term.GetFdInfo(os.Stdin)
|
|
outFd, outIsTerminal := term.GetFdInfo(os.Stdout)
|
|
|
|
if inIsTerminal {
|
|
state, err := term.SetRawTerminal(inFd)
|
|
if err != nil {
|
|
common.PrintErrorMessage(fmt.Errorf("failed to set raw terminal: %v", err))
|
|
return
|
|
}
|
|
defer term.RestoreTerminal(inFd, state)
|
|
}
|
|
|
|
// Start the exec instance
|
|
if err := cli.ContainerExecStart(ctx, execID.ID, container.ExecStartOptions{Tty: true}); err != nil {
|
|
common.PrintErrorMessage(fmt.Errorf("failed to start exec instance: %v", err))
|
|
return
|
|
}
|
|
|
|
// Handle terminal resize
|
|
go func() {
|
|
switch runtime.GOOS {
|
|
case "linux", "darwin":
|
|
sigchan := make(chan os.Signal, 1)
|
|
signal.Notify(sigchan, syscallsigwin())
|
|
defer signal.Stop(sigchan)
|
|
|
|
for range sigchan {
|
|
if outIsTerminal {
|
|
if size, err := term.GetWinsize(outFd); err == nil {
|
|
cli.ContainerExecResize(ctx, execID.ID, container.ResizeOptions{
|
|
Height: uint(size.Height),
|
|
Width: uint(size.Width),
|
|
})
|
|
}
|
|
}
|
|
}
|
|
case "windows":
|
|
ticker := time.NewTicker(500 * time.Millisecond)
|
|
defer ticker.Stop()
|
|
|
|
var lastHeight, lastWidth uint16
|
|
for range ticker.C {
|
|
if outIsTerminal {
|
|
if size, err := term.GetWinsize(outFd); err == nil {
|
|
if size.Height != lastHeight || size.Width != lastWidth {
|
|
cli.ContainerExecResize(ctx, execID.ID, container.ResizeOptions{
|
|
Height: uint(size.Height),
|
|
Width: uint(size.Width),
|
|
})
|
|
lastHeight = size.Height
|
|
lastWidth = size.Width
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}()
|
|
|
|
// Trigger initial resize
|
|
if outIsTerminal {
|
|
if size, err := term.GetWinsize(outFd); err == nil {
|
|
cli.ContainerExecResize(ctx, execID.ID, container.ResizeOptions{
|
|
Height: uint(size.Height),
|
|
Width: uint(size.Width),
|
|
})
|
|
}
|
|
}
|
|
|
|
// Handle I/O
|
|
outputDone := make(chan error)
|
|
go func() {
|
|
_, err := io.Copy(os.Stdout, attachResp.Reader)
|
|
outputDone <- err
|
|
}()
|
|
|
|
go func() {
|
|
if inIsTerminal {
|
|
io.Copy(attachResp.Conn, os.Stdin)
|
|
} else {
|
|
io.Copy(attachResp.Conn, os.Stdin)
|
|
}
|
|
attachResp.CloseWrite()
|
|
}()
|
|
|
|
select {
|
|
case err := <-outputDone:
|
|
if err != nil {
|
|
common.PrintErrorMessage(fmt.Errorf("error in output processing: %v", err))
|
|
}
|
|
}
|
|
|
|
common.PrintSuccessMessage(fmt.Sprintf("Shell session in container '%s' ended", containerName))
|
|
}
|
|
|
|
func ParseExposedPorts(exposedPortsStr string) nat.PortSet {
|
|
exposedPorts := nat.PortSet{}
|
|
|
|
if exposedPortsStr == "" {
|
|
return exposedPorts
|
|
}
|
|
|
|
// Split by commas to get individual ports
|
|
portEntries := strings.Split(exposedPortsStr, ",")
|
|
for _, entry := range portEntries {
|
|
port := strings.TrimSpace(entry) // Remove extra spaces
|
|
if port == "" {
|
|
continue
|
|
}
|
|
|
|
// Add to nat.PortSet (e.g., "80/tcp")
|
|
exposedPorts[nat.Port(port)] = struct{}{}
|
|
}
|
|
|
|
return exposedPorts
|
|
}
|
|
|
|
func ParseBindedPorts(bindedPortsStr string) nat.PortMap {
|
|
portBindings := nat.PortMap{}
|
|
|
|
if bindedPortsStr == "" || bindedPortsStr == "\"\"" {
|
|
return portBindings
|
|
}
|
|
common.PrintSuccessMessage(fmt.Sprintf("Binded: '%s'", bindedPortsStr))
|
|
|
|
// Split the input by ',' to get individual bindings
|
|
portEntries := strings.Split(bindedPortsStr, ",")
|
|
for _, entry := range portEntries {
|
|
// Expected format: containerPort[:hostAddress:]hostPort/protocol (e.g., 80:127.0.0.1:8080/tcp)
|
|
parts := strings.Split(entry, ":")
|
|
if len(parts) < 2 || len(parts) > 3 {
|
|
fmt.Printf("Invalid binded port format: %s (expected containerPort[:hostAddress:]hostPort/protocol)\n", entry)
|
|
continue
|
|
}
|
|
|
|
var containerPortProto, hostPort, hostAddress string
|
|
|
|
// Handle the optional hostAddress
|
|
if len(parts) == 3 {
|
|
containerPortProto = strings.TrimSpace(parts[0]) // e.g., 80
|
|
hostAddress = strings.TrimSpace(parts[1]) // e.g., 127.0.0.1
|
|
hostPort = strings.TrimSpace(parts[2]) // e.g., 8080/tcp
|
|
} else {
|
|
containerPortProto = strings.TrimSpace(parts[0]) // e.g., 80
|
|
hostPort = strings.TrimSpace(parts[1]) // e.g., 8080/tcp
|
|
}
|
|
|
|
// Split hostPort into hostPort and protocol
|
|
hostPortParts := strings.Split(hostPort, "/")
|
|
if len(hostPortParts) != 2 {
|
|
fmt.Printf("Invalid port format: %s (expected hostPort/protocol)\n", hostPort)
|
|
continue
|
|
}
|
|
|
|
hostPortValue := strings.TrimSpace(hostPortParts[0]) // e.g., 8080
|
|
protocol := strings.TrimSpace(hostPortParts[1]) // e.g., tcp
|
|
|
|
// Rearrange to containerPort/protocol (e.g., 80/tcp)
|
|
portKey := nat.Port(containerPortProto)
|
|
|
|
// Add the binding to the PortMap
|
|
portBindings[portKey] = append(portBindings[portKey], nat.PortBinding{
|
|
HostIP: hostAddress, // Optional host address
|
|
HostPort: fmt.Sprintf("%s/%s", hostPortValue, protocol),
|
|
})
|
|
}
|
|
|
|
return portBindings
|
|
}
|
|
|
|
func getDeviceMappingsFromString(devicesStr string) []container.DeviceMapping {
|
|
var devices []container.DeviceMapping
|
|
|
|
if devicesStr == "" {
|
|
return devices
|
|
}
|
|
|
|
devicesList := strings.Split(devicesStr, ",")
|
|
for _, deviceMapping := range devicesList {
|
|
parts := strings.Split(deviceMapping, ":")
|
|
if len(parts) == 2 {
|
|
devices = append(devices, container.DeviceMapping{
|
|
PathOnHost: parts[0],
|
|
PathInContainer: parts[1],
|
|
CgroupPermissions: "rwm",
|
|
})
|
|
}
|
|
}
|
|
|
|
return devices
|
|
}
|
|
|
|
func DockerRun(containerName string) {
|
|
/*
|
|
* Create a container with a specific name and run it
|
|
*/
|
|
ctx := context.Background()
|
|
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
|
|
if err != nil {
|
|
common.PrintErrorMessage(err)
|
|
return
|
|
}
|
|
defer cli.Close()
|
|
|
|
if !strings.Contains(dockerObj.imagename, ":") {
|
|
// Prepend Config.General.RepoTag if the format is missing
|
|
dockerObj.imagename = fmt.Sprintf("%s:%s", dockerObj.repotag, dockerObj.imagename)
|
|
}
|
|
|
|
bindings := combineBindings(dockerObj.x11forward, dockerObj.extrabinding)
|
|
extrahosts := splitAndCombine(dockerObj.extrahosts)
|
|
dockerenv := combineEnv(dockerObj.xdisplay, dockerObj.pulse_server, dockerObj.extraenv)
|
|
exposedPorts := ParseExposedPorts(dockerObj.exposed_ports)
|
|
bindedPorts := ParseBindedPorts(dockerObj.binded_ports)
|
|
|
|
// Prepare host config based on privileged flag
|
|
hostConfig := &container.HostConfig{
|
|
NetworkMode: container.NetworkMode(dockerObj.network_mode),
|
|
Binds: bindings,
|
|
ExtraHosts: extrahosts,
|
|
PortBindings: bindedPorts,
|
|
Privileged: dockerObj.privileged,
|
|
}
|
|
|
|
// If not in privileged mode, add device permissions
|
|
if !dockerObj.privileged {
|
|
devices := getDeviceMappingsFromString(dockerObj.devices)
|
|
|
|
if dockerObj.usbforward != "" {
|
|
parts := strings.Split(dockerObj.usbforward, ":")
|
|
if len(parts) == 2 {
|
|
devices = append(devices, container.DeviceMapping{
|
|
PathOnHost: parts[0],
|
|
PathInContainer: parts[1],
|
|
CgroupPermissions: "rwm",
|
|
})
|
|
}
|
|
}
|
|
|
|
// Update host config with device-specific permissions
|
|
hostConfig.Devices = devices
|
|
|
|
// adding cgroup rules
|
|
if dockerObj.cgroups != "" {
|
|
hostConfig.DeviceCgroupRules = strings.Split(dockerObj.cgroups, ",")
|
|
}
|
|
|
|
if dockerObj.seccomp != "" {
|
|
seccompOpts := strings.Split(dockerObj.seccomp, ",")
|
|
for i, opt := range seccompOpts {
|
|
// Make sure each option has the right format
|
|
if !strings.Contains(opt, "=") {
|
|
// If there's no equals sign, assume it's a seccomp value
|
|
seccompOpts[i] = "seccomp=" + opt
|
|
}
|
|
}
|
|
hostConfig.SecurityOpt = seccompOpts
|
|
}
|
|
if dockerObj.caps != "" {
|
|
hostConfig.CapAdd = strings.Split(dockerObj.caps, ",")
|
|
}
|
|
}
|
|
|
|
resp, err := cli.ContainerCreate(ctx, &container.Config{
|
|
Image: dockerObj.imagename,
|
|
Cmd: []string{dockerObj.shell},
|
|
Env: dockerenv,
|
|
ExposedPorts: exposedPorts,
|
|
OpenStdin: true,
|
|
StdinOnce: false,
|
|
AttachStdin: true,
|
|
AttachStdout: true,
|
|
AttachStderr: true,
|
|
Tty: true,
|
|
Labels: map[string]string{
|
|
"org.container.project": "rfswift",
|
|
},
|
|
}, hostConfig, &network.NetworkingConfig{}, nil, containerName)
|
|
|
|
if err != nil {
|
|
common.PrintErrorMessage(err)
|
|
return
|
|
}
|
|
|
|
waiter, err := cli.ContainerAttach(ctx, resp.ID, container.AttachOptions{
|
|
Stderr: true,
|
|
Stdout: true,
|
|
Stdin: true,
|
|
Stream: true,
|
|
})
|
|
if err != nil {
|
|
common.PrintErrorMessage(err)
|
|
return
|
|
}
|
|
defer waiter.Close()
|
|
|
|
if err := cli.ContainerStart(ctx, resp.ID, container.StartOptions{}); err != nil {
|
|
common.PrintErrorMessage(err)
|
|
return
|
|
}
|
|
|
|
props, err := getContainerProperties(ctx, cli, resp.ID)
|
|
if err != nil {
|
|
common.PrintErrorMessage(err)
|
|
return
|
|
}
|
|
size := props["Size"]
|
|
printContainerProperties(ctx, cli, containerName, props, size)
|
|
common.PrintSuccessMessage(fmt.Sprintf("Container '%s' started successfully", containerName))
|
|
|
|
handleIOStreams(waiter)
|
|
fd := int(os.Stdin.Fd())
|
|
if terminal.IsTerminal(fd) {
|
|
oldState, err := terminal.MakeRaw(fd)
|
|
if err != nil {
|
|
common.PrintErrorMessage(err)
|
|
return
|
|
}
|
|
defer terminal.Restore(fd, oldState)
|
|
go resizeTty(ctx, cli, resp.ID, fd)
|
|
go readAndWriteInput(waiter)
|
|
}
|
|
|
|
waitForContainer(ctx, cli, resp.ID)
|
|
}
|
|
|
|
func execCommandInContainer(ctx context.Context, cli *client.Client, contid, WorkingDir string) {
|
|
execShell := []string{}
|
|
if dockerObj.shell != "" {
|
|
execShell = append(execShell, strings.Split(dockerObj.shell, " ")...)
|
|
}
|
|
|
|
optionsCreate := container.ExecOptions{
|
|
WorkingDir: WorkingDir,
|
|
AttachStdin: true,
|
|
AttachStdout: true,
|
|
AttachStderr: true,
|
|
Detach: false,
|
|
Privileged: true,
|
|
Tty: true,
|
|
Cmd: execShell,
|
|
}
|
|
|
|
rst, err := cli.ContainerExecCreate(ctx, contid, optionsCreate)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
optionsStartCheck := container.ExecStartOptions{
|
|
Detach: false,
|
|
Tty: true,
|
|
}
|
|
|
|
response, err := cli.ContainerExecAttach(ctx, rst.ID, optionsStartCheck)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
defer response.Close()
|
|
|
|
handleIOStreams(response)
|
|
waitForContainer(ctx, cli, contid)
|
|
}
|
|
|
|
func attachAndInteract(ctx context.Context, cli *client.Client, contid string) {
|
|
response, err := cli.ContainerAttach(ctx, contid, container.AttachOptions{
|
|
Stderr: true,
|
|
Stdout: true,
|
|
Stdin: true,
|
|
Stream: true,
|
|
})
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
defer response.Close()
|
|
|
|
handleIOStreams(response)
|
|
|
|
fd := int(os.Stdin.Fd())
|
|
if terminal.IsTerminal(fd) {
|
|
oldState, err := terminal.MakeRaw(fd)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
defer terminal.Restore(fd, oldState)
|
|
|
|
go resizeTty(ctx, cli, contid, fd)
|
|
go readAndWriteInput(response)
|
|
}
|
|
|
|
waitForContainer(ctx, cli, contid)
|
|
}
|
|
|
|
func handleIOStreams(response types.HijackedResponse) {
|
|
go io.Copy(os.Stdout, response.Reader)
|
|
go io.Copy(os.Stderr, response.Reader)
|
|
go io.Copy(response.Conn, os.Stdin)
|
|
}
|
|
|
|
func readAndWriteInput(response types.HijackedResponse) {
|
|
reader := bufio.NewReaderSize(os.Stdin, 4096) // Increased buffer size for larger inputs
|
|
for {
|
|
input, err := reader.ReadByte()
|
|
if err != nil {
|
|
return
|
|
}
|
|
response.Conn.Write([]byte{input})
|
|
}
|
|
}
|
|
|
|
func waitForContainer(ctx context.Context, cli *client.Client, contid string) {
|
|
statusCh, errCh := cli.ContainerWait(ctx, contid, container.WaitConditionNextExit)
|
|
select {
|
|
case err := <-errCh:
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
case <-statusCh:
|
|
}
|
|
}
|
|
|
|
func combineBindings(x11forward, extrabinding string) []string {
|
|
var bindings []string
|
|
|
|
if extrabinding != "" {
|
|
bindings = append(bindings, strings.Split(extrabinding, ",")...)
|
|
}
|
|
if x11forward != "" {
|
|
bindings = append(bindings, strings.Split(x11forward, ",")...)
|
|
}
|
|
return bindings
|
|
}
|
|
|
|
func splitAndCombine(commaSeparated string) []string {
|
|
if commaSeparated == "" {
|
|
return []string{}
|
|
}
|
|
return strings.Split(commaSeparated, ",")
|
|
}
|
|
|
|
func combineEnv(xdisplay, pulse_server, extraenv string) []string {
|
|
dockerenv := append(strings.Split(xdisplay, ","), "PULSE_SERVER="+pulse_server)
|
|
if extraenv != "" {
|
|
dockerenv = append(dockerenv, strings.Split(extraenv, ",")...)
|
|
}
|
|
return dockerenv
|
|
}
|
|
|
|
func DockerCommit(contid string) {
|
|
ctx := context.Background()
|
|
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
defer cli.Close()
|
|
|
|
if err := cli.ContainerStart(ctx, contid, container.StartOptions{}); err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
commitResp, err := cli.ContainerCommit(ctx, contid, container.CommitOptions{Reference: dockerObj.imagename})
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
fmt.Println(commitResp.ID)
|
|
}
|
|
|
|
func DockerPull(imageref string, imagetag string) {
|
|
ctx := context.Background()
|
|
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
|
|
if err != nil {
|
|
common.PrintErrorMessage(err)
|
|
return
|
|
}
|
|
defer cli.Close()
|
|
|
|
if !strings.Contains(imageref, ":") {
|
|
imageref = fmt.Sprintf("%s:%s", dockerObj.repotag, imageref)
|
|
}
|
|
if imagetag == "" {
|
|
imagetag = imageref
|
|
}
|
|
|
|
// Check if the image exists locally
|
|
localInspect, _, err := cli.ImageInspectWithRaw(ctx, imageref)
|
|
localExists := err == nil
|
|
localDigest := ""
|
|
if localExists {
|
|
localDigest = localInspect.ID
|
|
}
|
|
|
|
// Pull the image from remote
|
|
common.PrintInfoMessage(fmt.Sprintf("Pulling image from: %s", imageref))
|
|
out, err := cli.ImagePull(ctx, imageref, image.PullOptions{})
|
|
if err != nil {
|
|
common.PrintErrorMessage(err)
|
|
return
|
|
}
|
|
defer out.Close()
|
|
|
|
// Process pull output
|
|
fd, isTerminal := term.GetFdInfo(os.Stdout)
|
|
jsonDecoder := json.NewDecoder(out)
|
|
for {
|
|
var msg jsonmessage.JSONMessage
|
|
if err := jsonDecoder.Decode(&msg); err == io.EOF {
|
|
break
|
|
} else if err != nil {
|
|
common.PrintErrorMessage(err)
|
|
return
|
|
}
|
|
if isTerminal {
|
|
_ = jsonmessage.DisplayJSONMessagesStream(out, os.Stdout, fd, isTerminal, nil)
|
|
} else {
|
|
fmt.Println(msg)
|
|
}
|
|
}
|
|
|
|
// Get information about the pulled image
|
|
remoteInspect, _, err := cli.ImageInspectWithRaw(ctx, imageref)
|
|
if err != nil {
|
|
common.PrintErrorMessage(err)
|
|
return
|
|
}
|
|
|
|
// Compare local and remote images
|
|
if localExists && localDigest != remoteInspect.ID {
|
|
common.PrintInfoMessage("The pulled image is different from the local one.")
|
|
reader := bufio.NewReader(os.Stdin)
|
|
fmt.Print("Do you want to rename the old image with a date tag? (y/n): ")
|
|
response, _ := reader.ReadString('\n')
|
|
response = strings.TrimSpace(strings.ToLower(response))
|
|
|
|
if response == "y" || response == "yes" {
|
|
currentTime := time.Now()
|
|
dateTag := fmt.Sprintf("%s-%02d%02d%d", imagetag, currentTime.Day(), currentTime.Month(), currentTime.Year())
|
|
err = cli.ImageTag(ctx, localDigest, dateTag)
|
|
if err != nil {
|
|
common.PrintErrorMessage(err)
|
|
return
|
|
}
|
|
common.PrintSuccessMessage(fmt.Sprintf("Old image '%s' retagged as '%s'", imagetag, dateTag))
|
|
}
|
|
}
|
|
|
|
// Tag the new image if needed
|
|
if imagetag != imageref {
|
|
err = cli.ImageTag(ctx, imageref, imagetag)
|
|
if err != nil {
|
|
common.PrintErrorMessage(err)
|
|
return
|
|
}
|
|
common.PrintSuccessMessage(fmt.Sprintf("Image tagged as '%s'", imagetag))
|
|
}
|
|
|
|
common.PrintSuccessMessage(fmt.Sprintf("Image '%s' installed successfully", imagetag))
|
|
}
|
|
|
|
func DockerTag(imageref string, imagetag string) {
|
|
/* Rename an image to another name
|
|
in(1): string Image reference
|
|
in(2): string Image tag target
|
|
*/
|
|
ctx := context.Background()
|
|
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
defer cli.Close()
|
|
|
|
err = cli.ImageTag(ctx, imageref, imagetag)
|
|
if err != nil {
|
|
panic(err)
|
|
} else {
|
|
fmt.Println("[+] Image renamed!")
|
|
}
|
|
}
|
|
|
|
func DockerRename(currentIdentifier string, newName string) {
|
|
/* Rename a container by ID or name
|
|
in(1): string current container ID or name
|
|
in(2): string new container name
|
|
*/
|
|
ctx := context.Background()
|
|
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
defer cli.Close()
|
|
|
|
// Attempt to find the container by the identifier (name or ID)
|
|
containers, err := cli.ContainerList(ctx, container.ListOptions{All: true})
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
var containerID string
|
|
for _, container := range containers {
|
|
if container.ID == currentIdentifier || container.Names[0] == "/"+currentIdentifier {
|
|
containerID = container.ID
|
|
break
|
|
}
|
|
}
|
|
|
|
if containerID == "" {
|
|
log.Fatalf("Container with ID or name '%s' not found.", currentIdentifier)
|
|
}
|
|
|
|
// Rename the container
|
|
err = cli.ContainerRename(ctx, containerID, newName)
|
|
if err != nil {
|
|
panic(err)
|
|
} else {
|
|
fmt.Printf("[+] Container '%s' renamed to '%s'!\n", currentIdentifier, newName)
|
|
}
|
|
}
|
|
|
|
func DockerRemove(containerIdentifier string) {
|
|
/* Remove a container by ID or name
|
|
in(1): string container ID or name
|
|
*/
|
|
ctx := context.Background()
|
|
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
|
|
if err != nil {
|
|
common.PrintErrorMessage(err)
|
|
return
|
|
}
|
|
defer cli.Close()
|
|
|
|
// Attempt to find the container by the identifier (name or ID)
|
|
containers, err := cli.ContainerList(ctx, container.ListOptions{All: true})
|
|
if err != nil {
|
|
common.PrintErrorMessage(err)
|
|
return
|
|
}
|
|
|
|
var containerID string
|
|
for _, container := range containers {
|
|
if container.ID == containerIdentifier || container.Names[0] == "/"+containerIdentifier {
|
|
containerID = container.ID
|
|
break
|
|
}
|
|
}
|
|
|
|
if containerID == "" {
|
|
common.PrintErrorMessage(fmt.Errorf("container with ID or name '%s' not found", containerIdentifier))
|
|
return
|
|
}
|
|
|
|
// Remove the container
|
|
err = cli.ContainerRemove(ctx, containerID, container.RemoveOptions{Force: true})
|
|
if err != nil {
|
|
common.PrintErrorMessage(err)
|
|
} else {
|
|
common.PrintSuccessMessage(fmt.Sprintf("Container '%s' removed successfully", containerIdentifier))
|
|
}
|
|
}
|
|
|
|
func ListImages(labelKey string, labelValue string) ([]image.Summary, error) {
|
|
/* List RF Swift Images
|
|
in(1): string labelKey
|
|
in(2): string labelValue
|
|
out: Tuple ImageSummary, error
|
|
*/
|
|
ctx := context.Background()
|
|
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer cli.Close()
|
|
|
|
// Filter images by the specified image label
|
|
imagesFilters := filters.NewArgs()
|
|
imagesFilters.Add("label", fmt.Sprintf("%s=%s", labelKey, labelValue))
|
|
|
|
images, err := cli.ImageList(ctx, image.ListOptions{
|
|
All: true,
|
|
Filters: imagesFilters,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Only display images with RepoTags
|
|
var filteredImages []image.Summary
|
|
for _, image := range images {
|
|
if len(image.RepoTags) > 0 {
|
|
filteredImages = append(filteredImages, image)
|
|
}
|
|
}
|
|
|
|
return filteredImages, nil
|
|
}
|
|
|
|
func PrintImagesTable(labelKey string, labelValue string) {
|
|
ctx := context.Background()
|
|
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
|
|
if err != nil {
|
|
log.Fatalf("Error creating Docker client: %v", err)
|
|
}
|
|
defer cli.Close()
|
|
|
|
images, err := ListImages(labelKey, labelValue)
|
|
if err != nil {
|
|
log.Fatalf("Error listing images: %v", err)
|
|
}
|
|
|
|
rfutils.ClearScreen()
|
|
|
|
// Prepare table data
|
|
tableData := [][]string{}
|
|
maxStatusLength := 0
|
|
for _, image := range images {
|
|
for _, repoTag := range image.RepoTags {
|
|
repoTagParts := strings.Split(repoTag, ":")
|
|
repository := repoTagParts[0]
|
|
tag := repoTagParts[1]
|
|
|
|
// Check image status
|
|
isUpToDate, isCustom, err := checkImageStatus(ctx, cli, repository, tag)
|
|
var status string
|
|
if err != nil {
|
|
status = "Error"
|
|
} else if isCustom {
|
|
status = "Custom"
|
|
if common.Disconnected {
|
|
status = "No network"
|
|
}
|
|
} else if isUpToDate {
|
|
status = "Up to date"
|
|
} else {
|
|
status = "Obsolete"
|
|
}
|
|
|
|
if len(status) > maxStatusLength {
|
|
maxStatusLength = len(status)
|
|
}
|
|
|
|
created := time.Unix(image.Created, 0).Format(time.RFC3339)
|
|
size := fmt.Sprintf("%.2f MB", float64(image.Size)/1024/1024)
|
|
|
|
tableData = append(tableData, []string{
|
|
repository,
|
|
tag,
|
|
image.ID[:12],
|
|
created,
|
|
size,
|
|
status,
|
|
})
|
|
}
|
|
}
|
|
|
|
headers := []string{"Repository", "Tag", "Image ID", "Created", "Size", "Status"}
|
|
width, _, err := terminal.GetSize(int(os.Stdout.Fd()))
|
|
if err != nil {
|
|
width = 80 // default width if terminal size cannot be determined
|
|
}
|
|
|
|
columnWidths := make([]int, len(headers))
|
|
for i, header := range headers {
|
|
columnWidths[i] = len(header)
|
|
}
|
|
for _, row := range tableData {
|
|
for i, col := range row {
|
|
if len(col) > columnWidths[i] {
|
|
columnWidths[i] = len(col)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Ensure the "Status" column is wide enough
|
|
columnWidths[len(columnWidths)-1] = max(columnWidths[len(columnWidths)-1], maxStatusLength)
|
|
|
|
// Adjust column widths to fit the terminal width
|
|
totalWidth := len(headers) + 1 // Adding 1 for the left border
|
|
for _, w := range columnWidths {
|
|
totalWidth += w + 2 // Adding 2 for padding
|
|
}
|
|
|
|
if totalWidth > width {
|
|
excess := totalWidth - width
|
|
for i := range columnWidths[:len(columnWidths)-1] { // Don't reduce the last (Status) column
|
|
reduction := excess / (len(columnWidths) - 1)
|
|
if columnWidths[i] > reduction {
|
|
columnWidths[i] -= reduction
|
|
excess -= reduction
|
|
}
|
|
}
|
|
totalWidth = width
|
|
}
|
|
|
|
yellow := "\033[33m"
|
|
white := "\033[37m"
|
|
reset := "\033[0m"
|
|
title := "📦 RF Swift Images"
|
|
|
|
fmt.Printf("%s%s%s%s%s\n", yellow, strings.Repeat(" ", 2), title, strings.Repeat(" ", totalWidth-2-len(title)), reset)
|
|
fmt.Print(white)
|
|
|
|
printHorizontalBorder(columnWidths, "┌", "┬", "┐")
|
|
printRow(headers, columnWidths, "│")
|
|
printHorizontalBorder(columnWidths, "├", "┼", "┤")
|
|
|
|
for i, row := range tableData {
|
|
printRowWithColor(row, columnWidths, "│")
|
|
if i < len(tableData)-1 {
|
|
printHorizontalBorder(columnWidths, "├", "┼", "┤")
|
|
}
|
|
}
|
|
|
|
printHorizontalBorder(columnWidths, "└", "┴", "┘")
|
|
|
|
fmt.Print(reset)
|
|
fmt.Println()
|
|
}
|
|
|
|
func printRowWithColor(row []string, columnWidths []int, separator string) {
|
|
fmt.Print(separator)
|
|
for i, col := range row {
|
|
if i == len(row)-1 { // Status column
|
|
status := col
|
|
color := ""
|
|
switch status {
|
|
case "Custom":
|
|
color = "\033[33m" // Yellow
|
|
case "Up to date":
|
|
color = "\033[32m" // Green
|
|
case "Obsolete":
|
|
color = "\033[31m" // Red
|
|
case "Error":
|
|
color = "\033[31m" // Red
|
|
}
|
|
fmt.Printf(" %s%-*s\033[0m ", color, columnWidths[i], status)
|
|
} else {
|
|
fmt.Printf(" %-*s ", columnWidths[i], truncateString(col, columnWidths[i]))
|
|
}
|
|
fmt.Print(separator)
|
|
}
|
|
fmt.Println()
|
|
}
|
|
|
|
func stripAnsiCodes(s string) string {
|
|
ansi := regexp.MustCompile(`\x1b\[[0-9;]*m`)
|
|
return ansi.ReplaceAllString(s, "")
|
|
}
|
|
|
|
func DeleteImage(imageIDOrTag string) error {
|
|
ctx := context.Background()
|
|
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
|
|
if err != nil {
|
|
common.PrintErrorMessage(fmt.Errorf("failed to create Docker client: %v", err))
|
|
return err
|
|
}
|
|
defer cli.Close()
|
|
|
|
common.PrintInfoMessage(fmt.Sprintf("Attempting to delete image: %s", imageIDOrTag))
|
|
|
|
// List all images
|
|
images, err := cli.ImageList(ctx, image.ListOptions{All: true})
|
|
if err != nil {
|
|
common.PrintErrorMessage(fmt.Errorf("failed to list images: %v", err))
|
|
return err
|
|
}
|
|
|
|
var imageToDelete image.Summary
|
|
imageFound := false
|
|
|
|
for _, img := range images {
|
|
// Check if the full image ID matches
|
|
if img.ID == "sha256:"+imageIDOrTag || img.ID == imageIDOrTag {
|
|
imageToDelete = img
|
|
imageFound = true
|
|
break
|
|
}
|
|
|
|
// Check if any RepoTags match exactly
|
|
for _, tag := range img.RepoTags {
|
|
if !strings.Contains(tag, ":") {
|
|
// Prepend Config.General.RepoTag if the format is missing
|
|
tag = fmt.Sprintf("%s:%s", dockerObj.repotag, tag)
|
|
}
|
|
if tag == imageIDOrTag {
|
|
imageToDelete = img
|
|
imageFound = true
|
|
break
|
|
}
|
|
}
|
|
|
|
// If image is found by tag, break the outer loop
|
|
if imageFound {
|
|
break
|
|
}
|
|
}
|
|
|
|
if !imageFound {
|
|
common.PrintErrorMessage(fmt.Errorf("image not found: %s", imageIDOrTag))
|
|
common.PrintInfoMessage("Available images:")
|
|
for _, img := range images {
|
|
common.PrintInfoMessage(fmt.Sprintf("ID: %s, Tags: %v", strings.TrimPrefix(img.ID, "sha256:"), img.RepoTags))
|
|
}
|
|
return fmt.Errorf("image not found: %s", imageIDOrTag)
|
|
}
|
|
|
|
imageID := imageToDelete.ID
|
|
common.PrintInfoMessage(fmt.Sprintf("Found image to delete: ID: %s, Tags: %v", strings.TrimPrefix(imageID, "sha256:"), imageToDelete.RepoTags))
|
|
|
|
// Ask for user confirmation
|
|
reader := bufio.NewReader(os.Stdin)
|
|
common.PrintWarningMessage(fmt.Sprintf("Are you sure you want to delete this image? (y/n): "))
|
|
response, err := reader.ReadString('\n')
|
|
if err != nil {
|
|
common.PrintErrorMessage(fmt.Errorf("failed to read user input: %v", err))
|
|
return err
|
|
}
|
|
response = strings.ToLower(strings.TrimSpace(response))
|
|
if response != "y" && response != "yes" {
|
|
common.PrintInfoMessage("Image deletion cancelled by user.")
|
|
return nil
|
|
}
|
|
|
|
// Find and remove containers using the image
|
|
containers, err := cli.ContainerList(ctx, container.ListOptions{All: true})
|
|
if err != nil {
|
|
common.PrintErrorMessage(fmt.Errorf("failed to list containers: %v", err))
|
|
return err
|
|
}
|
|
|
|
for _, icontainer := range containers {
|
|
if icontainer.ImageID == imageID {
|
|
common.PrintWarningMessage(fmt.Sprintf("Removing container: %s", icontainer.ID[:12]))
|
|
if err := cli.ContainerRemove(ctx, icontainer.ID, container.RemoveOptions{Force: true}); err != nil {
|
|
common.PrintWarningMessage(fmt.Sprintf("Failed to remove container %s: %v", icontainer.ID[:12], err))
|
|
}
|
|
}
|
|
}
|
|
|
|
// Attempt to delete the image
|
|
_, err = cli.ImageRemove(ctx, imageID, image.RemoveOptions{Force: true, PruneChildren: true})
|
|
if err != nil {
|
|
common.PrintErrorMessage(fmt.Errorf("failed to delete image %s: %v", imageIDOrTag, err))
|
|
return err
|
|
}
|
|
|
|
common.PrintSuccessMessage(fmt.Sprintf("Successfully deleted image: %s", imageIDOrTag))
|
|
return nil
|
|
}
|
|
|
|
func DockerInstallScript(containerIdentifier, scriptName, functionScript string) error {
|
|
ctx := context.Background()
|
|
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create Docker client: %v", err)
|
|
}
|
|
defer cli.Close()
|
|
|
|
// Check if the container is running; if not, start it
|
|
containerJSON, err := cli.ContainerInspect(ctx, containerIdentifier)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to inspect container: %v", err)
|
|
}
|
|
|
|
if containerJSON.State.Status != "running" {
|
|
if err := cli.ContainerStart(ctx, containerIdentifier, container.StartOptions{}); err != nil {
|
|
return fmt.Errorf("failed to start container: %v", err)
|
|
}
|
|
}
|
|
|
|
// Step 1: Run "apt update" with clock-based loading indicator
|
|
common.PrintInfoMessage("Running 'apt update'...")
|
|
if err := showLoadingIndicator(ctx, func() error {
|
|
return execCommand(ctx, cli, containerIdentifier, []string{"/bin/bash", "-c", "apt update"})
|
|
}, "apt update"); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Step 2: Run "apt --fix-broken install" with clock-based loading indicator
|
|
common.PrintInfoMessage("Running 'apt --fix-broken install'...")
|
|
if err := showLoadingIndicator(ctx, func() error {
|
|
return execCommand(ctx, cli, containerIdentifier, []string{"/bin/bash", "-c", "apt --fix-broken install -y"})
|
|
}, "apt --fix-broken install"); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Step 3: Run the provided script with clock-based loading indicator
|
|
common.PrintInfoMessage(fmt.Sprintf("Running script './%s %s'...", scriptName, functionScript))
|
|
if err := showLoadingIndicator(ctx, func() error {
|
|
return execCommand(ctx, cli, containerIdentifier, []string{"/bin/bash", "-c", fmt.Sprintf("./%s %s", scriptName, functionScript)}, "/root/scripts")
|
|
}, fmt.Sprintf("script './%s %s'", scriptName, functionScript)); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// execCommand executes a command in the container, capturing only errors if any
|
|
func execCommand(ctx context.Context, cli *client.Client, containerID string, cmd []string, workingDir ...string) error {
|
|
execConfig := container.ExecOptions{
|
|
AttachStdout: true,
|
|
AttachStderr: true,
|
|
Cmd: cmd,
|
|
}
|
|
|
|
// Optional working directory
|
|
if len(workingDir) > 0 {
|
|
execConfig.WorkingDir = workingDir[0]
|
|
}
|
|
|
|
execID, err := cli.ContainerExecCreate(ctx, containerID, execConfig)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create exec instance: %v", err)
|
|
}
|
|
|
|
attachResp, err := cli.ContainerExecAttach(ctx, execID.ID, container.ExecStartOptions{})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to attach to exec instance: %v", err)
|
|
}
|
|
defer attachResp.Close()
|
|
|
|
// Capture only error messages, suppressing standard output
|
|
_, err = io.Copy(io.Discard, attachResp.Reader)
|
|
return err
|
|
}
|
|
|
|
// showLoadingIndicator displays a loading animation with a rotating clock icon while the command runs
|
|
func showLoadingIndicator(ctx context.Context, commandFunc func() error, stepName string) error {
|
|
done := make(chan error)
|
|
go func() {
|
|
done <- commandFunc()
|
|
}()
|
|
|
|
// Clock emojis to create the rotating clock animation
|
|
clockEmojis := []string{"🕛", "🕐", "🕑", "🕒", "🕓", "🕔", "🕕", "🕖", "🕗", "🕘", "🕙", "🕚"}
|
|
i := 0
|
|
ticker := time.NewTicker(500 * time.Millisecond)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case err := <-done:
|
|
if err != nil {
|
|
common.PrintErrorMessage(fmt.Errorf("Error during %s: %v", stepName, err))
|
|
return err
|
|
}
|
|
fmt.Printf("\n")
|
|
common.PrintSuccessMessage(fmt.Sprintf("%s completed", stepName))
|
|
return nil
|
|
case <-ticker.C:
|
|
fmt.Printf("\r%s %s", clockEmojis[i%len(clockEmojis)], stepName)
|
|
i++
|
|
}
|
|
}
|
|
}
|
|
|
|
func UpdateMountBinding(containerName string, source string, target string, add bool) {
|
|
var timeout = 10 // Stop timeout
|
|
|
|
// Check if the system is Windows
|
|
if runtime.GOOS == "windows" {
|
|
title := "Unsupported on Windows"
|
|
message := `This function is not supported on Windows.
|
|
However, you can achieve similar functionality by using the following commands:
|
|
- "rfswift commit" to create a new image with a new tag.
|
|
- "rfswift remove" to remove the existing container.
|
|
- "rfswift run" to run a container with new bindings.`
|
|
|
|
rfutils.DisplayNotification(title, message, "warning")
|
|
os.Exit(1) // Exit since this function is not supported on Windows
|
|
}
|
|
|
|
if source == "" {
|
|
source = target
|
|
common.PrintWarningMessage(fmt.Sprintf("Source is empty. Defaulting source to target: %s", target))
|
|
}
|
|
|
|
ctx := context.Background()
|
|
|
|
common.PrintInfoMessage("Fetching container ID...")
|
|
containerID := getContainerIDByName(ctx, containerName)
|
|
if containerID == "" {
|
|
common.PrintErrorMessage(fmt.Errorf("container %s not found", containerName))
|
|
os.Exit(1)
|
|
}
|
|
common.PrintSuccessMessage(fmt.Sprintf("Container ID: %s", containerID))
|
|
|
|
// Stop the container
|
|
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
|
|
if err != nil {
|
|
common.PrintErrorMessage(fmt.Errorf("Error when instantiating a client"))
|
|
os.Exit(1)
|
|
}
|
|
common.PrintInfoMessage("Stopping the container...")
|
|
|
|
// Attempt graceful stop
|
|
if err := showLoadingIndicator(ctx, func() error {
|
|
return cli.ContainerStop(ctx, containerID, container.StopOptions{Timeout: &timeout})
|
|
}, "Stopping the container..."); err != nil {
|
|
common.PrintErrorMessage(fmt.Errorf("Failed to stop the container gracefully: %v", err))
|
|
os.Exit(1)
|
|
}
|
|
|
|
// Check if the container is still running
|
|
containerJSON, err := cli.ContainerInspect(ctx, containerID)
|
|
if err != nil {
|
|
common.PrintErrorMessage(fmt.Errorf("Error inspecting container: %v", err))
|
|
os.Exit(1)
|
|
}
|
|
if containerJSON.State.Running {
|
|
common.PrintWarningMessage("Container is still running. Forcing stop...")
|
|
err = cli.ContainerKill(ctx, containerID, "SIGKILL")
|
|
if err != nil {
|
|
common.PrintErrorMessage(fmt.Errorf("Failed to force stop the container: %v", err))
|
|
os.Exit(1)
|
|
}
|
|
common.PrintSuccessMessage("Container forcibly stopped.")
|
|
} else {
|
|
common.PrintSuccessMessage(fmt.Sprintf("Container '%s' stopped", containerID))
|
|
}
|
|
|
|
// Load and update hostconfig.json
|
|
common.PrintInfoMessage("Determining hostconfig.json path...")
|
|
hostConfigPath, err := GetHostConfigPath(containerID)
|
|
if err != nil {
|
|
common.PrintErrorMessage(err)
|
|
os.Exit(1)
|
|
}
|
|
common.PrintSuccessMessage(fmt.Sprintf("HostConfig path: %s", hostConfigPath))
|
|
|
|
common.PrintInfoMessage("Loading hostconfig.json...")
|
|
var hostConfig HostConfigFull
|
|
if err := loadJSON(hostConfigPath, &hostConfig); err != nil {
|
|
common.PrintErrorMessage(fmt.Errorf("failed to load hostconfig.json: %v", err))
|
|
os.Exit(1)
|
|
}
|
|
common.PrintSuccessMessage("HostConfig loaded successfully.")
|
|
|
|
// Load and update config.v2.json
|
|
common.PrintInfoMessage("Determining config.v2.json path...")
|
|
configV2Path := strings.Replace(hostConfigPath, "hostconfig.json", "config.v2.json", 1)
|
|
common.PrintInfoMessage(fmt.Sprintf("Loading config.v2.json from: %s", configV2Path))
|
|
var configV2 map[string]interface{}
|
|
if err := loadJSON(configV2Path, &configV2); err != nil {
|
|
common.PrintErrorMessage(fmt.Errorf("failed to load config.v2.json: %v", err))
|
|
os.Exit(1)
|
|
}
|
|
common.PrintSuccessMessage("config.v2.json loaded successfully.")
|
|
|
|
// Update mounts in both files
|
|
common.PrintInfoMessage("Updating mounts...")
|
|
newMount := fmt.Sprintf("%s:%s", source, target)
|
|
if add {
|
|
if !ocontains(hostConfig.Binds, newMount) {
|
|
hostConfig.Binds = append(hostConfig.Binds, newMount)
|
|
addMountPoint(configV2, source, target)
|
|
common.PrintSuccessMessage(fmt.Sprintf("Added mount: %s", newMount))
|
|
} else {
|
|
common.PrintWarningMessage("Mount already exists.")
|
|
}
|
|
} else {
|
|
hostConfig.Binds = removeFromSlice(hostConfig.Binds, newMount)
|
|
removeMountPoint(configV2, target)
|
|
common.PrintSuccessMessage(fmt.Sprintf("Removed mount: %s", newMount))
|
|
}
|
|
|
|
// Save changes
|
|
common.PrintInfoMessage("Saving updated hostconfig.json...")
|
|
if err := saveJSON(hostConfigPath, hostConfig); err != nil {
|
|
common.PrintErrorMessage(fmt.Errorf("failed to save hostconfig.json: %v", err))
|
|
os.Exit(1)
|
|
}
|
|
common.PrintSuccessMessage("hostconfig.json updated successfully.")
|
|
|
|
common.PrintInfoMessage("Saving updated config.v2.json...")
|
|
if err := saveJSON(configV2Path, configV2); err != nil {
|
|
common.PrintErrorMessage(fmt.Errorf("failed to save config.v2.json: %v", err))
|
|
os.Exit(1)
|
|
}
|
|
common.PrintSuccessMessage("config.v2.json updated successfully.")
|
|
|
|
// Restart the container
|
|
if err := showLoadingIndicator(ctx, func() error {
|
|
return RestartDockerService()
|
|
}, "Restarting Docker service..."); err != nil {
|
|
common.PrintErrorMessage(fmt.Errorf("failed to restart Docker service: %v", err))
|
|
os.Exit(1)
|
|
}
|
|
common.PrintSuccessMessage("Docker service restarted successfully.")
|
|
}
|
|
|
|
func addMountPoint(config map[string]interface{}, source string, target string) {
|
|
mountPoints, ok := config["MountPoints"].(map[string]interface{})
|
|
if !ok {
|
|
mountPoints = make(map[string]interface{})
|
|
config["MountPoints"] = mountPoints
|
|
}
|
|
|
|
mountPoints[target] = map[string]interface{}{
|
|
"Source": source,
|
|
"Destination": target,
|
|
"RW": true,
|
|
"Type": "bind",
|
|
"Propagation": "rprivate",
|
|
"Spec": map[string]string{
|
|
"Type": "bind",
|
|
"Source": source,
|
|
"Target": target,
|
|
},
|
|
"SkipMountpointCreation": false,
|
|
}
|
|
}
|
|
|
|
func removeMountPoint(config map[string]interface{}, target string) {
|
|
mountPoints, ok := config["MountPoints"].(map[string]interface{})
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
delete(mountPoints, target)
|
|
}
|
|
|
|
func UpdateDeviceBinding(containerName string, deviceHost string, deviceContainer string, add bool) {
|
|
var timeout = 10 // Stop timeout
|
|
|
|
// Check if the system is Windows
|
|
if runtime.GOOS == "windows" {
|
|
title := "Unsupported on Windows"
|
|
message := `This function is not supported on Windows.
|
|
However, you can achieve similar functionality by using the following commands:
|
|
- "rfswift commit" to create a new image with a new tag.
|
|
- "rfswift remove" to remove the existing container.
|
|
- "rfswift run" to run a container with new device bindings.`
|
|
|
|
rfutils.DisplayNotification(title, message, "warning")
|
|
os.Exit(1) // Exit since this function is not supported on Windows
|
|
}
|
|
|
|
if deviceHost == "" {
|
|
deviceHost = deviceContainer
|
|
common.PrintWarningMessage(fmt.Sprintf("Host device path is empty. Defaulting to container device path: %s", deviceContainer))
|
|
}
|
|
|
|
ctx := context.Background()
|
|
|
|
common.PrintInfoMessage("Fetching container ID...")
|
|
containerID := getContainerIDByName(ctx, containerName)
|
|
if containerID == "" {
|
|
common.PrintErrorMessage(fmt.Errorf("container %s not found", containerName))
|
|
os.Exit(1)
|
|
}
|
|
common.PrintSuccessMessage(fmt.Sprintf("Container ID: %s", containerID))
|
|
|
|
// Stop the container
|
|
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
|
|
if err != nil {
|
|
common.PrintErrorMessage(fmt.Errorf("Error when instantiating a client"))
|
|
os.Exit(1)
|
|
}
|
|
common.PrintInfoMessage("Stopping the container...")
|
|
|
|
// Attempt graceful stop
|
|
if err := showLoadingIndicator(ctx, func() error {
|
|
return cli.ContainerStop(ctx, containerID, container.StopOptions{Timeout: &timeout})
|
|
}, "Stopping the container..."); err != nil {
|
|
common.PrintErrorMessage(fmt.Errorf("Failed to stop the container gracefully: %v", err))
|
|
os.Exit(1)
|
|
}
|
|
|
|
// Check if the container is still running
|
|
containerJSON, err := cli.ContainerInspect(ctx, containerID)
|
|
if err != nil {
|
|
common.PrintErrorMessage(fmt.Errorf("Error inspecting container: %v", err))
|
|
os.Exit(1)
|
|
}
|
|
if containerJSON.State.Running {
|
|
common.PrintWarningMessage("Container is still running. Forcing stop...")
|
|
err = cli.ContainerKill(ctx, containerID, "SIGKILL")
|
|
if err != nil {
|
|
common.PrintErrorMessage(fmt.Errorf("Failed to force stop the container: %v", err))
|
|
os.Exit(1)
|
|
}
|
|
common.PrintSuccessMessage("Container forcibly stopped.")
|
|
} else {
|
|
common.PrintSuccessMessage(fmt.Sprintf("Container '%s' stopped", containerID))
|
|
}
|
|
|
|
// Load and update hostconfig.json
|
|
common.PrintInfoMessage("Determining hostconfig.json path...")
|
|
hostConfigPath, err := GetHostConfigPath(containerID)
|
|
if err != nil {
|
|
common.PrintErrorMessage(err)
|
|
os.Exit(1)
|
|
}
|
|
common.PrintSuccessMessage(fmt.Sprintf("HostConfig path: %s", hostConfigPath))
|
|
|
|
common.PrintInfoMessage("Loading hostconfig.json...")
|
|
var hostConfig HostConfigFull
|
|
if err := loadJSON(hostConfigPath, &hostConfig); err != nil {
|
|
common.PrintErrorMessage(fmt.Errorf("failed to load hostconfig.json: %v", err))
|
|
os.Exit(1)
|
|
}
|
|
common.PrintSuccessMessage("HostConfig loaded successfully.")
|
|
|
|
// Load and update config.v2.json
|
|
common.PrintInfoMessage("Determining config.v2.json path...")
|
|
configV2Path := strings.Replace(hostConfigPath, "hostconfig.json", "config.v2.json", 1)
|
|
common.PrintInfoMessage(fmt.Sprintf("Loading config.v2.json from: %s", configV2Path))
|
|
var configV2 map[string]interface{}
|
|
if err := loadJSON(configV2Path, &configV2); err != nil {
|
|
common.PrintErrorMessage(fmt.Errorf("failed to load config.v2.json: %v", err))
|
|
os.Exit(1)
|
|
}
|
|
common.PrintSuccessMessage("config.v2.json loaded successfully.")
|
|
|
|
// Update devices in both files
|
|
common.PrintInfoMessage("Updating devices...")
|
|
if add {
|
|
if !deviceExists(hostConfig.Devices, deviceHost, deviceContainer) {
|
|
newDevice := DeviceMapping{
|
|
PathOnHost: deviceHost,
|
|
PathInContainer: deviceContainer,
|
|
CgroupPermissions: "rwm", // Default to read, write, mknod permissions
|
|
}
|
|
hostConfig.Devices = append(hostConfig.Devices, newDevice)
|
|
addDeviceMapping(configV2, deviceHost, deviceContainer)
|
|
common.PrintSuccessMessage(fmt.Sprintf("Added device: %s to %s", deviceHost, deviceContainer))
|
|
} else {
|
|
common.PrintWarningMessage("Device mapping already exists.")
|
|
}
|
|
} else {
|
|
hostConfig.Devices = removeDeviceFromSlice(hostConfig.Devices, deviceHost, deviceContainer)
|
|
removeDeviceMapping(configV2, deviceHost, deviceContainer)
|
|
common.PrintSuccessMessage(fmt.Sprintf("Removed device: %s from %s", deviceHost, deviceContainer))
|
|
}
|
|
|
|
// Save changes
|
|
common.PrintInfoMessage("Saving updated hostconfig.json...")
|
|
if err := saveJSON(hostConfigPath, hostConfig); err != nil {
|
|
common.PrintErrorMessage(fmt.Errorf("failed to save hostconfig.json: %v", err))
|
|
os.Exit(1)
|
|
}
|
|
common.PrintSuccessMessage("hostconfig.json updated successfully.")
|
|
|
|
common.PrintInfoMessage("Saving updated config.v2.json...")
|
|
if err := saveJSON(configV2Path, configV2); err != nil {
|
|
common.PrintErrorMessage(fmt.Errorf("failed to save config.v2.json: %v", err))
|
|
os.Exit(1)
|
|
}
|
|
common.PrintSuccessMessage("config.v2.json updated successfully.")
|
|
|
|
// Restart the container
|
|
if err := showLoadingIndicator(ctx, func() error {
|
|
return RestartDockerService()
|
|
}, "Restarting Docker service..."); err != nil {
|
|
common.PrintErrorMessage(fmt.Errorf("failed to restart Docker service: %v", err))
|
|
os.Exit(1)
|
|
}
|
|
common.PrintSuccessMessage("Docker service restarted successfully.")
|
|
}
|
|
|
|
// Check if a device mapping already exists
|
|
func deviceExists(devices []DeviceMapping, hostPath string, containerPath string) bool {
|
|
for _, device := range devices {
|
|
if device.PathOnHost == hostPath && device.PathInContainer == containerPath {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Remove a device mapping from a slice
|
|
func removeDeviceFromSlice(devices []DeviceMapping, hostPath string, containerPath string) []DeviceMapping {
|
|
var result []DeviceMapping
|
|
for _, device := range devices {
|
|
if device.PathOnHost != hostPath || device.PathInContainer != containerPath {
|
|
result = append(result, device)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
// Add a device mapping to config.v2.json
|
|
func addDeviceMapping(config map[string]interface{}, hostPath string, containerPath string) {
|
|
// Check if "HostConfig" exists in the config
|
|
hostConfig, ok := config["HostConfig"].(map[string]interface{})
|
|
if !ok {
|
|
// Create HostConfig if it doesn't exist
|
|
hostConfig = make(map[string]interface{})
|
|
config["HostConfig"] = hostConfig
|
|
}
|
|
|
|
// Get existing devices or create new devices array
|
|
devices, ok := hostConfig["Devices"].([]interface{})
|
|
if !ok {
|
|
devices = make([]interface{}, 0)
|
|
}
|
|
|
|
// Create a new device mapping
|
|
newDevice := map[string]interface{}{
|
|
"PathOnHost": hostPath,
|
|
"PathInContainer": containerPath,
|
|
"CgroupPermissions": "rwm", // Default permissions
|
|
}
|
|
|
|
// Check if the device already exists
|
|
exists := false
|
|
for _, device := range devices {
|
|
if deviceMap, ok := device.(map[string]interface{}); ok {
|
|
if deviceMap["PathOnHost"] == hostPath && deviceMap["PathInContainer"] == containerPath {
|
|
exists = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add the new device mapping if it doesn't exist
|
|
if !exists {
|
|
devices = append(devices, newDevice)
|
|
hostConfig["Devices"] = devices
|
|
}
|
|
}
|
|
|
|
// Remove a device mapping from config.v2.json
|
|
func removeDeviceMapping(config map[string]interface{}, hostPath string, containerPath string) {
|
|
// Check if "HostConfig" exists in the config
|
|
hostConfig, ok := config["HostConfig"].(map[string]interface{})
|
|
if !ok {
|
|
return // No host config
|
|
}
|
|
|
|
// Get existing devices
|
|
devices, ok := hostConfig["Devices"].([]interface{})
|
|
if !ok {
|
|
return // No devices
|
|
}
|
|
|
|
// Filter out the device to remove
|
|
var updatedDevices []interface{}
|
|
for _, device := range devices {
|
|
if deviceMap, ok := device.(map[string]interface{}); ok {
|
|
if deviceMap["PathOnHost"] != hostPath || deviceMap["PathInContainer"] != containerPath {
|
|
updatedDevices = append(updatedDevices, device)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Update the devices list
|
|
hostConfig["Devices"] = updatedDevices
|
|
}
|
|
|
|
func ocontains(slice []string, item string) bool {
|
|
for _, s := range slice {
|
|
if s == item {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func loadJSON(path string, v interface{}) error {
|
|
data, err := ioutil.ReadFile(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return json.Unmarshal(data, v)
|
|
}
|
|
|
|
func saveJSON(path string, v interface{}) error {
|
|
data, err := json.MarshalIndent(v, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return ioutil.WriteFile(path, data, 0644)
|
|
}
|
|
|
|
func removeFromSlice(slice []string, item string) []string {
|
|
newSlice := []string{}
|
|
for _, s := range slice {
|
|
if s != item {
|
|
newSlice = append(newSlice, s)
|
|
}
|
|
}
|
|
return newSlice
|
|
}
|
|
|
|
func getContainerIDByName(ctx context.Context, containerName string) string {
|
|
cli, _ := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
|
|
containers, _ := cli.ContainerList(ctx, container.ListOptions{All: true})
|
|
for _, container := range containers {
|
|
for _, name := range container.Names {
|
|
if strings.TrimPrefix(name, "/") == containerName {
|
|
return container.ID
|
|
}
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func DockerStop(containerIdentifier string) {
|
|
ctx := context.Background()
|
|
|
|
// Initialize Docker client
|
|
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
|
|
if err != nil {
|
|
common.PrintErrorMessage(err)
|
|
return
|
|
}
|
|
defer cli.Close()
|
|
|
|
// Retrieve the latest container if no identifier is provided
|
|
if containerIdentifier == "" {
|
|
labelKey := "org.container.project"
|
|
labelValue := "rfswift"
|
|
containerIdentifier = latestDockerID(labelKey, labelValue)
|
|
if containerIdentifier == "" {
|
|
common.PrintErrorMessage(fmt.Errorf("no running containers found with label %s=%s", labelKey, labelValue))
|
|
return
|
|
}
|
|
}
|
|
|
|
// Inspect the container to get its current state
|
|
containerJSON, err := cli.ContainerInspect(ctx, containerIdentifier)
|
|
if err != nil {
|
|
common.PrintErrorMessage(fmt.Errorf("failed to inspect container: %v", err))
|
|
return
|
|
}
|
|
|
|
containerName := strings.TrimPrefix(containerJSON.Name, "/")
|
|
if !containerJSON.State.Running {
|
|
common.PrintSuccessMessage(fmt.Sprintf("Container '%s' is already stopped", containerName))
|
|
return
|
|
}
|
|
|
|
// Stop the container
|
|
timeout := 10 // Grace period in seconds before force stop
|
|
if err := cli.ContainerStop(ctx, containerIdentifier, container.StopOptions{Timeout: &timeout}); err != nil {
|
|
common.PrintErrorMessage(fmt.Errorf("failed to stop container: %v", err))
|
|
return
|
|
}
|
|
|
|
common.PrintSuccessMessage(fmt.Sprintf("Container '%s' stopped successfully", containerName))
|
|
}
|