Skip to content

Commit 3b7fe95

Browse files
vrubezhnydgolovin
andcommitted
Use SSO account to configure sandbox in one click
Uses a POC ['feat: use sso account to configure sandbox in one click #4232'](#4232) by @dgolovin. Allows logging in to the DevSandbox using a SSO account if Service Account pipeline token is configured, otherwise a token from the Clipboard is to be used. Co-authored-by: Denis Golovin <[email protected]> Signed-off-by: Victor Rubezhny <[email protected]>
1 parent fead656 commit 3b7fe95

File tree

5 files changed

+187
-41
lines changed

5 files changed

+187
-41
lines changed

src/oc/ocWrapper.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@ import * as tmp from 'tmp';
99
import validator from 'validator';
1010
import { CommandOption, CommandText } from '../base/command';
1111
import { CliChannel, ExecutionContext } from '../cli';
12+
import { CliExitData } from '../util/childProcessUtil';
1213
import { isOpenShiftCluster, KubeConfigUtils } from '../util/kubeUtils';
1314
import { Project } from './project';
1415
import { ClusterType, KubernetesConsole } from './types';
15-
import { CliExitData } from '../util/childProcessUtil';
1616

1717
/**
1818
* A wrapper around the `oc` CLI tool.
@@ -612,8 +612,10 @@ export class Oc {
612612
const currentUser = kcu.getCurrentUser();
613613
if (currentUser) {
614614
const projectPrefix = currentUser.name.substring(0, currentUser.name.indexOf('/'));
615-
if (projectPrefix.length > 0) {
616-
activeProject = fixedProjects.find((project) => project.name.includes(projectPrefix));
615+
const matches = projectPrefix.match(/^system:serviceaccount:([a-zA-Z-_.]+-dev):pipeline$/);
616+
const projectName = matches ? matches[1] : projectPrefix;
617+
if (projectName.length > 0) {
618+
activeProject = fixedProjects.find((project) => project.name.includes(projectName));
617619
if (activeProject) {
618620
activeProject.active = true;
619621
void Oc.Instance.setProject(activeProject.name, executionContext);

src/openshift/cluster.ts

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* Licensed under the MIT License. See LICENSE file in the project root for license information.
44
*-----------------------------------------------------------------------------------------------*/
55

6-
import { KubernetesObject } from '@kubernetes/client-node';
6+
import { CoreV1Api, KubeConfig, KubernetesObject, V1Secret, V1ServiceAccount } from '@kubernetes/client-node';
77
import { Cluster as KcuCluster, Context as KcuContext } from '@kubernetes/client-node/dist/config_types';
88
import * as https from 'https';
99
import { ExtensionContext, QuickInputButtons, QuickPickItem, QuickPickItemButtonEvent, ThemeIcon, Uri, commands, env, window, workspace } from 'vscode';
@@ -1070,4 +1070,82 @@ export class Cluster extends OpenShiftItem {
10701070
const asUrl = new URL(url);
10711071
return asUrl.hostname.endsWith('openshiftapps.com');
10721072
}
1073+
1074+
static prepareSSOInKubeConfig(proxy: string, username: string, accessToken: string): KubeConfig {
1075+
const kcu = new KubeConfig();
1076+
const clusterProxy = {
1077+
name: 'sandbox-proxy',
1078+
server: proxy,
1079+
};
1080+
const user = {
1081+
name: 'sso-user',
1082+
token: accessToken,
1083+
};
1084+
const context = {
1085+
cluster: clusterProxy.name,
1086+
name: 'sandbox-proxy-context',
1087+
user: user.name,
1088+
namespace: `${username}-dev`,
1089+
};
1090+
kcu.addCluster(clusterProxy);
1091+
kcu.addUser(user)
1092+
kcu.addContext(context);
1093+
kcu.setCurrentContext(context.name);
1094+
return kcu;
1095+
}
1096+
1097+
static async installPipelineSecretToken(k8sApi: CoreV1Api, pipelineServiceAccount: V1ServiceAccount, username: string): Promise<V1Secret> {
1098+
const v1Secret = {
1099+
apiVersion: 'v1',
1100+
kind: 'Secret',
1101+
metadata: {
1102+
name: `pipeline-secret-${username}-dev`,
1103+
annotations: {
1104+
'kubernetes.io/service-account.name': pipelineServiceAccount.metadata.name,
1105+
'kubernetes.io/service-account.uid': pipelineServiceAccount.metadata.uid
1106+
}
1107+
},
1108+
type: 'kubernetes.io/service-account-token'
1109+
} as V1Secret
1110+
1111+
try {
1112+
await k8sApi.createNamespacedSecret(`${username}-dev`, v1Secret);
1113+
} catch {
1114+
// Ignore
1115+
}
1116+
const newSecrets = await k8sApi.listNamespacedSecret(`${username}-dev`);
1117+
return newSecrets?.body.items.find((secret) => secret.metadata.name === `pipeline-secret-${username}-dev`);
1118+
}
1119+
1120+
static async getPipelineServiceAccountToken(k8sApi: CoreV1Api, username: string): Promise<string> {
1121+
try {
1122+
const serviceAccounts = await k8sApi.listNamespacedServiceAccount(`${username}-dev`);
1123+
const pipelineServiceAccount = serviceAccounts.body.items.find(serviceAccount => serviceAccount.metadata.name === 'pipeline');
1124+
if (!pipelineServiceAccount) {
1125+
return;
1126+
}
1127+
1128+
const secrets = await k8sApi.listNamespacedSecret(`${username}-dev`);
1129+
let pipelineTokenSecret = secrets?.body.items.find((secret) => secret.metadata.name === `pipeline-secret-${username}-dev`);
1130+
if (!pipelineTokenSecret) {
1131+
pipelineTokenSecret = await Cluster.installPipelineSecretToken(k8sApi, pipelineServiceAccount, username);
1132+
if (!pipelineTokenSecret) {
1133+
return;
1134+
}
1135+
}
1136+
return Buffer.from(pipelineTokenSecret.data.token, 'base64').toString();
1137+
} catch {
1138+
// Ignore
1139+
}
1140+
}
1141+
1142+
static async loginUsingPipelineServiceAccountToken(server: string, proxy: string, username: string, accessToken: string): Promise<string> {
1143+
const kcu = Cluster.prepareSSOInKubeConfig(proxy, username, accessToken);
1144+
const k8sApi = kcu.makeApiClient(CoreV1Api);
1145+
const pipelineToken = await this.getPipelineServiceAccountToken(k8sApi, username);
1146+
if (!pipelineToken) {
1147+
return;
1148+
}
1149+
return Cluster.tokenLogin(server, true, pipelineToken);
1150+
}
10731151
}

src/openshift/sandbox.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export interface SBSignupResponse {
2828
givenName: string;
2929
status: SBStatus;
3030
username: string;
31+
proxyURL: string;
3132
}
3233

3334
export interface SBResponseData {

src/webview/cluster/app/sandboxView.tsx

Lines changed: 44 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,10 @@ export default function addSandboxView(): JSX.Element {
5656
const [currentState, setCurrentState] = React.useState({
5757
action: 'sandboxPageDetectAuthSession',
5858
statusInfo: '',
59+
usePipelineToken: false,
5960
consoleDashboard: '',
6061
apiEndpoint: '',
62+
apiEndpointProxy: '',
6163
oauthTokenEndpoint: '',
6264
errorCode: undefined
6365
});
@@ -149,7 +151,9 @@ export default function addSandboxView(): JSX.Element {
149151
action: currentState.action,
150152
consoleDashboard: currentState.consoleDashboard,
151153
statusInfo: currentState.statusInfo,
154+
usePipelineToken: false,
152155
apiEndpoint: '',
156+
apiEndpointProxy: '',
153157
oauthTokenEndpoint: '',
154158
errorCode: undefined
155159
});
@@ -299,8 +303,10 @@ export default function addSandboxView(): JSX.Element {
299303
setCurrentState({
300304
action: 'sandboxPageRequestVerificationCode',
301305
statusInfo: '',
306+
usePipelineToken: false,
302307
consoleDashboard: '',
303308
apiEndpoint: '',
309+
apiEndpointProxy: '',
304310
oauthTokenEndpoint: '',
305311
errorCode: undefined
306312
});
@@ -379,11 +385,27 @@ export default function addSandboxView(): JSX.Element {
379385
const Provisioned = () => {
380386

381387
const handleLoginButton = () => {
382-
postMessage('sandboxLoginUsingDataInClipboard', {apiEndpointUrl: currentState.apiEndpoint, oauthRequestTokenUrl: `${currentState.oauthTokenEndpoint}/request`});
388+
if (!currentState.usePipelineToken) { // Try loging in using a token from the Clipboard
389+
postMessage('sandboxLoginUsingDataInClipboard', {
390+
apiEndpointUrl: currentState.apiEndpoint,
391+
oauthRequestTokenUrl: `${currentState.oauthTokenEndpoint}/request`
392+
});
393+
} else { // Try loging in using a Pipeline Token
394+
postMessage('sandboxLoginUsingPipelineToken', {
395+
apiEndpointUrl: currentState.apiEndpoint,
396+
oauthRequestTokenUrl: `${currentState.oauthTokenEndpoint}/request`,
397+
username: currentState.statusInfo,
398+
apiEndpointProxy: currentState.apiEndpointProxy,
399+
});
400+
}
383401
};
384402

385403
const invalidToken = currentState.errorCode === 'invalidToken';
386-
const loginSandboxTitle = !invalidToken ? 'Login to DevSandbox OpenShift cluster with token from clipboard' : 'Token in clipboard is invalid. Select the Get Token option and copy to clipboard';
404+
const loginSandboxTitle = !invalidToken ?
405+
currentState.usePipelineToken ?
406+
'Login to DevSandbox OpenShift cluster using a service account provided token' :
407+
'Login to DevSandbox OpenShift cluster with token from clipboard' :
408+
'Token in clipboard is invalid. Select the Get Token option and copy to clipboard';
387409

388410
return (
389411
<>
@@ -403,19 +425,29 @@ export default function addSandboxView(): JSX.Element {
403425
</Tooltip>
404426
Your sandbox account has been provisioned and is ready to use.
405427
</Typography>
406-
<Typography variant='caption' color='inherit' display='block' style={{ textAlign:'left', margin: '20px 70px' }}>
407-
Next steps to connect with Developer Sandbox:<br></br>
408-
1. Click on <strong>Get token</strong> button. In the browser, login using <strong>DevSandbox</strong> button.<br></br>
409-
2. Click on <strong>Display token</strong> link and copy token to clipboard.<br></br>
410-
3. Switch back to IDE and press <strong>'Login To DevSandbox'</strong> button. This will login you to DevSandbox with token from clipboard.<br></br>
411-
4. Once successfully logged in, start creating applications and deploy on cluster.
412-
</Typography>
428+
{( !currentState.usePipelineToken ) ? (
429+
<Typography variant='caption' color='inherit' display='block' style={{ textAlign:'left', margin: '20px 70px' }}>
430+
Next steps to connect with Developer Sandbox:<br></br>
431+
1. Click on <strong>Get token</strong> button. In the browser, login using <strong>DevSandbox</strong> button.<br></br>
432+
2. Click on <strong>Display token</strong> link and copy token to clipboard.<br></br>
433+
3. Switch back to IDE and press <strong>'Login To DevSandbox'</strong> button. This will login you to DevSandbox with token from clipboard.<br></br>
434+
4. Once successfully logged in, start creating applications and deploy on cluster.
435+
</Typography>
436+
) : (
437+
<Typography variant='caption' color='inherit' display='block' style={{ textAlign:'left', margin: '20px 70px' }}>
438+
Next steps to connect with Developer Sandbox:<br></br>
439+
1. Press <strong>'Login To DevSandbox'</strong> button. This will login you to DevSandbox using a service account provided token.<br></br>
440+
2. Once successfully logged in, start creating applications and deploy on cluster.
441+
</Typography>
442+
)}
413443
<Tooltip title='Launch your DevSandbox console in browser' placement='bottom'>
414444
<Button variant='contained' className='button' href={currentState.consoleDashboard}>Open Dashboard</Button>
415445
</Tooltip>
416-
<Tooltip title='Open the DevSandbox console page and copy the login token' placement='bottom'>
417-
<Button variant='contained' className='button' href={`${currentState.oauthTokenEndpoint}/request`}>Get token</Button>
418-
</Tooltip>
446+
{( !currentState.usePipelineToken ) && (
447+
<Tooltip title='Open the DevSandbox console page and copy the login token' placement='bottom'>
448+
<Button variant='contained' className='button' href={`${currentState.oauthTokenEndpoint}/request`}>Get token</Button>
449+
</Tooltip>
450+
)}
419451
<Tooltip title={loginSandboxTitle} placement='bottom'>
420452
<div style={{ display: 'inline-block', margin: '8px 0px 8px 0px' }}><Button variant='contained' className='buttonRed' disabled={invalidToken} onClick={handleLoginButton}>Login to DevSandbox</Button></div>
421453
</Tooltip>

0 commit comments

Comments
 (0)