Skip to content

Commit ff1422c

Browse files
committed
commit
1 parent 21fb7b2 commit ff1422c

File tree

5 files changed

+276
-183
lines changed

5 files changed

+276
-183
lines changed

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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.GitconfigSecretConfigurator;
51+
import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.configurator.GitconfigAutomauntSecretConfigurator;
5252
import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.configurator.GitconfigUserDataConfigurator;
5353
import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.configurator.NamespaceConfigurator;
5454
import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.configurator.OAuthTokenSecretsConfigurator;
@@ -111,7 +111,7 @@ protected void configure() {
111111
namespaceConfigurators.addBinding().to(UserProfileConfigurator.class);
112112
namespaceConfigurators.addBinding().to(UserPreferencesConfigurator.class);
113113
namespaceConfigurators.addBinding().to(GitconfigUserDataConfigurator.class);
114-
namespaceConfigurators.addBinding().to(GitconfigSecretConfigurator.class);
114+
namespaceConfigurators.addBinding().to(GitconfigAutomauntSecretConfigurator.class);
115115

116116
bind(AuthorizationChecker.class).to(KubernetesAuthorizationCheckerImpl.class);
117117
bind(PermissionsCleaner.class).asEagerSingleton();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
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+
64+
@Inject
65+
public GitconfigAutomauntSecretConfigurator(
66+
CheServerKubernetesClientFactory cheServerKubernetesClientFactory,
67+
Set<GitUserDataFetcher> gitUserDataFetchers) {
68+
this.cheServerKubernetesClientFactory = cheServerKubernetesClientFactory;
69+
this.gitUserDataFetchers = gitUserDataFetchers;
70+
}
71+
72+
@Override
73+
public void configure(NamespaceResolutionContext namespaceResolutionContext, String namespaceName)
74+
throws InfrastructureException {
75+
KubernetesClient client = cheServerKubernetesClientFactory.create();
76+
Optional<String> gitconfigOptional = getGitconfig(client, namespaceName);
77+
Optional<Pair<String, String>> usernameAndEmailFromGitconfigOptional = Optional.empty();
78+
Optional<Pair<String, String>> usernameAndEmailFromFetcherOptional =
79+
getUsernameAndEmailFromFetcher();
80+
Optional<String> tokenFromGitconfigOptional = Optional.empty();
81+
Optional<String> tokenFromSecretOptional = getTokenFromSecret(client, namespaceName);
82+
if (gitconfigOptional.isPresent()) {
83+
String gitconfig = gitconfigOptional.get();
84+
usernameAndEmailFromGitconfigOptional = getUsernameAndEmailFromGitconfig(gitconfig);
85+
tokenFromGitconfigOptional = getTokenFromGitconfig(gitconfig);
86+
}
87+
if (needUpdateGitconfigSecret(
88+
usernameAndEmailFromGitconfigOptional,
89+
usernameAndEmailFromFetcherOptional,
90+
tokenFromGitconfigOptional,
91+
tokenFromSecretOptional)) {
92+
Secret gitconfigSecret = buildGitconfigSecret();
93+
Optional<Pair<String, String>> usernameAndEmailOptional =
94+
usernameAndEmailFromGitconfigOptional.isPresent()
95+
? usernameAndEmailFromGitconfigOptional
96+
: usernameAndEmailFromFetcherOptional;
97+
Optional<String> gitconfigSectionsOptional =
98+
generateGitconfigSections(usernameAndEmailOptional, tokenFromSecretOptional);
99+
if (gitconfigSectionsOptional.isPresent()) {
100+
gitconfigSecret.setData(
101+
ImmutableMap.of(CONFIGMAP_DATA_KEY, encode(gitconfigSectionsOptional.get())));
102+
client.secrets().inNamespace(namespaceName).createOrReplace(gitconfigSecret);
103+
}
104+
}
105+
}
106+
107+
private Secret buildGitconfigSecret() {
108+
return new SecretBuilder()
109+
.withNewMetadata()
110+
.withName(GITCONFIG_SECRET_NAME)
111+
.withLabels(GITCONFIG_SECRET_LABELS)
112+
.withAnnotations(GITCONFIG_SECRET_ANNOTATIONS)
113+
.endMetadata()
114+
.build();
115+
}
116+
117+
private boolean needUpdateGitconfigSecret(
118+
Optional<Pair<String, String>> usernameAndEmailFromGitconfigOptional,
119+
Optional<Pair<String, String>> usernameAndEmailFromFetcher,
120+
Optional<String> tokenFromGitconfigOptional,
121+
Optional<String> tokenFromSecretOptional) {
122+
if (tokenFromGitconfigOptional.isPresent() && tokenFromSecretOptional.isPresent()) {
123+
return !tokenFromGitconfigOptional.get().equals(tokenFromSecretOptional.get());
124+
} else
125+
return tokenFromSecretOptional.isPresent()
126+
|| (usernameAndEmailFromGitconfigOptional.isEmpty()
127+
&& usernameAndEmailFromFetcher.isPresent());
128+
}
129+
130+
private Optional<String> generateGitconfigSections(
131+
Optional<Pair<String, String>> usernameAndEmailOptional, Optional<String> tokenOptional) {
132+
Optional<String> userSectionOptional = Optional.empty();
133+
Optional<String> httpSectionOPtional = Optional.empty();
134+
if (usernameAndEmailOptional.isPresent()) {
135+
userSectionOptional =
136+
Optional.of(
137+
generateUserSection(
138+
usernameAndEmailOptional.get().first, usernameAndEmailOptional.get().second));
139+
}
140+
if (tokenOptional.isPresent()) {
141+
httpSectionOPtional = Optional.of(generateHttpSection(tokenOptional.get()));
142+
}
143+
StringJoiner joiner = new StringJoiner("\n");
144+
userSectionOptional.ifPresent(joiner::add);
145+
httpSectionOPtional.ifPresent(joiner::add);
146+
return joiner.length() > 0 ? Optional.of(joiner.toString()) : Optional.empty();
147+
}
148+
149+
private Optional<Pair<String, String>> getUsernameAndEmailFromGitconfig(String gitconfig) {
150+
if (gitconfig.contains("[user]")) {
151+
Matcher usernameMatcher =
152+
Pattern.compile("\\[user](.|\\s)*name\\s*=\\s*(?<username>.*)").matcher(gitconfig);
153+
Matcher emailatcher =
154+
Pattern.compile("\\[user](.|\\s)*email\\s*=\\s*(?<email>.*)").matcher(gitconfig);
155+
if (usernameMatcher.find() && emailatcher.find()) {
156+
return Optional.of(
157+
new Pair<>(usernameMatcher.group("username"), emailatcher.group("email")));
158+
}
159+
}
160+
return Optional.empty();
161+
}
162+
163+
private Optional<Pair<String, String>> getUsernameAndEmailFromFetcher() {
164+
GitUserData gitUserData;
165+
for (GitUserDataFetcher fetcher : gitUserDataFetchers) {
166+
try {
167+
gitUserData = fetcher.fetchGitUserData();
168+
if (!isNullOrEmpty(gitUserData.getScmUsername())
169+
&& !isNullOrEmpty(gitUserData.getScmUserEmail())) {
170+
return Optional.of(
171+
new Pair<>(gitUserData.getScmUsername(), gitUserData.getScmUserEmail()));
172+
}
173+
} catch (ScmUnauthorizedException
174+
| ScmCommunicationException
175+
| ScmConfigurationPersistenceException
176+
| ScmItemNotFoundException
177+
| ScmBadRequestException e) {
178+
LOG.debug("No GitUserDataFetcher is configured. " + e.getMessage());
179+
}
180+
}
181+
return Optional.empty();
182+
}
183+
184+
private Optional<String> getTokenFromSecret(KubernetesClient client, String namespaceName) {
185+
for (Secret tokenSecret :
186+
client
187+
.secrets()
188+
.inNamespace(namespaceName)
189+
.withLabels(TOKEN_SECRET_LABELS)
190+
.list()
191+
.getItems()) {
192+
if ("azure-devops"
193+
.equals(
194+
tokenSecret
195+
.getMetadata()
196+
.getAnnotations()
197+
.get("che.eclipse.org/scm-provider-name"))) {
198+
return Optional.of(decode(tokenSecret.getData().get("token")));
199+
}
200+
}
201+
return Optional.empty();
202+
}
203+
204+
private Optional<String> getTokenFromGitconfig(String gitconfig) {
205+
if (gitconfig.contains("[http]")) {
206+
Matcher matcher =
207+
Pattern.compile(
208+
"\\[http]\\n\\s*extra[hH]eader\\s*=\\s*[\"']?Authorization: Basic (?<tokenEncoded>.*)[\"']?")
209+
.matcher(gitconfig);
210+
if (matcher.find()) {
211+
return Optional.of(decode(matcher.group("tokenEncoded")));
212+
}
213+
}
214+
return Optional.empty();
215+
}
216+
217+
private String generateHttpSection(String token) {
218+
return "[http]\n\textraHeader = Authorization: Basic " + encode(":" + token);
219+
}
220+
221+
private String generateUserSection(String username, String email) {
222+
return String.format("[user]\n\tname = %1$s\n\temail = %2$s", username, email);
223+
}
224+
225+
private Optional<String> getGitconfig(KubernetesClient client, String namespaceName) {
226+
Secret gitconfigAutomauntSecret =
227+
client.secrets().inNamespace(namespaceName).withName(GITCONFIG_SECRET_NAME).get();
228+
if (gitconfigAutomauntSecret != null) {
229+
String gitconfig = gitconfigAutomauntSecret.getData().get(CONFIGMAP_DATA_KEY);
230+
if (!isNullOrEmpty(gitconfig)) {
231+
return Optional.of(decode(gitconfig));
232+
}
233+
}
234+
return Optional.empty();
235+
}
236+
237+
private String encode(String value) {
238+
return Base64.getEncoder().encodeToString(value.getBytes(UTF_8));
239+
}
240+
241+
private String decode(String value) {
242+
return new String(Base64.getDecoder().decode(value.getBytes(UTF_8)));
243+
}
244+
}

0 commit comments

Comments
 (0)