Skip to content

Commit

Permalink
fixup! Add Azure DevOps Server support
Browse files Browse the repository at this point in the history
  • Loading branch information
vinokurig committed Feb 4, 2025
1 parent f36f456 commit 6432ec2
Show file tree
Hide file tree
Showing 4 changed files with 309 additions and 2 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2012-2024 Red Hat, Inc.
* Copyright (c) 2012-2025 Red Hat, Inc.
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
Expand Down Expand Up @@ -48,6 +48,7 @@
import org.eclipse.che.workspace.infrastructure.kubernetes.environment.KubernetesEnvironmentFactory;
import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.RemoveNamespaceOnWorkspaceRemove;
import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.configurator.CredentialsSecretConfigurator;
import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.configurator.GitconfigHttpHeaderConfigurator;
import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.configurator.GitconfigUserDataConfigurator;
import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.configurator.NamespaceConfigurator;
import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.configurator.OAuthTokenSecretsConfigurator;
Expand Down Expand Up @@ -110,6 +111,7 @@ protected void configure() {
namespaceConfigurators.addBinding().to(UserProfileConfigurator.class);
namespaceConfigurators.addBinding().to(UserPreferencesConfigurator.class);
namespaceConfigurators.addBinding().to(GitconfigUserDataConfigurator.class);
namespaceConfigurators.addBinding().to(GitconfigHttpHeaderConfigurator.class);

bind(AuthorizationChecker.class).to(KubernetesAuthorizationCheckerImpl.class);
bind(PermissionsCleaner.class).asEagerSingleton();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/*
* Copyright (c) 2012-2025 Red Hat, Inc.
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
*/
package org.eclipse.che.workspace.infrastructure.kubernetes.namespace.configurator;

import static com.google.common.base.Strings.isNullOrEmpty;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Collections.singletonMap;

import com.google.common.collect.ImmutableMap;
import io.fabric8.kubernetes.api.model.Secret;
import io.fabric8.kubernetes.api.model.SecretBuilder;
import io.fabric8.kubernetes.client.KubernetesClient;
import java.util.Base64;
import java.util.Map;
import javax.inject.Inject;
import org.eclipse.che.api.workspace.server.spi.InfrastructureException;
import org.eclipse.che.api.workspace.server.spi.NamespaceResolutionContext;
import org.eclipse.che.workspace.infrastructure.kubernetes.CheServerKubernetesClientFactory;

public class GitconfigHttpHeaderConfigurator implements NamespaceConfigurator {
// private static final Logger LOG =
// LoggerFactory.getLogger(GitconfigHttpHeaderConfigurator.class);
private final CheServerKubernetesClientFactory cheServerKubernetesClientFactory;
private static final String EXTRA_PROPERTIES_SECRET_NAME =
"devworkspace-gitconfig-extra-properties";
private static final Map<String, String> TOKEN_SECRET_LABELS =
ImmutableMap.of(
"app.kubernetes.io/part-of", "che.eclipse.org",
"app.kubernetes.io/component", "scm-personal-access-token");
private static final Map<String, String> EXTRA_PROPERTIES_SECRET_LABELS =
ImmutableMap.of(
"controller.devfile.io/mount-to-devworkspace",
"true",
"controller.devfile.io/watch-secret",
"true");

@Inject
public GitconfigHttpHeaderConfigurator(
CheServerKubernetesClientFactory cheServerKubernetesClientFactory) {
this.cheServerKubernetesClientFactory = cheServerKubernetesClientFactory;
}

@Override
public void configure(NamespaceResolutionContext namespaceResolutionContext, String namespaceName)
throws InfrastructureException {
KubernetesClient client = cheServerKubernetesClientFactory.create();
String httpDataEncoded = getHttpData(client, namespaceName);
client
.secrets()
.inNamespace(namespaceName)
.withLabels(TOKEN_SECRET_LABELS)
.list()
.getItems()
.forEach(
tokenSecret -> {
if ("azure-devops"
.equals(
tokenSecret
.getMetadata()
.getAnnotations()
.get("che.eclipse.org/scm-provider-name"))) {
String token = decode(tokenSecret.getData().get("token"));
Secret extraPropertiesSecret =
new SecretBuilder()
.withNewMetadata()
.withName(EXTRA_PROPERTIES_SECRET_NAME)
.withLabels(EXTRA_PROPERTIES_SECRET_LABELS)
.endMetadata()
.build();
String tokenHeader = "extraHeader = Basic " + encode(":" + token);
if (httpDataEncoded == null || !decode(httpDataEncoded).contains(tokenHeader)) {
extraPropertiesSecret.setData(
singletonMap(
"http",
(isNullOrEmpty(httpDataEncoded)
? encode(tokenHeader)
: encode(
// We support only one http.extraHeader option.
decode(httpDataEncoded)
.replaceAll("\\nextraHeader\\s?=\\s?Basic\\s[^\\s-]*", "")
+ "\n"
+ tokenHeader))));
client
.secrets()
.inNamespace(namespaceName)
.createOrReplace(extraPropertiesSecret);
}
}
});
}

private String getHttpData(KubernetesClient client, String namespaceName) {
Secret headerSecret =
client.secrets().inNamespace(namespaceName).withName(EXTRA_PROPERTIES_SECRET_NAME).get();
if (headerSecret != null) {
return headerSecret.getData().get("http");
}
return null;
}

private String encode(String value) {
return Base64.getEncoder().encodeToString(value.getBytes(UTF_8));
}

private String decode(String value) {
return new String(Base64.getDecoder().decode(value.getBytes(UTF_8)));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
/*
* Copyright (c) 2012-2025 Red Hat, Inc.
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
*/
package org.eclipse.che.workspace.infrastructure.kubernetes.namespace.configurator;

import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;

import com.google.common.collect.ImmutableMap;
import io.fabric8.kubernetes.api.model.Secret;
import io.fabric8.kubernetes.api.model.SecretBuilder;
import io.fabric8.kubernetes.client.KubernetesClient;
import io.fabric8.kubernetes.client.server.mock.KubernetesServer;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Collections;
import java.util.Map;
import org.eclipse.che.api.factory.server.scm.GitUserDataFetcher;
import org.eclipse.che.api.factory.server.scm.exception.*;
import org.eclipse.che.api.workspace.server.spi.InfrastructureException;
import org.eclipse.che.api.workspace.server.spi.NamespaceResolutionContext;
import org.eclipse.che.workspace.infrastructure.kubernetes.CheServerKubernetesClientFactory;
import org.mockito.Mock;
import org.mockito.testng.MockitoTestNGListener;
import org.testng.Assert;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Listeners;
import org.testng.annotations.Test;

@Listeners(MockitoTestNGListener.class)
public class GitconfigHttpHeaderConfiguratorTest {

private NamespaceConfigurator configurator;

@Mock private CheServerKubernetesClientFactory cheServerKubernetesClientFactory;
@Mock private GitUserDataFetcher gitUserDataFetcher;
private KubernetesServer serverMock;

private NamespaceResolutionContext namespaceResolutionContext;
private final String TEST_NAMESPACE_NAME = "namespace123";
private final String TEST_WORKSPACE_ID = "workspace123";
private final String TEST_USER_ID = "user123";
private final String TEST_USERNAME = "jondoe";
private static final Map<String, String> EXTRA_PROPERTIES_SECRET_LABELS =
ImmutableMap.of(
"controller.devfile.io/mount-to-devworkspace",
"true",
"controller.devfile.io/watch-secret",
"true");
private static final Map<String, String> TOKEN_SECRET_LABELS =
ImmutableMap.of(
"app.kubernetes.io/part-of", "che.eclipse.org",
"app.kubernetes.io/component", "scm-personal-access-token");

@BeforeMethod
public void setUp()
throws InfrastructureException, ScmCommunicationException, ScmUnauthorizedException {
configurator = new GitconfigHttpHeaderConfigurator(cheServerKubernetesClientFactory);

serverMock = new KubernetesServer(true, true);
serverMock.before();
KubernetesClient client = spy(serverMock.getClient());
when(cheServerKubernetesClientFactory.create()).thenReturn(client);

namespaceResolutionContext =
new NamespaceResolutionContext(TEST_WORKSPACE_ID, TEST_USER_ID, TEST_USERNAME);
}

@Test
public void shouldAddHttpHeaderToExistedData()
throws InfrastructureException, InterruptedException {
// given
Secret tokenSecret =
new SecretBuilder()
.withNewMetadata()
.withName("personal-access-token-name")
.withLabels(TOKEN_SECRET_LABELS)
.withAnnotations(ImmutableMap.of("che.eclipse.org/scm-provider-name", "azure-devops"))
.endMetadata()
.build();
tokenSecret.setData(Collections.singletonMap("token", encode("token-data")));
serverMock.getClient().secrets().inNamespace(TEST_NAMESPACE_NAME).create(tokenSecret);
Secret extraPropertiesSecret =
new SecretBuilder()
.withNewMetadata()
.withName("devworkspace-gitconfig-extra-properties")
.withLabels(EXTRA_PROPERTIES_SECRET_LABELS)
.endMetadata()
.build();
extraPropertiesSecret.setData(Collections.singletonMap("http", encode("some-data")));
serverMock.getClient().secrets().inNamespace(TEST_NAMESPACE_NAME).create(extraPropertiesSecret);
// when
configurator.configure(namespaceResolutionContext, TEST_NAMESPACE_NAME);
// then
Assert.assertEquals(serverMock.getLastRequest().getMethod(), "PUT");
var secrets =
serverMock.getClient().secrets().inNamespace(TEST_NAMESPACE_NAME).list().getItems();
Assert.assertEquals(secrets.size(), 2);
String expected = "some-data\nextraHeader = Basic " + encode(":token-data");
Assert.assertEquals(secrets.get(1).getData().get("http"), encode(expected));
}

private String encode(String data) {
return Base64.getEncoder().encodeToString(data.getBytes(StandardCharsets.UTF_8));
}

@Test
public void shouldSubstituteHttpHeaderWithTheNewToken()
throws InfrastructureException, InterruptedException {
// given
Secret tokenSecret =
new SecretBuilder()
.withNewMetadata()
.withName("personal-access-token-name")
.withLabels(TOKEN_SECRET_LABELS)
.withAnnotations(ImmutableMap.of("che.eclipse.org/scm-provider-name", "azure-devops"))
.endMetadata()
.build();
tokenSecret.setData(Collections.singletonMap("token", encode("new-token-data")));
serverMock.getClient().secrets().inNamespace(TEST_NAMESPACE_NAME).create(tokenSecret);
Secret extraPropertiesSecret =
new SecretBuilder()
.withNewMetadata()
.withName("devworkspace-gitconfig-extra-properties")
.withLabels(EXTRA_PROPERTIES_SECRET_LABELS)
.endMetadata()
.build();
extraPropertiesSecret.setData(
Collections.singletonMap(
"http", encode("some-data\nextraHeader = Basic " + encode((":token-data")))));
serverMock.getClient().secrets().inNamespace(TEST_NAMESPACE_NAME).create(extraPropertiesSecret);
// when
configurator.configure(namespaceResolutionContext, TEST_NAMESPACE_NAME);
// then
Assert.assertEquals(serverMock.getLastRequest().getMethod(), "PUT");
var secrets =
serverMock.getClient().secrets().inNamespace(TEST_NAMESPACE_NAME).list().getItems();
Assert.assertEquals(secrets.size(), 2);
String expected = "some-data\nextraHeader = Basic " + encode(":new-token-data");
Assert.assertEquals(secrets.get(1).getData().get("http"), encode(expected));
}

@Test
public void shouldNotUpdateTheSecretWithExistedToken()
throws InfrastructureException, InterruptedException {
// given
Secret tokenSecret =
new SecretBuilder()
.withNewMetadata()
.withName("personal-access-token-name")
.withLabels(TOKEN_SECRET_LABELS)
.withAnnotations(ImmutableMap.of("che.eclipse.org/scm-provider-name", "azure-devops"))
.endMetadata()
.build();
tokenSecret.setData(Collections.singletonMap("token", encode("token-data")));
serverMock.getClient().secrets().inNamespace(TEST_NAMESPACE_NAME).create(tokenSecret);
Secret extraPropertiesSecret =
new SecretBuilder()
.withNewMetadata()
.withName("devworkspace-gitconfig-extra-properties")
.withLabels(EXTRA_PROPERTIES_SECRET_LABELS)
.endMetadata()
.build();
extraPropertiesSecret.setData(
Collections.singletonMap(
"http", encode("some-data\nextraHeader = Basic " + encode((":token-data")))));
serverMock.getClient().secrets().inNamespace(TEST_NAMESPACE_NAME).create(extraPropertiesSecret);
// when
configurator.configure(namespaceResolutionContext, TEST_NAMESPACE_NAME);
// then
Assert.assertEquals(serverMock.getLastRequest().getMethod(), "GET");
var secrets =
serverMock.getClient().secrets().inNamespace(TEST_NAMESPACE_NAME).list().getItems();
Assert.assertEquals(secrets.size(), 2);
String expected = "some-data\nextraHeader = Basic " + encode(":token-data");
Assert.assertEquals(secrets.get(1).getData().get("http"), encode(expected));
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2012-2024 Red Hat, Inc.
* Copyright (c) 2012-2025 Red Hat, Inc.
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
Expand Down Expand Up @@ -51,6 +51,7 @@
import org.eclipse.che.workspace.infrastructure.kubernetes.environment.KubernetesEnvironmentFactory;
import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.KubernetesNamespaceFactory;
import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.configurator.CredentialsSecretConfigurator;
import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.configurator.GitconfigHttpHeaderConfigurator;
import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.configurator.GitconfigUserDataConfigurator;
import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.configurator.NamespaceConfigurator;
import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.configurator.OAuthTokenSecretsConfigurator;
Expand Down Expand Up @@ -116,6 +117,7 @@ protected void configure() {
namespaceConfigurators.addBinding().to(PreferencesConfigMapConfigurator.class);
namespaceConfigurators.addBinding().to(OpenShiftWorkspaceServiceAccountConfigurator.class);
namespaceConfigurators.addBinding().to(GitconfigUserDataConfigurator.class);
namespaceConfigurators.addBinding().to(GitconfigHttpHeaderConfigurator.class);

bind(AuthorizationChecker.class).to(OpenShiftAuthorizationCheckerImpl.class);
bind(PermissionsCleaner.class).asEagerSingleton();
Expand Down

0 comments on commit 6432ec2

Please sign in to comment.