Skip to content

Commit 80c8b96

Browse files
committed
fixup! Add Azure DevOps Server support
1 parent f36f456 commit 80c8b96

File tree

4 files changed

+311
-2
lines changed

4 files changed

+311
-2
lines changed

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

Lines changed: 3 additions & 1 deletion
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,6 +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.GitconfigHttpHeaderConfigurator;
5152
import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.configurator.GitconfigUserDataConfigurator;
5253
import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.configurator.NamespaceConfigurator;
5354
import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.configurator.OAuthTokenSecretsConfigurator;
@@ -110,6 +111,7 @@ protected void configure() {
110111
namespaceConfigurators.addBinding().to(UserProfileConfigurator.class);
111112
namespaceConfigurators.addBinding().to(UserPreferencesConfigurator.class);
112113
namespaceConfigurators.addBinding().to(GitconfigUserDataConfigurator.class);
114+
namespaceConfigurators.addBinding().to(GitconfigHttpHeaderConfigurator.class);
113115

114116
bind(AuthorizationChecker.class).to(KubernetesAuthorizationCheckerImpl.class);
115117
bind(PermissionsCleaner.class).asEagerSingleton();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
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 java.util.Collections.singletonMap;
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 javax.inject.Inject;
25+
import org.eclipse.che.api.workspace.server.spi.InfrastructureException;
26+
import org.eclipse.che.api.workspace.server.spi.NamespaceResolutionContext;
27+
import org.eclipse.che.workspace.infrastructure.kubernetes.CheServerKubernetesClientFactory;
28+
29+
public class GitconfigHttpHeaderConfigurator implements NamespaceConfigurator {
30+
// private static final Logger LOG =
31+
// LoggerFactory.getLogger(GitconfigHttpHeaderConfigurator.class);
32+
private final CheServerKubernetesClientFactory cheServerKubernetesClientFactory;
33+
private static final String EXTRA_PROPERTIES_SECRET_NAME =
34+
"devworkspace-gitconfig-extra-properties";
35+
private static final Map<String, String> TOKEN_SECRET_LABELS =
36+
ImmutableMap.of(
37+
"app.kubernetes.io/part-of", "che.eclipse.org",
38+
"app.kubernetes.io/component", "scm-personal-access-token");
39+
private static final Map<String, String> EXTRA_PROPERTIES_SECRET_LABELS =
40+
ImmutableMap.of(
41+
"controller.devfile.io/mount-to-devworkspace",
42+
"true",
43+
"controller.devfile.io/watch-secret",
44+
"true");
45+
46+
@Inject
47+
public GitconfigHttpHeaderConfigurator(
48+
CheServerKubernetesClientFactory cheServerKubernetesClientFactory) {
49+
this.cheServerKubernetesClientFactory = cheServerKubernetesClientFactory;
50+
}
51+
52+
@Override
53+
public void configure(NamespaceResolutionContext namespaceResolutionContext, String namespaceName)
54+
throws InfrastructureException {
55+
KubernetesClient client = cheServerKubernetesClientFactory.create();
56+
String httpDataEncoded = getHttpData(client, namespaceName);
57+
client
58+
.secrets()
59+
.inNamespace(namespaceName)
60+
.withLabels(TOKEN_SECRET_LABELS)
61+
.list()
62+
.getItems()
63+
.forEach(
64+
tokenSecret -> {
65+
if ("azure-devops"
66+
.equals(
67+
tokenSecret
68+
.getMetadata()
69+
.getAnnotations()
70+
.get("che.eclipse.org/scm-provider-name"))) {
71+
String token = decode(tokenSecret.getData().get("token"));
72+
Secret extraPropertiesSecret =
73+
new SecretBuilder()
74+
.withNewMetadata()
75+
.withName(EXTRA_PROPERTIES_SECRET_NAME)
76+
.withLabels(EXTRA_PROPERTIES_SECRET_LABELS)
77+
.endMetadata()
78+
.build();
79+
String tokenHeader = "Authorization: extraHeader = Basic " + encode(":" + token);
80+
if (httpDataEncoded == null || !decode(httpDataEncoded).contains(tokenHeader)) {
81+
extraPropertiesSecret.setData(
82+
singletonMap(
83+
"http",
84+
(isNullOrEmpty(httpDataEncoded)
85+
? encode(tokenHeader)
86+
: encode(
87+
// We support only one http.extraHeader option.
88+
decode(httpDataEncoded)
89+
.replaceAll(
90+
"\\nAuthorization:\\sextraHeader\\s?=\\s?Basic\\s[^\\s-]*",
91+
"")
92+
+ "\n"
93+
+ tokenHeader))));
94+
client
95+
.secrets()
96+
.inNamespace(namespaceName)
97+
.createOrReplace(extraPropertiesSecret);
98+
}
99+
}
100+
});
101+
}
102+
103+
private String getHttpData(KubernetesClient client, String namespaceName) {
104+
Secret headerSecret =
105+
client.secrets().inNamespace(namespaceName).withName(EXTRA_PROPERTIES_SECRET_NAME).get();
106+
if (headerSecret != null) {
107+
return headerSecret.getData().get("http");
108+
}
109+
return null;
110+
}
111+
112+
private String encode(String value) {
113+
return Base64.getEncoder().encodeToString(value.getBytes(UTF_8));
114+
}
115+
116+
private String decode(String value) {
117+
return new String(Base64.getDecoder().decode(value.getBytes(UTF_8)));
118+
}
119+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
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 org.mockito.Mockito.spy;
15+
import static org.mockito.Mockito.when;
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 io.fabric8.kubernetes.client.server.mock.KubernetesServer;
22+
import java.nio.charset.StandardCharsets;
23+
import java.util.Base64;
24+
import java.util.Collections;
25+
import java.util.Map;
26+
import org.eclipse.che.api.factory.server.scm.GitUserDataFetcher;
27+
import org.eclipse.che.api.factory.server.scm.exception.*;
28+
import org.eclipse.che.api.workspace.server.spi.InfrastructureException;
29+
import org.eclipse.che.api.workspace.server.spi.NamespaceResolutionContext;
30+
import org.eclipse.che.workspace.infrastructure.kubernetes.CheServerKubernetesClientFactory;
31+
import org.mockito.Mock;
32+
import org.mockito.testng.MockitoTestNGListener;
33+
import org.testng.Assert;
34+
import org.testng.annotations.BeforeMethod;
35+
import org.testng.annotations.Listeners;
36+
import org.testng.annotations.Test;
37+
38+
@Listeners(MockitoTestNGListener.class)
39+
public class GitconfigHttpHeaderConfiguratorTest {
40+
41+
private NamespaceConfigurator configurator;
42+
43+
@Mock private CheServerKubernetesClientFactory cheServerKubernetesClientFactory;
44+
@Mock private GitUserDataFetcher gitUserDataFetcher;
45+
private KubernetesServer serverMock;
46+
47+
private NamespaceResolutionContext namespaceResolutionContext;
48+
private final String TEST_NAMESPACE_NAME = "namespace123";
49+
private final String TEST_WORKSPACE_ID = "workspace123";
50+
private final String TEST_USER_ID = "user123";
51+
private final String TEST_USERNAME = "jondoe";
52+
private static final Map<String, String> EXTRA_PROPERTIES_SECRET_LABELS =
53+
ImmutableMap.of(
54+
"controller.devfile.io/mount-to-devworkspace",
55+
"true",
56+
"controller.devfile.io/watch-secret",
57+
"true");
58+
private static final Map<String, String> TOKEN_SECRET_LABELS =
59+
ImmutableMap.of(
60+
"app.kubernetes.io/part-of", "che.eclipse.org",
61+
"app.kubernetes.io/component", "scm-personal-access-token");
62+
63+
@BeforeMethod
64+
public void setUp()
65+
throws InfrastructureException, ScmCommunicationException, ScmUnauthorizedException {
66+
configurator = new GitconfigHttpHeaderConfigurator(cheServerKubernetesClientFactory);
67+
68+
serverMock = new KubernetesServer(true, true);
69+
serverMock.before();
70+
KubernetesClient client = spy(serverMock.getClient());
71+
when(cheServerKubernetesClientFactory.create()).thenReturn(client);
72+
73+
namespaceResolutionContext =
74+
new NamespaceResolutionContext(TEST_WORKSPACE_ID, TEST_USER_ID, TEST_USERNAME);
75+
}
76+
77+
@Test
78+
public void shouldAddHttpHeaderToExistedData()
79+
throws InfrastructureException, InterruptedException {
80+
// given
81+
Secret tokenSecret =
82+
new SecretBuilder()
83+
.withNewMetadata()
84+
.withName("personal-access-token-name")
85+
.withLabels(TOKEN_SECRET_LABELS)
86+
.withAnnotations(ImmutableMap.of("che.eclipse.org/scm-provider-name", "azure-devops"))
87+
.endMetadata()
88+
.build();
89+
tokenSecret.setData(Collections.singletonMap("token", encode("token-data")));
90+
serverMock.getClient().secrets().inNamespace(TEST_NAMESPACE_NAME).create(tokenSecret);
91+
Secret extraPropertiesSecret =
92+
new SecretBuilder()
93+
.withNewMetadata()
94+
.withName("devworkspace-gitconfig-extra-properties")
95+
.withLabels(EXTRA_PROPERTIES_SECRET_LABELS)
96+
.endMetadata()
97+
.build();
98+
extraPropertiesSecret.setData(Collections.singletonMap("http", encode("some-data")));
99+
serverMock.getClient().secrets().inNamespace(TEST_NAMESPACE_NAME).create(extraPropertiesSecret);
100+
// when
101+
configurator.configure(namespaceResolutionContext, TEST_NAMESPACE_NAME);
102+
// then
103+
Assert.assertEquals(serverMock.getLastRequest().getMethod(), "PUT");
104+
var secrets =
105+
serverMock.getClient().secrets().inNamespace(TEST_NAMESPACE_NAME).list().getItems();
106+
Assert.assertEquals(secrets.size(), 2);
107+
String expected = "some-data\nAuthorization: extraHeader = Basic " + encode(":token-data");
108+
Assert.assertEquals(secrets.get(1).getData().get("http"), encode(expected));
109+
}
110+
111+
private String encode(String data) {
112+
return Base64.getEncoder().encodeToString(data.getBytes(StandardCharsets.UTF_8));
113+
}
114+
115+
@Test
116+
public void shouldSubstituteHttpHeaderWithTheNewToken()
117+
throws InfrastructureException, InterruptedException {
118+
// given
119+
Secret tokenSecret =
120+
new SecretBuilder()
121+
.withNewMetadata()
122+
.withName("personal-access-token-name")
123+
.withLabels(TOKEN_SECRET_LABELS)
124+
.withAnnotations(ImmutableMap.of("che.eclipse.org/scm-provider-name", "azure-devops"))
125+
.endMetadata()
126+
.build();
127+
tokenSecret.setData(Collections.singletonMap("token", encode("new-token-data")));
128+
serverMock.getClient().secrets().inNamespace(TEST_NAMESPACE_NAME).create(tokenSecret);
129+
Secret extraPropertiesSecret =
130+
new SecretBuilder()
131+
.withNewMetadata()
132+
.withName("devworkspace-gitconfig-extra-properties")
133+
.withLabels(EXTRA_PROPERTIES_SECRET_LABELS)
134+
.endMetadata()
135+
.build();
136+
extraPropertiesSecret.setData(
137+
Collections.singletonMap(
138+
"http", encode("some-data\nAuthorization: extraHeader = Basic " + encode((":token-data")))));
139+
serverMock.getClient().secrets().inNamespace(TEST_NAMESPACE_NAME).create(extraPropertiesSecret);
140+
// when
141+
configurator.configure(namespaceResolutionContext, TEST_NAMESPACE_NAME);
142+
// then
143+
Assert.assertEquals(serverMock.getLastRequest().getMethod(), "PUT");
144+
var secrets =
145+
serverMock.getClient().secrets().inNamespace(TEST_NAMESPACE_NAME).list().getItems();
146+
Assert.assertEquals(secrets.size(), 2);
147+
String expected = "some-data\nAuthorization: extraHeader = Basic " + encode(":new-token-data");
148+
Assert.assertEquals(secrets.get(1).getData().get("http"), encode(expected));
149+
}
150+
151+
@Test
152+
public void shouldNotUpdateTheSecretWithExistedToken()
153+
throws InfrastructureException, InterruptedException {
154+
// given
155+
Secret tokenSecret =
156+
new SecretBuilder()
157+
.withNewMetadata()
158+
.withName("personal-access-token-name")
159+
.withLabels(TOKEN_SECRET_LABELS)
160+
.withAnnotations(ImmutableMap.of("che.eclipse.org/scm-provider-name", "azure-devops"))
161+
.endMetadata()
162+
.build();
163+
tokenSecret.setData(Collections.singletonMap("token", encode("token-data")));
164+
serverMock.getClient().secrets().inNamespace(TEST_NAMESPACE_NAME).create(tokenSecret);
165+
Secret extraPropertiesSecret =
166+
new SecretBuilder()
167+
.withNewMetadata()
168+
.withName("devworkspace-gitconfig-extra-properties")
169+
.withLabels(EXTRA_PROPERTIES_SECRET_LABELS)
170+
.endMetadata()
171+
.build();
172+
extraPropertiesSecret.setData(
173+
Collections.singletonMap(
174+
"http", encode("some-data\nAuthorization: extraHeader = Basic " + encode((":token-data")))));
175+
serverMock.getClient().secrets().inNamespace(TEST_NAMESPACE_NAME).create(extraPropertiesSecret);
176+
// when
177+
configurator.configure(namespaceResolutionContext, TEST_NAMESPACE_NAME);
178+
// then
179+
Assert.assertEquals(serverMock.getLastRequest().getMethod(), "GET");
180+
var secrets =
181+
serverMock.getClient().secrets().inNamespace(TEST_NAMESPACE_NAME).list().getItems();
182+
Assert.assertEquals(secrets.size(), 2);
183+
String expected = "some-data\nAuthorization: extraHeader = Basic " + encode(":token-data");
184+
Assert.assertEquals(secrets.get(1).getData().get("http"), encode(expected));
185+
}
186+
}

infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/OpenShiftInfraModule.java

Lines changed: 3 additions & 1 deletion
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/
@@ -51,6 +51,7 @@
5151
import org.eclipse.che.workspace.infrastructure.kubernetes.environment.KubernetesEnvironmentFactory;
5252
import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.KubernetesNamespaceFactory;
5353
import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.configurator.CredentialsSecretConfigurator;
54+
import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.configurator.GitconfigHttpHeaderConfigurator;
5455
import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.configurator.GitconfigUserDataConfigurator;
5556
import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.configurator.NamespaceConfigurator;
5657
import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.configurator.OAuthTokenSecretsConfigurator;
@@ -116,6 +117,7 @@ protected void configure() {
116117
namespaceConfigurators.addBinding().to(PreferencesConfigMapConfigurator.class);
117118
namespaceConfigurators.addBinding().to(OpenShiftWorkspaceServiceAccountConfigurator.class);
118119
namespaceConfigurators.addBinding().to(GitconfigUserDataConfigurator.class);
120+
namespaceConfigurators.addBinding().to(GitconfigHttpHeaderConfigurator.class);
119121

120122
bind(AuthorizationChecker.class).to(OpenShiftAuthorizationCheckerImpl.class);
121123
bind(PermissionsCleaner.class).asEagerSingleton();

0 commit comments

Comments
 (0)