Skip to content

Commit 2e1f709

Browse files
committed
Add Azure DevOps Server support
1 parent 0d5b12b commit 2e1f709

File tree

24 files changed

+1519
-309
lines changed

24 files changed

+1519
-309
lines changed

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2012-2024 Red Hat, Inc.
2+
* Copyright (c) 2012-2025 Red Hat, Inc.
33
* This program and the accompanying materials are made
44
* available under the terms of the Eclipse Public License 2.0
55
* which is available at https://www.eclipse.org/legal/epl-2.0/
@@ -48,7 +48,7 @@
4848
import org.eclipse.che.workspace.infrastructure.kubernetes.environment.KubernetesEnvironmentFactory;
4949
import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.RemoveNamespaceOnWorkspaceRemove;
5050
import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.configurator.CredentialsSecretConfigurator;
51-
import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.configurator.GitconfigUserDataConfigurator;
51+
import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.configurator.GitconfigAutomauntSecretConfigurator;
5252
import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.configurator.NamespaceConfigurator;
5353
import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.configurator.OAuthTokenSecretsConfigurator;
5454
import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.configurator.PreferencesConfigMapConfigurator;
@@ -109,7 +109,7 @@ protected void configure() {
109109
namespaceConfigurators.addBinding().to(WorkspaceServiceAccountConfigurator.class);
110110
namespaceConfigurators.addBinding().to(UserProfileConfigurator.class);
111111
namespaceConfigurators.addBinding().to(UserPreferencesConfigurator.class);
112-
namespaceConfigurators.addBinding().to(GitconfigUserDataConfigurator.class);
112+
namespaceConfigurators.addBinding().to(GitconfigAutomauntSecretConfigurator.class);
113113

114114
bind(AuthorizationChecker.class).to(KubernetesAuthorizationCheckerImpl.class);
115115
bind(PermissionsCleaner.class).asEagerSingleton();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
/*
2+
* Copyright (c) 2012-2025 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.nio.charset.StandardCharsets.UTF_8;
16+
17+
import com.google.common.collect.ImmutableMap;
18+
import io.fabric8.kubernetes.api.model.Secret;
19+
import io.fabric8.kubernetes.api.model.SecretBuilder;
20+
import io.fabric8.kubernetes.client.KubernetesClient;
21+
import java.util.Base64;
22+
import java.util.Map;
23+
import java.util.Optional;
24+
import java.util.Set;
25+
import java.util.StringJoiner;
26+
import java.util.regex.Matcher;
27+
import java.util.regex.Pattern;
28+
import javax.inject.Inject;
29+
import org.eclipse.che.api.factory.server.scm.GitUserData;
30+
import org.eclipse.che.api.factory.server.scm.GitUserDataFetcher;
31+
import org.eclipse.che.api.factory.server.scm.exception.ScmBadRequestException;
32+
import org.eclipse.che.api.factory.server.scm.exception.ScmCommunicationException;
33+
import org.eclipse.che.api.factory.server.scm.exception.ScmConfigurationPersistenceException;
34+
import org.eclipse.che.api.factory.server.scm.exception.ScmItemNotFoundException;
35+
import org.eclipse.che.api.factory.server.scm.exception.ScmUnauthorizedException;
36+
import org.eclipse.che.api.workspace.server.spi.InfrastructureException;
37+
import org.eclipse.che.api.workspace.server.spi.NamespaceResolutionContext;
38+
import org.eclipse.che.commons.lang.Pair;
39+
import org.eclipse.che.workspace.infrastructure.kubernetes.CheServerKubernetesClientFactory;
40+
import org.slf4j.Logger;
41+
import org.slf4j.LoggerFactory;
42+
43+
public class GitconfigAutomauntSecretConfigurator implements NamespaceConfigurator {
44+
private final CheServerKubernetesClientFactory cheServerKubernetesClientFactory;
45+
private final Set<GitUserDataFetcher> gitUserDataFetchers;
46+
private static final Logger LOG =
47+
LoggerFactory.getLogger(GitconfigAutomauntSecretConfigurator.class);
48+
private static final String CONFIGMAP_DATA_KEY = "gitconfig";
49+
private static final String GITCONFIG_SECRET_NAME = "devworkspace-gitconfig-automaunt-secret";
50+
private static final Map<String, String> TOKEN_SECRET_LABELS =
51+
ImmutableMap.of(
52+
"app.kubernetes.io/part-of", "che.eclipse.org",
53+
"app.kubernetes.io/component", "scm-personal-access-token");
54+
private static final Map<String, String> GITCONFIG_SECRET_LABELS =
55+
ImmutableMap.of(
56+
"controller.devfile.io/mount-to-devworkspace",
57+
"true",
58+
"controller.devfile.io/watch-secret",
59+
"true");
60+
private static final Map<String, String> GITCONFIG_SECRET_ANNOTATIONS =
61+
ImmutableMap.of(
62+
"controller.devfile.io/mount-as", "subpath", "controller.devfile.io/mount-path", "/etc");
63+
private final Pattern usernmaePattern =
64+
Pattern.compile("\\[user](.|\\s)*name\\s*=\\s*(?<username>.*)");
65+
private final Pattern emailPattern =
66+
Pattern.compile("\\[user](.|\\s)*email\\s*=\\s*(?<email>.*)");
67+
private final Pattern emptyStringPattern = Pattern.compile("[\"']\\s*[\"']");
68+
69+
@Inject
70+
public GitconfigAutomauntSecretConfigurator(
71+
CheServerKubernetesClientFactory cheServerKubernetesClientFactory,
72+
Set<GitUserDataFetcher> gitUserDataFetchers) {
73+
this.cheServerKubernetesClientFactory = cheServerKubernetesClientFactory;
74+
this.gitUserDataFetchers = gitUserDataFetchers;
75+
}
76+
77+
@Override
78+
public void configure(NamespaceResolutionContext namespaceResolutionContext, String namespaceName)
79+
throws InfrastructureException {
80+
KubernetesClient client = cheServerKubernetesClientFactory.create();
81+
Optional<String> gitconfigOptional = getGitconfig(client, namespaceName);
82+
Optional<Pair<String, String>> usernameAndEmailFromGitconfigOptional = Optional.empty();
83+
Optional<Pair<String, String>> usernameAndEmailFromFetcherOptional =
84+
getUsernameAndEmailFromFetcher();
85+
Optional<String> tokenFromGitconfigOptional = Optional.empty();
86+
Optional<String> tokenFromSecretOptional = getTokenFromSecret(client, namespaceName);
87+
if (gitconfigOptional.isPresent()) {
88+
String gitconfig = gitconfigOptional.get();
89+
usernameAndEmailFromGitconfigOptional = getUsernameAndEmailFromGitconfig(gitconfig);
90+
tokenFromGitconfigOptional = getTokenFromGitconfig(gitconfig);
91+
}
92+
if (needUpdateGitconfigSecret(
93+
usernameAndEmailFromGitconfigOptional,
94+
usernameAndEmailFromFetcherOptional,
95+
tokenFromGitconfigOptional,
96+
tokenFromSecretOptional)) {
97+
Secret gitconfigSecret = buildGitconfigSecret();
98+
Optional<Pair<String, String>> usernameAndEmailOptional =
99+
usernameAndEmailFromGitconfigOptional.isPresent()
100+
? usernameAndEmailFromGitconfigOptional
101+
: usernameAndEmailFromFetcherOptional;
102+
Optional<String> gitconfigSectionsOptional =
103+
generateGitconfigSections(usernameAndEmailOptional, tokenFromSecretOptional);
104+
gitconfigSecret.setData(
105+
ImmutableMap.of(
106+
CONFIGMAP_DATA_KEY,
107+
gitconfigSectionsOptional.isPresent()
108+
? encode(gitconfigSectionsOptional.get())
109+
: ""));
110+
client.secrets().inNamespace(namespaceName).createOrReplace(gitconfigSecret);
111+
}
112+
}
113+
114+
private Secret buildGitconfigSecret() {
115+
return new SecretBuilder()
116+
.withNewMetadata()
117+
.withName(GITCONFIG_SECRET_NAME)
118+
.withLabels(GITCONFIG_SECRET_LABELS)
119+
.withAnnotations(GITCONFIG_SECRET_ANNOTATIONS)
120+
.endMetadata()
121+
.build();
122+
}
123+
124+
private boolean needUpdateGitconfigSecret(
125+
Optional<Pair<String, String>> usernameAndEmailFromGitconfigOptional,
126+
Optional<Pair<String, String>> usernameAndEmailFromFetcher,
127+
Optional<String> tokenFromGitconfigOptional,
128+
Optional<String> tokenFromSecretOptional) {
129+
if (tokenFromGitconfigOptional.isPresent() && tokenFromSecretOptional.isPresent()) {
130+
return !tokenFromGitconfigOptional.get().equals(tokenFromSecretOptional.get());
131+
} else
132+
return (tokenFromSecretOptional.isPresent() || tokenFromGitconfigOptional.isPresent())
133+
|| (usernameAndEmailFromGitconfigOptional.isEmpty()
134+
&& usernameAndEmailFromFetcher.isPresent());
135+
}
136+
137+
private Optional<String> generateGitconfigSections(
138+
Optional<Pair<String, String>> usernameAndEmailOptional, Optional<String> tokenOptional) {
139+
Optional<String> userSectionOptional = Optional.empty();
140+
Optional<String> httpSectionOPtional = Optional.empty();
141+
if (usernameAndEmailOptional.isPresent()) {
142+
userSectionOptional =
143+
Optional.of(
144+
generateUserSection(
145+
usernameAndEmailOptional.get().first, usernameAndEmailOptional.get().second));
146+
}
147+
if (tokenOptional.isPresent()) {
148+
httpSectionOPtional = Optional.of(generateHttpSection(tokenOptional.get()));
149+
}
150+
StringJoiner joiner = new StringJoiner("\n");
151+
userSectionOptional.ifPresent(joiner::add);
152+
httpSectionOPtional.ifPresent(joiner::add);
153+
return joiner.length() > 0 ? Optional.of(joiner.toString()) : Optional.empty();
154+
}
155+
156+
private Optional<Pair<String, String>> getUsernameAndEmailFromGitconfig(String gitconfig) {
157+
if (gitconfig.contains("[user]")) {
158+
Matcher usernameMatcher = usernmaePattern.matcher(gitconfig);
159+
Matcher emailaMatcher = emailPattern.matcher(gitconfig);
160+
if (usernameMatcher.find() && emailaMatcher.find()) {
161+
String username = usernameMatcher.group("username");
162+
String email = emailaMatcher.group("email");
163+
if (!emptyStringPattern.matcher(username).matches()
164+
&& !emptyStringPattern.matcher(email).matches()) {
165+
return Optional.of(new Pair<>(username, email));
166+
}
167+
}
168+
}
169+
return Optional.empty();
170+
}
171+
172+
private Optional<Pair<String, String>> getUsernameAndEmailFromFetcher() {
173+
GitUserData gitUserData;
174+
for (GitUserDataFetcher fetcher : gitUserDataFetchers) {
175+
try {
176+
gitUserData = fetcher.fetchGitUserData();
177+
if (!isNullOrEmpty(gitUserData.getScmUsername())
178+
&& !isNullOrEmpty(gitUserData.getScmUserEmail())) {
179+
return Optional.of(
180+
new Pair<>(gitUserData.getScmUsername(), gitUserData.getScmUserEmail()));
181+
}
182+
} catch (ScmUnauthorizedException
183+
| ScmCommunicationException
184+
| ScmConfigurationPersistenceException
185+
| ScmItemNotFoundException
186+
| ScmBadRequestException e) {
187+
LOG.debug("No GitUserDataFetcher is configured. " + e.getMessage());
188+
}
189+
}
190+
return Optional.empty();
191+
}
192+
193+
private Optional<String> getTokenFromSecret(KubernetesClient client, String namespaceName) {
194+
for (Secret tokenSecret :
195+
client
196+
.secrets()
197+
.inNamespace(namespaceName)
198+
.withLabels(TOKEN_SECRET_LABELS)
199+
.list()
200+
.getItems()) {
201+
if ("azure-devops"
202+
.equals(
203+
tokenSecret
204+
.getMetadata()
205+
.getAnnotations()
206+
.get("che.eclipse.org/scm-provider-name"))) {
207+
return Optional.of(decode(tokenSecret.getData().get("token")));
208+
}
209+
}
210+
return Optional.empty();
211+
}
212+
213+
private Optional<String> getTokenFromGitconfig(String gitconfig) {
214+
if (gitconfig.contains("[http]")) {
215+
Matcher matcher =
216+
Pattern.compile(
217+
"\\[http]\\n\\s*extra[hH]eader\\s*=\\s*[\"']?Authorization: Basic (?<tokenEncoded>.*)[\"']?")
218+
.matcher(gitconfig);
219+
if (matcher.find()) {
220+
// remove the first character which is ':' from the token value
221+
return Optional.of(decode(matcher.group("tokenEncoded")).substring(1));
222+
}
223+
}
224+
return Optional.empty();
225+
}
226+
227+
private String generateHttpSection(String token) {
228+
return "[http]\n\textraHeader = Authorization: Basic " + encode(":" + token);
229+
}
230+
231+
private String generateUserSection(String username, String email) {
232+
return String.format("[user]\n\tname = %1$s\n\temail = %2$s", username, email);
233+
}
234+
235+
private Optional<String> getGitconfig(KubernetesClient client, String namespaceName) {
236+
Secret gitconfigAutomauntSecret =
237+
client.secrets().inNamespace(namespaceName).withName(GITCONFIG_SECRET_NAME).get();
238+
if (gitconfigAutomauntSecret != null) {
239+
String gitconfig = gitconfigAutomauntSecret.getData().get(CONFIGMAP_DATA_KEY);
240+
if (!isNullOrEmpty(gitconfig)) {
241+
return Optional.of(decode(gitconfig));
242+
}
243+
}
244+
return Optional.empty();
245+
}
246+
247+
private String encode(String value) {
248+
return Base64.getEncoder().encodeToString(value.getBytes(UTF_8));
249+
}
250+
251+
private String decode(String value) {
252+
return new String(Base64.getDecoder().decode(value.getBytes(UTF_8)));
253+
}
254+
}

infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/configurator/GitconfigUserDataConfigurator.java

Lines changed: 0 additions & 116 deletions
This file was deleted.

0 commit comments

Comments
 (0)