/******************************************************************************* * Copyright (c) 2016 Red Hat. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Red Hat - Initial Contribution *******************************************************************************/ package org.eclipse.linuxtools.docker.core; import static javax.ws.rs.HttpMethod.GET; import static javax.ws.rs.core.MediaType.APPLICATION_JSON_TYPE; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.stream.Collectors; import javax.ws.rs.client.Client; import javax.ws.rs.client.ClientBuilder; import javax.ws.rs.client.WebTarget; import javax.ws.rs.core.GenericType; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; import org.eclipse.linuxtools.internal.docker.core.DockerImageSearchResult; import org.eclipse.linuxtools.internal.docker.core.ImageSearchResultV1; import org.eclipse.linuxtools.internal.docker.core.ImageSearchResultV2; import org.eclipse.linuxtools.internal.docker.core.OAuth2Utils; import org.eclipse.linuxtools.internal.docker.core.OAuth2Utils.BearerTokenResponse; import org.eclipse.linuxtools.internal.docker.core.RepositoryTag; import org.eclipse.linuxtools.internal.docker.core.RepositoryTagV2; import org.eclipse.osgi.util.NLS; import org.glassfish.jersey.client.ClientConfig; import org.glassfish.jersey.jackson.JacksonFeature; import com.spotify.docker.client.ObjectMapperProvider; import com.spotify.docker.client.messages.ImageSearchResult; /** * @since 2.0 */ public abstract class AbstractRegistry implements IRegistry { /** Aliases of the Docker Hub hostname. */ public static final String[] DOCKERHUB_REGISTRY_ALIASES = new String[] { "registry.hub.docker.com", //$NON-NLS-1$ "index.docker.io" //$NON-NLS-1$ }; private static final ClientConfig DEFAULT_CONFIG = new ClientConfig( ObjectMapperProvider.class, JacksonFeature.class); public static final String DOCKERHUB_REGISTRY = "https://index.docker.io"; //$NON-NLS-1$ // Cache the URL for searches to avoid excessive calls private String cachedHTTPServerAddress; // Cache the result of isVersion2 to avoid excessive calls private Boolean isV2 = null; @Override public abstract String getServerAddress(); /** * * @return the server host (and optional port) to prepend to an image name * when pushing or pulling */ // TODO: add this method in the IRegistry interface public abstract String getServerHost(); /** * @return <code>true</code> if this {@link IRegistry} is for Docker Hub, * <code>false</code> otherwise. */ // TODO: add this method in the IRegistry interface public abstract boolean isDockerHubRegistry(); /** * @return <code>true</code> if this {@link IRegistry} includes credentials, * <code>false</code> otherwise. */ // TODO: add this method in the IRegistry interface public abstract boolean isAuthProvided(); private String getHTTPServerAddress() { if (cachedHTTPServerAddress != null) { return cachedHTTPServerAddress; /* * This is wrong because serverAddress prefixed with http won't work * for push/pull API but let's support this. */ } else if (getServerAddress().startsWith("http")) { //$NON-NLS-1$ return getServerAddress(); } // We haven't cached the result, so let's evaluate String[] versions = new String[] { "v1", "v2" }; //$NON-NLS-1$ //$NON-NLS-2$ String[] schemes = new String[] { "http://", "https://" }; //$NON-NLS-1$ //$NON-NLS-2$ final ClientConfig DEFAULT_CONFIG = new ClientConfig( ObjectMapperProvider.class, JacksonFeature.class); final Client client = ClientBuilder.newClient(DEFAULT_CONFIG); for (String scheme : schemes) { for (String ver : versions) { String url = scheme + getServerAddress(); WebTarget queryServer = client.target(url).path(ver); try { enableDockerAuthenticator(); Response resp = queryServer.request(APPLICATION_JSON_TYPE) .async().method(GET).get(); int code = resp.getStatus(); if (code >= 200 && code < 300) { cachedHTTPServerAddress = url; return url; } } catch (InterruptedException | ExecutionException e) { } finally { restoreAuthenticator(); } } } // URL is probably wrong return "http://" + getServerAddress(); //$NON-NLS-1$ } @Override public List<IDockerImageSearchResult> getImages(String term) throws DockerException { final Client client = ClientBuilder.newClient(DEFAULT_CONFIG); List<IDockerImageSearchResult> result = new ArrayList<>(); WebTarget queryImagesResource; if (isVersion2()) { final GenericType<ImageSearchResultV2> IMAGE_SEARCH_RESULT_LIST = new GenericType<ImageSearchResultV2>() { }; ImageSearchResultV2 cisr = null; queryImagesResource = client.target(getHTTPServerAddress()) .path("v2").path("_catalog"); //$NON-NLS-1$ //$NON-NLS-2$ try { cisr = queryImagesResource.request(APPLICATION_JSON_TYPE) .async().method(GET, IMAGE_SEARCH_RESULT_LIST).get(); } catch (InterruptedException | ExecutionException e) { throw new DockerException(e); } List<ImageSearchResult> tmp = cisr.getRepositories().stream() .filter(e -> e.getName().contains(term)) .collect(Collectors.toList()); result.addAll(tmp.stream() .map(r -> new DockerImageSearchResult(r.getDescription(), r.isOfficial(), r.isAutomated(), r.getName(), r.getStarCount())) .collect(Collectors.toList())); } else { ImageSearchResultV1 pisr = null; final GenericType<ImageSearchResultV1> IMAGE_SEARCH_RESULT_LIST = new GenericType<ImageSearchResultV1>() { }; int page = 0; try { while (pisr == null || pisr.getPage() < pisr.getTotalPages()) { page++; queryImagesResource = client.target(getHTTPServerAddress()) .path("v1").path("search") //$NON-NLS-1$ //$NON-NLS-2$ .queryParam("q", term) //$NON-NLS-1$ .queryParam("page", page); //$NON-NLS-1$ pisr = queryImagesResource.request(APPLICATION_JSON_TYPE) .async().method(GET, IMAGE_SEARCH_RESULT_LIST) .get(); List<ImageSearchResult> tmp = pisr.getResult(); result.addAll(tmp.stream() .map(r -> new DockerImageSearchResult( r.getDescription(), r.isOfficial(), r.isAutomated(), r.getName(), r.getStarCount())) .collect(Collectors.toList())); } } catch (InterruptedException | ExecutionException e) { throw new DockerException(e); } } return result; } @Override public List<IRepositoryTag> getTags(String repository) throws DockerException { final Client client = ClientBuilder.newClient(DEFAULT_CONFIG); try { if (isVersion2()) { return retrieveTagsFromRegistryV2(client, repository); } else if (isDockerHubRegistry()) { return retrieveTagsFromDockerHub(client, repository); } else { return retrieveTagsFromRegistryV1(client, repository); } } catch (InterruptedException | ExecutionException e) { throw new DockerException(e); } } /** * Retrieves the list of tags for a given repository, assuming that the * target registry is Docker Hub. * * @param client * the client to use * @param repository * the repository to look-up * @return the list of tags for the given repository * @throws CancellationException * if the computation was cancelled */ private List<IRepositoryTag> retrieveTagsFromDockerHub(final Client client, final String repository) throws DockerException { try { // if the given repository is an official repository, we need to // prepend // with 'library' final String repoName = repository.contains("/") ? repository //$NON-NLS-1$ : "library/" + repository; //$NON-NLS-1$ // Docker Hub Registry may require a Bearer token which needs to be // retrieved if the initial request returned a 401/Forbidden // response. // In that case, the "Www-Authenticate" response header provides the // information needed to obtain a token. // attempt to query the registry without a bearer token final WebTarget queryTagsResource = client .target(getHTTPServerAddress()).path("v2") //$NON-NLS-1$ .path(repoName).path("tags").path("list"); //$NON-NLS-1$ //$NON-NLS-2$ // return queryTagsResource.request(APPLICATION_JSON_TYPE).async() // .method(GET, REPOSITORY_TAGS_RESULT_LIST).get(); final Response response = queryTagsResource .request(APPLICATION_JSON_TYPE).async().get() .get(10, TimeUnit.SECONDS); if (response.getStatus() == 200) { return response.readEntity(RepositoryTagV2.class).getTags(); } else if (response.getStatus() != 401) { // anything but // "Unauthorized" throw new DockerException( NLS.bind(Messages.ImageTagsList_failure, repository, response.readEntity(String.class))); } // for "Unauthorized response, let's get a Bearer token and try // again final String wwwAuthenticateResponseHeader = response .getHeaderString("Www-Authenticate"); //$NON-NLS-1$ // parse the header which should have the following form: // Bearer // realm="https://auth.docker.io/token",service="registry.docker.io",scope="repository:jboss/wildfly:pull final Map<String, String> authenticateInfo = OAuth2Utils .parseWwwAuthenticateHeader(wwwAuthenticateResponseHeader); if (authenticateInfo == null || !authenticateInfo.containsKey("realm") //$NON-NLS-1$ || !authenticateInfo.containsKey("service") //$NON-NLS-1$ || !authenticateInfo.containsKey("scope")) { //$NON-NLS-1$ throw new DockerException(NLS.bind( Messages.ImageTagsList_failure_invalidWwwAuthenticateFormat, repository)); } // now, call the auth service to obtain a Bearer token: final String realm = authenticateInfo.get("realm"); //$NON-NLS-1$ final String service = authenticateInfo.get("service"); //$NON-NLS-1$ final String scope = authenticateInfo.get("scope"); //$NON-NLS-1$ final WebTarget bearerTokenRetrievalTarget = client.target(realm) .queryParam("service", service) //$NON-NLS-1$ .queryParam("scope", scope); //$NON-NLS-1$ final BearerTokenResponse bearerTokenRetrievalResponse = bearerTokenRetrievalTarget .request(APPLICATION_JSON_TYPE).async() .get(BearerTokenResponse.class).get(10, TimeUnit.SECONDS); // finally, perform the same request, using the Bearer token: final WebTarget queryTagsResourceWithBearerTokenTarget = client .target(getHTTPServerAddress()).path("v2") //$NON-NLS-1$ .path(repoName).path("tags").path("list"); //$NON-NLS-1$ //$NON-NLS-2$ return queryTagsResourceWithBearerTokenTarget .request(APPLICATION_JSON_TYPE) .header("Authorization", //$NON-NLS-1$ "Bearer " + bearerTokenRetrievalResponse.getToken()) //$NON-NLS-1$ .async().get(RepositoryTagV2.class) .get(10, TimeUnit.SECONDS) .getTags(); } catch (TimeoutException | ExecutionException | InterruptedException e) { throw new DockerException(NLS.bind(Messages.ImageTagsList_failure, repository, e.getMessage()), e); } } /** * Retrieves the list of tags for a given repository, assuming that the * target registry is a registry v2 instance. * * @param client * the client to use * @param repository * the repository to look-up * @return the list of tags for the given repository * @throws CancellationException * - if the computation was cancelled * @throws ExecutionException * - if the computation threw an exception * @throws InterruptedException * - if the current thread was interrupted while waiting */ private List<IRepositoryTag> retrieveTagsFromRegistryV1(final Client client, final String repository) throws InterruptedException, ExecutionException { final GenericType<Map<String, String>> REPOSITORY_TAGS_RESULT_LIST = new GenericType<Map<String, String>>() { }; final WebTarget queryTagsResource = client .target(getHTTPServerAddress()).path("v1") //$NON-NLS-1$ .path("repositories").path(repository).path("tags"); //$NON-NLS-1$ //$NON-NLS-2$ return queryTagsResource.request(APPLICATION_JSON_TYPE).async() .method(GET, REPOSITORY_TAGS_RESULT_LIST).get().entrySet() .stream().map(e -> new RepositoryTag(e.getKey(), e.getValue())) .collect(Collectors.toList()); } /** * Retrieves the list of tags for a given repository, assuming that the * target registry is a registry v1 instance. * * @param client * the client to use * @param repository * the repository to look-up * @return the list of tags for the given repository * @throws CancellationException * - if the computation was cancelled * @throws ExecutionException * - if the computation threw an exception * @throws InterruptedException * - if the current thread was interrupted while waiting */ private List<IRepositoryTag> retrieveTagsFromRegistryV2(final Client client, final String repository) throws InterruptedException, ExecutionException { final GenericType<RepositoryTagV2> REPOSITORY_TAGS_RESULT_LIST = new GenericType<RepositoryTagV2>() { }; final WebTarget queryTagsResource = client .target(getHTTPServerAddress()).path("v2") //$NON-NLS-1$ .path(repository).path("tags").path("list"); //$NON-NLS-1$ //$NON-NLS-2$ final RepositoryTagV2 crts = queryTagsResource .request(APPLICATION_JSON_TYPE).async() .method(GET, REPOSITORY_TAGS_RESULT_LIST).get(); return crts.getTags(); } @Override public boolean isVersion2() { if (isV2 != null) { return isV2; } // We haven't cached the result, so let's evaluate final ClientConfig DEFAULT_CONFIG = new ClientConfig( ObjectMapperProvider.class, JacksonFeature.class); final Client client = ClientBuilder.newClient(DEFAULT_CONFIG); final WebTarget pingApiv2Resource = client .target(getHTTPServerAddress()).path("v2"); //$NON-NLS-1$ try { final Response response = pingApiv2Resource .request(APPLICATION_JSON_TYPE).async().get().get(); if (response.getStatus() == Status.OK.getStatusCode()) { isV2 = true; return true; } } catch (ExecutionException | InterruptedException e) { // do nothing } isV2 = false; return false; } /** * Enable an Authenticator used to pass this registry's authentication * credentials to HTTP Authentication requests. */ protected abstract void enableDockerAuthenticator(); /** * Restore the default Authenticator (likely * org.eclipse.ui.internal.net.auth.NetAuthenticator) */ protected abstract void restoreAuthenticator(); }