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 }