55import com .github .dockerjava .api .model .AuthConfig ;
66import com .google .common .annotations .VisibleForTesting ;
77import org .apache .commons .lang .StringUtils ;
8- import org .apache .commons .lang .SystemUtils ;
98import org .slf4j .Logger ;
9+ import org .zeroturnaround .exec .InvalidResultException ;
1010import org .zeroturnaround .exec .ProcessExecutor ;
1111
1212import java .io .ByteArrayInputStream ;
1313import java .io .File ;
14+ import java .io .IOException ;
1415import java .util .Base64 ;
16+ import java .util .HashMap ;
1517import java .util .Iterator ;
1618import java .util .Map ;
1719import java .util .concurrent .TimeUnit ;
20+ import java .util .concurrent .TimeoutException ;
1821
1922import static org .apache .commons .lang .StringUtils .isBlank ;
2023import static org .slf4j .LoggerFactory .getLogger ;
@@ -28,14 +31,27 @@ public class RegistryAuthLocator {
2831 private static final Logger log = getLogger (RegistryAuthLocator .class );
2932 private static final String DEFAULT_REGISTRY_NAME = "index.docker.io" ;
3033 private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper ();
34+
3135 private static RegistryAuthLocator instance ;
36+
3237 private final String commandPathPrefix ;
38+ private final String commandExtension ;
3339 private final File configFile ;
3440
41+ /**
42+ * key - credential helper's name
43+ * value - helper's response for "credentials not found" use case
44+ */
45+ private final Map <String , String > CREDENTIALS_HELPERS_NOT_FOUND_MESSAGE_CACHE ;
46+
3547 @ VisibleForTesting
36- RegistryAuthLocator (File configFile , String commandPathPrefix ) {
48+ RegistryAuthLocator (File configFile , String commandPathPrefix , String commandExtension ,
49+ Map <String , String > notFoundMessageHolderReference ) {
3750 this .configFile = configFile ;
3851 this .commandPathPrefix = commandPathPrefix ;
52+ this .commandExtension = commandExtension ;
53+
54+ this .CREDENTIALS_HELPERS_NOT_FOUND_MESSAGE_CACHE = notFoundMessageHolderReference ;
3955 }
4056
4157 /**
@@ -45,6 +61,9 @@ protected RegistryAuthLocator() {
4561 System .getProperty ("user.home" ) + "/.docker" );
4662 this .configFile = new File (dockerConfigLocation + "/config.json" );
4763 this .commandPathPrefix = "" ;
64+ this .commandExtension = "" ;
65+
66+ this .CREDENTIALS_HELPERS_NOT_FOUND_MESSAGE_CACHE = new HashMap <>();
4867 }
4968
5069 public synchronized static RegistryAuthLocator instance () {
@@ -79,12 +98,6 @@ static void setInstance(RegistryAuthLocator overrideInstance) {
7998 */
8099 public AuthConfig lookupAuthConfig (DockerImageName dockerImageName , AuthConfig defaultAuthConfig ) {
81100
82- if (SystemUtils .IS_OS_WINDOWS ) {
83- log .debug ("RegistryAuthLocator is not supported on Windows. Please help test or improve it and update " +
84- "https://github.com/testcontainers/testcontainers-java/issues/756" );
85- return defaultAuthConfig ;
86- }
87-
88101 log .debug ("Looking up auth config for image: {}" , dockerImageName );
89102
90103 log .debug ("RegistryAuthLocator has configFile: {} ({}) and commandPathPrefix: {}" ,
@@ -119,7 +132,7 @@ public AuthConfig lookupAuthConfig(DockerImageName dockerImageName, AuthConfig d
119132 log .debug ("no matching Auth Configs - falling back to defaultAuthConfig [{}]" , toSafeString (defaultAuthConfig ));
120133 // otherwise, defaultAuthConfig should already contain any credentials available
121134 } catch (Exception e ) {
122- log .debug ("Failure when attempting to lookup auth config (dockerImageName: {}, configFile: {}. Falling back to docker-java default behaviour. Exception message: {}" ,
135+ log .warn ("Failure when attempting to lookup auth config (dockerImageName: {}, configFile: {}. Falling back to docker-java default behaviour. Exception message: {}" ,
123136 dockerImageName ,
124137 configFile ,
125138 e .getMessage ());
@@ -189,38 +202,126 @@ private Map.Entry<String, JsonNode> findAuthNode(final JsonNode config, final St
189202 return null ;
190203 }
191204
192- private AuthConfig runCredentialProvider (String hostName , String credHelper ) throws Exception {
193- final String credentialHelperName = commandPathPrefix + "docker-credential-" + credHelper ;
194- String data ;
205+ private AuthConfig runCredentialProvider (String hostName , String helperOrStoreName ) throws Exception {
206+
207+ if (isBlank (hostName )) {
208+ log .debug ("There is no point to locate AuthConfig for blank hostName. Return NULL to allow fallback" );
209+ return null ;
210+ }
211+
212+ final String credentialProgramName = getCredentialProgramName (helperOrStoreName );
213+ final String data ;
195214
196- log .debug ("Executing docker credential helper : {} to locate auth config for: {}" ,
197- credentialHelperName , hostName );
215+ log .debug ("Executing docker credential provider : {} to locate auth config for: {}" ,
216+ credentialProgramName , hostName );
198217
199218 try {
200- data = new ProcessExecutor ()
201- .command (credentialHelperName , "get" )
202- .redirectInput (new ByteArrayInputStream (hostName .getBytes ()))
203- .readOutput (true )
204- .exitValueNormal ()
205- .timeout (30 , TimeUnit .SECONDS )
206- .execute ()
207- .outputUTF8 ()
208- .trim ();
219+ data = runCredentialProgram (hostName , credentialProgramName );
220+ } catch (InvalidResultException e ) {
221+
222+ final String responseErrorMsg = extractCredentialProviderErrorMessage (e );
223+
224+ if (!isBlank (responseErrorMsg )) {
225+ String credentialsNotFoundMsg = getGenericCredentialsNotFoundMsg (credentialProgramName );
226+ if (credentialsNotFoundMsg != null && credentialsNotFoundMsg .equals (responseErrorMsg )) {
227+ log .info ("Credentials not found for host ({}) when using credential helper/store ({})" ,
228+ hostName ,
229+ credentialProgramName );
230+
231+ return null ;
232+ }
233+
234+ log .debug ("Failure running docker credential helper/store ({}) with output '{}'" ,
235+ credentialProgramName , responseErrorMsg );
236+
237+ } else {
238+ log .debug ("Failure running docker credential helper/store ({})" , credentialProgramName );
239+ }
240+
241+ throw e ;
209242 } catch (Exception e ) {
210- log .debug ("Failure running docker credential helper ({})" , credentialHelperName );
243+ log .debug ("Failure running docker credential helper/store ({})" , credentialProgramName );
211244 throw e ;
212245 }
213246
214247 final JsonNode helperResponse = OBJECT_MAPPER .readTree (data );
215- log .debug ("Credential helper provided auth config for: {}" , hostName );
248+ log .debug ("Credential helper/store provided auth config for: {}" , hostName );
216249
217250 return new AuthConfig ()
218251 .withRegistryAddress (helperResponse .at ("/ServerURL" ).asText ())
219252 .withUsername (helperResponse .at ("/Username" ).asText ())
220253 .withPassword (helperResponse .at ("/Secret" ).asText ());
221254 }
222255
256+ private String getCredentialProgramName (String credHelper ) {
257+ return commandPathPrefix + "docker-credential-" + credHelper + commandExtension ;
258+ }
259+
223260 private String effectiveRegistryName (DockerImageName dockerImageName ) {
224261 return StringUtils .defaultIfEmpty (dockerImageName .getRegistry (), DEFAULT_REGISTRY_NAME );
225262 }
263+
264+ private String getGenericCredentialsNotFoundMsg (String credentialHelperName ) {
265+ if (!CREDENTIALS_HELPERS_NOT_FOUND_MESSAGE_CACHE .containsKey (credentialHelperName )) {
266+ String credentialsNotFoundMsg = discoverCredentialsHelperNotFoundMessage (credentialHelperName );
267+ if (!isBlank (credentialsNotFoundMsg )) {
268+ CREDENTIALS_HELPERS_NOT_FOUND_MESSAGE_CACHE .put (credentialHelperName , credentialsNotFoundMsg );
269+ }
270+ }
271+
272+ return CREDENTIALS_HELPERS_NOT_FOUND_MESSAGE_CACHE .get (credentialHelperName );
273+ }
274+
275+ private String discoverCredentialsHelperNotFoundMessage (String credentialHelperName ) {
276+ // will do fake call to given credential helper to find out with which message
277+ // it response when there are no credentials for given hostName
278+
279+ // hostName should be valid, but most probably not existing
280+ // IF its not enough, then should probably run 'list' command first to be sure...
281+ final String notExistentFakeHostName = "https://not.a.real.registry/url" ;
282+
283+ String credentialsNotFoundMsg = null ;
284+ try {
285+ runCredentialProgram (notExistentFakeHostName , credentialHelperName );
286+
287+ // should not reach here
288+ log .warn ("Failure running docker credential helper ({}) with fake call, expected 'credentials not found' response" ,
289+ credentialHelperName );
290+ } catch (Exception e ) {
291+ if (e instanceof InvalidResultException ) {
292+ credentialsNotFoundMsg = extractCredentialProviderErrorMessage ((InvalidResultException )e );
293+ }
294+
295+ if (isBlank (credentialsNotFoundMsg )) {
296+ log .warn ("Failure running docker credential helper ({}) with fake call, expected 'credentials not found' response. Exception message: {}" ,
297+ credentialHelperName ,
298+ e .getMessage ());
299+ } else {
300+ log .debug ("Got credentials not found error message from docker credential helper - {}" , credentialsNotFoundMsg );
301+ }
302+ }
303+
304+ return credentialsNotFoundMsg ;
305+ }
306+
307+ private String extractCredentialProviderErrorMessage (InvalidResultException invalidResultEx ) {
308+ if (invalidResultEx .getResult () != null && invalidResultEx .getResult ().hasOutput ()) {
309+ return invalidResultEx .getResult ().outputString ().trim ();
310+ }
311+ return null ;
312+ }
313+
314+ private String runCredentialProgram (String hostName , String credentialHelperName )
315+ throws InvalidResultException , InterruptedException , TimeoutException , IOException {
316+
317+ return new ProcessExecutor ()
318+ .command (credentialHelperName , "get" )
319+ .redirectInput (new ByteArrayInputStream (hostName .getBytes ()))
320+ .readOutput (true )
321+ .exitValueNormal ()
322+ .timeout (30 , TimeUnit .SECONDS )
323+ .execute ()
324+ .outputUTF8 ()
325+ .trim ();
326+ }
226327}
0 commit comments