org.netbeans.modules.docker.api.DockerAction Maven / Gradle / Ivy
The newest version!
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.netbeans.modules.docker.api;
import org.netbeans.modules.docker.StreamItem;
import org.netbeans.modules.docker.ConnectionListener;
import org.netbeans.modules.docker.DockerRemoteException;
import org.netbeans.modules.docker.FolderUploader;
import org.netbeans.modules.docker.MuxedStreamResult;
import org.netbeans.modules.docker.ChunkedInputStream;
import org.netbeans.modules.docker.tls.ContextProvider;
import org.netbeans.modules.docker.IgnoreFileFilter;
import org.netbeans.modules.docker.HttpUtils;
import org.netbeans.modules.docker.DirectStreamResult;
import org.netbeans.modules.docker.Demuxer;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.Reader;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.InetSocketAddress;
import java.net.MalformedURLException;
import java.net.ProxySelector;
import java.net.Socket;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import javax.net.ssl.SSLContext;
import javax.swing.SwingUtilities;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import org.json.simple.parser.ParseException;
import org.netbeans.api.annotations.common.NonNull;
import org.netbeans.api.annotations.common.NullAllowed;
import org.netbeans.modules.docker.DockerActionAccessor;
import org.netbeans.modules.docker.DockerConfig;
import org.netbeans.modules.docker.DockerUtils;
import org.netbeans.modules.docker.Endpoint;
import org.netbeans.modules.docker.StreamResult;
import org.newsclub.net.unix.AFUNIXSocket;
import org.newsclub.net.unix.AFUNIXSocketAddress;
import org.openide.filesystems.FileObject;
import org.openide.util.Pair;
import org.openide.util.Parameters;
import org.openide.util.io.NullInputStream;
import org.openide.util.io.NullOutputStream;
import static java.nio.charset.StandardCharsets.ISO_8859_1;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.netbeans.modules.docker.api.DockerEntityType.Container;
/**
*
* @author Petr Hejl
*/
public class DockerAction {
public static final String DOCKER_FILE = "Dockerfile"; // NOI18N
private static final Logger LOGGER = Logger.getLogger(DockerAction.class.getName());
private static final Pattern ID_PATTERN = Pattern.compile(".*([0-9a-f]{12}([0-9a-f]{52})?).*");
private static final Pattern PORT_PATTERN = Pattern.compile("^(\\d+)/(tcp|udp)$");
private static final Set START_STOP_CONTAINER_CODES = new HashSet<>();
private static final Set REMOVE_CONTAINER_CODES = new HashSet<>();
private static final Set REMOVE_IMAGE_CODES = new HashSet<>();
private static final Pair ACCEPT_JSON_HEADER = Pair.of("Accept", "application/json");
static {
DockerActionAccessor.setDefault(new DockerActionAccessor() {
@Override
public void events(DockerAction action, Long since, DockerEvent.Listener listener, ConnectionListener connectionListener) throws DockerException {
action.events(since, listener, connectionListener);
}
});
Collections.addAll(START_STOP_CONTAINER_CODES, HttpURLConnection.HTTP_NO_CONTENT, HttpURLConnection.HTTP_NOT_MODIFIED);
Collections.addAll(REMOVE_CONTAINER_CODES, HttpURLConnection.HTTP_NO_CONTENT, HttpURLConnection.HTTP_NOT_FOUND);
Collections.addAll(REMOVE_IMAGE_CODES, HttpURLConnection.HTTP_OK, HttpURLConnection.HTTP_NOT_FOUND);
}
private final DockerInstance instance;
private final boolean emitEvents;
public DockerAction(DockerInstance instance) {
this(instance, true);
}
// not needed in the api for now
private DockerAction(DockerInstance instance, boolean emitEvents) {
this.instance = instance;
this.emitEvents = emitEvents;
}
public List getImages() {
try {
JSONArray value = (JSONArray) doGetRequest("/images/json",
Collections.singleton(HttpURLConnection.HTTP_OK));
List ret = new ArrayList<>(value.size());
for (Object o : value) {
JSONObject json = (JSONObject) o;
JSONArray repoTags = (JSONArray) json.get("RepoTags");
String id = (String) json.get("Id");
long created = (long) json.getOrDefault("Created", 0L);
long size = (long) json.getOrDefault("Size", 0L);
long virtualSize = (long) json.getOrDefault("VirtualSize", size);
ret.add(new DockerImage(instance, repoTags, id, created, size, virtualSize));
}
return ret;
} catch (DockerException ex) {
LOGGER.log(Level.INFO, null, ex);
}
return Collections.emptyList();
}
public List getContainers() {
try {
JSONArray value = (JSONArray) doGetRequest("/containers/json?all=1",
Collections.singleton(HttpURLConnection.HTTP_OK));
List ret = new ArrayList<>(value.size());
for (Object o : value) {
JSONObject json = (JSONObject) o;
String id = (String) json.get("Id");
String image = (String) json.get("Image");
String name = null;
JSONArray names = (JSONArray) json.get("Names");
if (names != null && !names.isEmpty()) {
name = (String) names.get(0);
}
DockerContainer.Status status = DockerUtils.getContainerStatus((String) json.get("Status"));
ret.add(new DockerContainer(instance, id, image, name, status));
}
return ret;
} catch (DockerException ex) {
LOGGER.log(Level.INFO, null, ex);
}
return Collections.emptyList();
}
public List search(String searchTerm) {
// the api does not allow this TAG and DIGEST separator characters
if (searchTerm.contains(":") || searchTerm.contains("@")) { // NOI18N
return Collections.emptyList();
}
try {
JSONArray value = (JSONArray) doGetRequest(
"/images/search?term=" + HttpUtils.encodeParameter(searchTerm),
Collections.singleton(HttpURLConnection.HTTP_OK));
List ret = new ArrayList<>(value.size());
for (Object o : value) {
JSONObject json = (JSONObject) o;
String name = (String) json.get("name");
String description = (String) json.get("description");
long stars = ((Number) getOrDefault(json, "star_count", 0)).longValue();
boolean official = (boolean) getOrDefault(json, "is_official", false);
boolean automated = (boolean) getOrDefault(json, "is_automated", false);
ret.add(new DockerRegistryImage(name, description, stars, official, automated));
}
return ret;
} catch (DockerException | UnsupportedEncodingException ex) {
LOGGER.log(Level.INFO, null, ex);
}
return Collections.emptyList();
}
public DockerImage commit(DockerContainer container, String repository, String tag,
String author, String message, boolean pause) throws DockerException {
if (repository == null && tag != null) {
throw new IllegalArgumentException("Repository can't be empty when using tag");
}
try {
StringBuilder action = new StringBuilder("/commit");
action.append("?");
action.append("container=").append(container.getId());
if (repository != null) {
action.append("&repo=").append(HttpUtils.encodeParameter(repository));
if (tag != null) {
action.append("&tag=").append(HttpUtils.encodeParameter(tag));
}
}
if (author != null) {
action.append("&author=").append(HttpUtils.encodeParameter(author));
}
if (message != null) {
action.append("&comment=").append(HttpUtils.encodeParameter(message));
}
if (!pause) {
action.append("&pause=0");
}
JSONObject value = (JSONObject) doPostRequest(action.toString(),
true, Collections.singleton(HttpURLConnection.HTTP_CREATED));
String id = (String) value.get("Id");
long time = System.currentTimeMillis() / 1000;
// XXX we send it as older API does not have the commit event
if (emitEvents) {
instance.getEventBus().sendEvent(
new DockerEvent(instance, DockerEvent.Status.COMMIT,
id, container.getId(), time));
}
// FIXME image size and time parameters
return new DockerImage(instance, Collections.singletonList(DockerUtils.getTag(repository, tag)),
(String) value.get("Id"), time, 0, 0);
} catch (UnsupportedEncodingException ex) {
throw new DockerException(ex);
}
}
public void rename(DockerContainer container, String name) throws DockerException {
Parameters.notNull("name", name);
try {
doPostRequest("/containers/" + container.getId() + "/rename?name=" + HttpUtils.encodeParameter(name),
false, Collections.singleton(HttpURLConnection.HTTP_NO_CONTENT));
long time = System.currentTimeMillis() / 1000;
// XXX we send it as older API does not have the commit event
if (emitEvents) {
instance.getEventBus().sendEvent(
new DockerEvent(instance, DockerEvent.Status.RENAME,
container.getId(), container.getId(), time));
}
} catch (UnsupportedEncodingException ex) {
throw new DockerException(ex);
}
}
public DockerTag tag(DockerTag source, String repository, String tag, boolean force) throws DockerException {
if (repository == null) {
throw new IllegalArgumentException("Repository can't be empty");
}
StringBuilder action = new StringBuilder("/images/");
action.append(source.getId());
action.append("/tag");
action.append("?");
action.append("repo=").append(repository);
if (force) {
action.append("&force=1");
}
if (tag != null) {
action.append("&tag=").append(tag);
}
doPostRequest(action.toString(),
false, Collections.singleton(HttpURLConnection.HTTP_CREATED));
String tagResult = DockerUtils.getTag(repository, tag);
long time = System.currentTimeMillis() / 1000;
// XXX we send it as older API does not have the commit event
if (emitEvents) {
instance.getEventBus().sendEvent(
new DockerEvent(instance, DockerEvent.Status.TAG,
source.getId(), tagResult, time));
}
return new DockerTag(source.getImage(), tagResult);
}
public JSONObject getRawDetails(DockerEntityType entityType, String containerId) throws DockerException {
JSONObject value = (JSONObject) doGetRequest(entityType.getUrlPath() + containerId + "/json",
Collections.singleton(HttpURLConnection.HTTP_OK));
return value;
}
public DockerContainerDetail getDetail(DockerContainer container) throws DockerException {
JSONObject value = getRawDetails(DockerEntityType.Container, container.getId());
String name = (String) value.get("Name");
DockerContainer.Status status = DockerContainer.Status.STOPPED;
JSONObject state = (JSONObject) value.get("State");
if (state != null) {
boolean paused = (Boolean) getOrDefault(state, "Paused", false);
if (paused) {
status = DockerContainer.Status.PAUSED;
} else {
boolean running = (Boolean) getOrDefault(state, "Running", false);
if (running) {
status = DockerContainer.Status.RUNNING;
}
}
}
boolean tty = false;
boolean stdin = false;
JSONObject config = (JSONObject) value.get("Config");
if (config != null) {
tty = (boolean) getOrDefault(config, "Tty", false);
stdin = (boolean) getOrDefault(config, "OpenStdin", false);
}
JSONObject ports = (JSONObject) ((JSONObject) value.get("NetworkSettings")).get("Ports");
if (ports == null || ports.isEmpty()) {
return new DockerContainerDetail(name, status, stdin, tty);
} else {
List portMapping = new ArrayList<>();
for (String containerPortData : (Set) ports.keySet()) {
JSONArray hostPortsArray = (JSONArray) ports.get(containerPortData);
if (hostPortsArray != null && !hostPortsArray.isEmpty()) {
Matcher m = PORT_PATTERN.matcher(containerPortData);
if (m.matches()) {
int containerPort = Integer.parseInt(m.group(1));
String type = m.group(2).toUpperCase(Locale.ENGLISH);
int hostPort = Integer.parseInt((String) ((JSONObject) hostPortsArray.get(0)).get("HostPort"));
String hostIp = (String) ((JSONObject) hostPortsArray.get(0)).get("HostIp");
portMapping.add(new PortMapping(ExposedPort.Type.valueOf(type), containerPort, hostPort, hostIp));
} else {
LOGGER.log(Level.FINE, "Unparsable port: {0}", containerPortData);
}
}
}
return new DockerContainerDetail(name, status, stdin, tty, portMapping);
}
}
public DockerImageDetail getDetail(DockerImage image) throws DockerException {
JSONObject value = (JSONObject) doGetRequest("/images/" + image.getId() + "/json",
Collections.singleton(HttpURLConnection.HTTP_OK));
List ports = new LinkedList<>();
JSONObject config = (JSONObject) value.get("Config");
if (config != null) {
JSONObject portsObject = (JSONObject) config.get("ExposedPorts");
if (portsObject != null) {
for (Object k : portsObject.keySet()) {
String portStr = (String) k;
Matcher m = PORT_PATTERN.matcher(portStr);
if (m.matches()) {
int port = Integer.parseInt(m.group(1));
ExposedPort.Type type = ExposedPort.Type.valueOf(m.group(2).toUpperCase(Locale.ENGLISH));
ports.add(new ExposedPort(port, type));
} else {
LOGGER.log(Level.FINE, "Unparsable port: {0}", portStr);
}
}
}
}
return new DockerImageDetail(ports);
}
public DockerfileDetail getDetail(FileObject dockerfile) throws IOException {
// Each ARG line looks like:
// "(\w)*ARG(\w)*key=val(\w)*"
// Filter this lines and remove ARG from the beginning
List argLines = dockerfile.asLines().stream()
.filter((line) -> line.trim().matches("^(?i)arg(.*)$")) // NOI18N
.map((argLine) -> argLine.trim().replaceFirst("^(?i)arg", "").trim()) // NOI18N
.collect(Collectors.toList());
// Now each line looks like: "key=val"
Map pairs = new HashMap<>();
for (String line : argLines) {
String[] split = line.split("=", 2); // NOI18N
pairs.put(split[0], split.length == 2 ? split[1] : ""); //NOI18N
}
return new DockerfileDetail(pairs);
}
public void start(DockerContainer container) throws DockerException {
doPostRequest("/containers/" + container.getId() + "/start", false, START_STOP_CONTAINER_CODES);
if (emitEvents) {
instance.getEventBus().sendEvent(
new DockerEvent(instance, DockerEvent.Status.START,
container.getId(), container.getImage(), System.currentTimeMillis() / 1000));
}
}
public void stop(DockerContainer container) throws DockerException {
doPostRequest("/containers/" + container.getId() + "/stop", false, START_STOP_CONTAINER_CODES);
if (emitEvents) {
instance.getEventBus().sendEvent(
new DockerEvent(instance, DockerEvent.Status.DIE,
container.getId(), container.getImage(), System.currentTimeMillis() / 1000));
}
}
public void pause(DockerContainer container) throws DockerException {
doPostRequest("/containers/" + container.getId() + "/pause", false,
Collections.singleton(HttpURLConnection.HTTP_NO_CONTENT));
if (emitEvents) {
instance.getEventBus().sendEvent(
new DockerEvent(instance, DockerEvent.Status.PAUSE,
container.getId(), container.getImage(), System.currentTimeMillis() / 1000));
}
}
public void unpause(DockerContainer container) throws DockerException {
doPostRequest("/containers/" + container.getId() + "/unpause", false,
Collections.singleton(HttpURLConnection.HTTP_NO_CONTENT));
if (emitEvents) {
instance.getEventBus().sendEvent(
new DockerEvent(instance, DockerEvent.Status.UNPAUSE,
container.getId(), container.getImage(), System.currentTimeMillis() / 1000));
}
}
public void remove(DockerTag tag) throws DockerException {
String id = getImage(tag);
doDeleteRequest("/images/" + id, true, REMOVE_IMAGE_CODES);
// XXX to be precise we should emit DELETE event if we
// delete the last image, but for our purpose this is enough
if (emitEvents) {
instance.getEventBus().sendEvent(
new DockerEvent(instance, DockerEvent.Status.UNTAG,
tag.getId(), null, System.currentTimeMillis() / 1000));
}
}
public void remove(DockerContainer container) throws DockerException {
doDeleteRequest("/containers/" + container.getId(), false,
REMOVE_CONTAINER_CODES);
if (emitEvents) {
instance.getEventBus().sendEvent(
new DockerEvent(instance, DockerEvent.Status.DESTROY,
container.getId(), container.getImage(), System.currentTimeMillis() / 1000));
}
}
public void resizeTerminal(DockerContainer container, int rows, int columns) throws DockerException {
// formally there should be restart so changes take place
doPostRequest("/containers/" + container.getId() + "/resize?h=" + rows + "&w=" + columns,
false, Collections.singleton(HttpURLConnection.HTTP_OK));
}
// this call is BLOCKING
public ActionStreamResult attach(DockerContainer container, boolean stdin, boolean logs) throws DockerException {
assert !SwingUtilities.isEventDispatchThread() : "Remote access invoked from EDT";
DockerContainerDetail info = DockerAction.this.getDetail(container);
Endpoint s = null;
try {
s = createEndpoint();
OutputStream os = s.getOutputStream();
os.write(("POST /containers/" + container.getId()
+ "/attach?logs=" + (logs ? 1 : 0)
+ "&stream=1&stdout=1&stdin=" + (stdin ? 1 : 0)
+ "&stderr=1 HTTP/1.1\r\n").getBytes(ISO_8859_1));
HttpUtils.configureHeaders(os, DockerConfig.getDefault().getHttpHeaders(),
getHostHeader(),
Pair.of("Connection", "Upgrade"),
Pair.of("Upgrade", "tcp"));
os.write("\r\n".getBytes(ISO_8859_1));
os.flush();
InputStream is = s.getInputStream();
HttpUtils.Response response = HttpUtils.readResponse(is);
int responseCode = response.getCode();
if (responseCode != 101 && responseCode != HttpURLConnection.HTTP_OK) {
String error = HttpUtils.readContent(is, response);
throw new DockerRemoteException(responseCode, error != null ? error : response.getMessage());
}
if (emitEvents) {
instance.getEventBus().sendEvent(
new DockerEvent(instance, DockerEvent.Status.ATTACH,
container.getId(), container.getImage(), System.currentTimeMillis() / 1000));
}
Charset ch = HttpUtils.getCharset(response);
Integer length = HttpUtils.getLength(response.getHeaders());
if (length != null && length <= 0) {
closeEndpoint(s);
return new ActionStreamResult(new EmptyStreamResult(info.isTty()));
}
is = HttpUtils.getResponseStream(is, response, true);
if (info.isTty()) {
return new ActionStreamResult(new DirectStreamResult(s, ch, is));
} else {
return new ActionStreamResult(new MuxedStreamResult(s, ch, is));
}
} catch (MalformedURLException e) {
closeEndpoint(s);
throw new DockerException(e);
} catch (IOException e) {
closeEndpoint(s);
throw new DockerException(e);
} catch (DockerException e) {
closeEndpoint(s);
throw e;
}
}
// this call is BLOCKING
public void pull(String imageName, StatusEvent.Listener listener) throws DockerException {
assert !SwingUtilities.isEventDispatchThread() : "Remote access invoked from EDT";
try {
DockerName parsed = DockerName.parse(imageName);
Endpoint s = createEndpoint();
try {
OutputStream os = s.getOutputStream();
os.write(("POST /images/create?fromImage="
+ HttpUtils.encodeParameter(imageName) + " HTTP/1.1\r\n").getBytes(ISO_8859_1));
Pair authHeader = null;
JSONObject auth = createAuthObject(CredentialsManager.getDefault().getCredentials(parsed.getRegistry()));
authHeader = Pair.of("X-Registry-Auth", HttpUtils.encodeBase64(auth.toJSONString()));
HttpUtils.configureHeaders(os, DockerConfig.getDefault().getHttpHeaders(),
getHostHeader(), ACCEPT_JSON_HEADER, authHeader);
os.write(("\r\n").getBytes(ISO_8859_1));
os.flush();
InputStream is = s.getInputStream();
HttpUtils.Response response = HttpUtils.readResponse(is);
int responseCode = response.getCode();
is = HttpUtils.getResponseStream(is, response, false);
if (responseCode != HttpURLConnection.HTTP_OK) {
String error = HttpUtils.readContent(is, response);
throw codeToException(responseCode,
error != null ? error : response.getMessage());
}
String authFailure = null;
JSONParser parser = new JSONParser();
try (InputStreamReader r = new InputStreamReader(is, HttpUtils.getCharset(response))) {
String line;
while ((line = readEventObject(r)) != null) {
JSONObject o = (JSONObject) parser.parse(line);
StatusEvent e = parseStatusEvent(o);
if (e != null) {
if (authFailure == null) {
authFailure = getAuthenticationFailure(e);
}
listener.onEvent(e);
}
parser.reset();
}
} catch (ParseException ex) {
throw new DockerException(ex);
}
if (authFailure != null) {
throw new DockerAuthenticationException(authFailure);
}
} finally {
closeEndpoint(s);
}
} catch (MalformedURLException e) {
throw new DockerException(e);
} catch (IOException e) {
throw new DockerException(e);
}
}
// this call is BLOCKING
public void push(DockerTag tag, StatusEvent.Listener listener) throws DockerException {
assert !SwingUtilities.isEventDispatchThread() : "Remote access invoked from EDT";
try {
String name = tag.getTag();
DockerName parsed = DockerName.parse(name);
String tagString = parsed.getTag();
StringBuilder action = new StringBuilder();
action.append("/images/");
if (tagString == null) {
action.append(name);
} else {
action.append(name.substring(0, name.length() - tagString.length() - 1));
}
action.append("/push");
if (tagString != null) {
action.append("?tag=").append(HttpUtils.encodeParameter(tagString));
}
Endpoint s = createEndpoint();
try {
OutputStream os = s.getOutputStream();
os.write(("POST " + action.toString() + " HTTP/1.1\r\n").getBytes(ISO_8859_1));
Pair authHeader = null;
JSONObject auth = createAuthObject(CredentialsManager.getDefault().getCredentials(parsed.getRegistry()));
authHeader = Pair.of("X-Registry-Auth", HttpUtils.encodeBase64(auth.toJSONString()));
HttpUtils.configureHeaders(os, DockerConfig.getDefault().getHttpHeaders(),
getHostHeader(), ACCEPT_JSON_HEADER, authHeader);
os.write(("\r\n").getBytes(ISO_8859_1));
os.flush();
InputStream is = s.getInputStream();
HttpUtils.Response response = HttpUtils.readResponse(is);
int responseCode = response.getCode();
is = HttpUtils.getResponseStream(is, response, false);
if (responseCode != HttpURLConnection.HTTP_OK) {
String error = HttpUtils.readContent(is, response);
throw codeToException(responseCode,
error != null ? error : response.getMessage());
}
String authFailure = null;
JSONParser parser = new JSONParser();
try (InputStreamReader r = new InputStreamReader(is, HttpUtils.getCharset(response))) {
String line;
while ((line = readEventObject(r)) != null) {
JSONObject o = (JSONObject) parser.parse(line);
StatusEvent e = parseStatusEvent(o);
if (e != null) {
if (authFailure == null) {
authFailure = getAuthenticationFailure(e);
}
listener.onEvent(e);
}
parser.reset();
}
} catch (ParseException ex) {
throw new DockerException(ex);
}
if (authFailure != null) {
throw new DockerAuthenticationException(authFailure);
}
} finally {
closeEndpoint(s);
}
} catch (MalformedURLException e) {
throw new DockerException(e);
} catch (IOException e) {
throw new DockerException(e);
}
}
public FutureTask createBuildTask(@NonNull FileObject buildContext, @NullAllowed FileObject dockerfile,
@NullAllowed Map buildargs,
String repository, String tag, boolean pull, boolean noCache,
final BuildEvent.Listener buildListener, final StatusEvent.Listener statusListener) {
final CancelHandler handler = new CancelHandler();
Callable callable = new Callable() {
@Override
public DockerImage call() throws Exception {
assert !SwingUtilities.isEventDispatchThread() : "Remote access invoked from EDT";
if (!buildContext.isFolder()) {
throw new IllegalArgumentException("Build context has to be a directory");
}
if (dockerfile != null && !dockerfile.isData()) {
throw new IllegalArgumentException("Dockerfile has to be a file");
}
if (repository == null && tag != null) {
throw new IllegalArgumentException("Repository can't be empty when using tag");
}
String dockerfileName = null;
if (dockerfile != null) {
dockerfileName = dockerfile.getName();
}
Endpoint s = null;
try {
s = createEndpoint();
synchronized (handler) {
if (handler.isCancelled()) {
return null;
}
handler.setEndpoint(s);
}
StringBuilder request = new StringBuilder();
request.append("POST /build?");
request.append("pull=").append(pull ? 1 : 0);
request.append("&nocache=").append(noCache ? 1 : 0);
if (dockerfileName != null) {
request.append("&dockerfile=").append(HttpUtils.encodeParameter(dockerfileName));
}
if (repository != null) {
request.append("&t=").append(HttpUtils.encodeParameter(repository));
if (tag != null) {
request.append(":").append(tag);
}
}
if (buildargs != null && !buildargs.isEmpty()) {
request.append("&buildargs=").append((new JSONObject(buildargs)).toString());
}
request.append(" HTTP/1.1\r\n");
JSONObject registryConfig = new JSONObject();
JSONObject configs = new JSONObject();
registryConfig.put("configs", configs);
for (Credentials c : DockerConfig.getDefault().getAllCredentials()) {
configs.put(c.getRegistry(), createAuthObject(c));
}
Pair configHeader = null;
if (!configs.isEmpty()) {
configHeader = Pair.of("X-Registry-Config", HttpUtils.encodeBase64(registryConfig.toJSONString()));
}
HttpUtils.configureHeaders(request, DockerConfig.getDefault().getHttpHeaders(),
configHeader,
getHostHeader(),
Pair.of("Transfer-Encoding", "chunked"),
Pair.of("Content-Type", "application/tar"),
Pair.of("Content-Encoding", "gzip"));
request.append("\r\n");
OutputStream os = s.getOutputStream();
os.write(request.toString().getBytes(ISO_8859_1));
os.flush();
buildListener.onEvent(new BuildEvent(instance, request.toString(), false, null, false));
// FIXME should we allow \ as separator as that would be formally
// separator on windows without possibility to escape anything
// If we would allow that we have to use File comparison
Future task = new FolderUploader(instance, os).upload(buildContext,
new IgnoreFileFilter(buildContext, dockerfile, '/'), new FolderUploader.Listener() {
@Override
public void onUpload(String path) {
buildListener.onEvent(new BuildEvent(instance, path, false, null, true));
}
});
InputStream is = s.getInputStream();
HttpUtils.Response response = HttpUtils.readResponse(is);
int responseCode = response.getCode();
if (responseCode != HttpURLConnection.HTTP_OK) {
task.cancel(true);
String error = HttpUtils.readContent(is, response);
throw codeToException(responseCode,
error != null ? error : response.getMessage());
}
try {
if (task.isDone()) {
task.get();
} else {
LOGGER.log(Level.INFO, "Server responded OK yet upload has not finished");
task.cancel(true);
}
} catch (InterruptedException ex) {
LOGGER.log(Level.INFO, null, ex);
Thread.currentThread().interrupt();
} catch (ExecutionException ex) {
throw new DockerException(ex.getCause());
}
is = HttpUtils.getResponseStream(is, response, false);
JSONParser parser = new JSONParser();
try (InputStreamReader r = new InputStreamReader(is, HttpUtils.getCharset(response))) {
String line;
String stream = null;
while ((line = readEventObject(r)) != null) {
JSONObject o = (JSONObject) parser.parse(line);
stream = (String) o.get("stream");
if (stream != null) {
buildListener.onEvent(new BuildEvent(instance, stream.trim(), false, null, false));
} else if (o.containsKey("status")) {
StatusEvent e = parseStatusEvent(o);
if (e != null) {
statusListener.onEvent(e);
}
} else {
String error = (String) o.get("error");
if (error != null) {
BuildEvent.Error detail = null;
JSONObject detailObj = (JSONObject) o.get("errorDetail");
if (detailObj != null) {
long code = ((Number) getOrDefault(detailObj, "code", 0)).longValue();
String mesage = (String) detailObj.get("message");
detail = new BuildEvent.Error(code, mesage);
}
buildListener.onEvent(new BuildEvent(instance, error, true, detail, false));
} else {
LOGGER.log(Level.INFO, "Unknown event {0}", o);
}
}
parser.reset();
}
if (stream != null) {
Matcher m = ID_PATTERN.matcher(stream.trim());
if (m.matches()) {
// the docker itself does not emit any event for built image
// we assume the last stream contains the built image id
// FIXME as there is no BUILD event we use PULL event
long time = System.currentTimeMillis() / 1000;
if (emitEvents) {
instance.getEventBus().sendEvent(
new DockerEvent(instance, DockerEvent.Status.PULL,
m.group(1), null, time));
}
// FIXME image size and time parameters
return new DockerImage(instance, Collections.singletonList(DockerUtils.getTag(repository, tag)),
m.group(1), time, 0, 0);
}
}
} catch (ParseException ex) {
throw new DockerException(ex);
}
return null;
} catch (MalformedURLException e) {
closeEndpoint(s);
throw new DockerException(e);
} catch (IOException e) {
closeEndpoint(s);
if (!handler.isCancelled()) {
throw new DockerException(e);
}
return null;
} catch (DockerException e) {
closeEndpoint(s);
throw e;
}
}
};
return new FutureTask(callable) {
@Override
public boolean cancel(boolean mayInterruptIfRunning) {
super.cancel(false);
if (mayInterruptIfRunning) {
handler.cancel();
}
return true;
}
};
}
// this call is BLOCKING
public ActionChunkedResult logs(DockerContainer container) throws DockerException {
assert !SwingUtilities.isEventDispatchThread() : "Remote access invoked from EDT";
DockerContainerDetail info = getDetail(container);
Endpoint s = null;
try {
s = createEndpoint();
OutputStream os = s.getOutputStream();
os.write(("GET /containers/" + container.getId() + "/logs?stderr=1&stdout=1×tamps=1&follow=1 HTTP/1.1\r\n").getBytes(ISO_8859_1));
HttpUtils.configureHeaders(os, DockerConfig.getDefault().getHttpHeaders(), getHostHeader());
os.write("\r\n".getBytes(ISO_8859_1));
os.flush();
InputStream is = s.getInputStream();
HttpUtils.Response response = HttpUtils.readResponse(is);
int responseCode = response.getCode();
if (responseCode != 101 && responseCode != HttpURLConnection.HTTP_OK) {
String error = HttpUtils.readContent(is, response);
throw new DockerRemoteException(responseCode,
error != null ? error : response.getMessage());
}
is = HttpUtils.getResponseStream(is, response, true);
StreamItem.Fetcher fetcher;
Integer length = HttpUtils.getLength(response.getHeaders());
// if there was no log it may return just standard reply with content length 0
if (length != null && length == 0) {
assert !(is instanceof ChunkedInputStream);
LOGGER.log(Level.INFO, "Empty logs");
fetcher = new StreamItem.Fetcher() {
@Override
public StreamItem fetch() {
return null;
}
};
} else if (info.isTty()) {
fetcher = new DirectFetcher(is);
} else {
fetcher = new Demuxer(is);
}
return new ActionChunkedResult(s, fetcher, HttpUtils.getCharset(response));
} catch (MalformedURLException e) {
closeEndpoint(s);
throw new DockerException(e);
} catch (IOException e) {
closeEndpoint(s);
throw new DockerException(e);
} catch (DockerException e) {
closeEndpoint(s);
throw e;
}
}
// this call is BLOCKING
public Pair run(String name, JSONObject configuration) throws DockerException {
Endpoint s = null;
try {
s = createEndpoint();
byte[] data = configuration.toJSONString().getBytes(UTF_8);
Map defaultHeaders = DockerConfig.getDefault().getHttpHeaders();
OutputStream os = s.getOutputStream();
os.write(("POST " + (name != null ? "/containers/create?name=" + HttpUtils.encodeParameter(name) : "/containers/create") + " HTTP/1.1\r\n").getBytes(ISO_8859_1));
HttpUtils.configureHeaders(os, defaultHeaders,
getHostHeader(),
Pair.of("Content-Type", "application/json"),
Pair.of("Content-Length", Integer.toString(data.length)));
os.write("\r\n".getBytes(ISO_8859_1));
os.write(data);
os.flush();
InputStream is = s.getInputStream();
HttpUtils.Response response = HttpUtils.readResponse(is);
if (response.getCode() != HttpURLConnection.HTTP_CREATED) {
String error = HttpUtils.readContent(is, response);
throw new DockerRemoteException(response.getCode(),
error != null ? error : response.getMessage());
}
// we send a second request to the stream later
InputStream nis = HttpUtils.getResponseStream(is, response, false);
JSONObject value;
try {
JSONParser parser = new JSONParser();
value = (JSONObject) parser.parse(HttpUtils.readContent(nis, response));
} catch (ParseException ex) {
throw new DockerException(ex);
}
String id = (String) value.get("Id");
DockerContainer container = new DockerContainer(
instance,
id,
(String) configuration.get("Image"),
"/" + name,
DockerContainer.Status.STOPPED);
ActionStreamResult r = attach(container, true, true);
os.write(("POST /containers/" + id + "/start HTTP/1.1\r\n").getBytes(ISO_8859_1));
HttpUtils.configureHeaders(os, defaultHeaders, getHostHeader());
os.write("\r\n".getBytes(ISO_8859_1));
os.flush();
response = HttpUtils.readResponse(is);
if (response.getCode() != HttpURLConnection.HTTP_NO_CONTENT) {
String error = HttpUtils.readContent(is, response);
throw codeToException(response.getCode(),
error != null ? error : response.getMessage());
}
return Pair.of(container, r);
} catch (MalformedURLException e) {
closeEndpoint(s);
throw new DockerException(e);
} catch (IOException e) {
closeEndpoint(s);
throw new DockerException(e);
} catch (DockerException e) {
closeEndpoint(s);
throw e;
}
}
public boolean pingWithExceptions() throws Exception {
assert !SwingUtilities.isEventDispatchThread() : "Remote access invoked from EDT";
Endpoint s = createEndpoint();
try {
OutputStream os = s.getOutputStream();
// FIXME should we use default headers ?
os.write(("GET /_ping HTTP/1.1\r\n"
+ "Host: " + getHostHeader().second() + "\r\n\r\n").getBytes(ISO_8859_1));
os.flush();
InputStream is = s.getInputStream();
HttpUtils.Response response = HttpUtils.readResponse(is);
return response.getCode() == HttpURLConnection.HTTP_OK;
} finally {
closeEndpoint(s);
}
}
public boolean ping() {
try {
return pingWithExceptions();
} catch (MalformedURLException ex) {
LOGGER.log(Level.INFO, null, ex);
} catch (Exception ex) {
LOGGER.log(Level.FINE, null, ex);
}
return false;
}
// this call is BLOCKING
private void events(Long since, DockerEvent.Listener listener, ConnectionListener connectionListener) throws DockerException {
assert !SwingUtilities.isEventDispatchThread() : "Remote access invoked from EDT";
try {
Endpoint s = createEndpoint();
try {
OutputStream os = s.getOutputStream();
os.write(("GET " + (since != null ? "/events?since=" + since : "/events") + " HTTP/1.1\r\n"
+ "Host: " + getHostHeader().second() + "\r\n"
+ "Accept: application/json\r\n\r\n").getBytes(ISO_8859_1));
os.flush();
InputStream is = s.getInputStream();
HttpUtils.Response response = HttpUtils.readResponse(is);
int responseCode = response.getCode();
if (responseCode != HttpURLConnection.HTTP_OK) {
String error = HttpUtils.readContent(is, response);
throw new DockerRemoteException(responseCode,
error != null ? error : response.getMessage());
}
is = HttpUtils.getResponseStream(is, response, false);
if (connectionListener != null) {
connectionListener.onConnect(s);
}
JSONParser parser = new JSONParser();
try (InputStreamReader r = new InputStreamReader(is, HttpUtils.getCharset(response))) {
String line;
while ((line = readEventObject(r)) != null) {
JSONObject o = (JSONObject) parser.parse(line);
DockerEvent.Status status = DockerEvent.Status.parse((String) o.get("status"));
String id = (String) o.get("id");
String from = (String) o.get("from");
long time = (Long) o.get("time");
if (status == null) {
LOGGER.log(Level.INFO, "Unknown event {0}", o);
} else {
listener.onEvent(new DockerEvent(instance, status, id, from, time));
}
parser.reset();
}
} catch (ParseException ex) {
throw new DockerException(ex);
} finally {
if (connectionListener != null) {
connectionListener.onDisconnect();
}
}
} finally {
closeEndpoint(s);
}
} catch (MalformedURLException e) {
throw new DockerException(e);
} catch (IOException e) {
throw new DockerException(e);
}
}
private Object doGetRequest(@NonNull String action, Set okCodes) throws DockerException {
assert !SwingUtilities.isEventDispatchThread() : "Remote access invoked from EDT";
try {
Endpoint s = createEndpoint();
try {
OutputStream os = s.getOutputStream();
os.write(("GET " + action + " HTTP/1.1\r\n").getBytes(ISO_8859_1));
HttpUtils.configureHeaders(os, DockerConfig.getDefault().getHttpHeaders(),
getHostHeader(), ACCEPT_JSON_HEADER);
os.write(("\r\n").getBytes(ISO_8859_1));
os.flush();
InputStream is = s.getInputStream();
HttpUtils.Response response = HttpUtils.readResponse(is);
int responseCode = response.getCode();
if (!okCodes.contains(responseCode)) {
String error = HttpUtils.readContent(is, response);
throw codeToException(responseCode,
error != null ? error : response.getMessage());
}
is = HttpUtils.getResponseStream(is, response, false);
try (BufferedReader br = new BufferedReader(new InputStreamReader(
is, HttpUtils.getCharset(response)))) {
JSONParser parser = new JSONParser();
return parser.parse(br);
} catch (ParseException ex) {
throw new DockerException(ex);
}
} finally {
closeEndpoint(s);
}
} catch (MalformedURLException e) {
throw new DockerException(e);
} catch (IOException e) {
throw new DockerException(e);
}
}
private Object doPostRequest(@NonNull String action, boolean output, Set okCodes) throws DockerException {
assert !SwingUtilities.isEventDispatchThread() : "Remote access invoked from EDT";
try {
Endpoint s = createEndpoint();
try {
OutputStream os = s.getOutputStream();
os.write(("POST " + action + " HTTP/1.1\r\n").getBytes(ISO_8859_1));
HttpUtils.configureHeaders(os, DockerConfig.getDefault().getHttpHeaders(),
getHostHeader(), Pair.of("Content-Type", "application/json"));
os.write(("\r\n").getBytes(ISO_8859_1));
os.flush();
InputStream is = s.getInputStream();
HttpUtils.Response response = HttpUtils.readResponse(is);
int responseCode = response.getCode();
if (!okCodes.contains(responseCode)) {
String error = HttpUtils.readContent(is, response);
throw codeToException(responseCode,
error != null ? error : response.getMessage());
}
if (output) {
is = HttpUtils.getResponseStream(is, response, false);
try (BufferedReader br = new BufferedReader(new InputStreamReader(
is, HttpUtils.getCharset(response)))) {
JSONParser parser = new JSONParser();
return parser.parse(br);
} catch (ParseException ex) {
throw new DockerException(ex);
}
} else {
return null;
}
} finally {
closeEndpoint(s);
}
} catch (MalformedURLException e) {
throw new DockerException(e);
} catch (IOException e) {
throw new DockerException(e);
}
}
private Object doDeleteRequest(@NonNull String action, boolean output, Set okCodes) throws DockerException {
assert !SwingUtilities.isEventDispatchThread() : "Remote access invoked from EDT";
try {
Endpoint s = createEndpoint();
try {
OutputStream os = s.getOutputStream();
os.write(("DELETE " + action + " HTTP/1.1\r\n").getBytes(ISO_8859_1));
HttpUtils.configureHeaders(os, DockerConfig.getDefault().getHttpHeaders(),
getHostHeader(), ACCEPT_JSON_HEADER);
os.write(("\r\n").getBytes(ISO_8859_1));
os.flush();
InputStream is = s.getInputStream();
HttpUtils.Response response = HttpUtils.readResponse(is);
int responseCode = response.getCode();
if (!okCodes.contains(responseCode)) {
String error = HttpUtils.readContent(is, response);
throw codeToException(responseCode,
error != null ? error : response.getMessage());
}
if (output) {
is = HttpUtils.getResponseStream(is, response, false);
try (BufferedReader br = new BufferedReader(new InputStreamReader(
is, HttpUtils.getCharset(response)))) {
JSONParser parser = new JSONParser();
return parser.parse(br);
} catch (ParseException ex) {
throw new DockerException(ex);
}
} else {
return null;
}
} finally {
closeEndpoint(s);
}
} catch (MalformedURLException e) {
throw new DockerException(e);
} catch (IOException e) {
throw new DockerException(e);
}
}
private StatusEvent parseStatusEvent(JSONObject o) {
boolean error = false;
String id = (String) o.get("id");
String status = (String) o.get("status");
if (status == null) {
status = (String) o.get("error");
error = status != null;
}
if (status == null) {
LOGGER.log(Level.INFO, "Unknown event {0}", o);
return null;
}
String progress = (String) o.get("progress");
StatusEvent.Progress detail = null;
JSONObject detailObj = (JSONObject) o.get("progressDetail");
if (detailObj != null) {
long current = ((Number) getOrDefault(detailObj, "current", 1)).longValue();
long total = ((Number) getOrDefault(detailObj, "total", 1)).longValue();
detail = new StatusEvent.Progress(current, total);
}
return new StatusEvent(instance, id, status, progress, error, detail);
}
private Endpoint createEndpoint() throws IOException {
URL realUrl = getUrl();
try {
if ("https".equals(realUrl.getProtocol())) { // NOI18N
SSLContext context = ContextProvider.getInstance().getSSLContext(instance);
return Endpoint.forSocket(context.getSocketFactory().createSocket(realUrl.getHost(), realUrl.getPort()));
} else if ("http".equals(realUrl.getProtocol())) { // NOI18N
Socket s = new Socket(ProxySelector.getDefault().select(realUrl.toURI()).get(0));
int port = realUrl.getPort();
if (port < 0) {
port = realUrl.getDefaultPort();
}
s.connect(new InetSocketAddress(realUrl.getHost(), port));
return Endpoint.forSocket(s);
} else if ("file".equals(realUrl.getProtocol())) {
AFUNIXSocket s = AFUNIXSocket.newInstance();
AFUNIXSocketAddress sockAdd = AFUNIXSocketAddress.of(new File(realUrl.getFile()));
s.connect(sockAdd);
return Endpoint.forSocket(s);
} else {
throw new IOException("Unknown protocol: " + realUrl.getProtocol());
}
} catch (URISyntaxException ex) {
throw new IOException(ex);
}
}
private Pair getHostHeader() throws MalformedURLException {
URL url = getUrl();
int port = url.getPort();
if (port <= 0) {
return Pair.of("Host", url.getHost()); // NOI18N
} else {
return Pair.of("Host", url.getHost() + ":" + port); // NOI18N
}
}
private URL getUrl() throws MalformedURLException {
String url = instance.getUrl();
if (url.startsWith("tcp://")) { // NOI18N
url = "http://" + url.substring(6); // NOI18N
} else if (url.startsWith("unix://")) { // NOI18N
url = "file://" + url.substring(7); // NOI18N
}
return new URL(url);
}
private static JSONObject createAuthObject(Credentials credentials) {
JSONObject value = new JSONObject();
if (credentials == null) {
value.put("auth", ""); // NOI18N
value.put("email", ""); // NOI18N
} else {
value.put("username", credentials.getUsername()); // NOI18N
value.put("password", new String(credentials.getPassword())); // NOI18N
value.put("email", credentials.getEmail()); // NOI18N
value.put("serveraddress", credentials.getRegistry()); // NOI18N
value.put("auth", ""); // NOI18N
}
return value;
}
private static String getAuthenticationFailure(StatusEvent e) {
if (!e.isError()) {
return null;
}
// this is how the docker client handles it
// (as the server returns HTTP 200 anyway)
if (e.getMessage().contains("Authentication is required")
|| e.getMessage().contains("Status 401")
|| e.getMessage().contains("401 Unauthorized")
|| e.getMessage().contains("status code 401")) {
return e.getMessage();
}
return null;
}
private static DockerException codeToException(int code, String message) {
if (code == HttpURLConnection.HTTP_CONFLICT) {
return new DockerConflictException(message);
} else if (code == HttpURLConnection.HTTP_UNAUTHORIZED) {
return new DockerAuthenticationException(message);
}
return new DockerRemoteException(code, message);
}
private static String readEventObject(Reader is) throws IOException {
StringWriter sw = new StringWriter();
int b;
int balance = -1;
while ((b = is.read()) != -1) {
if (balance < 0) {
if (b == '{') {
balance = 1;
sw.write(b);
}
continue;
}
if (b == '{') {
balance++;
} else if (b == '}') {
balance--;
}
sw.write(b);
if (balance == 0) {
return sw.toString();
}
}
return null;
}
private static void closeEndpoint(Endpoint s) {
if (s != null) {
try {
s.close();
} catch (IOException ex) {
LOGGER.log(Level.INFO, null, ex);
}
}
}
private static Object getOrDefault(Map map, Object key, Object def) {
Object ret = map.get(key);
if (ret == null) {
ret = def;
}
return ret;
}
private static String getImage(DockerTag tag) {
String id = tag.getTag();
if (id.equals(":")) { // NOI18N
id = tag.getImage().getId();
}
return id;
}
public JSONObject getRunningProcessesList(DockerContainer container) throws DockerException {
JSONObject value = (JSONObject) doGetRequest(Container.getUrlPath() + container.getId() + "/top",
Collections.singleton(HttpURLConnection.HTTP_OK));
return value;
}
private static class DirectFetcher implements StreamItem.Fetcher {
private final InputStream is;
private final byte[] buffer = new byte[1024];
public DirectFetcher(InputStream is) {
this.is = is;
}
@Override
public StreamItem fetch() {
try {
int count = is.read(buffer);
if (count < 0) {
return null;
}
return new StreamItem(ByteBuffer.wrap(buffer, 0, count), false);
} catch (IOException ex) {
LOGGER.log(Level.FINE, null, ex);
return null;
}
}
}
private static class EmptyStreamResult implements StreamResult {
private final OutputStream os = new NullOutputStream();
private final InputStream is = new NullInputStream();
private final boolean tty;
public EmptyStreamResult(boolean tty) {
this.tty = tty;
}
@Override
public OutputStream getStdIn() {
return os;
}
@Override
public InputStream getStdOut() {
return is;
}
@Override
public InputStream getStdErr() {
return null;
}
@Override
public boolean hasTty() {
return tty;
}
@Override
public Charset getCharset() {
return UTF_8;
}
@Override
public void close() throws IOException {
// noop
}
}
private static class CancelHandler {
private Endpoint endpoint;
private boolean cancelled;
public synchronized void setEndpoint(Endpoint endpoint) {
this.endpoint = endpoint;
}
public synchronized void cancel() {
if (endpoint != null) {
closeEndpoint(endpoint);
endpoint = null;
cancelled = true;
}
}
public synchronized boolean isCancelled() {
return cancelled;
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy