Skip to content

Commit 563822d

Browse files
authored
Merge pull request #60 from PizzaFactory/prp-update-to-the-upstream
[Scheduled] Update to the upstream
2 parents f3d89ae + 1586437 commit 563822d

File tree

15 files changed

+555
-112
lines changed

15 files changed

+555
-112
lines changed

assembly/assembly-wsmaster-war/.deps/EXCLUDED/prod.md

+1
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,5 @@ This file lists dependencies that do not need CQs or auto-detection does not wor
88
| `org.jgroups.kubernetes/[email protected]` | [CQ20984](https://dev.eclipse.org/ipzilla/show_bug.cgi?id=20984) |
99
| `org.flywaydb/[email protected]` | [CQ14689](https://dev.eclipse.org/ipzilla/show_bug.cgi?id=14689) |
1010
| `com.auth0/[email protected]` | [CQ23769](https://dev.eclipse.org/ipzilla/show_bug.cgi?id=23769) |
11+
| `org.apache.tomcat/[email protected]` | [CQ23765](https://dev.eclipse.org/ipzilla/show_bug.cgi?id=23765) |
1112

assembly/assembly-wsmaster-war/.deps/dev.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
| `org.antlr/[email protected]` | NOASSERTION | [CQ14504](https://dev.eclipse.org/ipzilla/show_bug.cgi?id=14504) |
1111
| `org.antlr/[email protected]` | BSD-3-Clause | clearlydefined |
1212
| `org.antlr/[email protected]` | BSD-2-Clause | [CQ6759](https://dev.eclipse.org/ipzilla/show_bug.cgi?id=6759) |
13-
| `org.eclipse.che.core/che-core-commons-test@7.40.0-SNAPSHOT` | - | eclipse |
13+
| `org.eclipse.che.core/che-core-commons-test@7.41.0-SNAPSHOT` | EPL-2.0 | ecd.che |
1414
| `org.mockito/[email protected]` | MIT | clearlydefined |
1515
| `org.objenesis/[email protected]` | Apache-2.0 | clearlydefined |
1616
| `org.reflections/[email protected]` | MIT | [CQ10054](https://dev.eclipse.org/ipzilla/show_bug.cgi?id=10054) |

assembly/assembly-wsmaster-war/.deps/problems.md

+1
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@
22

33
## UNRESOLVED Production dependencies
44

5+
1. `org.apache.tomcat/[email protected]`

assembly/assembly-wsmaster-war/.deps/prod.md

+93-93
Large diffs are not rendered by default.

devfile.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ components:
88
mountSources: true
99
memoryLimit: 2Gi
1010
volumes:
11-
- name: maven
11+
- name: m2
1212
containerPath: /home/user/.m2
1313
- type: chePlugin
1414
id: redhat/java11/latest

infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/Constants.java

+3
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ public final class Constants {
5050
public static final String DEV_WORKSPACE_MOUNT_LABEL =
5151
"controller.devfile.io/mount-to-devworkspace";
5252

53+
public static final String DEV_WORKSPACE_WATCH_SECRET_LABEL =
54+
"controller.devfile.io/watch-secret";
55+
5356
public static final String DEV_WORKSPACE_MOUNT_PATH_ANNOTATION =
5457
"controller.devfile.io/mount-path";
5558
public static final String DEV_WORKSPACE_MOUNT_AS_ANNOTATION = "controller.devfile.io/mount-as";

infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/KubernetesInfraModule.java

+2
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.configurator.CredentialsSecretConfigurator;
5252
import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.configurator.NamespaceConfigurator;
5353
import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.configurator.PreferencesConfigMapConfigurator;
54+
import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.configurator.SshKeysConfigurator;
5455
import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.configurator.UserPermissionConfigurator;
5556
import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.configurator.UserPreferencesConfigurator;
5657
import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.configurator.UserProfileConfigurator;
@@ -116,6 +117,7 @@ protected void configure() {
116117
namespaceConfigurators.addBinding().to(WorkspaceServiceAccountConfigurator.class);
117118
namespaceConfigurators.addBinding().to(UserProfileConfigurator.class);
118119
namespaceConfigurators.addBinding().to(UserPreferencesConfigurator.class);
120+
namespaceConfigurators.addBinding().to(SshKeysConfigurator.class);
119121

120122
bind(KubernetesNamespaceService.class);
121123

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
/*
2+
* Copyright (c) 2012-2021 Red Hat, Inc.
3+
* This program and the accompanying materials are made
4+
* available under the terms of the Eclipse Public License 2.0
5+
* which is available at https://www.eclipse.org/legal/epl-2.0/
6+
*
7+
* SPDX-License-Identifier: EPL-2.0
8+
*
9+
* Contributors:
10+
* Red Hat, Inc. - initial API and implementation
11+
*/
12+
package org.eclipse.che.workspace.infrastructure.kubernetes.namespace.configurator;
13+
14+
import static com.google.common.base.Strings.isNullOrEmpty;
15+
import static java.lang.String.format;
16+
import static java.util.stream.Collectors.toList;
17+
import static org.eclipse.che.workspace.infrastructure.kubernetes.Constants.DEV_WORKSPACE_MOUNT_LABEL;
18+
import static org.eclipse.che.workspace.infrastructure.kubernetes.Constants.DEV_WORKSPACE_MOUNT_PATH_ANNOTATION;
19+
import static org.eclipse.che.workspace.infrastructure.kubernetes.Constants.DEV_WORKSPACE_WATCH_SECRET_LABEL;
20+
import static org.eclipse.che.workspace.infrastructure.kubernetes.namespace.KubernetesObjectUtil.isValidConfigMapKeyName;
21+
22+
import com.google.common.annotations.VisibleForTesting;
23+
import io.fabric8.kubernetes.api.model.ObjectMeta;
24+
import io.fabric8.kubernetes.api.model.ObjectMetaBuilder;
25+
import io.fabric8.kubernetes.api.model.Secret;
26+
import io.fabric8.kubernetes.api.model.SecretBuilder;
27+
import io.fabric8.kubernetes.client.KubernetesClient;
28+
import jakarta.validation.constraints.NotNull;
29+
import java.util.Base64;
30+
import java.util.HashMap;
31+
import java.util.List;
32+
import java.util.Map;
33+
import java.util.regex.Pattern;
34+
import javax.inject.Inject;
35+
import org.eclipse.che.api.core.ServerException;
36+
import org.eclipse.che.api.ssh.server.SshManager;
37+
import org.eclipse.che.api.ssh.server.model.impl.SshPairImpl;
38+
import org.eclipse.che.api.ssh.shared.model.SshPair;
39+
import org.eclipse.che.api.workspace.server.spi.InfrastructureException;
40+
import org.eclipse.che.api.workspace.server.spi.NamespaceResolutionContext;
41+
import org.eclipse.che.workspace.infrastructure.kubernetes.KubernetesClientFactory;
42+
import org.slf4j.Logger;
43+
import org.slf4j.LoggerFactory;
44+
45+
/**
46+
* This class mounts existing user SSH Keys into a special Kubernetes Secret on user-s namespace.
47+
*/
48+
public class SshKeysConfigurator implements NamespaceConfigurator {
49+
50+
public static final String SSH_KEY_SECRET_NAME = "che-git-ssh-key";
51+
private static final String SSH_KEYS_WILL_NOT_BE_MOUNTED_MESSAGE =
52+
"Ssh keys %s have invalid names and can't be mounted to namespace %s.";
53+
54+
private static final String SSH_BASE_CONFIG_PATH = "/etc/ssh/";
55+
56+
private final SshManager sshManager;
57+
58+
private final KubernetesClientFactory clientFactory;
59+
60+
private static final Logger LOG = LoggerFactory.getLogger(SshKeysConfigurator.class);
61+
62+
public static final Pattern VALID_DOMAIN_PATTERN =
63+
Pattern.compile("^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$");
64+
65+
@Inject
66+
public SshKeysConfigurator(SshManager sshManager, KubernetesClientFactory clientFactory) {
67+
this.sshManager = sshManager;
68+
this.clientFactory = clientFactory;
69+
}
70+
71+
@Override
72+
public void configure(NamespaceResolutionContext namespaceResolutionContext, String namespaceName)
73+
throws InfrastructureException {
74+
75+
var client = clientFactory.create();
76+
List<SshPairImpl> vcsSshPairs = getVcsSshPairs(namespaceResolutionContext);
77+
78+
List<String> invalidSshKeyNames =
79+
vcsSshPairs
80+
.stream()
81+
.filter(keyPair -> !isValidSshKeyPair(keyPair))
82+
.map(SshPairImpl::getName)
83+
.collect(toList());
84+
85+
if (!invalidSshKeyNames.isEmpty()) {
86+
String message =
87+
format(
88+
SSH_KEYS_WILL_NOT_BE_MOUNTED_MESSAGE, invalidSshKeyNames.toString(), namespaceName);
89+
LOG.warn(message);
90+
// filter
91+
vcsSshPairs = vcsSshPairs.stream().filter(this::isValidSshKeyPair).collect(toList());
92+
}
93+
94+
if (vcsSshPairs.size() == 0) {
95+
// nothing to provision
96+
return;
97+
}
98+
doProvisionSshKeys(vcsSshPairs, client, namespaceName);
99+
}
100+
101+
/**
102+
* Return list of keys related to the VCS (Version Control Systems), Git, SVN and etc. Usually
103+
* managed by user
104+
*
105+
* @param context NamespaceResolutionContext
106+
* @return list of ssh pairs
107+
*/
108+
private List<SshPairImpl> getVcsSshPairs(NamespaceResolutionContext context)
109+
throws InfrastructureException {
110+
List<SshPairImpl> sshPairs;
111+
try {
112+
sshPairs = sshManager.getPairs(context.getUserId(), "vcs");
113+
} catch (ServerException e) {
114+
String message = format("Unable to get SSH Keys. Cause: %s", e.getMessage());
115+
LOG.warn(message);
116+
throw new InfrastructureException(e);
117+
}
118+
return sshPairs;
119+
}
120+
121+
@VisibleForTesting
122+
boolean isValidSshKeyPair(SshPairImpl keyPair) {
123+
return isValidConfigMapKeyName(keyPair.getName())
124+
&& VALID_DOMAIN_PATTERN.matcher(keyPair.getName()).matches();
125+
}
126+
127+
private void doProvisionSshKeys(
128+
List<SshPairImpl> sshPairs, KubernetesClient client, String namespaceName) {
129+
130+
StringBuilder sshConfigData = new StringBuilder();
131+
Map<String, String> data = new HashMap<>();
132+
133+
for (SshPair sshPair : sshPairs) {
134+
sshConfigData.append(buildConfig(sshPair.getName()));
135+
if (!isNullOrEmpty(sshPair.getPrivateKey())) {
136+
data.put(
137+
sshPair.getName(),
138+
Base64.getEncoder().encodeToString(sshPair.getPrivateKey().getBytes()));
139+
data.put(
140+
sshPair.getName() + ".pub",
141+
Base64.getEncoder().encodeToString(sshPair.getPublicKey().getBytes()));
142+
}
143+
}
144+
data.put("ssh_config", Base64.getEncoder().encodeToString(sshConfigData.toString().getBytes()));
145+
Secret secret =
146+
new SecretBuilder()
147+
.addToData(data)
148+
.withType("generic")
149+
.withMetadata(buildMetadata())
150+
.build();
151+
152+
client.secrets().inNamespace(namespaceName).createOrReplace(secret);
153+
}
154+
155+
private ObjectMeta buildMetadata() {
156+
return new ObjectMetaBuilder()
157+
.withName(SSH_KEY_SECRET_NAME)
158+
.withLabels(
159+
Map.of(
160+
DEV_WORKSPACE_MOUNT_LABEL, "true",
161+
DEV_WORKSPACE_WATCH_SECRET_LABEL, "true"))
162+
.withAnnotations(Map.of(DEV_WORKSPACE_MOUNT_PATH_ANNOTATION, SSH_BASE_CONFIG_PATH))
163+
.build();
164+
}
165+
166+
/**
167+
* Returns the ssh configuration entry which includes host, identity file location and Host Key
168+
* checking policy
169+
*
170+
* <p>Example of provided configuration:
171+
*
172+
* <pre>
173+
* host github.com
174+
* IdentityFile /.ssh/private/github-com/private
175+
* StrictHostKeyChecking = no
176+
* </pre>
177+
*
178+
* or
179+
*
180+
* <pre>
181+
* host *
182+
* IdentityFile /.ssh/private/default-123456/private
183+
* StrictHostKeyChecking = no
184+
* </pre>
185+
*
186+
* @param name the of key given during generate for vcs service we will consider it as host of
187+
* version control service (e.g. github.com, gitlab.com and etc) if name starts from
188+
* "default-{anyString}" it will be replaced on wildcard "*" host name. Name with format
189+
* "default-{anyString}" will be generated on client side by Theia SSH Plugin, if user doesn't
190+
* provide own name. Details see here:
191+
* https://github.com/eclipse/che/issues/13494#issuecomment-512761661. Note: behavior can be
192+
* improved in 7.x releases after 7.0.0
193+
* @return the ssh configuration which include host, identity file location and Host Key checking
194+
* policy
195+
*/
196+
private String buildConfig(@NotNull String name) {
197+
String host = name.startsWith("default-") ? "*" : name;
198+
return "host "
199+
+ host
200+
+ "\nIdentityFile "
201+
+ SSH_BASE_CONFIG_PATH
202+
+ name
203+
+ "\nStrictHostKeyChecking = no"
204+
+ "\n\n";
205+
}
206+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
/*
2+
* Copyright (c) 2012-2021 Red Hat, Inc.
3+
* This program and the accompanying materials are made
4+
* available under the terms of the Eclipse Public License 2.0
5+
* which is available at https://www.eclipse.org/legal/epl-2.0/
6+
*
7+
* SPDX-License-Identifier: EPL-2.0
8+
*
9+
* Contributors:
10+
* Red Hat, Inc. - initial API and implementation
11+
*/
12+
package org.eclipse.che.workspace.infrastructure.kubernetes.namespace.configurator;
13+
14+
import static org.eclipse.che.workspace.infrastructure.kubernetes.Constants.DEV_WORKSPACE_MOUNT_LABEL;
15+
import static org.eclipse.che.workspace.infrastructure.kubernetes.Constants.DEV_WORKSPACE_WATCH_SECRET_LABEL;
16+
import static org.mockito.Mockito.when;
17+
import static org.testng.Assert.assertEquals;
18+
19+
import io.fabric8.kubernetes.api.model.Secret;
20+
import io.fabric8.kubernetes.client.server.mock.KubernetesServer;
21+
import java.util.Base64;
22+
import java.util.Collections;
23+
import java.util.List;
24+
import java.util.Map;
25+
import org.eclipse.che.api.core.NotFoundException;
26+
import org.eclipse.che.api.core.ServerException;
27+
import org.eclipse.che.api.ssh.server.SshManager;
28+
import org.eclipse.che.api.ssh.server.model.impl.SshPairImpl;
29+
import org.eclipse.che.api.workspace.server.spi.InfrastructureException;
30+
import org.eclipse.che.api.workspace.server.spi.NamespaceResolutionContext;
31+
import org.eclipse.che.workspace.infrastructure.kubernetes.KubernetesClientFactory;
32+
import org.mockito.InjectMocks;
33+
import org.mockito.Mock;
34+
import org.mockito.testng.MockitoTestNGListener;
35+
import org.testng.annotations.AfterMethod;
36+
import org.testng.annotations.BeforeMethod;
37+
import org.testng.annotations.Listeners;
38+
import org.testng.annotations.Test;
39+
40+
@Listeners(MockitoTestNGListener.class)
41+
public class SshKeysConfiguratorTest {
42+
43+
private static final String USER_ID = "user-id";
44+
private static final String USER_NAME = "user-name";
45+
private static final String USER_NAMESPACE = "user-namespace";
46+
47+
@Mock private KubernetesClientFactory clientFactory;
48+
@Mock private SshManager sshManager;
49+
50+
@InjectMocks private SshKeysConfigurator sshKeysConfigurator;
51+
52+
private KubernetesServer kubernetesServer;
53+
private NamespaceResolutionContext context;
54+
55+
private final SshPairImpl sshPair =
56+
new SshPairImpl(USER_ID, "vcs", "github.com", "public-key", "private-key");
57+
58+
@BeforeMethod
59+
public void setUp() throws InfrastructureException, NotFoundException, ServerException {
60+
context = new NamespaceResolutionContext(null, USER_ID, USER_NAME);
61+
kubernetesServer = new KubernetesServer(true, true);
62+
kubernetesServer.before();
63+
64+
when(sshManager.getPairs(USER_ID, "vcs")).thenReturn(Collections.singletonList(sshPair));
65+
when(clientFactory.create()).thenReturn(kubernetesServer.getClient());
66+
}
67+
68+
@AfterMethod
69+
public void cleanUp() {
70+
kubernetesServer.getClient().secrets().inNamespace(USER_NAMESPACE).delete();
71+
kubernetesServer.after();
72+
}
73+
74+
@Test
75+
public void shouldCreateSSHKeysSecret() throws InfrastructureException {
76+
sshKeysConfigurator.configure(context, USER_NAMESPACE);
77+
List<Secret> secrets =
78+
kubernetesServer
79+
.getClient()
80+
.secrets()
81+
.inNamespace(USER_NAMESPACE)
82+
.withLabels(
83+
Map.of(
84+
DEV_WORKSPACE_MOUNT_LABEL, "true",
85+
DEV_WORKSPACE_WATCH_SECRET_LABEL, "true"))
86+
.list()
87+
.getItems();
88+
assertEquals(secrets.size(), 1);
89+
assertEquals(secrets.get(0).getMetadata().getName(), "che-git-ssh-key");
90+
assertEquals(secrets.get(0).getData().size(), 3);
91+
assertEquals(
92+
new String(Base64.getDecoder().decode(secrets.get(0).getData().get("github.com"))),
93+
"private-key");
94+
assertEquals(
95+
new String(Base64.getDecoder().decode(secrets.get(0).getData().get("github.com.pub"))),
96+
"public-key");
97+
assertEquals(
98+
new String(Base64.getDecoder().decode(secrets.get(0).getData().get("ssh_config"))),
99+
"host github.com\n"
100+
+ "IdentityFile /etc/ssh/github.com\n"
101+
+ "StrictHostKeyChecking = no\n\n");
102+
}
103+
104+
@Test
105+
public void shouldNotCreateSSHKeysSecretFromBadSecret() throws Exception {
106+
SshPairImpl sshPairLocal =
107+
new SshPairImpl(USER_ID, "vcs", "%sd$$$", "public-key", "private-key");
108+
when(sshManager.getPairs(USER_ID, "vcs")).thenReturn(List.of(sshPairLocal));
109+
sshKeysConfigurator.configure(context, USER_NAMESPACE);
110+
List<Secret> secrets =
111+
kubernetesServer
112+
.getClient()
113+
.secrets()
114+
.inNamespace(USER_NAMESPACE)
115+
.withLabels(
116+
Map.of(
117+
DEV_WORKSPACE_MOUNT_LABEL, "true",
118+
DEV_WORKSPACE_WATCH_SECRET_LABEL, "true"))
119+
.list()
120+
.getItems();
121+
assertEquals(secrets.size(), 0);
122+
}
123+
}

0 commit comments

Comments
 (0)