Skip to content

Commit

Permalink
feat: podman login into external docker registries on workspace start (
Browse files Browse the repository at this point in the history
…#1094)

* feat: podman login into external docker registries on workspace start

Signed-off-by: Anatolii Bazko <abazko@redhat.com>
  • Loading branch information
tolusha authored Apr 15, 2024
1 parent 46d98bf commit 9d95008
Show file tree
Hide file tree
Showing 3 changed files with 240 additions and 9 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
/*
* Copyright (c) 2018-2024 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
*/

/* eslint-disable @typescript-eslint/no-unused-vars */

import * as mockClient from '@kubernetes/client-node';
import { CoreV1Api, HttpError, V1PodList, V1Secret } from '@kubernetes/client-node';

import * as helper from '@/devworkspaceClient/services/helpers/exec';
import { PodmanApiService } from '@/devworkspaceClient/services/podmanApi';

jest.mock('@/helpers/getUserName.ts');

const userNamespace = 'user-che';
const workspaceName = 'workspace-1';
const containerName = 'container-1';
const workspaceId = 'workspace-id-1';

const spyExec = jest
.spyOn(helper, 'exec')
.mockImplementation((..._args: Parameters<typeof helper.exec>) => {
return Promise.resolve({
stdOut: '',
stdError: '',
});
});

describe('podman Config API Service', () => {
let podmanApiService: PodmanApiService;
const mockReadNamespaceSecret = jest.fn();

beforeEach(() => {
jest.resetModules();

const { KubeConfig } = mockClient;
const kubeConfig = new KubeConfig();

kubeConfig.makeApiClient = jest.fn().mockImplementation(_api => {
return {
listNamespacedPod: () => {
return Promise.resolve(buildListNamespacedPod());
},
readNamespacedSecret: () => mockReadNamespaceSecret(),
} as unknown as CoreV1Api;
});

podmanApiService = new PodmanApiService(kubeConfig);
});

afterEach(() => {
jest.clearAllMocks();
});

test('Container registry secret contains correct data', async () => {
mockReadNamespaceSecret.mockResolvedValueOnce(buildSecretWithCorrectCredentials());

await podmanApiService.podmanLogin(userNamespace, workspaceId);

expect(spyExec).toHaveBeenCalledWith(
workspaceName,
userNamespace,
containerName,
expect.arrayContaining([
'sh',
'-c',
expect.stringContaining(
'podman login registry1 -u user1 -p password1 || true\npodman login registry2 -u user2 -p password2 || true',
),
]),
expect.anything(),
);
});

test('Container registry secret contains incorrect data', async () => {
mockReadNamespaceSecret.mockResolvedValueOnce(buildSecretWithIncorrectCredentials());

await podmanApiService.podmanLogin(userNamespace, workspaceId);

expect(spyExec).toHaveBeenCalledWith(
workspaceName,
userNamespace,
containerName,
expect.not.arrayContaining(['sh', '-c', expect.stringMatching('podman login registry')]),
expect.anything(),
);
});

test('Container registry secret not found', async () => {
mockReadNamespaceSecret.mockRejectedValueOnce(new HttpError({} as any, null, 404));

await podmanApiService.podmanLogin(userNamespace, workspaceId);
expect(spyExec).toHaveBeenCalledWith(
workspaceName,
userNamespace,
containerName,
expect.not.arrayContaining(['sh', '-c', expect.stringMatching('podman login registry')]),
expect.anything(),
);
});

function buildSecretWithCorrectCredentials(): { body: V1Secret } {
return {
body: {
apiVersion: 'v1',
data: {
'.dockerconfigjson': Buffer.from(
'{"auths":' +
'{' +
'"registry1":{"username":"user1","password":"password1"},' +
'"registry2":{"auth":"' +
Buffer.from('user2:password2', 'binary').toString('base64') +
'"}}' +
'}',
'binary',
).toString('base64'),
},
kind: 'Secret',
},
};
}

function buildSecretWithIncorrectCredentials(): { body: V1Secret } {
return {
body: {
apiVersion: 'v1',
data: {
'.dockerconfigjson': Buffer.from(
'{"auths":' +
'{' +
'"registry1":{"username":"user"},' +
'"registry2":{"password":"password"},' +
'"registry3":{"auth":"dXNlcg=="},' + // user
'"registry4":{"auth":"dXNlcjo="},' + // user:
'"registry5":{}' +
'}' +
'}',
'binary',
).toString('base64'),
},
kind: 'Secret',
},
};
}

function buildListNamespacedPod(): { body: V1PodList } {
return {
body: {
apiVersion: 'v1',
items: [
{
metadata: {
name: workspaceName,
namespace: userNamespace,
},
spec: {
containers: [{ name: containerName }],
},
},
],
kind: 'PodList',
},
};
}
});
Original file line number Diff line number Diff line change
Expand Up @@ -13,25 +13,28 @@
import { helpers } from '@eclipse-che/common';
import * as k8s from '@kubernetes/client-node';

import { DockerConfigApiService } from '@/devworkspaceClient/services/dockerConfigApi';
import { exec, ServerConfig } from '@/devworkspaceClient/services/helpers/exec';
import {
CoreV1API,
prepareCoreV1API,
} from '@/devworkspaceClient/services/helpers/prepareCoreV1API';
import { IPodmanApi } from '@/devworkspaceClient/types';
import { IDockerConfigApi, IPodmanApi } from '@/devworkspaceClient/types';
import { logger } from '@/utils/logger';

const EXCLUDED_CONTAINERS = ['che-gateway', 'che-machine-exec'];

export class PodmanApiService implements IPodmanApi {
private readonly corev1API: CoreV1API;
private dockerConfig: IDockerConfigApi;
private readonly kubeConfig: string;
private readonly getServerConfig: () => ServerConfig;

constructor(kc: k8s.KubeConfig) {
this.corev1API = prepareCoreV1API(kc);

this.kubeConfig = kc.exportConfig();
this.dockerConfig = new DockerConfigApiService(kc);

const server = kc.getCurrentCluster()?.server || '';
const opts = {};
Expand All @@ -52,6 +55,14 @@ export class PodmanApiService implements IPodmanApi {
const podName = currentPod.metadata?.name || '';
const currentPodContainers = currentPod.spec?.containers || [];

let externalDockerRegistriesPodmanLoginCommand = '';
try {
externalDockerRegistriesPodmanLoginCommand =
await this.generateExternalDockerRegistriesPodmanLoginCommand(namespace);
} catch (e) {
logger.warn(e);
}

let resolved = false;
for (const container of currentPodContainers) {
const containerName = container.name;
Expand All @@ -68,14 +79,22 @@ export class PodmanApiService implements IPodmanApi {
'sh',
'-c',
`
command -v oc >/dev/null 2>&1 && command -v podman >/dev/null 2>&1 && [[ -n "$HOME" ]] || { echo "oc, podman, or HOME is not set"; exit 1; }
command -v podman >/dev/null 2>&1 || { echo "podman is absent in the container"; exit 1; }
# Login to external docker registries configured by user on the dashboard
${externalDockerRegistriesPodmanLoginCommand}
command -v oc >/dev/null 2>&1 || { echo "oc is absent in the container"; exit 1; }
[[ -n "$HOME" ]] || { echo "HOME is not set"; exit 1; }
export CERTS_SRC="/var/run/secrets/kubernetes.io/serviceaccount"
export CERTS_DEST="$HOME/.config/containers/certs.d/image-registry.openshift-image-registry.svc:5000"
mkdir -p "$CERTS_DEST"
ln -s "$CERTS_SRC/service-ca.crt" "$CERTS_DEST/service-ca.crt"
ln -s "$CERTS_SRC/ca.crt" "$CERTS_DEST/ca.crt"
export OC_USER=$(oc whoami)
[[ "$OC_USER" == "kube:admin" ]] && export OC_USER="kubeadmin"
# Login to internal OpenShift registry
podman login -u "$OC_USER" -p $(oc whoami -t) image-registry.openshift-image-registry.svc:5000
`,
],
Expand Down Expand Up @@ -124,4 +143,44 @@ export class PodmanApiService implements IPodmanApi {
);
}
}

private async generateExternalDockerRegistriesPodmanLoginCommand(
namespace: string,
): Promise<string> {
let externalDockerRegistriesPodmanLoginCommand = '';

const dockerConfigBase64Encoded = await this.dockerConfig.read(namespace);
const dockerConfig = JSON.parse(
Buffer.from(dockerConfigBase64Encoded.dockerconfig, 'base64').toString('binary'),
);

const auths = dockerConfig['auths'];
if (auths) {
for (const registry of Object.keys(auths)) {
let username = '';
let password = '';

const credentials = auths[registry];
if (credentials) {
username = credentials['username'];
password = credentials['password'];
}

const authBase64Encoded = credentials['auth'];
if (!username && !password && authBase64Encoded) {
const auth = Buffer.from(authBase64Encoded, 'base64').toString('binary');
const usernamePassword = auth.split(':');
username = usernamePassword[0];
password = usernamePassword[1];
}

if (username && password) {
// `|| true` ensures that `podman login` won't fail if credentials are invalid
externalDockerRegistriesPodmanLoginCommand += `podman login ${registry} -u ${username} -p ${password} || true\n`;
}
}
}

return externalDockerRegistriesPodmanLoginCommand.trimEnd();
}
}
13 changes: 6 additions & 7 deletions run/prepare-local-run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ CHECLUSTER_CR_NAME=$(grep -o 'CHECLUSTER_CR_NAME:.*' run/.che-dashboard-pod | gr

kubectl get checluster -n "$CHE_NAMESPACE" "$CHECLUSTER_CR_NAME" -o=json > run/.custom-resources

rm -rf "$CHE_SELF_SIGNED_MOUNT_PATH"
mkdir -p "$CHE_SELF_SIGNED_MOUNT_PATH"

# copy certificate from the dashboard pod
kubectl cp $CHE_NAMESPACE/$DASHBOARD_POD_NAME:/public-certs/che-self-signed/..data/ca.crt "$CHE_SELF_SIGNED_MOUNT_PATH/ca.crt"

if [[ -n "$(oc whoami -t)" ]]; then
echo 'Cluster access token found. Nothing needs to be patched.'
echo 'Done.'
Expand All @@ -54,13 +60,6 @@ if [[ -z "$CHE_HOST_ORIGIN" ]]; then
exit 1
fi

if [ ! -d "$CHE_SELF_SIGNED_MOUNT_PATH" ]; then
mkdir -p "$CHE_SELF_SIGNED_MOUNT_PATH"
fi

# copy certificate from the dashboard pod
kubectl cp $CHE_NAMESPACE/$DASHBOARD_POD_NAME:/public-certs/che-self-signed/..data/ca.crt "$CHE_SELF_SIGNED_MOUNT_PATH/ca.crt"

GATEWAY=$(kubectl get deployments.apps -n "$CHE_NAMESPACE" che-gateway --ignore-not-found -o=json | jq -e '.spec.template.spec.containers|any(.name == "oauth-proxy")')
if [ "$GATEWAY" == "true" ]; then
echo 'Detected gateway and oauth-proxy inside. Running in native auth mode.'
Expand Down

0 comments on commit 9d95008

Please sign in to comment.