From dd46b80708d617f5f0ef26cb402151acb3f3aaba Mon Sep 17 00:00:00 2001 From: Matthias Erll Date: Mon, 9 Mar 2026 12:35:13 +0100 Subject: [PATCH 1/2] feat: update manifests for gateway api --- src/api/v1/cloudtty.ts | 3 +- src/api/v2/cloudtty.ts | 3 +- src/k8s_operations.ts | 35 +++++++------------ src/otomi-stack.ts | 10 +++--- .../adminTtyManifests/tty_02_Pod.yaml | 2 +- .../adminTtyManifests/tty_05_HttpRoute.yaml | 32 +++++++++++++++++ .../adminTtyManifests/tty_05_Vs.yaml | 34 ------------------ src/ttyManifests/tty_00_Authz.yaml | 3 +- src/ttyManifests/tty_01_Sa.yaml | 3 +- src/ttyManifests/tty_02_Pod.yaml | 4 +-- src/ttyManifests/tty_03_Rolebinding.yaml | 3 +- src/ttyManifests/tty_04_Svc.yaml | 3 +- src/ttyManifests/tty_05_HttpRoute.yaml | 32 +++++++++++++++++ src/ttyManifests/tty_05_Vs.yaml | 34 ------------------ 14 files changed, 93 insertions(+), 108 deletions(-) create mode 100644 src/ttyManifests/adminTtyManifests/tty_05_HttpRoute.yaml delete mode 100644 src/ttyManifests/adminTtyManifests/tty_05_Vs.yaml create mode 100644 src/ttyManifests/tty_05_HttpRoute.yaml delete mode 100644 src/ttyManifests/tty_05_Vs.yaml diff --git a/src/api/v1/cloudtty.ts b/src/api/v1/cloudtty.ts index 0b3f8c25a..9c2c147dd 100644 --- a/src/api/v1/cloudtty.ts +++ b/src/api/v1/cloudtty.ts @@ -23,6 +23,7 @@ export const connectCloudtty = async (req: OpenApiRequestExt, res: Response): Pr export const deleteCloudtty = async (req: OpenApiRequestExt, res: Response): Promise => { const sessionUser = req.user debug(`deleteCloudtty - ${sessionUser.email} - ${sessionUser.sub}`) - await req.otomi.deleteCloudtty(sessionUser) + const { teamId } = req.query as { teamId: string } + await req.otomi.deleteCloudtty(teamId, sessionUser) res.json({}) } diff --git a/src/api/v2/cloudtty.ts b/src/api/v2/cloudtty.ts index 1c0051cfc..7575dc48d 100644 --- a/src/api/v2/cloudtty.ts +++ b/src/api/v2/cloudtty.ts @@ -23,6 +23,7 @@ export const connectAplCloudtty = async (req: OpenApiRequestExt, res: Response): export const deleteAplCloudtty = async (req: OpenApiRequestExt, res: Response): Promise => { const sessionUser = req.user debug(`deleteCloudtty - ${sessionUser.email} - ${sessionUser.sub}`) - await req.otomi.deleteCloudtty(sessionUser) + const { teamId } = req.query as { teamId: string } + await req.otomi.deleteCloudtty(teamId, sessionUser) res.json({}) } diff --git a/src/k8s_operations.ts b/src/k8s_operations.ts index 67f2bc8f4..5f509e8f9 100644 --- a/src/k8s_operations.ts +++ b/src/k8s_operations.ts @@ -101,34 +101,25 @@ export async function checkPodExists(namespace: string, podName: string): Promis } } -export async function k8sdelete({ - sub, - isPlatformAdmin, - userTeams, -}: { - sub: string - isPlatformAdmin: boolean - userTeams: string[] -}): Promise { +export async function k8sdelete( + teamId: string, + sub: string, + isPlatformAdmin: boolean, + userTeams: string[], +): Promise { const kc = new KubeConfig() kc.loadFromDefault() const k8sApi = kc.makeApiClient(CoreV1Api) const customObjectsApi = kc.makeApiClient(CustomObjectsApi) const rbacAuthorizationV1Api = kc.makeApiClient(RbacAuthorizationV1Api) const resourceName = sub - const namespace = 'team-admin' + const namespace = `team-${teamId}` try { - const apiVersion = 'v1beta1' - const apiGroupAuthz = 'security.istio.io' - const apiGroupVS = 'networking.istio.io' - const pluralAuth = 'authorizationpolicies' - const pluralVS = 'virtualservices' - await customObjectsApi.deleteNamespacedCustomObject({ - group: apiGroupAuthz, - version: apiVersion, + group: 'security.istio.io', + version: 'v1beta1', namespace, - plural: pluralAuth, + plural: 'authorizationpolicies', name: `tty-${resourceName}`, }) @@ -147,10 +138,10 @@ export async function k8sdelete({ await k8sApi.deleteNamespacedService({ name: `tty-${resourceName}`, namespace }) await customObjectsApi.deleteNamespacedCustomObject({ - group: apiGroupVS, - version: apiVersion, + group: 'gateway.networking.k8s.io', + version: 'v1', namespace, - plural: pluralVS, + plural: 'httproutes', name: `tty-${resourceName}`, }) } catch (error) { diff --git a/src/otomi-stack.ts b/src/otomi-stack.ts index e6ed9d2dc..5c5c665be 100644 --- a/src/otomi-stack.ts +++ b/src/otomi-stack.ts @@ -1,4 +1,4 @@ -import { CoreV1Api, User as k8sUser, KubeConfig, V1ObjectReference } from '@kubernetes/client-node' +import { CoreV1Api, KubeConfig, User as k8sUser, V1ObjectReference } from '@kubernetes/client-node' import Debug from 'debug' import { getRegions, ObjectStorageKeyRegions } from '@linode/api-v4' @@ -1582,7 +1582,7 @@ export default class OtomiStack { const intervalId = setInterval(() => { getCloudttyActiveTime('team-admin', `tty-${sessionUser.sub}`).then((activeTime: number) => { if (activeTime > TERMINATE_TIMEOUT) { - this.deleteCloudtty(sessionUser) + this.deleteCloudtty(teamId, sessionUser) clearInterval(intervalId) debug(`Cloudtty terminated after ${TERMINATE_TIMEOUT / (60 * 60 * 1000)} hours of inactivity`) } @@ -1592,12 +1592,12 @@ export default class OtomiStack { return { iFrameUrl: `https://tty.${variables.FQDN}/${sessionUser.sub}` } } - async deleteCloudtty(sessionUser: SessionUser): Promise { + async deleteCloudtty(teamId: string, sessionUser: SessionUser): Promise { const { sub, isPlatformAdmin, teams } = sessionUser as { sub: string; isPlatformAdmin: boolean; teams: string[] } const userTeams = teams.map((teamName) => `team-${teamName}`) try { - if (await checkPodExists('team-admin', `tty-${sessionUser.sub}`)) { - await k8sdelete({ sub, isPlatformAdmin, userTeams }) + if (await checkPodExists(`team-${teamId}`, `tty-${sessionUser.sub}`)) { + await k8sdelete(teamId, sub, isPlatformAdmin, userTeams) } } catch (error) { debug('Failed to delete cloudtty') diff --git a/src/ttyManifests/adminTtyManifests/tty_02_Pod.yaml b/src/ttyManifests/adminTtyManifests/tty_02_Pod.yaml index c2e2d2dfd..d84fe2a15 100644 --- a/src/ttyManifests/adminTtyManifests/tty_02_Pod.yaml +++ b/src/ttyManifests/adminTtyManifests/tty_02_Pod.yaml @@ -27,7 +27,7 @@ spec: cpu: '500m' env: - name: NAMESPACE - value: team-$TARGET_TEAM + value: $TARGET_TEAM securityContext: allowPrivilegeEscalation: false capabilities: diff --git a/src/ttyManifests/adminTtyManifests/tty_05_HttpRoute.yaml b/src/ttyManifests/adminTtyManifests/tty_05_HttpRoute.yaml new file mode 100644 index 000000000..68e55a8f1 --- /dev/null +++ b/src/ttyManifests/adminTtyManifests/tty_05_HttpRoute.yaml @@ -0,0 +1,32 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: tty-$SUB + namespace: team-admin +spec: + hostnames: + - tty.$FQDN + parentRefs: + - group: gateway.networking.k8s.io + kind: Gateway + name: platform + namespace: istio-system + sectionName: https + rules: + - backendRefs: + - group: "" + kind: Service + name: tty-$SUB + port: 8080 + weight: 1 + matches: + - path: + type: PathPrefix + value: /$SUB + filters: + - type: URLRewrite + urlRewrite: + path: + type: ReplaceFullPath + replaceFullPath: / +--- diff --git a/src/ttyManifests/adminTtyManifests/tty_05_Vs.yaml b/src/ttyManifests/adminTtyManifests/tty_05_Vs.yaml deleted file mode 100644 index 9dd644a58..000000000 --- a/src/ttyManifests/adminTtyManifests/tty_05_Vs.yaml +++ /dev/null @@ -1,34 +0,0 @@ -apiVersion: networking.istio.io/v1beta1 -kind: VirtualService -metadata: - name: tty-$SUB - namespace: team-admin -spec: - gateways: - - team-admin/team-admin-public-tlsterm - hosts: - - tty.$FQDN - http: - - match: - - uri: - prefix: /platform-logout - redirect: - authority: auth.$FQDN - uri: /oauth2/sign_out?rd=https%3A%2F%2Fkeycloak.$FQDN%2Frealms%2Fotomi%2Fprotocol%2Fopenid-connect%2Flogout%3Fpost_logout_redirect_uri%3Dhttps%3A%2F%2Fconsole.$FQDN%26client_id%3Dotomi - redirectCode: 302 - - match: - - uri: - prefix: /$SUB - rewrite: - uri: / - route: - - destination: - host: tty-$SUB.team-admin.svc.cluster.local - port: - number: 8080 - headers: - request: - set: - X-Forwarded-Proto: https ---- - diff --git a/src/ttyManifests/tty_00_Authz.yaml b/src/ttyManifests/tty_00_Authz.yaml index 1d64c4038..0e022c1ce 100644 --- a/src/ttyManifests/tty_00_Authz.yaml +++ b/src/ttyManifests/tty_00_Authz.yaml @@ -2,7 +2,7 @@ apiVersion: security.istio.io/v1beta1 kind: AuthorizationPolicy metadata: name: tty-$SUB - namespace: team-admin + namespace: $TARGET_TEAM spec: selector: matchLabels: @@ -13,4 +13,3 @@ spec: - key: request.auth.claims[sub] values: ['$SUB'] --- - diff --git a/src/ttyManifests/tty_01_Sa.yaml b/src/ttyManifests/tty_01_Sa.yaml index 3c2993caa..1abeb0fd3 100644 --- a/src/ttyManifests/tty_01_Sa.yaml +++ b/src/ttyManifests/tty_01_Sa.yaml @@ -2,6 +2,5 @@ apiVersion: v1 kind: ServiceAccount metadata: name: tty-$SUB - namespace: team-admin + namespace: $TARGET_TEAM --- - diff --git a/src/ttyManifests/tty_02_Pod.yaml b/src/ttyManifests/tty_02_Pod.yaml index c2e2d2dfd..a1a17a19f 100644 --- a/src/ttyManifests/tty_02_Pod.yaml +++ b/src/ttyManifests/tty_02_Pod.yaml @@ -5,7 +5,7 @@ metadata: app: tty-$SUB otomi: tty name: tty-$SUB - namespace: team-admin + namespace: $TARGET_TEAM spec: serviceAccountName: tty-$SUB securityContext: @@ -27,7 +27,7 @@ spec: cpu: '500m' env: - name: NAMESPACE - value: team-$TARGET_TEAM + value: $TARGET_TEAM securityContext: allowPrivilegeEscalation: false capabilities: diff --git a/src/ttyManifests/tty_03_Rolebinding.yaml b/src/ttyManifests/tty_03_Rolebinding.yaml index 0f045b4f8..99d3a166f 100644 --- a/src/ttyManifests/tty_03_Rolebinding.yaml +++ b/src/ttyManifests/tty_03_Rolebinding.yaml @@ -10,6 +10,5 @@ roleRef: subjects: - kind: ServiceAccount name: tty-$SUB - namespace: team-admin + namespace: $TARGET_TEAM --- - diff --git a/src/ttyManifests/tty_04_Svc.yaml b/src/ttyManifests/tty_04_Svc.yaml index 7ad6ae4f5..5d5c9d1d2 100644 --- a/src/ttyManifests/tty_04_Svc.yaml +++ b/src/ttyManifests/tty_04_Svc.yaml @@ -4,7 +4,7 @@ metadata: labels: app: tty-$SUB name: tty-$SUB - namespace: team-admin + namespace: $TARGET_TEAM spec: ports: - name: 8080-8080 @@ -15,4 +15,3 @@ spec: app: tty-$SUB type: ClusterIP --- - diff --git a/src/ttyManifests/tty_05_HttpRoute.yaml b/src/ttyManifests/tty_05_HttpRoute.yaml new file mode 100644 index 000000000..2d9680855 --- /dev/null +++ b/src/ttyManifests/tty_05_HttpRoute.yaml @@ -0,0 +1,32 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: tty-$SUB + namespace: $TARGET_TEAM +spec: + hostnames: + - tty.$FQDN + parentRefs: + - group: gateway.networking.k8s.io + kind: Gateway + name: platform + namespace: istio-system + sectionName: https + rules: + - backendRefs: + - group: "" + kind: Service + name: tty-$SUB + port: 8080 + weight: 1 + matches: + - path: + type: PathPrefix + value: /$SUB + filters: + - type: URLRewrite + urlRewrite: + path: + type: ReplaceFullPath + replaceFullPath: / +--- diff --git a/src/ttyManifests/tty_05_Vs.yaml b/src/ttyManifests/tty_05_Vs.yaml deleted file mode 100644 index 9dd644a58..000000000 --- a/src/ttyManifests/tty_05_Vs.yaml +++ /dev/null @@ -1,34 +0,0 @@ -apiVersion: networking.istio.io/v1beta1 -kind: VirtualService -metadata: - name: tty-$SUB - namespace: team-admin -spec: - gateways: - - team-admin/team-admin-public-tlsterm - hosts: - - tty.$FQDN - http: - - match: - - uri: - prefix: /platform-logout - redirect: - authority: auth.$FQDN - uri: /oauth2/sign_out?rd=https%3A%2F%2Fkeycloak.$FQDN%2Frealms%2Fotomi%2Fprotocol%2Fopenid-connect%2Flogout%3Fpost_logout_redirect_uri%3Dhttps%3A%2F%2Fconsole.$FQDN%26client_id%3Dotomi - redirectCode: 302 - - match: - - uri: - prefix: /$SUB - rewrite: - uri: / - route: - - destination: - host: tty-$SUB.team-admin.svc.cluster.local - port: - number: 8080 - headers: - request: - set: - X-Forwarded-Proto: https ---- - From 680d1ca21a57777b18ce8706b010a02a6139917b Mon Sep 17 00:00:00 2001 From: Matthias Erll Date: Mon, 9 Mar 2026 13:18:15 +0100 Subject: [PATCH 2/2] fix: namespace assignment of resources --- src/k8s_operations.ts | 3 +-- src/otomi-stack.ts | 15 +++++++++------ 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/k8s_operations.ts b/src/k8s_operations.ts index 5f509e8f9..4ada7a91c 100644 --- a/src/k8s_operations.ts +++ b/src/k8s_operations.ts @@ -102,7 +102,7 @@ export async function checkPodExists(namespace: string, podName: string): Promis } export async function k8sdelete( - teamId: string, + namespace: string, sub: string, isPlatformAdmin: boolean, userTeams: string[], @@ -113,7 +113,6 @@ export async function k8sdelete( const customObjectsApi = kc.makeApiClient(CustomObjectsApi) const rbacAuthorizationV1Api = kc.makeApiClient(RbacAuthorizationV1Api) const resourceName = sub - const namespace = `team-${teamId}` try { await customObjectsApi.deleteNamespacedCustomObject({ group: 'security.istio.io', diff --git a/src/otomi-stack.ts b/src/otomi-stack.ts index 5c5c665be..f70fcda58 100644 --- a/src/otomi-stack.ts +++ b/src/otomi-stack.ts @@ -1506,6 +1506,8 @@ export default class OtomiStack { } async connectCloudtty(teamId: string, sessionUser: SessionUser): Promise { + const isAdmin = sessionUser.isPlatformAdmin + const targetNamespace = isAdmin ? 'team-admin' : `team-${teamId}` if (!sessionUser.sub) { debug('No user sub found, cannot connect to shell.') throw new OtomiError(500, 'No user sub found, cannot connect to shell.') @@ -1527,14 +1529,14 @@ export default class OtomiStack { } // if cloudtty shell does not exists then check if the pod is running and return it - if (await checkPodExists('team-admin', `tty-${sessionUser.sub}`)) { + if (await checkPodExists(targetNamespace, `tty-${sessionUser.sub}`)) { return { iFrameUrl: `https://tty.${variables.FQDN}/${sessionUser.sub}` } } if (await pathExists('/tmp/ttyd.yaml')) await unlink('/tmp/ttyd.yaml') //if user is admin then read the manifests from ./dist/src/ttyManifests/adminTtyManifests - const files = sessionUser.isPlatformAdmin + const files = isAdmin ? await readdir('./dist/src/ttyManifests/adminTtyManifests', 'utf-8') : await readdir('./dist/src/ttyManifests', 'utf-8') const filteredFiles = files.filter((file) => file.startsWith('tty')) @@ -1574,13 +1576,13 @@ export default class OtomiStack { ) await writeFile('/tmp/ttyd.yaml', fileContents, 'utf-8') await apply('/tmp/ttyd.yaml') - await watchPodUntilRunning('team-admin', `tty-${sessionUser.sub}`) + await watchPodUntilRunning(targetNamespace, `tty-${sessionUser.sub}`) // check the pod every 30 minutes and terminate it after 2 hours of inactivity const ISACTIVE_INTERVAL = 30 * 60 * 1000 const TERMINATE_TIMEOUT = 2 * 60 * 60 * 1000 const intervalId = setInterval(() => { - getCloudttyActiveTime('team-admin', `tty-${sessionUser.sub}`).then((activeTime: number) => { + getCloudttyActiveTime(targetNamespace, `tty-${sessionUser.sub}`).then((activeTime: number) => { if (activeTime > TERMINATE_TIMEOUT) { this.deleteCloudtty(teamId, sessionUser) clearInterval(intervalId) @@ -1594,10 +1596,11 @@ export default class OtomiStack { async deleteCloudtty(teamId: string, sessionUser: SessionUser): Promise { const { sub, isPlatformAdmin, teams } = sessionUser as { sub: string; isPlatformAdmin: boolean; teams: string[] } + const namespace = isPlatformAdmin ? 'team-admin' : `team-${teamId}` const userTeams = teams.map((teamName) => `team-${teamName}`) try { - if (await checkPodExists(`team-${teamId}`, `tty-${sessionUser.sub}`)) { - await k8sdelete(teamId, sub, isPlatformAdmin, userTeams) + if (await checkPodExists(namespace, `tty-${sessionUser.sub}`)) { + await k8sdelete(namespace, sub, isPlatformAdmin, userTeams) } } catch (error) { debug('Failed to delete cloudtty')