755 lines
18 KiB
Go
755 lines
18 KiB
Go
/*
|
||
* Copyright © 2019 – 2020 Red Hat Inc.
|
||
*
|
||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||
* you may not use this file except in compliance with the License.
|
||
* You may obtain a copy of the License at
|
||
*
|
||
* http://www.apache.org/licenses/LICENSE-2.0
|
||
*
|
||
* Unless required by applicable law or agreed to in writing, software
|
||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
* See the License for the specific language governing permissions and
|
||
* limitations under the License.
|
||
*/
|
||
|
||
package utils
|
||
|
||
import (
|
||
"errors"
|
||
"fmt"
|
||
"os"
|
||
"os/exec"
|
||
"os/user"
|
||
"path"
|
||
"path/filepath"
|
||
"regexp"
|
||
"sort"
|
||
"strconv"
|
||
"strings"
|
||
"syscall"
|
||
"time"
|
||
|
||
"github.com/acobaugh/osrelease"
|
||
"github.com/containers/toolbox/pkg/shell"
|
||
"github.com/docker/go-units"
|
||
"github.com/godbus/dbus/v5"
|
||
"github.com/sirupsen/logrus"
|
||
"golang.org/x/sys/unix"
|
||
)
|
||
|
||
type ParseReleaseFunc func(string) (string, error)
|
||
|
||
type Distro struct {
|
||
ContainerNamePrefix string
|
||
ImageBasename string
|
||
ParseRelease ParseReleaseFunc
|
||
Registry string
|
||
Repository string
|
||
RepositoryNeedsRelease bool
|
||
}
|
||
|
||
const (
|
||
idTruncLength = 12
|
||
releaseDefaultFallback = "32"
|
||
)
|
||
|
||
const (
|
||
// Based on the nameRegex value in:
|
||
// https://github.com/containers/libpod/blob/master/libpod/options.go
|
||
ContainerNameRegexp = "[a-zA-Z0-9][a-zA-Z0-9_.-]*"
|
||
)
|
||
|
||
var (
|
||
distroDefault = "fedora"
|
||
|
||
preservedEnvironmentVariables = []string{
|
||
"COLORTERM",
|
||
"DBUS_SESSION_BUS_ADDRESS",
|
||
"DBUS_SYSTEM_BUS_ADDRESS",
|
||
"DESKTOP_SESSION",
|
||
"DISPLAY",
|
||
"LANG",
|
||
"SHELL",
|
||
"SSH_AUTH_SOCK",
|
||
"TERM",
|
||
"TOOLBOX_PATH",
|
||
"USER",
|
||
"VTE_VERSION",
|
||
"WAYLAND_DISPLAY",
|
||
"XAUTHORITY",
|
||
"XDG_CURRENT_DESKTOP",
|
||
"XDG_DATA_DIRS",
|
||
"XDG_MENU_PREFIX",
|
||
"XDG_RUNTIME_DIR",
|
||
"XDG_SEAT",
|
||
"XDG_SESSION_DESKTOP",
|
||
"XDG_SESSION_ID",
|
||
"XDG_SESSION_TYPE",
|
||
"XDG_VTNR",
|
||
}
|
||
|
||
releaseDefault string
|
||
|
||
supportedDistros = map[string]Distro{
|
||
"fedora": {
|
||
"fedora-toolbox",
|
||
"fedora-toolbox",
|
||
parseReleaseFedora,
|
||
"registry.fedoraproject.org",
|
||
"f%s",
|
||
true,
|
||
},
|
||
}
|
||
)
|
||
|
||
var (
|
||
ContainerNameDefault string
|
||
ContainerNamePrefixDefault = "fedora-toolbox"
|
||
)
|
||
|
||
func init() {
|
||
releaseDefault = releaseDefaultFallback
|
||
|
||
hostID, err := GetHostID()
|
||
if err == nil {
|
||
if distroObj, supportedDistro := supportedDistros[hostID]; supportedDistro {
|
||
release, err := GetHostVersionID()
|
||
if err == nil {
|
||
ContainerNamePrefixDefault = distroObj.ContainerNamePrefix
|
||
distroDefault = hostID
|
||
releaseDefault = release
|
||
}
|
||
}
|
||
}
|
||
|
||
ContainerNameDefault = ContainerNamePrefixDefault + "-" + releaseDefault
|
||
}
|
||
|
||
func AskForConfirmation(prompt string) bool {
|
||
var retVal bool
|
||
|
||
for {
|
||
fmt.Printf("%s ", prompt)
|
||
|
||
var response string
|
||
|
||
fmt.Scanf("%s", &response)
|
||
if response == "" {
|
||
response = "n"
|
||
} else {
|
||
response = strings.ToLower(response)
|
||
}
|
||
|
||
if response == "no" || response == "n" {
|
||
break
|
||
} else if response == "yes" || response == "y" {
|
||
retVal = true
|
||
break
|
||
}
|
||
}
|
||
|
||
return retVal
|
||
}
|
||
|
||
func CallFlatpakSessionHelper() (string, error) {
|
||
logrus.Debug("Calling org.freedesktop.Flatpak.SessionHelper.RequestSession")
|
||
|
||
connection, err := dbus.SessionBus()
|
||
if err != nil {
|
||
return "", errors.New("failed to connect to the D-Bus session instance")
|
||
}
|
||
|
||
sessionHelper := connection.Object("org.freedesktop.Flatpak", "/org/freedesktop/Flatpak/SessionHelper")
|
||
call := sessionHelper.Call("org.freedesktop.Flatpak.SessionHelper.RequestSession", 0)
|
||
|
||
var result map[string]dbus.Variant
|
||
err = call.Store(&result)
|
||
if err != nil {
|
||
logrus.Debugf("Call to org.freedesktop.Flatpak.SessionHelper.RequestSession failed: %s", err)
|
||
return "", errors.New("failed to call org.freedesktop.Flatpak.SessionHelper.RequestSession")
|
||
}
|
||
|
||
pathVariant := result["path"]
|
||
pathVariantSignature := pathVariant.Signature().String()
|
||
if pathVariantSignature != "s" {
|
||
return "", errors.New("unknown reply from org.freedesktop.Flatpak.SessionHelper.RequestSession")
|
||
}
|
||
|
||
pathValue := pathVariant.Value()
|
||
path := pathValue.(string)
|
||
return path, nil
|
||
}
|
||
|
||
func CreateErrorContainerNotFound(container, executableBase string) error {
|
||
var builder strings.Builder
|
||
fmt.Fprintf(&builder, "container %s not found\n", container)
|
||
fmt.Fprintf(&builder, "Use the 'create' command to create a toolbox.\n")
|
||
fmt.Fprintf(&builder, "Run '%s --help' for usage.", executableBase)
|
||
|
||
errMsg := builder.String()
|
||
return errors.New(errMsg)
|
||
}
|
||
|
||
func CreateErrorInvalidRelease(executableBase string) error {
|
||
var builder strings.Builder
|
||
fmt.Fprintf(&builder, "invalid argument for '--release'\n")
|
||
fmt.Fprintf(&builder, "Run '%s --help' for usage.", executableBase)
|
||
|
||
errMsg := builder.String()
|
||
return errors.New(errMsg)
|
||
}
|
||
|
||
func EnsureXdgRuntimeDirIsSet(uid int) {
|
||
if xdgRuntimeDir, ok := os.LookupEnv("XDG_RUNTIME_DIR"); !ok {
|
||
logrus.Debug("XDG_RUNTIME_DIR is unset")
|
||
|
||
xdgRuntimeDir = fmt.Sprintf("/run/user/%d", uid)
|
||
os.Setenv("XDG_RUNTIME_DIR", xdgRuntimeDir)
|
||
|
||
logrus.Debugf("XDG_RUNTIME_DIR set to %s", xdgRuntimeDir)
|
||
}
|
||
}
|
||
|
||
func ForwardToHost() (int, error) {
|
||
envOptions := GetEnvOptionsForPreservedVariables()
|
||
toolboxPath := os.Getenv("TOOLBOX_PATH")
|
||
commandLineArgs := os.Args[1:]
|
||
|
||
var flatpakSpawnArgs []string
|
||
|
||
flatpakSpawnArgs = append(flatpakSpawnArgs, envOptions...)
|
||
|
||
flatpakSpawnArgs = append(flatpakSpawnArgs, []string{
|
||
"--host",
|
||
toolboxPath,
|
||
}...)
|
||
|
||
flatpakSpawnArgs = append(flatpakSpawnArgs, commandLineArgs...)
|
||
|
||
logrus.Debug("Forwarding to host:")
|
||
logrus.Debugf("%s", toolboxPath)
|
||
for _, arg := range commandLineArgs {
|
||
logrus.Debugf("%s", arg)
|
||
}
|
||
|
||
exitCode, err := shell.RunWithExitCode("flatpak-spawn", os.Stdin, os.Stdout, nil, flatpakSpawnArgs...)
|
||
if err != nil {
|
||
return exitCode, err
|
||
}
|
||
|
||
return exitCode, nil
|
||
}
|
||
|
||
// GetCgroupsVersion returns the cgroups version of the host
|
||
//
|
||
// Based on the IsCgroup2UnifiedMode function in:
|
||
// https://github.com/containers/libpod/tree/master/pkg/cgroups
|
||
func GetCgroupsVersion() (int, error) {
|
||
var st syscall.Statfs_t
|
||
|
||
if err := syscall.Statfs("/sys/fs/cgroup", &st); err != nil {
|
||
return -1, err
|
||
}
|
||
|
||
version := 1
|
||
if st.Type == unix.CGROUP2_SUPER_MAGIC {
|
||
version = 2
|
||
}
|
||
|
||
return version, nil
|
||
}
|
||
|
||
func GetContainerNamePrefixForImage(image string) (string, error) {
|
||
basename := ImageReferenceGetBasename(image)
|
||
if basename == "" {
|
||
return "", fmt.Errorf("failed to get the basename of image %s", image)
|
||
}
|
||
|
||
for _, distroObj := range supportedDistros {
|
||
if distroObj.ImageBasename != basename {
|
||
continue
|
||
}
|
||
|
||
return distroObj.ContainerNamePrefix, nil
|
||
}
|
||
|
||
return basename, nil
|
||
}
|
||
|
||
func GetDefaultImageForDistro(distro, release string) string {
|
||
if _, supportedDistro := supportedDistros[distro]; !supportedDistro {
|
||
distro = "fedora"
|
||
}
|
||
|
||
distroObj, supportedDistro := supportedDistros[distro]
|
||
if !supportedDistro {
|
||
panicMsg := fmt.Sprintf("failed to find %s in the list of supported distributions", distro)
|
||
panic(panicMsg)
|
||
}
|
||
|
||
image := distroObj.ImageBasename + ":" + release
|
||
return image
|
||
}
|
||
|
||
func GetEnvOptionsForPreservedVariables() []string {
|
||
logrus.Debug("Creating list of environment variables to forward")
|
||
|
||
var envOptions []string
|
||
|
||
for _, variable := range preservedEnvironmentVariables {
|
||
value, found := os.LookupEnv(variable)
|
||
if !found {
|
||
logrus.Debugf("%s is unset", variable)
|
||
continue
|
||
}
|
||
|
||
logrus.Debugf("%s=%s", variable, value)
|
||
envOptions = append(envOptions, fmt.Sprintf("--env=%s=%s", variable, value))
|
||
}
|
||
|
||
return envOptions
|
||
}
|
||
|
||
func GetFullyQualifiedImageFromDistros(image, release string) (string, error) {
|
||
logrus.Debugf("Resolving fully qualified name for image %s from known registries", image)
|
||
|
||
if ImageReferenceHasDomain(image) {
|
||
return image, nil
|
||
}
|
||
|
||
basename := ImageReferenceGetBasename(image)
|
||
if basename == "" {
|
||
return "", fmt.Errorf("failed to get the basename of image %s", image)
|
||
}
|
||
|
||
for _, distroObj := range supportedDistros {
|
||
if distroObj.ImageBasename != basename {
|
||
continue
|
||
}
|
||
|
||
var repository string
|
||
|
||
if distroObj.RepositoryNeedsRelease {
|
||
repository = fmt.Sprintf(distroObj.Repository, release)
|
||
} else {
|
||
repository = distroObj.Repository
|
||
}
|
||
|
||
imageFull := distroObj.Registry + "/" + repository + "/" + image
|
||
|
||
logrus.Debugf("Resolved image %s to %s", image, imageFull)
|
||
|
||
return imageFull, nil
|
||
}
|
||
|
||
return "", fmt.Errorf("failed to resolve image %s")
|
||
}
|
||
|
||
// GetGroupForSudo returns the name of the sudoers group.
|
||
//
|
||
// Some distros call it 'sudo' (eg. Ubuntu) and some call it 'wheel' (eg. Fedora).
|
||
func GetGroupForSudo() (string, error) {
|
||
logrus.Debug("Looking up group for sudo")
|
||
|
||
groups := []string{"sudo", "wheel"}
|
||
|
||
for _, group := range groups {
|
||
if _, err := user.LookupGroup(group); err == nil {
|
||
logrus.Debugf("Group for sudo is %s", group)
|
||
return group, nil
|
||
}
|
||
}
|
||
|
||
return "", errors.New("group for sudo not found")
|
||
}
|
||
|
||
// GetHostID returns the ID from the os-release files
|
||
//
|
||
// Examples:
|
||
// - host is Fedora, returned string is 'fedora'
|
||
func GetHostID() (string, error) {
|
||
osRelease, err := osrelease.Read()
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
|
||
return osRelease["ID"], nil
|
||
}
|
||
|
||
// GetHostVariantID returns the VARIANT_ID from the os-release files
|
||
//
|
||
// Examples:
|
||
// - host is Fedora Workstation, returned string is 'workstation'
|
||
func GetHostVariantID() (string, error) {
|
||
osRelease, err := osrelease.Read()
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
|
||
return osRelease["VARIANT_ID"], nil
|
||
}
|
||
|
||
// GetHostVersionID returns the VERSION_ID from the os-release files
|
||
//
|
||
// Examples:
|
||
// - host is Fedora 32, returned string is '32'
|
||
func GetHostVersionID() (string, error) {
|
||
osRelease, err := osrelease.Read()
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
|
||
return osRelease["VERSION_ID"], nil
|
||
}
|
||
|
||
// GetMountPoint returns the mount point of a target.
|
||
func GetMountPoint(target string) (string, error) {
|
||
var stdout strings.Builder
|
||
|
||
if err := shell.Run("df", nil, &stdout, nil, "--output=target", target); err != nil {
|
||
return "", err
|
||
}
|
||
|
||
output := stdout.String()
|
||
options := strings.Split(output, "\n")
|
||
if len(options) != 3 {
|
||
return "", errors.New("unexpected output from df(1)")
|
||
}
|
||
|
||
mountPoint := strings.TrimSpace(options[1])
|
||
return mountPoint, nil
|
||
}
|
||
|
||
// GetMountOptions returns the mount options of a target.
|
||
func GetMountOptions(target string) (string, error) {
|
||
var stdout strings.Builder
|
||
findMntArgs := []string{"--noheadings", "--output", "OPTIONS", target}
|
||
|
||
if err := shell.Run("findmnt", nil, &stdout, nil, findMntArgs...); err != nil {
|
||
return "", err
|
||
}
|
||
|
||
output := stdout.String()
|
||
options := strings.Split(output, "\n")
|
||
if len(options) != 2 {
|
||
return "", errors.New("unexpected output from findmnt(1)")
|
||
}
|
||
|
||
mountOptions := strings.TrimSpace(options[0])
|
||
return mountOptions, nil
|
||
}
|
||
|
||
func GetRuntimeDirectory(targetUser *user.User) (string, error) {
|
||
uid, err := strconv.Atoi(targetUser.Uid)
|
||
if err != nil {
|
||
return "", fmt.Errorf("failed to convert user ID to integer: %w", err)
|
||
}
|
||
|
||
var runtimeDirectory string
|
||
|
||
if uid == 0 {
|
||
runtimeDirectory = "/run"
|
||
} else {
|
||
runtimeDirectory = os.Getenv("XDG_RUNTIME_DIR")
|
||
}
|
||
|
||
toolboxRuntimeDirectory := path.Join(runtimeDirectory, "toolbox")
|
||
logrus.Debugf("Creating runtime directory %s", toolboxRuntimeDirectory)
|
||
|
||
if err := os.MkdirAll(toolboxRuntimeDirectory, 0700); err != nil {
|
||
wrapped_err := fmt.Errorf("failed to create runtime directory %s: %w",
|
||
toolboxRuntimeDirectory,
|
||
err)
|
||
return "", wrapped_err
|
||
}
|
||
|
||
if err := os.Chown(toolboxRuntimeDirectory, uid, uid); err != nil {
|
||
wrapped_err := fmt.Errorf("failed to change ownership of the runtime directory %s: %w",
|
||
toolboxRuntimeDirectory,
|
||
err)
|
||
return "", wrapped_err
|
||
}
|
||
|
||
return toolboxRuntimeDirectory, nil
|
||
}
|
||
|
||
// HumanDuration accepts a Unix time value and converts it into a human readable
|
||
// string.
|
||
//
|
||
// Examples: "5 minutes ago", "2 hours ago", "3 days ago"
|
||
func HumanDuration(duration int64) string {
|
||
return units.HumanDuration(time.Since(time.Unix(duration, 0))) + " ago"
|
||
}
|
||
|
||
// ImageReferenceCanBeID checks if 'image' might be the ID of an image
|
||
func ImageReferenceCanBeID(image string) (bool, error) {
|
||
matched, err := regexp.MatchString("^[a-f0-9]\\{6,64\\}$", image)
|
||
return matched, err
|
||
}
|
||
|
||
func ImageReferenceGetBasename(image string) string {
|
||
var i int
|
||
|
||
if ImageReferenceHasDomain(image) {
|
||
i = strings.IndexRune(image, '/')
|
||
}
|
||
|
||
remainder := image[i:]
|
||
j := strings.IndexRune(remainder, ':')
|
||
if j == -1 {
|
||
j = len(remainder)
|
||
}
|
||
|
||
path := remainder[:j]
|
||
basename := filepath.Base(path)
|
||
return basename
|
||
}
|
||
|
||
func ImageReferenceGetDomain(image string) string {
|
||
if !ImageReferenceHasDomain(image) {
|
||
return ""
|
||
}
|
||
|
||
i := strings.IndexRune(image, '/')
|
||
domain := image[:i]
|
||
return domain
|
||
}
|
||
|
||
func ImageReferenceGetTag(image string) string {
|
||
var i int
|
||
|
||
if ImageReferenceHasDomain(image) {
|
||
i = strings.IndexRune(image, '/')
|
||
}
|
||
|
||
remainder := image[i:]
|
||
j := strings.IndexRune(remainder, ':')
|
||
if j == -1 {
|
||
return ""
|
||
}
|
||
|
||
tag := remainder[j+1:]
|
||
return tag
|
||
}
|
||
|
||
// ImageReferenceHasDomain checks if the provided image has a domain definition in it.
|
||
func ImageReferenceHasDomain(image string) bool {
|
||
i := strings.IndexRune(image, '/')
|
||
if i == -1 {
|
||
return false
|
||
}
|
||
|
||
prefix := image[:i]
|
||
|
||
// A domain should contain a top level domain name. An exception is 'localhost'
|
||
if !strings.ContainsAny(prefix, ".:") && prefix != "localhost" {
|
||
return false
|
||
}
|
||
|
||
return true
|
||
}
|
||
|
||
// ShortID shortens provided id to first 12 characters.
|
||
func ShortID(id string) string {
|
||
if len(id) > idTruncLength {
|
||
return id[:idTruncLength]
|
||
}
|
||
return id
|
||
}
|
||
|
||
func ParseRelease(distro, str string) (string, error) {
|
||
if distro == "" {
|
||
distro = distroDefault
|
||
}
|
||
|
||
if _, supportedDistro := supportedDistros[distro]; !supportedDistro {
|
||
distro = "fedora"
|
||
}
|
||
|
||
distroObj, supportedDistro := supportedDistros[distro]
|
||
if !supportedDistro {
|
||
panicMsg := fmt.Sprintf("failed to find %s in the list of supported distributions", distro)
|
||
panic(panicMsg)
|
||
}
|
||
|
||
parseRelease := distroObj.ParseRelease
|
||
release, err := parseRelease(str)
|
||
return release, err
|
||
}
|
||
|
||
func parseReleaseFedora(str string) (string, error) {
|
||
var release string
|
||
|
||
if strings.HasPrefix(str, "F") || strings.HasPrefix(str, "f") {
|
||
release = str[1:]
|
||
} else {
|
||
release = str
|
||
}
|
||
|
||
releaseN, err := strconv.Atoi(release)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
|
||
if releaseN <= 0 {
|
||
return "", errors.New("release must be a positive integer")
|
||
}
|
||
|
||
return release, nil
|
||
}
|
||
|
||
// PathExists wraps around os.Stat providing a nice interface for checking an existence of a path.
|
||
func PathExists(path string) bool {
|
||
if _, err := os.Stat(path); !os.IsNotExist(err) {
|
||
return true
|
||
}
|
||
|
||
return false
|
||
}
|
||
|
||
// IsContainerNameValid checks if the name of a container matches the right pattern
|
||
func IsContainerNameValid(containerName string) bool {
|
||
pattern := "^" + ContainerNameRegexp + "$"
|
||
matched, err := regexp.MatchString(pattern, containerName)
|
||
if err != nil {
|
||
panicMsg := fmt.Sprintf("failed to parse regular expression for container name: %v", err)
|
||
panic(panicMsg)
|
||
}
|
||
|
||
return matched
|
||
}
|
||
|
||
func IsInsideContainer() bool {
|
||
if PathExists("/run/.containerenv") {
|
||
return true
|
||
}
|
||
|
||
return false
|
||
}
|
||
|
||
func IsInsideToolboxContainer() bool {
|
||
if PathExists("/run/.toolboxenv") {
|
||
return true
|
||
}
|
||
|
||
return false
|
||
}
|
||
|
||
func JoinJSON(joinkey string, maps ...[]map[string]interface{}) []map[string]interface{} {
|
||
var json []map[string]interface{}
|
||
found := make(map[string]bool)
|
||
|
||
// Iterate over every json provided and check if it is already in the final json
|
||
// If it contains some invalid entry (equals nil), then skip that entry
|
||
|
||
for _, m := range maps {
|
||
for _, entry := range m {
|
||
if entry["names"] == nil && entry["Names"] == nil {
|
||
continue
|
||
}
|
||
key := entry[joinkey].(string)
|
||
if _, ok := found[key]; !ok {
|
||
found[key] = true
|
||
json = append(json, entry)
|
||
}
|
||
}
|
||
}
|
||
return json
|
||
}
|
||
|
||
// ResolveContainerAndImageNames takes care of standardizing names of containers and images.
|
||
//
|
||
// If no image name is specified then the base image will reflect the platform of the host (even the version).
|
||
// If no container name is specified then the name of the image will be used.
|
||
//
|
||
// If the host system is unknown then the base image will be 'fedora-toolbox' with a default version
|
||
func ResolveContainerAndImageNames(container, distro, image, release string) (string, string, string, error) {
|
||
logrus.Debug("Resolving container and image names")
|
||
logrus.Debugf("Container: '%s'", container)
|
||
logrus.Debugf("Distribution: '%s'", distro)
|
||
logrus.Debugf("Image: '%s'", image)
|
||
logrus.Debugf("Release: '%s'", release)
|
||
|
||
if distro == "" {
|
||
distro = distroDefault
|
||
}
|
||
|
||
if distro != distroDefault && release == "" {
|
||
return "", "", "", fmt.Errorf("release not found for non-default distribution %s", distro)
|
||
}
|
||
|
||
if release == "" {
|
||
release = releaseDefault
|
||
}
|
||
|
||
if image == "" {
|
||
image = GetDefaultImageForDistro(distro, release)
|
||
} else {
|
||
release = ImageReferenceGetTag(image)
|
||
if release == "" {
|
||
release = releaseDefault
|
||
}
|
||
}
|
||
|
||
if container == "" {
|
||
var err error
|
||
container, err = GetContainerNamePrefixForImage(image)
|
||
if err != nil {
|
||
return "", "", "", err
|
||
}
|
||
|
||
tag := ImageReferenceGetTag(image)
|
||
if tag != "" {
|
||
container = container + "-" + tag
|
||
}
|
||
}
|
||
|
||
logrus.Debug("Resolved container and image names")
|
||
logrus.Debugf("Container: '%s'", container)
|
||
logrus.Debugf("Image: '%s'", image)
|
||
logrus.Debugf("Release: '%s'", release)
|
||
|
||
return container, image, release, nil
|
||
}
|
||
|
||
func ShowManual(manual string) error {
|
||
manBinary, err := exec.LookPath("man")
|
||
if err != nil {
|
||
if errors.Is(err, exec.ErrNotFound) {
|
||
return errors.New("man(1) not found")
|
||
}
|
||
|
||
return errors.New("failed to lookup man(1)")
|
||
}
|
||
|
||
manualArgs := []string{"man", manual}
|
||
env := os.Environ()
|
||
|
||
stderrFd := os.Stderr.Fd()
|
||
stderrFdInt := int(stderrFd)
|
||
stdoutFd := os.Stdout.Fd()
|
||
stdoutFdInt := int(stdoutFd)
|
||
if err := syscall.Dup3(stdoutFdInt, stderrFdInt, 0); err != nil {
|
||
return errors.New("failed to redirect standard error to standard output")
|
||
}
|
||
|
||
if err := syscall.Exec(manBinary, manualArgs, env); err != nil {
|
||
return errors.New("failed to invoke man(1)")
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
func SortJSON(json []map[string]interface{}, key string, hasInterface bool) []map[string]interface{} {
|
||
sort.Slice(json, func(i, j int) bool {
|
||
if hasInterface {
|
||
return json[i][key].([]interface{})[0].(string) < json[j][key].([]interface{})[0].(string)
|
||
}
|
||
return json[i][key].(string) < json[j][key].(string)
|
||
})
|
||
|
||
return json
|
||
}
|