From 331be6c45c17cb98f0746ac10e46001ecf09f6ea Mon Sep 17 00:00:00 2001 From: Jeff Johnston Date: Thu, 1 Nov 2018 18:19:44 -0400 Subject: [PATCH] Bug 540423 - C/C++ Container Launcher missing Port Binding - add new ContainerPortDialog class to allow user to specify ports in the launch configuration Container tab - add new ContainerTabModel and ExposedPortModel classes to support new functionality - add new Ports group to ContainerTab and have a table where a user can add, edit, remove, and select ports for publishing to the host - add new attribute ATTR_EXPOSED_PORTS to ILaunchConstants for saving and restoring user selected ports in C launch configuration - add needed internal messages to support new port settings functionality - bump up org.eclipse.cdt.docker.launcher version Change-Id: I93b7503bdc141e3077418800352507ef38e65ab1 --- .../ContainerLaunchConfigurationDelegate.java | 76 +++++- .../docker/launcher/ContainerPortDialog.java | 230 ++++++++++++++++ .../docker/launcher/ContainerTab.java | 247 ++++++++++++++++++ .../docker/launcher/ContainerTabModel.java | 87 ++++++ .../docker/launcher/ExposedPortModel.java | 246 +++++++++++++++++ .../docker/launcher/ILaunchConstants.java | 3 + .../internal/docker/launcher/Messages.java | 19 +- .../docker/launcher/messages.properties | 15 ++ 8 files changed, 917 insertions(+), 6 deletions(-) create mode 100644 launch/org.eclipse.cdt.docker.launcher/src/org/eclipse/cdt/internal/docker/launcher/ContainerPortDialog.java create mode 100644 launch/org.eclipse.cdt.docker.launcher/src/org/eclipse/cdt/internal/docker/launcher/ContainerTabModel.java create mode 100644 launch/org.eclipse.cdt.docker.launcher/src/org/eclipse/cdt/internal/docker/launcher/ExposedPortModel.java diff --git a/launch/org.eclipse.cdt.docker.launcher/src/org/eclipse/cdt/internal/docker/launcher/ContainerLaunchConfigurationDelegate.java b/launch/org.eclipse.cdt.docker.launcher/src/org/eclipse/cdt/internal/docker/launcher/ContainerLaunchConfigurationDelegate.java index 73872226ca1..8a63dbdbb03 100644 --- a/launch/org.eclipse.cdt.docker.launcher/src/org/eclipse/cdt/internal/docker/launcher/ContainerLaunchConfigurationDelegate.java +++ b/launch/org.eclipse.cdt.docker.launcher/src/org/eclipse/cdt/internal/docker/launcher/ContainerLaunchConfigurationDelegate.java @@ -15,7 +15,7 @@ import java.net.NetworkInterface; import java.net.SocketException; import java.net.UnknownHostException; import java.util.ArrayList; -import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -223,6 +223,39 @@ public class ContainerLaunchConfigurationDelegate extends GdbLaunchDelegate } additionalDirs = dirs; } + + List ports = new ArrayList<>(); + List portInfos = configuration.getAttribute( + ILaunchConstants.ATTR_EXPOSED_PORTS, + Collections.emptyList()); + for (String portInfo : portInfos) { + ExposedPortModel m = ExposedPortModel + .createPortModel(portInfo); + if (m.getSelected()) { + StringBuilder b1 = new StringBuilder(); + if (m.getHostAddress() != null + && !m.getHostAddress().isEmpty()) { + b1.append(m.getHostAddress()); + b1.append(":"); //$NON-NLS-1$ + } + if (m.getHostPort() != null + && !m.getHostPort().isEmpty()) { + b1.append(m.getHostPort()); + } + // regardless if we have a host port or not, + // we may need to add a separator so we can determine + // the case where we don't have a host port vs where we + // don't have a host address + if (b1.length() > 0) { + b1.append(":"); //$NON-NLS-1$ + } + String containerPort = m.getContainerPort() + "/" //$NON-NLS-1$ + + m.getPortType(); + b1.append(containerPort); + ports.add(b1.toString()); + } + } + String image = configuration.getAttribute( ILaunchConstants.ATTR_IMAGE, (String) null); String connectionUri = configuration.getAttribute( @@ -240,14 +273,49 @@ public class ContainerLaunchConfigurationDelegate extends GdbLaunchDelegate connectionUri, image, command, commandDir, workingDir, additionalDirs, origEnv, - envMap, null, keepContainer, supportStdin, + envMap, ports.isEmpty() ? null : ports, keepContainer, + supportStdin, privilegedMode, labels); } else if (mode.equals(ILaunchManager.DEBUG_MODE)) { String gdbserverPortNumber = configuration.getAttribute( ILaunchConstants.ATTR_GDBSERVER_PORT, ILaunchConstants.ATTR_GDBSERVER_PORT_DEFAULT); - List ports = Arrays - .asList(gdbserverPortNumber + "/tcp"); //$NON-NLS-1$ + + List ports = new ArrayList<>(); + List portInfos = configuration.getAttribute( + ILaunchConstants.ATTR_EXPOSED_PORTS, Collections.emptyList()); + String gdbserverPort = gdbserverPortNumber + "/tcp"; //$NON-NLS-1$ + boolean gdbserverPortSpecified = false; + for (String portInfo : portInfos) { + ExposedPortModel m = ExposedPortModel.createPortModel(portInfo); + if (m.getSelected()) { + StringBuilder b = new StringBuilder(); + if (m.getHostAddress() != null && !m.getHostAddress().isEmpty()) { + b.append(m.getHostAddress()); + b.append(":"); //$NON-NLS-1$ + } + if (m.getHostPort() != null && !m.getHostPort().isEmpty()) { + b.append(m.getHostPort()); + } + // regardless if we have a host port or not, + // we may need to add a separator so we can determine + // the case where we don't have a host port vs where we + // don't have a host address + if (b.length() > 0) { + b.append(":"); //$NON-NLS-1$ + } + String containerPort = m.getContainerPort() + "/" + m.getPortType(); //$NON-NLS-1$ + b.append(containerPort); + if (gdbserverPort.equals(containerPort)) { + gdbserverPortSpecified = true; + } + ports.add(b.toString()); + } + } + // if user hasn't already specified gdbserver port, we need to add it by default + if (!gdbserverPortSpecified) { + ports.add(gdbserverPortNumber + "/tcp"); //$NON-NLS-1$ + } String gdbserverCommand = configuration.getAttribute( ILaunchConstants.ATTR_GDBSERVER_COMMAND, ILaunchConstants.ATTR_GDBSERVER_COMMAND_DEFAULT); diff --git a/launch/org.eclipse.cdt.docker.launcher/src/org/eclipse/cdt/internal/docker/launcher/ContainerPortDialog.java b/launch/org.eclipse.cdt.docker.launcher/src/org/eclipse/cdt/internal/docker/launcher/ContainerPortDialog.java new file mode 100644 index 00000000000..d803707948f --- /dev/null +++ b/launch/org.eclipse.cdt.docker.launcher/src/org/eclipse/cdt/internal/docker/launcher/ContainerPortDialog.java @@ -0,0 +1,230 @@ +/******************************************************************************* + * Copyright (c) 2015, 2018 Red Hat Inc. and others. + * 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.cdt.internal.docker.launcher; + +import org.eclipse.core.databinding.DataBindingContext; +import org.eclipse.core.databinding.beans.BeanProperties; +import org.eclipse.core.databinding.observable.value.IValueChangeListener; +import org.eclipse.jface.databinding.swt.ISWTObservableValue; +import org.eclipse.jface.databinding.swt.WidgetProperties; +import org.eclipse.jface.dialogs.Dialog; +import org.eclipse.jface.dialogs.IDialogConstants; +import org.eclipse.jface.layout.GridDataFactory; +import org.eclipse.jface.layout.GridLayoutFactory; +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Text; + +/** + * @author xcoulon + * + */ +public class ContainerPortDialog extends Dialog { + + private static final String PORT_TYPE = "tcp"; //$NON-NLS-1$ + + private final ContainerPortDialogModel model; + + private final DataBindingContext dbc = new DataBindingContext(); + + public ContainerPortDialog(final Shell parentShell) { + super(parentShell); + this.model = new ContainerPortDialogModel(); + } + + public ContainerPortDialog(final Shell parentShell, + final ExposedPortModel selectedContainerPort) { + super(parentShell); + this.model = new ContainerPortDialogModel( + selectedContainerPort.getContainerPort(), + selectedContainerPort.getHostAddress(), + selectedContainerPort.getHostPort()); + } + + @Override + protected void configureShell(final Shell shell) { + super.configureShell(shell); + setShellStyle(getShellStyle() | SWT.RESIZE); + shell.setText(Messages.ContainerPortDialog_shellTitle); + } + + /** + * Disable the 'OK' button by default + */ + @Override + protected Button createButton(Composite parent, int id, String label, + boolean defaultButton) { + final Button button = super.createButton(parent, id, label, + defaultButton); + if (id == IDialogConstants.OK_ID) { + button.setEnabled(false); + } + return button; + } + + @Override + protected Point getInitialSize() { + return new Point(400, super.getInitialSize().y); + } + + @SuppressWarnings("unchecked") + @Override + protected Control createDialogArea(Composite parent) { + final int COLUMNS = 2; + final Composite container = new Composite(parent, SWT.NONE); + GridDataFactory.fillDefaults().align(SWT.FILL, SWT.FILL) + .span(COLUMNS, 1).grab(true, true).applyTo(container); + GridLayoutFactory.fillDefaults().numColumns(COLUMNS).margins(10, 10) + .applyTo(container); + final Label explanationLabel = new Label(container, SWT.NONE); + explanationLabel.setText(Messages.ContainerPortDialog_explanationLabel); + GridDataFactory.fillDefaults().align(SWT.FILL, SWT.CENTER) + .span(COLUMNS, 1).grab(false, false).applyTo(explanationLabel); + final Label containerLabel = new Label(container, SWT.NONE); + containerLabel.setText(Messages.ContainerPortDialog_containerLabel); + GridDataFactory.fillDefaults().align(SWT.FILL, SWT.CENTER) + .grab(false, false).applyTo(containerLabel); + final Text containerPortText = new Text(container, SWT.BORDER); + GridDataFactory.fillDefaults().align(SWT.FILL, SWT.CENTER) + .grab(true, false).applyTo(containerPortText); + final Label hostAddressLabel = new Label(container, SWT.NONE); + hostAddressLabel.setText(Messages.ContainerPortDialog_hostAddressLabel); + GridDataFactory.fillDefaults().align(SWT.FILL, SWT.CENTER) + .grab(false, false).applyTo(hostAddressLabel); + final Text hostAddressText = new Text(container, SWT.BORDER); + GridDataFactory.fillDefaults().align(SWT.FILL, SWT.CENTER) + .grab(true, false).applyTo(hostAddressText); + final Label hostPortLabel = new Label(container, SWT.NONE); + hostPortLabel.setText( + Messages.ContainerPortDialog_hostPortLabel); + GridDataFactory.fillDefaults().align(SWT.FILL, SWT.CENTER) + .grab(false, false).applyTo(hostPortLabel); + final Text hostPortText = new Text(container, SWT.BORDER); + GridDataFactory.fillDefaults().align(SWT.FILL, SWT.CENTER) + .grab(true, false).applyTo(hostPortText); + // error message + final Label errorMessageLabel = new Label(container, SWT.NONE); + GridDataFactory.fillDefaults().align(SWT.FILL, SWT.CENTER) + .span(COLUMNS, 1).grab(true, false).applyTo(errorMessageLabel); + + // listening to changes + final ISWTObservableValue containerPortObservable = WidgetProperties + .text(SWT.Modify).observe(containerPortText); + dbc.bindValue(containerPortObservable, + BeanProperties + .value(ContainerPortDialogModel.class, + ContainerPortDialogModel.CONTAINER_PORT) + .observe(model)); + final ISWTObservableValue hostAddressObservable = WidgetProperties + .text(SWT.Modify).observe(hostAddressText); + dbc.bindValue(hostAddressObservable, + BeanProperties + .value(ContainerPortDialogModel.class, + ContainerPortDialogModel.HOST_ADDRESS) + .observe(model)); + final ISWTObservableValue hostPortObservable = WidgetProperties + .text(SWT.Modify).observe(hostPortText); + dbc.bindValue(hostPortObservable, + BeanProperties + .value(ContainerPortDialogModel.class, + ContainerPortDialogModel.HOST_PORT) + .observe(model)); + + containerPortObservable.addValueChangeListener( + onContainerPortSettingsChanged()); + hostPortObservable.addValueChangeListener( + onContainerPortSettingsChanged()); + hostAddressObservable.addValueChangeListener( + onContainerPortSettingsChanged()); + return container; + } + + private IValueChangeListener onContainerPortSettingsChanged() { + return event -> validateInput(); + } + + private void validateInput() { + final String containerPort = model.getContainerPort(); + if (containerPort == null || containerPort.isEmpty()) { + setOkButtonEnabled(false); + } else { + setOkButtonEnabled(true); + } + } + + private void setOkButtonEnabled(final boolean enabled) { + getButton(IDialogConstants.OK_ID).setEnabled(enabled); + } + + public ExposedPortModel getPort() { + return new ExposedPortModel(model.getContainerPort(), PORT_TYPE, + model.getHostAddress(), model.getHostPort()); + } + + class ContainerPortDialogModel extends BaseDatabindingModel { + + public static final String CONTAINER_PORT = "containerPort"; //$NON-NLS-1$ + + public static final String HOST_ADDRESS = "hostAddress"; //$NON-NLS-1$ + + public static final String HOST_PORT = "hostPort"; //$NON-NLS-1$ + + private String containerPort; + + private String hostAddress; + + private String hostPort; + + public ContainerPortDialogModel() { + } + + public ContainerPortDialogModel(final String containerPort, + final String hostAddress, final String hostPort) { + this.containerPort = containerPort; + this.hostAddress = hostAddress; + this.hostPort = hostPort; + } + + public String getContainerPort() { + return containerPort; + } + + public void setContainerPort(final String containerPort) { + firePropertyChange(CONTAINER_PORT, this.containerPort, + this.containerPort = containerPort); + } + + public String getHostAddress() { + return hostAddress; + } + + public void setHostAddress(final String hostName) { + firePropertyChange(HOST_ADDRESS, this.hostAddress, + this.hostAddress = hostName); + } + + public String getHostPort() { + return hostPort; + } + + public void setHostPort(final String hostPort) { + firePropertyChange(HOST_PORT, this.hostPort, + this.hostPort = hostPort); + } + } + +} diff --git a/launch/org.eclipse.cdt.docker.launcher/src/org/eclipse/cdt/internal/docker/launcher/ContainerTab.java b/launch/org.eclipse.cdt.docker.launcher/src/org/eclipse/cdt/internal/docker/launcher/ContainerTab.java index ab76710de35..27118558091 100644 --- a/launch/org.eclipse.cdt.docker.launcher/src/org/eclipse/cdt/internal/docker/launcher/ContainerTab.java +++ b/launch/org.eclipse.cdt.docker.launcher/src/org/eclipse/cdt/internal/docker/launcher/ContainerTab.java @@ -12,13 +12,31 @@ package org.eclipse.cdt.internal.docker.launcher; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; import org.eclipse.cdt.docker.launcher.DockerLaunchUIPlugin; +import org.eclipse.core.databinding.DataBindingContext; +import org.eclipse.core.databinding.beans.BeanProperties; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.preferences.InstanceScope; import org.eclipse.debug.core.ILaunchConfiguration; import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy; import org.eclipse.debug.ui.AbstractLaunchConfigurationTab; +import org.eclipse.jface.databinding.viewers.ObservableListContentProvider; +import org.eclipse.jface.databinding.viewers.ViewerSupport; +import org.eclipse.jface.databinding.viewers.ViewersObservables; +import org.eclipse.jface.dialogs.IDialogConstants; +import org.eclipse.jface.layout.GridDataFactory; +import org.eclipse.jface.layout.GridLayoutFactory; +import org.eclipse.jface.viewers.CheckStateChangedEvent; +import org.eclipse.jface.viewers.CheckboxTableViewer; +import org.eclipse.jface.viewers.ICheckStateListener; +import org.eclipse.jface.viewers.ISelectionChangedListener; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.jface.viewers.TableViewer; +import org.eclipse.jface.viewers.TableViewerColumn; import org.eclipse.linuxtools.docker.core.DockerConnectionManager; import org.eclipse.linuxtools.docker.core.IDockerConnection; import org.eclipse.linuxtools.docker.core.IDockerConnectionManagerListener; @@ -47,6 +65,8 @@ import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Group; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.List; +import org.eclipse.swt.widgets.Table; +import org.eclipse.swt.widgets.TableColumn; import org.osgi.service.prefs.Preferences; public class ContainerTab extends AbstractLaunchConfigurationTab implements @@ -65,12 +85,21 @@ public class ContainerTab extends AbstractLaunchConfigurationTab implements private Button newButton; private Button removeButton; + + private CheckboxTableViewer tableViewer; + private Button keepButton; private Button stdinButton; private Button privilegedButton; private Combo imageCombo; private Combo connectionSelector; + private ContainerTabModel model; + + private static final int INDENT = 1; + + private final DataBindingContext dbc = new DataBindingContext(); + private ModifyListener connectionModifyListener = new ModifyListener() { @Override @@ -92,6 +121,7 @@ public class ContainerTab extends AbstractLaunchConfigurationTab implements public ContainerTab() { super(); containerTab = this; + model = new ContainerTabModel(); } @Override @@ -146,6 +176,7 @@ public class ContainerTab extends AbstractLaunchConfigurationTab implements createDirectoryList(mainComposite); createButtons(mainComposite); + createPortSettingsSection(mainComposite); createOptions(mainComposite); } @@ -214,6 +245,205 @@ public class ContainerTab extends AbstractLaunchConfigurationTab implements removeButton.setEnabled(false); } + @SuppressWarnings("unchecked") + private void createPortSettingsSection(final Composite parent) { + Font font = parent.getFont(); + Composite comp = createComposite(parent, 1, 2, GridData.FILL_BOTH); + + Group group = new Group(comp, SWT.NONE); + group.setFont(font); + group.setText(Messages.ContainerTab_Ports_Group_Name); + + GridData gd2 = new GridData(GridData.FILL_BOTH); + group.setLayoutData(gd2); + + group.setLayout(new GridLayout()); + + GridDataFactory.fillDefaults().align(SWT.FILL, SWT.CENTER).span(3, 1) + .grab(true, false).applyTo(group); + + group.setLayout(new GridLayout()); + // specify ports + final Label portSettingsLabel = new Label(group, SWT.NONE); + portSettingsLabel.setText(Messages.ContainerTab_Specify_Ports_Label); + GridDataFactory.fillDefaults().align(SWT.FILL, SWT.CENTER) + .grab(true, false).span(3, 1) + .applyTo(portSettingsLabel); + final CheckboxTableViewer exposedPortsTableViewer = createPortSettingsTable( + group); + tableViewer = exposedPortsTableViewer; + GridDataFactory.fillDefaults().align(SWT.FILL, SWT.TOP) + .grab(true, false).span(3 - 1, 1).indent(INDENT, 0) + .hint(200, 70).applyTo(exposedPortsTableViewer.getTable()); + // buttons + final Composite buttonsContainers = new Composite(parent, SWT.NONE); + GridDataFactory.fillDefaults().align(SWT.FILL, SWT.TOP) + .grab(false, false).applyTo(buttonsContainers); + GridLayoutFactory.fillDefaults().numColumns(1).margins(0, 0) + .spacing(SWT.DEFAULT, 0).applyTo(buttonsContainers); + + final Button addButton = new Button(buttonsContainers, SWT.NONE); + GridDataFactory.fillDefaults().align(SWT.FILL, SWT.TOP) + .grab(true, false).applyTo(addButton); + addButton.setText(Messages.ContainerTab_Add_Button); + addButton.addSelectionListener(onAddPort(exposedPortsTableViewer)); + final Button editButton = new Button(buttonsContainers, SWT.NONE); + GridDataFactory.fillDefaults().align(SWT.FILL, SWT.TOP) + .grab(true, false).applyTo(editButton); + editButton.setText(Messages.ContainerTab_Edit_Button); + editButton.setEnabled(false); + editButton.addSelectionListener(onEditPort(exposedPortsTableViewer)); + final Button removeButton = new Button(buttonsContainers, SWT.NONE); + GridDataFactory.fillDefaults().align(SWT.FILL, SWT.TOP) + .grab(true, false).applyTo(removeButton); + removeButton.setText(Messages.ContainerTab_Remove_Button); + removeButton + .addSelectionListener(onRemovePorts(exposedPortsTableViewer)); + ViewerSupport.bind(exposedPortsTableViewer, model.getExposedPorts(), + BeanProperties.values(ExposedPortModel.class, + ExposedPortModel.CONTAINER_PORT, + ExposedPortModel.PORT_TYPE, + ExposedPortModel.HOST_ADDRESS, + ExposedPortModel.HOST_PORT)); + dbc.bindSet( + ViewersObservables.observeCheckedElements( + exposedPortsTableViewer, ExposedPortModel.class), + BeanProperties.set(ContainerTabModel.SELECTED_PORTS) + .observe(model)); + checkAllElements(exposedPortsTableViewer); + + // disable the edit and removeButton if the table is empty + exposedPortsTableViewer.addSelectionChangedListener( + onSelectionChanged(editButton, removeButton)); + exposedPortsTableViewer + .addCheckStateListener(new ICheckStateListener() { + @Override + public void checkStateChanged( + CheckStateChangedEvent event) { + ExposedPortModel e = (ExposedPortModel) event + .getElement(); + e.setSelected(event.getChecked()); + updateLaunchConfigurationDialog(); + } + }); + } + + private void checkAllElements( + final CheckboxTableViewer exposedPortsTableViewer) { + exposedPortsTableViewer.setAllChecked(true); + model.setSelectedPorts(new HashSet<>(model.getExposedPorts())); + } + + private SelectionListener onAddPort( + final CheckboxTableViewer exposedPortsTableViewer) { + return SelectionListener.widgetSelectedAdapter(e -> { + final ContainerPortDialog dialog = new ContainerPortDialog( + getShell()); + dialog.create(); + if (dialog.open() == IDialogConstants.OK_ID) { + final ExposedPortModel port = dialog.getPort(); + port.setSelected(true); + model.addAvailablePort(port); + model.getSelectedPorts().add(port); + exposedPortsTableViewer.setChecked(port, true); + updateLaunchConfigurationDialog(); + } + }); + } + + private SelectionListener onEditPort( + final CheckboxTableViewer exposedPortsTableViewer) { + return SelectionListener.widgetSelectedAdapter(e -> { + final IStructuredSelection selection = exposedPortsTableViewer + .getStructuredSelection(); + final ExposedPortModel selectedContainerPort = (ExposedPortModel) selection + .getFirstElement(); + final ContainerPortDialog dialog = new ContainerPortDialog( + getShell(), selectedContainerPort); + dialog.create(); + if (dialog.open() == IDialogConstants.OK_ID) { + final ExposedPortModel configuredPort = dialog.getPort(); + selectedContainerPort + .setContainerPort(configuredPort.getContainerPort()); + selectedContainerPort + .setHostAddress(configuredPort.getHostAddress()); + selectedContainerPort.setHostPort(configuredPort.getHostPort()); + exposedPortsTableViewer.refresh(); + updateLaunchConfigurationDialog(); + } + }); + } + + private SelectionListener onRemovePorts( + final TableViewer portsTableViewer) { + return SelectionListener.widgetSelectedAdapter(e -> { + final IStructuredSelection selection = portsTableViewer + .getStructuredSelection(); + for (@SuppressWarnings("unchecked") + Iterator iterator = selection.iterator(); iterator + .hasNext();) { + final ExposedPortModel port = iterator.next(); + model.removeAvailablePort(port); + model.getSelectedPorts().remove(port); + updateLaunchConfigurationDialog(); + } + }); + } + + private ISelectionChangedListener onSelectionChanged( + final Button... targetButtons) { + return e -> { + if (e.getSelection().isEmpty()) { + setControlsEnabled(targetButtons, false); + } else { + setControlsEnabled(targetButtons, true); + } + }; + } + + private static void setControlsEnabled(final Control[] controls, + final boolean enabled) { + for (Control control : controls) { + control.setEnabled(enabled); + } + } + + private CheckboxTableViewer createPortSettingsTable( + final Composite container) { + final Table table = new Table(container, SWT.BORDER | SWT.FULL_SELECTION + | SWT.V_SCROLL | SWT.H_SCROLL | SWT.CHECK); + final CheckboxTableViewer tableViewer = new CheckboxTableViewer(table); + table.setHeaderVisible(true); + table.setLinesVisible(true); + createTableViewerColum(tableViewer, + Messages.ContainerTab_Port_Column, + 100); + createTableViewerColum(tableViewer, + Messages.ContainerTab_Type_Column, + 50); + createTableViewerColum(tableViewer, + Messages.ContainerTab_HostAddress_Column, + 100); + createTableViewerColum(tableViewer, + Messages.ContainerTab_HostPort_Column, + 100); + tableViewer.setContentProvider(new ObservableListContentProvider()); + return tableViewer; + } + + private TableViewerColumn createTableViewerColum( + final TableViewer tableViewer, final String title, + final int width) { + final TableViewerColumn viewerColumn = new TableViewerColumn( + tableViewer, SWT.NONE); + final TableColumn column = viewerColumn.getColumn(); + if (title != null) { + column.setText(title); + } + column.setWidth(width); + return viewerColumn; + } + private void createOptions(Composite parent) { Font font = parent.getFont(); Composite comp = createComposite(parent, 1, 3, GridData.FILL_BOTH); @@ -406,6 +636,8 @@ public class ContainerTab extends AbstractLaunchConfigurationTab implements public void setDefaults(ILaunchConfigurationWorkingCopy configuration) { configuration.setAttribute(ILaunchConstants.ATTR_ADDITIONAL_DIRS, (String) null); + configuration.setAttribute(ILaunchConstants.ATTR_EXPOSED_PORTS, + (String) null); configuration.setAttribute(ILaunchConstants.ATTR_CONNECTION_URI, ""); //$NON-NLS-1$ Preferences prefs = InstanceScope.INSTANCE .getNode(DockerLaunchUIPlugin.PLUGIN_ID); @@ -427,6 +659,19 @@ public class ContainerTab extends AbstractLaunchConfigurationTab implements if (additionalDirs != null) directoriesList.setItems(additionalDirs.toArray(new String[0])); + + java.util.List exposedPortInfos = configuration + .getAttribute(ILaunchConstants.ATTR_EXPOSED_PORTS, + Collections. emptyList()); + model.removeExposedPorts(); + for (String port : exposedPortInfos) { + ExposedPortModel m = ExposedPortModel.createPortModel(port); + model.addAvailablePort(m); + if (m.getSelected()) { + model.getSelectedPorts().add(m); + tableViewer.setChecked(m, true); + } + } connectionUri = configuration.getAttribute( ILaunchConstants.ATTR_CONNECTION_URI, (String) ""); int defaultIndex = 0; @@ -480,6 +725,8 @@ public class ContainerTab extends AbstractLaunchConfigurationTab implements stdinButton.getSelection()); configuration.setAttribute(ILaunchConstants.ATTR_PRIVILEGED_MODE, privilegedButton.getSelection()); + configuration.setAttribute(ILaunchConstants.ATTR_EXPOSED_PORTS, + ExposedPortModel.toArrayString(model.getExposedPorts())); } @Override diff --git a/launch/org.eclipse.cdt.docker.launcher/src/org/eclipse/cdt/internal/docker/launcher/ContainerTabModel.java b/launch/org.eclipse.cdt.docker.launcher/src/org/eclipse/cdt/internal/docker/launcher/ContainerTabModel.java new file mode 100644 index 00000000000..6fe7e02b98f --- /dev/null +++ b/launch/org.eclipse.cdt.docker.launcher/src/org/eclipse/cdt/internal/docker/launcher/ContainerTabModel.java @@ -0,0 +1,87 @@ +/******************************************************************************* + * Copyright (c) 2018 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.cdt.internal.docker.launcher; + +import java.util.List; +import java.util.Set; + +import org.eclipse.core.databinding.observable.list.WritableList; + +/** + * @since 1.2.1 + * @author jjohnstn + * + */ +public class ContainerTabModel extends BaseDatabindingModel { + + public static final String PUBLISH_ALL_PORTS = "publishAllPorts"; //$NON-NLS-1$ + + public static final String EXPOSED_PORTS = "exposedPorts"; //$NON-NLS-1$ + + public static final String SELECTED_PORTS = "selectedPorts"; //$NON-NLS-1$ + + private boolean publishAllPorts = true; + + private final WritableList exposedPorts = new WritableList<>(); + + private Set selectedPorts; + + public boolean isPublishAllPorts() { + return publishAllPorts; + } + + public void setPublishAllPorts(boolean publishAllPorts) { + firePropertyChange(PUBLISH_ALL_PORTS, this.publishAllPorts, + this.publishAllPorts = publishAllPorts); + } + + public WritableList getExposedPorts() { + return exposedPorts; + } + + public void addAvailablePort(final ExposedPortModel port) { + this.exposedPorts.add(port); + } + + public void removeAvailablePort(final ExposedPortModel port) { + this.exposedPorts.remove(port); + } + + public void setExposedPorts(final List exposedPorts) { + this.exposedPorts.clear(); + this.exposedPorts.addAll(exposedPorts); + // FIXME: also add all given exposedPorts to selectedExposedPorts ? + } + + public void addExposedPort(final ExposedPortModel exposedPort) { + if (!this.exposedPorts.contains(exposedPort)) { + this.exposedPorts.add(exposedPort); + } + } + + public void removeExposedPort(final ExposedPortModel exposedPort) { + this.exposedPorts.remove(exposedPort); + } + + public void removeExposedPorts() { + this.exposedPorts.clear(); + } + + public Set getSelectedPorts() { + return this.selectedPorts; + } + + public void setSelectedPorts(final Set ports) { + firePropertyChange(SELECTED_PORTS, this.selectedPorts, + this.selectedPorts = ports); + } + +} diff --git a/launch/org.eclipse.cdt.docker.launcher/src/org/eclipse/cdt/internal/docker/launcher/ExposedPortModel.java b/launch/org.eclipse.cdt.docker.launcher/src/org/eclipse/cdt/internal/docker/launcher/ExposedPortModel.java new file mode 100644 index 00000000000..000e65fd227 --- /dev/null +++ b/launch/org.eclipse.cdt.docker.launcher/src/org/eclipse/cdt/internal/docker/launcher/ExposedPortModel.java @@ -0,0 +1,246 @@ +/******************************************************************************* + * Copyright (c) 2018 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.cdt.internal.docker.launcher; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.UUID; + +import org.eclipse.core.runtime.Assert; + +public class ExposedPortModel extends BaseDatabindingModel + implements Comparable { + + private static final String SEPARATOR = ":"; //$NON-NLS-1$ + + private static final String CONTAINER_TYPE_SEPARATOR = "/"; //$NON-NLS-1$ + + public static final String SELECTED = "selected"; //$NON-NLS-1$ + + public static final String CONTAINER_PORT = "containerPort"; //$NON-NLS-1$ + + public static final String PORT_TYPE = "portType"; //$NON-NLS-1$ + + public static final String HOST_ADDRESS = "hostAddress"; //$NON-NLS-1$ + + public static final String HOST_PORT = "hostPort"; //$NON-NLS-1$ + + private final String id = UUID.randomUUID().toString(); + + private boolean selected; + + private String containerPort; + + private String portType; + + private String hostAddress; + + private String hostPort; + + /** + * Parses and converts the {@link List} of the given {@link String} values + * into a {@link List} of {@link ExposedPortModel} + * + * @param exposedPortInfos + * the input values + * @return the corresponding {@link ExposedPortModel}s + */ + public static List fromStrings( + final Collection exposedPortInfos) { + final List exposedPorts = new ArrayList<>(); + for (String exposedPortInfo : exposedPortInfos) { + final ExposedPortModel exposedPort = ExposedPortModel + .fromString(exposedPortInfo); + if (exposedPort != null) { + exposedPorts.add(exposedPort); + } + } + return exposedPorts; + } + + /** + * Converts a collection of ExposedPortModel to a {@link List} of + * {@link String} values + * + * + * @param exposedPorts + * collection of ExposedPortModel instances + * @return the corresponding {@link List} of {@link String}s + */ + public static List toArrayString( + final Collection exposedPorts) { + final List exposedPortList = new ArrayList<>(); + for (ExposedPortModel exposedPort : exposedPorts) { + final String exposedPortString = exposedPort.toString(); + if (exposedPort != null) { + exposedPortList.add(exposedPortString); + } + } + return exposedPortList; + } + + /** + * Parse the given value and returns an instance of + * {@link ExposedPortModel}. + * + * @param exposedPortInfo + * the value to parse + * @return the corresponding {@link ExposedPortModel} + */ + public static ExposedPortModel fromString(final String exposedPortInfo) { + final String privatePort = exposedPortInfo.substring(0, + exposedPortInfo.indexOf(CONTAINER_TYPE_SEPARATOR)); + // exposed ports without host IP/port info + final int firstColumnSeparator = exposedPortInfo.indexOf(SEPARATOR); + if (firstColumnSeparator == -1 + && exposedPortInfo.indexOf(CONTAINER_TYPE_SEPARATOR) != -1) { + final String type = exposedPortInfo.substring( + exposedPortInfo.indexOf(CONTAINER_TYPE_SEPARATOR)); // $NON-NLS-1$ + final ExposedPortModel exposedPort = new ExposedPortModel( + privatePort, type, "", privatePort); // $NON-NLS-1$ + return exposedPort; // $NON-NLS-1$ + } else { + final int secondColumnSeparator = exposedPortInfo.indexOf(SEPARATOR, + firstColumnSeparator + 1); + final String type = exposedPortInfo.substring( + exposedPortInfo.indexOf(CONTAINER_TYPE_SEPARATOR), // $NON-NLS-1$ + firstColumnSeparator); // $NON-NLS-1$ + final String hostIP = exposedPortInfo + .substring(firstColumnSeparator + 1, secondColumnSeparator); + final String hostPort = exposedPortInfo + .substring(secondColumnSeparator + 1); + final ExposedPortModel exposedPort = new ExposedPortModel( + privatePort, type, hostIP, hostPort); // $NON-NLS-1$ + return exposedPort; // $NON-NLS-1$ + } + } + + /** + * Full constructor + * + * @param privatePort + * @param portType + * @param hostAddress + * @param hostPort + */ + public ExposedPortModel(final String privatePort, final String type, + final String hostAddress, final String hostPort) { + Assert.isNotNull(privatePort, + "Port Mapping privatePort cannot be null"); //$NON-NLS-1$ + Assert.isNotNull(type, "Port Mapping portType cannot be null"); //$NON-NLS-1$ + this.containerPort = privatePort; + this.hostPort = hostPort; + this.portType = type; + this.hostAddress = hostAddress; + } + + /** + * Create an ExposedPortModel from its toString output + * + * @param stringValue + * @return ExposedPortModel + */ + static public ExposedPortModel createPortModel(String stringValue) { + final String[] elements = stringValue.split(SEPARATOR); + final String[] containerPortElements = elements[0] + .split(CONTAINER_TYPE_SEPARATOR); + ExposedPortModel model = new ExposedPortModel(containerPortElements[0], + containerPortElements[1], elements[1], elements[2]); + // check the last argument if exists otherwise assume 'true' + model.selected = (elements.length == 4) ? Boolean.valueOf(elements[3]) + : true; + return model; + } + + public String getContainerPort() { + return containerPort; + } + + public void setContainerPort(final String containerPort) { + firePropertyChange(CONTAINER_PORT, this.containerPort, + this.containerPort = containerPort); + } + + public String getPortType() { + return portType; + } + + public void setPortType(final String type) { + firePropertyChange(PORT_TYPE, this.portType, this.portType = type); + } + + public boolean getSelected() { + return selected; + } + + public void setSelected(final boolean selected) { + firePropertyChange(SELECTED, this.selected, this.selected = selected); + } + + public String getHostPort() { + return hostPort; + } + + public void setHostPort(final String hostPort) { + firePropertyChange(HOST_PORT, this.hostPort, this.hostPort = hostPort); + } + + public String getHostAddress() { + return hostAddress; + } + + public void setHostAddress(final String hostAddress) { + firePropertyChange(HOST_ADDRESS, this.hostAddress, + this.hostAddress = hostAddress); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((id == null) ? 0 : id.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + ExposedPortModel other = (ExposedPortModel) obj; + if (id == null) { + if (other.id != null) + return false; + } else if (!id.equals(other.id)) + return false; + return true; + } + + @Override + public int compareTo(final ExposedPortModel other) { + return this.containerPort.compareTo(other.containerPort); + } + + // FIXME we should have a dedicated method to serialize the bean + @Override + public String toString() { + StringBuffer buffer = new StringBuffer(); + buffer.append(containerPort + CONTAINER_TYPE_SEPARATOR + portType + + SEPARATOR + (hostAddress != null ? hostAddress : "") + + SEPARATOR + hostPort + SEPARATOR + selected); + return buffer.toString(); + } + +} diff --git a/launch/org.eclipse.cdt.docker.launcher/src/org/eclipse/cdt/internal/docker/launcher/ILaunchConstants.java b/launch/org.eclipse.cdt.docker.launcher/src/org/eclipse/cdt/internal/docker/launcher/ILaunchConstants.java index f430fc7825e..e4134f66043 100644 --- a/launch/org.eclipse.cdt.docker.launcher/src/org/eclipse/cdt/internal/docker/launcher/ILaunchConstants.java +++ b/launch/org.eclipse.cdt.docker.launcher/src/org/eclipse/cdt/internal/docker/launcher/ILaunchConstants.java @@ -21,6 +21,9 @@ public interface ILaunchConstants { public final static String ATTR_ADDITIONAL_DIRS = DockerLaunchUIPlugin .getUniqueIdentifier() + ".additional_dirs"; //$NON-NLS-1$ + public final static String ATTR_EXPOSED_PORTS = DockerLaunchUIPlugin + .getUniqueIdentifier() + ".exposed_ports"; //$NON-NLS-1$ + public final static String ATTR_IMAGE = DockerLaunchUIPlugin.getUniqueIdentifier() + ".image"; //$NON-NLS-1$ diff --git a/launch/org.eclipse.cdt.docker.launcher/src/org/eclipse/cdt/internal/docker/launcher/Messages.java b/launch/org.eclipse.cdt.docker.launcher/src/org/eclipse/cdt/internal/docker/launcher/Messages.java index aa071ed6959..20efb046862 100644 --- a/launch/org.eclipse.cdt.docker.launcher/src/org/eclipse/cdt/internal/docker/launcher/Messages.java +++ b/launch/org.eclipse.cdt.docker.launcher/src/org/eclipse/cdt/internal/docker/launcher/Messages.java @@ -15,8 +15,6 @@ import org.eclipse.osgi.util.NLS; public class Messages extends NLS { private static final String BUNDLE_NAME = "org.eclipse.cdt.internal.docker.launcher.messages"; //$NON-NLS-1$ - - public static String LaunchShortcut_Binaries; public static String LaunchShortcut_Binary_not_found; public static String LaunchShortcut_Choose_a_launch_configuration; @@ -34,14 +32,25 @@ public class Messages extends NLS { public static String ContainerTab_Name; public static String ContainerTab_Group_Name; public static String ContainerTab_Option_Group_Name; + public static String ContainerTab_Ports_Group_Name; + public static String ContainerTab_Specify_Ports_Label; + + public static String ContainerTab_Add_Button; + public static String ContainerTab_Edit_Button; public static String ContainerTab_New_Button; public static String ContainerTab_Remove_Button; public static String ContainerTab_Keep_Label; + public static String ContainerTab_Publish_All_Ports_Label; public static String ContainerTab_Stdin_Support_Label; public static String ContainerTab_Privileged_Mode_Label; public static String ContainerTab_Error_Reading_Configuration; public static String ContainerTab_Connection_Selector_Label; public static String ContainerTab_Image_Selector_Label; + public static String ContainerTab_Port_Column; + public static String ContainerTab_Type_Column; + public static String ContainerTab_HostAddress_Column; + public static String ContainerTab_HostPort_Column; + public static String ContainerTab_Error_No_Connections; public static String ContainerTab_Error_No_Images; public static String ContainerTab_Warning_Connection_Not_Found; @@ -113,6 +122,12 @@ public class Messages extends NLS { public static String Gdbserver_Settings_Remotetimeout_tooltip; + public static String ContainerPortDialog_hostAddressLabel; + public static String ContainerPortDialog_hostPortLabel; + public static String ContainerPortDialog_shellTitle; + public static String ContainerPortDialog_containerLabel; + public static String ContainerPortDialog_explanationLabel; + static { // initialize resource bundle NLS.initializeMessages(BUNDLE_NAME, Messages.class); diff --git a/launch/org.eclipse.cdt.docker.launcher/src/org/eclipse/cdt/internal/docker/launcher/messages.properties b/launch/org.eclipse.cdt.docker.launcher/src/org/eclipse/cdt/internal/docker/launcher/messages.properties index 5b8e30e9590..e54ac0ed21b 100644 --- a/launch/org.eclipse.cdt.docker.launcher/src/org/eclipse/cdt/internal/docker/launcher/messages.properties +++ b/launch/org.eclipse.cdt.docker.launcher/src/org/eclipse/cdt/internal/docker/launcher/messages.properties @@ -26,20 +26,35 @@ Keep_Container_After_Launch=Keep Container after launch ContainerTab_Name=Container ContainerTab_New_Button=New... +ContainerTab_Add_Button=Add... +ContainerTab_Edit_Button=Edit... ContainerTab_Remove_Button=Remove ContainerTab_Keep_Label=Keep Container after launch ContainerTab_Stdin_Support_Label=Support stdin input ContainerTab_Privileged_Mode_Label=Run in privileged mode ContainerTab_Group_Name=Required host directories +ContainerTab_Ports_Group_Name=Ports +ContainerTab_Specify_Ports_Label=Manually specify ports and only publish selected entries to the host: +ContainerTab_Publish_All_Ports_Label=Publish all default exposed ports for image to random ports on the host ContainerTab_Option_Group_Name=Additional Options ContainerTab_Connection_Selector_Label=Connection: ContainerTab_Image_Selector_Label=Image: +ContainerTab_Port_Column=Container Port +ContainerTab_Type_Column=Type +ContainerTab_HostAddress_Column=Host Address +ContainerTab_HostPort_Column=Host Port ContainerTab_Error_Reading_Configuration=Error occurred reading the launch configuration: {0} ContainerTab_Error_No_Connections=No Docker Connections exist ContainerTab_Error_No_Images=No Docker Images exist ContainerTab_Warning_Connection_Not_Found=Docker Connection: {0} for Launch Configuration not found: defaulting to {1} ContainerTab_Warning_Image_Not_Found=Docker Image: {0} is not a valid pulled image in current Connection: {1} +ContainerPortDialog_shellTitle=Exposing a Container Port +ContainerPortDialog_explanationLabel=Specify the container port to expose: +ContainerPortDialog_containerLabel=Container port: +ContainerPortDialog_hostAddressLabel=Host address: +ContainerPortDialog_hostPortLabel=Host port: + ContainerPropertyTab_Title=Container Settings ContainerPropertyTab_Enable_Msg=Build inside Docker Image ContainerPropertyTab_Run_Autotools_In_Container_Msg=Run all Autotools in Container