github.com/jfrog/jfrog-cli-core/v2@v2.52.0/artifactory/utils/container/containermanager.go (about)

     1  package container
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"os"
     7  	"os/exec"
     8  	"regexp"
     9  	"strings"
    10  
    11  	"github.com/jfrog/jfrog-cli-core/v2/utils/config"
    12  	"github.com/jfrog/jfrog-cli-core/v2/utils/coreutils"
    13  	"github.com/jfrog/jfrog-client-go/auth"
    14  	"github.com/jfrog/jfrog-client-go/utils"
    15  	"github.com/jfrog/jfrog-client-go/utils/errorutils"
    16  	"github.com/jfrog/jfrog-client-go/utils/log"
    17  )
    18  
    19  // Search for docker API version format pattern e.g. 1.40
    20  var ApiVersionRegex = regexp.MustCompile(`^(\d+)\.(\d+)$`)
    21  
    22  // Docker API version 1.31 is compatible with Docker version 17.07.0, according to https://docs.docker.com/engine/api/#api-version-matrix
    23  const MinSupportedApiVersion string = "1.31"
    24  
    25  // Docker login error message
    26  const LoginFailureMessage string = "%s login failed for: %s.\n%s image must be in the form: registry-domain/path-in-repository/image-name:version."
    27  
    28  func NewManager(containerManagerType ContainerManagerType) ContainerManager {
    29  	return &containerManager{Type: containerManagerType}
    30  }
    31  
    32  type ContainerManagerType int
    33  
    34  const (
    35  	DockerClient ContainerManagerType = iota
    36  	Podman
    37  )
    38  
    39  func (cmt ContainerManagerType) String() string {
    40  	return [...]string{"docker", "podman"}[cmt]
    41  }
    42  
    43  // Container image
    44  type ContainerManager interface {
    45  	// Image ID is basically the image's SHA256
    46  	Id(image *Image) (string, error)
    47  	OsCompatibility(image *Image) (string, string, error)
    48  	RunNativeCmd(cmdParams []string) error
    49  	GetContainerManagerType() ContainerManagerType
    50  }
    51  
    52  type containerManager struct {
    53  	Type ContainerManagerType
    54  }
    55  
    56  type ContainerManagerLoginConfig struct {
    57  	ServerDetails *config.ServerDetails
    58  }
    59  
    60  // Run native command of the container buildtool
    61  func (containerManager *containerManager) RunNativeCmd(cmdParams []string) error {
    62  	cmd := &nativeCmd{cmdParams: cmdParams, containerManager: containerManager.Type}
    63  	return cmd.RunCmd()
    64  }
    65  
    66  // Get image ID
    67  func (containerManager *containerManager) Id(image *Image) (string, error) {
    68  	cmd := &getImageIdCmd{image: image, containerManager: containerManager.Type}
    69  	content, err := cmd.RunCmd()
    70  	if err != nil {
    71  		return "", err
    72  	}
    73  	return strings.Split(content, "\n")[0], nil
    74  }
    75  
    76  // Return the OS and architecture on which the image runs e.g. (linux, amd64, nil).
    77  func (containerManager *containerManager) OsCompatibility(image *Image) (string, string, error) {
    78  	cmd := &getImageSystemCompatibilityCmd{image: image, containerManager: containerManager.Type}
    79  	log.Debug("Running image inspect...")
    80  	content, err := cmd.RunCmd()
    81  	if err != nil {
    82  		return "", "", err
    83  	}
    84  	content = strings.Trim(content, "\n")
    85  	firstSeparator := strings.Index(content, ",")
    86  	if firstSeparator == -1 {
    87  		return "", "", errorutils.CheckErrorf("couldn't find OS and architecture of image:" + image.name)
    88  	}
    89  	return content[:firstSeparator], content[firstSeparator+1:], err
    90  }
    91  
    92  func (containerManager *containerManager) GetContainerManagerType() ContainerManagerType {
    93  	return containerManager.Type
    94  }
    95  
    96  // Image push command
    97  type nativeCmd struct {
    98  	cmdParams        []string
    99  	containerManager ContainerManagerType
   100  }
   101  
   102  func (nc *nativeCmd) GetCmd() *exec.Cmd {
   103  	return exec.Command(nc.containerManager.String(), nc.cmdParams...)
   104  }
   105  
   106  func (nc *nativeCmd) RunCmd() error {
   107  	command := nc.GetCmd()
   108  	command.Stderr = os.Stderr
   109  	command.Stdout = os.Stderr
   110  	if nc.containerManager == DockerClient {
   111  		command.Env = append(os.Environ(), "DOCKER_SCAN_SUGGEST=false")
   112  	}
   113  	return command.Run()
   114  }
   115  
   116  // Image get image id command
   117  type getImageIdCmd struct {
   118  	image            *Image
   119  	containerManager ContainerManagerType
   120  }
   121  
   122  func (getImageId *getImageIdCmd) GetCmd() *exec.Cmd {
   123  	var cmd []string
   124  	cmd = append(cmd, "images")
   125  	cmd = append(cmd, "--format", "{{.ID}}")
   126  	cmd = append(cmd, "--no-trunc")
   127  	cmd = append(cmd, getImageId.image.name)
   128  	return exec.Command(getImageId.containerManager.String(), cmd...)
   129  }
   130  
   131  func (getImageId *getImageIdCmd) RunCmd() (string, error) {
   132  	command := getImageId.GetCmd()
   133  	buffer := bytes.NewBuffer([]byte{})
   134  	command.Stderr = buffer
   135  	command.Stdout = buffer
   136  	err := command.Run()
   137  	return buffer.String(), err
   138  }
   139  
   140  // Get image system compatibility details
   141  type getImageSystemCompatibilityCmd struct {
   142  	image            *Image
   143  	containerManager ContainerManagerType
   144  }
   145  
   146  func (getImageSystemCompatibilityCmd *getImageSystemCompatibilityCmd) GetCmd() *exec.Cmd {
   147  	var cmd []string
   148  	cmd = append(cmd, "image")
   149  	cmd = append(cmd, "inspect")
   150  	cmd = append(cmd, getImageSystemCompatibilityCmd.image.name)
   151  	cmd = append(cmd, "--format")
   152  	cmd = append(cmd, "{{ .Os}},{{ .Architecture}}")
   153  	return exec.Command(getImageSystemCompatibilityCmd.containerManager.String(), cmd...)
   154  }
   155  
   156  func (getImageSystemCompatibilityCmd *getImageSystemCompatibilityCmd) RunCmd() (string, error) {
   157  	command := getImageSystemCompatibilityCmd.GetCmd()
   158  	buffer := bytes.NewBuffer([]byte{})
   159  	command.Stderr = buffer
   160  	command.Stdout = buffer
   161  	err := command.Run()
   162  	return buffer.String(), err
   163  }
   164  
   165  // Login command
   166  type LoginCmd struct {
   167  	DockerRegistry   string
   168  	Username         string
   169  	Password         string
   170  	containerManager ContainerManagerType
   171  }
   172  
   173  func (loginCmd *LoginCmd) GetCmd() *exec.Cmd {
   174  	if coreutils.IsWindows() {
   175  		return exec.Command("cmd", "/C", "echo", "%CONTAINER_MANAGER_PASS%|", "docker", "login", loginCmd.DockerRegistry, "--username", loginCmd.Username, "--password-stdin")
   176  	}
   177  	cmd := "echo $CONTAINER_MANAGER_PASS " + fmt.Sprintf(`| `+loginCmd.containerManager.String()+` login %s --username="%s" --password-stdin`, loginCmd.DockerRegistry, loginCmd.Username)
   178  	return exec.Command("sh", "-c", cmd)
   179  }
   180  
   181  func (loginCmd *LoginCmd) RunCmd() error {
   182  	command := loginCmd.GetCmd()
   183  	command.Stderr = os.Stderr
   184  	command.Stdout = os.Stderr
   185  	command.Env = os.Environ()
   186  	command.Env = append(command.Env, "CONTAINER_MANAGER_PASS="+loginCmd.Password)
   187  	return command.Run()
   188  }
   189  
   190  // First we'll try to log in assuming a proxy-less tag (e.g. "registry-address/docker-repo/image:ver").
   191  // If fails, we will try assuming a reverse proxy tag (e.g. "registry-address-docker-repo/image:ver").
   192  func ContainerManagerLogin(image *Image, config *ContainerManagerLoginConfig, containerManager ContainerManagerType) error {
   193  	imageRegistry, err := image.GetRegistry()
   194  	if err != nil {
   195  		return err
   196  	}
   197  	username := config.ServerDetails.User
   198  	password := config.ServerDetails.Password
   199  	// If access-token exists, perform login with it.
   200  	if config.ServerDetails.AccessToken != "" {
   201  		log.Debug("Using access-token details in " + containerManager.String() + "-login command.")
   202  		if username == "" {
   203  			username = auth.ExtractUsernameFromAccessToken(config.ServerDetails.AccessToken)
   204  		}
   205  		password = config.ServerDetails.AccessToken
   206  	}
   207  	// Perform login.
   208  	cmd := &LoginCmd{DockerRegistry: imageRegistry, Username: username, Password: password, containerManager: containerManager}
   209  	err = cmd.RunCmd()
   210  	if exitCode := coreutils.GetExitCode(err, 0, 0, false); exitCode == coreutils.ExitCodeNoError {
   211  		// Login succeeded
   212  		return nil
   213  	}
   214  	log.Debug(containerManager.String()+" login while assuming proxy-less failed:", err)
   215  	indexOfSlash := strings.Index(imageRegistry, "/")
   216  	if indexOfSlash < 0 {
   217  		return errorutils.CheckErrorf(LoginFailureMessage, containerManager.String(), imageRegistry, containerManager.String())
   218  	}
   219  	cmd = &LoginCmd{DockerRegistry: imageRegistry[:indexOfSlash], Username: config.ServerDetails.User, Password: config.ServerDetails.Password}
   220  	err = cmd.RunCmd()
   221  	if err != nil {
   222  		// Login failed for both attempts
   223  		return errorutils.CheckErrorf(LoginFailureMessage,
   224  			containerManager.String(), fmt.Sprintf("%s, %s", imageRegistry, imageRegistry[:indexOfSlash]), containerManager.String()+" "+err.Error())
   225  	}
   226  	// Login succeeded
   227  	return nil
   228  }
   229  
   230  // Version command
   231  // Docker-client provides an API for interacting with the Docker daemon. This cmd should be used for docker client only.
   232  type VersionCmd struct{}
   233  
   234  func (versionCmd *VersionCmd) GetCmd() *exec.Cmd {
   235  	var cmd []string
   236  	cmd = append(cmd, "docker")
   237  	cmd = append(cmd, "version")
   238  	cmd = append(cmd, "--format", "{{.Client.APIVersion}}")
   239  	return exec.Command(cmd[0], cmd[1:]...)
   240  }
   241  
   242  func (versionCmd *VersionCmd) RunCmd() (string, error) {
   243  	command := versionCmd.GetCmd()
   244  	buffer := bytes.NewBuffer([]byte{})
   245  	command.Stderr = buffer
   246  	command.Stdout = buffer
   247  	err := command.Run()
   248  	return buffer.String(), err
   249  }
   250  
   251  func ValidateClientApiVersion() error {
   252  	cmd := &VersionCmd{}
   253  	// 'docker version' may return 1 in case of errors from daemon. We should ignore this kind of errors.
   254  	content, err := cmd.RunCmd()
   255  	content = strings.TrimSpace(content)
   256  	if !ApiVersionRegex.Match([]byte(content)) {
   257  		// The Api version is expected to be 'major.minor'. Anything else should return an error.
   258  		log.Error("The Docker client Api version is expected to be 'major.minor'. The actual output is:", content)
   259  		return errorutils.CheckError(err)
   260  	}
   261  	return utils.ValidateMinimumVersion(utils.DockerApi, content, MinSupportedApiVersion)
   262  }