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)) }