1package pull 2 3import ( 4 "context" 5 "fmt" 6 "regexp" 7 "strings" 8 "sync" 9 10 cli "github.com/docker/cli/cli/config/types" 11 "github.com/docker/docker/api/types" 12 13 "gitlab.com/gitlab-org/gitlab-runner/common" 14 "gitlab.com/gitlab-org/gitlab-runner/helpers/docker" 15 "gitlab.com/gitlab-org/gitlab-runner/helpers/docker/auth" 16) 17 18type Manager interface { 19 GetDockerImage(imageName string) (*types.ImageInspect, error) 20} 21 22type ManagerConfig struct { 23 DockerConfig *common.DockerConfig 24 AuthConfig string 25 ShellUser string 26 Credentials []common.Credentials 27} 28 29type pullLogger interface { 30 Debugln(args ...interface{}) 31 Infoln(args ...interface{}) 32 Warningln(args ...interface{}) 33 Println(args ...interface{}) 34} 35 36type manager struct { 37 usedImages map[string]string 38 usedImagesLock sync.RWMutex 39 40 context context.Context 41 config ManagerConfig 42 client docker.Client 43 onPullImageHookFunc func() 44 45 logger pullLogger 46} 47 48func NewManager( 49 ctx context.Context, 50 logger pullLogger, 51 config ManagerConfig, 52 client docker.Client, 53 onPullImageHookFunc func(), 54) Manager { 55 return &manager{ 56 context: ctx, 57 client: client, 58 config: config, 59 logger: logger, 60 onPullImageHookFunc: onPullImageHookFunc, 61 } 62} 63 64func (m *manager) GetDockerImage(imageName string) (*types.ImageInspect, error) { 65 pullPolicies, err := m.config.DockerConfig.GetPullPolicies() 66 if err != nil { 67 return nil, err 68 } 69 70 var imageErr error 71 for idx, pullPolicy := range pullPolicies { 72 attempt := 1 + idx 73 if attempt > 1 { 74 m.logger.Infoln(fmt.Sprintf("Attempt #%d: Trying %q pull policy", attempt, pullPolicy)) 75 } 76 77 var img *types.ImageInspect 78 img, imageErr = m.getImageUsingPullPolicy(imageName, pullPolicy) 79 if imageErr != nil { 80 m.logger.Warningln(fmt.Sprintf("Failed to pull image with policy %q: %v", pullPolicy, imageErr)) 81 continue 82 } 83 84 m.markImageAsUsed(imageName, img) 85 86 return img, nil 87 } 88 89 return nil, fmt.Errorf( 90 "failed to pull image %q with specified policies %v: %w", 91 imageName, 92 pullPolicies, 93 imageErr, 94 ) 95} 96 97func (m *manager) wasImageUsed(imageName, imageID string) bool { 98 m.usedImagesLock.RLock() 99 defer m.usedImagesLock.RUnlock() 100 101 return m.usedImages[imageName] == imageID 102} 103 104func (m *manager) markImageAsUsed(imageName string, image *types.ImageInspect) { 105 m.usedImagesLock.Lock() 106 defer m.usedImagesLock.Unlock() 107 108 if m.usedImages == nil { 109 m.usedImages = make(map[string]string) 110 } 111 m.usedImages[imageName] = image.ID 112 113 if imageName == image.ID { 114 return 115 } 116 117 if len(image.RepoDigests) > 0 { 118 m.logger.Println("Using docker image", image.ID, "for", imageName, "with digest", image.RepoDigests[0], "...") 119 } else { 120 m.logger.Println("Using docker image", image.ID, "for", imageName, "...") 121 } 122} 123 124func (m *manager) getImageUsingPullPolicy( 125 imageName string, 126 pullPolicy common.DockerPullPolicy, 127) (*types.ImageInspect, error) { 128 m.logger.Debugln("Looking for image", imageName, "...") 129 existingImage, _, err := m.client.ImageInspectWithRaw(m.context, imageName) 130 131 // Return early if we already used that image 132 if err == nil && m.wasImageUsed(imageName, existingImage.ID) { 133 return &existingImage, nil 134 } 135 136 // If never is specified then we return what inspect did return 137 if pullPolicy == common.PullPolicyNever { 138 return &existingImage, err 139 } 140 141 if err == nil { 142 // Don't pull image that is passed by ID 143 if existingImage.ID == imageName { 144 return &existingImage, nil 145 } 146 147 // If not-present is specified 148 if pullPolicy == common.PullPolicyIfNotPresent { 149 m.logger.Println(fmt.Sprintf("Using locally found image version due to %q pull policy", pullPolicy)) 150 return &existingImage, err 151 } 152 } 153 154 authConfig, err := m.resolveAuthConfigForImage(imageName) 155 if err != nil { 156 return nil, err 157 } 158 159 return m.pullDockerImage(imageName, authConfig) 160} 161 162func (m *manager) resolveAuthConfigForImage(imageName string) (*cli.AuthConfig, error) { 163 registryInfo, err := auth.ResolveConfigForImage( 164 imageName, 165 m.config.AuthConfig, 166 m.config.ShellUser, 167 m.config.Credentials, 168 ) 169 if err != nil { 170 return nil, err 171 } 172 173 if registryInfo == nil { 174 m.logger.Debugln(fmt.Sprintf("No credentials found for %v", imageName)) 175 return nil, nil 176 } 177 178 authConfig := ®istryInfo.AuthConfig 179 m.logger.Println(fmt.Sprintf("Authenticating with credentials from %v", registryInfo.Source)) 180 m.logger.Debugln(fmt.Sprintf( 181 "Using %v to connect to %v in order to resolve %v...", 182 authConfig.Username, 183 authConfig.ServerAddress, 184 imageName, 185 )) 186 return authConfig, nil 187} 188 189func (m *manager) pullDockerImage(imageName string, ac *cli.AuthConfig) (*types.ImageInspect, error) { 190 if m.onPullImageHookFunc != nil { 191 m.onPullImageHookFunc() 192 } 193 m.logger.Println("Pulling docker image", imageName, "...") 194 195 ref := imageName 196 // Add :latest to limit the download results 197 if !strings.ContainsAny(ref, ":@") { 198 ref += ":latest" 199 } 200 201 options := types.ImagePullOptions{} 202 options.RegistryAuth, _ = auth.EncodeConfig(ac) 203 204 errorRegexp := regexp.MustCompile("(repository does not exist|not found)") 205 if err := m.client.ImagePullBlocking(m.context, ref, options); err != nil { 206 if errorRegexp.MatchString(err.Error()) { 207 return nil, &common.BuildError{Inner: err, FailureReason: common.ScriptFailure} 208 } 209 return nil, err 210 } 211 212 image, _, err := m.client.ImageInspectWithRaw(m.context, imageName) 213 return &image, err 214} 215