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 }