Skip to content

Commit 3531467

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

File tree

24 files changed

+1552
-309
lines changed

24 files changed

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

0 commit comments

Comments
 (0)