github.com/jfrog/jfrog-cli-go@v1.22.1-0.20200318093948-4826ef344ffd/artifactory/utils/docker/docker.go (about)

     1  package docker
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"io"
     7  	"os/exec"
     8  	"path"
     9  	"regexp"
    10  	"strings"
    11  
    12  	gofrogcmd "github.com/jfrog/gofrog/io"
    13  	"github.com/jfrog/jfrog-cli-go/utils/cliutils"
    14  	"github.com/jfrog/jfrog-cli-go/utils/config"
    15  	"github.com/jfrog/jfrog-client-go/artifactory"
    16  	"github.com/jfrog/jfrog-client-go/auth"
    17  	clientConfig "github.com/jfrog/jfrog-client-go/config"
    18  	"github.com/jfrog/jfrog-client-go/utils/errorutils"
    19  	"github.com/jfrog/jfrog-client-go/utils/log"
    20  	"github.com/jfrog/jfrog-client-go/utils/version"
    21  )
    22  
    23  // Search for docker API version format pattern e.g. 1.40
    24  var ApiVersionRegex = regexp.MustCompile(`^(\d+)\.(\d+)$`)
    25  
    26  // Docker API version 1.31 is compatible with Docker version 17.07.0, according to https://docs.docker.com/engine/api/#api-version-matrix
    27  const MinSupportedApiVersion string = "1.31"
    28  
    29  // Docker login error message
    30  const DockerLoginFailureMessage string = "Docker login failed for: %s.\nDocker image must be in the form: docker-registry-domain/path-in-repository/image-name:version."
    31  
    32  func New(imageTag string) Image {
    33  	return &image{tag: imageTag}
    34  }
    35  
    36  // Docker image
    37  type Image interface {
    38  	Push() error
    39  	Id() (string, error)
    40  	ParentId() (string, error)
    41  	Manifest() (string, error)
    42  	Tag() string
    43  	Path() string
    44  	Name() string
    45  	Pull() error
    46  }
    47  
    48  // Internal implementation of docker image
    49  type image struct {
    50  	tag string
    51  }
    52  
    53  type DockerLoginConfig struct {
    54  	ArtifactoryDetails *config.ArtifactoryDetails
    55  }
    56  
    57  // Push docker image
    58  func (image *image) Push() error {
    59  	cmd := &pushCmd{image: image}
    60  	return gofrogcmd.RunCmd(cmd)
    61  }
    62  
    63  // Get docker image tag
    64  func (image *image) Tag() string {
    65  	return image.tag
    66  }
    67  
    68  // Get docker image ID
    69  func (image *image) Id() (string, error) {
    70  	cmd := &getImageIdCmd{image: image}
    71  	content, err := gofrogcmd.RunCmdOutput(cmd)
    72  	return strings.Trim(content, "\n"), err
    73  }
    74  
    75  // Get docker parent image ID
    76  func (image *image) ParentId() (string, error) {
    77  	cmd := &getParentId{image: image}
    78  	content, err := gofrogcmd.RunCmdOutput(cmd)
    79  	return strings.Trim(content, "\n"), err
    80  }
    81  
    82  // Get docker image relative path in Artifactory
    83  func (image *image) Path() string {
    84  	indexOfFirstSlash := strings.Index(image.tag, "/")
    85  	indexOfLastColon := strings.LastIndex(image.tag, ":")
    86  
    87  	if indexOfLastColon < 0 || indexOfLastColon < indexOfFirstSlash {
    88  		return path.Join(image.tag[indexOfFirstSlash:], "latest")
    89  	}
    90  	return path.Join(image.tag[indexOfFirstSlash:indexOfLastColon], image.tag[indexOfLastColon+1:])
    91  }
    92  
    93  // Get docker image manifest
    94  func (image *image) Manifest() (string, error) {
    95  	cmd := &getImageManifestCmd{image: image}
    96  	content, err := gofrogcmd.RunCmdOutput(cmd)
    97  	return content, err
    98  }
    99  
   100  // Get docker image name
   101  func (image *image) Name() string {
   102  	indexOfLastSlash := strings.LastIndex(image.tag, "/")
   103  	indexOfLastColon := strings.LastIndex(image.tag, ":")
   104  
   105  	if indexOfLastColon < 0 || indexOfLastColon < indexOfLastSlash {
   106  		return image.tag[indexOfLastSlash+1:] + ":latest"
   107  	}
   108  	return image.tag[indexOfLastSlash+1:]
   109  }
   110  
   111  // Pull docker image
   112  func (image *image) Pull() error {
   113  	cmd := &pullCmd{image: image}
   114  	return gofrogcmd.RunCmd(cmd)
   115  }
   116  
   117  // Image push command
   118  type pushCmd struct {
   119  	image *image
   120  }
   121  
   122  func (pushCmd *pushCmd) GetCmd() *exec.Cmd {
   123  	var cmd []string
   124  	cmd = append(cmd, "docker")
   125  	cmd = append(cmd, "push")
   126  	cmd = append(cmd, pushCmd.image.tag)
   127  	return exec.Command(cmd[0], cmd[1:]...)
   128  }
   129  
   130  func (pushCmd *pushCmd) GetEnv() map[string]string {
   131  	return map[string]string{}
   132  }
   133  
   134  func (pushCmd *pushCmd) GetStdWriter() io.WriteCloser {
   135  	return nil
   136  }
   137  func (pushCmd *pushCmd) GetErrWriter() io.WriteCloser {
   138  	return nil
   139  }
   140  
   141  // Image get image id command
   142  type getImageIdCmd struct {
   143  	image *image
   144  }
   145  
   146  func (getImageId *getImageIdCmd) GetCmd() *exec.Cmd {
   147  	var cmd []string
   148  	cmd = append(cmd, "docker")
   149  	cmd = append(cmd, "images")
   150  	cmd = append(cmd, "--format", "{{.ID}}")
   151  	cmd = append(cmd, "--no-trunc")
   152  	cmd = append(cmd, getImageId.image.tag)
   153  	return exec.Command(cmd[0], cmd[1:]...)
   154  }
   155  
   156  func (getImageId *getImageIdCmd) GetEnv() map[string]string {
   157  	return map[string]string{}
   158  }
   159  
   160  func (getImageId *getImageIdCmd) GetStdWriter() io.WriteCloser {
   161  	return nil
   162  }
   163  
   164  func (getImageId *getImageIdCmd) GetErrWriter() io.WriteCloser {
   165  	return nil
   166  }
   167  
   168  type Manifest struct {
   169  	Descriptor       Descriptor       `json:"descriptor"`
   170  	SchemaV2Manifest SchemaV2Manifest `json:"SchemaV2Manifest"`
   171  }
   172  
   173  type Descriptor struct {
   174  	Digest *string `json:"digest"`
   175  }
   176  
   177  type SchemaV2Manifest struct {
   178  	Config Config `json:"config"`
   179  }
   180  
   181  type Config struct {
   182  	Digest *string `json:"digest"`
   183  }
   184  
   185  // Image get parent image id command
   186  type getParentId struct {
   187  	image *image
   188  }
   189  
   190  func (getImageId *getParentId) GetCmd() *exec.Cmd {
   191  	var cmd []string
   192  	cmd = append(cmd, "docker")
   193  	cmd = append(cmd, "inspect")
   194  	cmd = append(cmd, "--format", "{{.Parent}}")
   195  	cmd = append(cmd, getImageId.image.tag)
   196  	return exec.Command(cmd[0], cmd[1:]...)
   197  }
   198  
   199  func (getImageId *getParentId) GetEnv() map[string]string {
   200  	return map[string]string{}
   201  }
   202  
   203  func (getImageId *getParentId) GetStdWriter() io.WriteCloser {
   204  	return nil
   205  }
   206  
   207  func (getImageId *getParentId) GetErrWriter() io.WriteCloser {
   208  	return nil
   209  }
   210  
   211  // Get image manifest command
   212  type getImageManifestCmd struct {
   213  	image *image
   214  }
   215  
   216  func (getImageManifest *getImageManifestCmd) GetCmd() *exec.Cmd {
   217  	var cmd []string
   218  	cmd = append(cmd, "docker")
   219  	cmd = append(cmd, "manifest")
   220  	cmd = append(cmd, "inspect")
   221  	cmd = append(cmd, getImageManifest.image.tag)
   222  	cmd = append(cmd, "--verbose")
   223  	return exec.Command(cmd[0], cmd[1:]...)
   224  }
   225  
   226  func (getImageManifest *getImageManifestCmd) GetEnv() map[string]string {
   227  	return map[string]string{}
   228  }
   229  
   230  func (getImageManifest *getImageManifestCmd) GetStdWriter() io.WriteCloser {
   231  	return nil
   232  }
   233  
   234  func (getImageManifest *getImageManifestCmd) GetErrWriter() io.WriteCloser {
   235  	return nil
   236  }
   237  
   238  // Get docker registry from tag
   239  func ResolveRegistryFromTag(imageTag string) (string, error) {
   240  	indexOfFirstSlash := strings.Index(imageTag, "/")
   241  	if indexOfFirstSlash < 0 {
   242  		err := errorutils.CheckError(errors.New("Invalid image tag received for pushing to Artifactory - tag does not include a slash."))
   243  		return "", err
   244  	}
   245  
   246  	indexOfSecondSlash := strings.Index(imageTag[indexOfFirstSlash+1:], "/")
   247  	// Reverse proxy Artifactory
   248  	if indexOfSecondSlash < 0 {
   249  		return imageTag[:indexOfFirstSlash], nil
   250  	}
   251  	// Can be reverse proxy or proxy-less Artifactory
   252  	indexOfSecondSlash += indexOfFirstSlash + 1
   253  	return imageTag[:indexOfSecondSlash], nil
   254  }
   255  
   256  // Login command
   257  type LoginCmd struct {
   258  	DockerRegistry string
   259  	Username       string
   260  	Password       string
   261  }
   262  
   263  func (loginCmd *LoginCmd) GetCmd() *exec.Cmd {
   264  	if cliutils.IsWindows() {
   265  		return exec.Command("cmd", "/C", "echo", "%DOCKER_PASS%|", "docker", "login", loginCmd.DockerRegistry, "--username", loginCmd.Username, "--password-stdin")
   266  	}
   267  	cmd := "echo $DOCKER_PASS " + fmt.Sprintf(`| docker login %s --username="%s" --password-stdin`, loginCmd.DockerRegistry, loginCmd.Username)
   268  	return exec.Command("sh", "-c", cmd)
   269  }
   270  
   271  func (loginCmd *LoginCmd) GetEnv() map[string]string {
   272  	return map[string]string{"DOCKER_PASS": loginCmd.Password}
   273  }
   274  
   275  func (loginCmd *LoginCmd) GetStdWriter() io.WriteCloser {
   276  	return nil
   277  }
   278  
   279  func (loginCmd *LoginCmd) GetErrWriter() io.WriteCloser {
   280  	return nil
   281  }
   282  
   283  // Image pull command
   284  type pullCmd struct {
   285  	image *image
   286  }
   287  
   288  func (pullCmd *pullCmd) GetCmd() *exec.Cmd {
   289  	var cmd []string
   290  	cmd = append(cmd, "docker")
   291  	cmd = append(cmd, "pull")
   292  	cmd = append(cmd, pullCmd.image.tag)
   293  	return exec.Command(cmd[0], cmd[1:]...)
   294  }
   295  
   296  func (pullCmd *pullCmd) GetEnv() map[string]string {
   297  	return map[string]string{}
   298  }
   299  
   300  func (pullCmd *pullCmd) GetStdWriter() io.WriteCloser {
   301  	return nil
   302  }
   303  
   304  func (pullCmd *pullCmd) GetErrWriter() io.WriteCloser {
   305  	return nil
   306  }
   307  
   308  func CreateServiceManager(artDetails *config.ArtifactoryDetails, threads int) (*artifactory.ArtifactoryServicesManager, error) {
   309  	certPath, err := cliutils.GetJfrogSecurityDir()
   310  	if err != nil {
   311  		return nil, err
   312  	}
   313  	artAuth, err := artDetails.CreateArtAuthConfig()
   314  	if err != nil {
   315  		return nil, err
   316  	}
   317  
   318  	configBuilder := clientConfig.NewConfigBuilder().
   319  		SetArtDetails(artAuth).
   320  		SetCertificatesPath(certPath).
   321  		SetInsecureTls(artDetails.InsecureTls).
   322  		SetThreads(threads)
   323  
   324  	if threads != 0 {
   325  		configBuilder.SetThreads(threads)
   326  	}
   327  
   328  	serviceConfig, err := configBuilder.Build()
   329  	return artifactory.New(&artAuth, serviceConfig)
   330  }
   331  
   332  // First will try to login assuming a proxy-less tag (e.g. "registry-address/docker-repo/image:ver").
   333  // If fails, we will try assuming a reverse proxy tag (e.g. "registry-address-docker-repo/image:ver").
   334  func DockerLogin(imageTag string, config *DockerLoginConfig) error {
   335  	imageRegistry, err := ResolveRegistryFromTag(imageTag)
   336  	if err != nil {
   337  		return err
   338  	}
   339  
   340  	username := config.ArtifactoryDetails.User
   341  	password := config.ArtifactoryDetails.Password
   342  	// If access-token exists, perform login with it.
   343  	if config.ArtifactoryDetails.AccessToken != "" {
   344  		log.Debug("Using access-token details in docker-login command.")
   345  		username, err = auth.ExtractUsernameFromAccessToken(config.ArtifactoryDetails.AccessToken)
   346  		if err != nil {
   347  			return err
   348  		}
   349  		password = config.ArtifactoryDetails.AccessToken
   350  	}
   351  
   352  	// Perform login.
   353  	cmd := &LoginCmd{DockerRegistry: imageRegistry, Username: username, Password: password}
   354  	err = gofrogcmd.RunCmd(cmd)
   355  
   356  	if exitCode := cliutils.GetExitCode(err, 0, 0, false); exitCode == cliutils.ExitCodeNoError {
   357  		// Login succeeded
   358  		return nil
   359  	}
   360  	log.Debug("Docker login while assuming proxy-less failed:", err)
   361  
   362  	indexOfSlash := strings.Index(imageRegistry, "/")
   363  	if indexOfSlash < 0 {
   364  		return errorutils.CheckError(errors.New(fmt.Sprintf(DockerLoginFailureMessage, imageRegistry)))
   365  	}
   366  
   367  	cmd = &LoginCmd{DockerRegistry: imageRegistry[:indexOfSlash], Username: config.ArtifactoryDetails.User, Password: config.ArtifactoryDetails.Password}
   368  	err = gofrogcmd.RunCmd(cmd)
   369  	if err != nil {
   370  		// Login failed for both attempts
   371  		return errorutils.CheckError(errors.New(fmt.Sprintf(DockerLoginFailureMessage,
   372  			fmt.Sprintf("%s, %s", imageRegistry, imageRegistry[:indexOfSlash])) + " " + err.Error()))
   373  	}
   374  
   375  	// Login succeeded
   376  	return nil
   377  }
   378  
   379  // Version command
   380  type VersionCmd struct{}
   381  
   382  func (versionCmd *VersionCmd) GetCmd() *exec.Cmd {
   383  	var cmd []string
   384  	cmd = append(cmd, "docker")
   385  	cmd = append(cmd, "version")
   386  	cmd = append(cmd, "--format", "{{.Client.APIVersion}}")
   387  	return exec.Command(cmd[0], cmd[1:]...)
   388  }
   389  
   390  func (versionCmd *VersionCmd) GetEnv() map[string]string {
   391  	return map[string]string{}
   392  }
   393  
   394  func (versionCmd *VersionCmd) GetStdWriter() io.WriteCloser {
   395  	return nil
   396  }
   397  
   398  func (versionCmd *VersionCmd) GetErrWriter() io.WriteCloser {
   399  	return nil
   400  }
   401  
   402  func ValidateClientApiVersion() error {
   403  	cmd := &VersionCmd{}
   404  	// 'docker version' may return 1 in case of errors from daemon. We should ignore this kind of errors.
   405  	content, err := gofrogcmd.RunCmdOutput(cmd)
   406  	content = strings.TrimSpace(content)
   407  	if !ApiVersionRegex.Match([]byte(content)) {
   408  		// The Api version is expected to be 'major.minor'. Anything else should return an error.
   409  		return errorutils.CheckError(err)
   410  	}
   411  	if !IsCompatibleApiVersion(content) {
   412  		return errorutils.CheckError(errors.New("This operation requires Docker API version " + MinSupportedApiVersion + " or higher."))
   413  	}
   414  	return nil
   415  }
   416  
   417  func IsCompatibleApiVersion(dockerOutput string) bool {
   418  	currentVersion := version.NewVersion(dockerOutput)
   419  	return currentVersion.AtLeast(MinSupportedApiVersion)
   420  }