Skip to content

Commit

Permalink
feat: gitea operator for mapping teams (#95)
Browse files Browse the repository at this point in the history
* feat: gitea operator

* feat: placeholder

* feat: added gitea group mapping

* feat: group mapping through operator

* feat: commands

* feat: added logs

* feat: more logs

* feat: even more logs

* feat: added try catch

* feat: try catch more

* fix: types any

* feat: await exec

* feat: more debugging

* feat: added catch to exec

* fix: better error

* feat: load from cluster

* feat: check for gitea container readiness

* feat: better separation

* feat: debug giteacontainer

* feat: check for gitea undefined

* feat: removed clutter

* fix: fix comments

* feat: removed pod watcher added interval

* fix: checking correct object

* fix: kc from cluster

* feat: added extra check

* fix: pod not found

* fix: from cluster

* fix: smaller pod error message

* fix: jeho remarks

* feat: exec and retry

* feat: added unit test

* fix: more errors

* fix: turn back load from cluster

* fix: moved gitea test to correct map

* fix: different catch

---------

Co-authored-by: jeho <[email protected]>
  • Loading branch information
ElderMatt and jeho authored Dec 29, 2023
1 parent 324090a commit e38d297
Show file tree
Hide file tree
Showing 4 changed files with 156 additions and 1 deletion.
12 changes: 11 additions & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,13 +68,23 @@
{
"type": "node",
"request": "launch",
"name": "Debug operator",
"name": "Debug secrets operator",
"runtimeExecutable": "npm",
"runtimeArgs": ["run-script", "operator:secrets-dev"],
"cwd": "${workspaceRoot}",
"console": "integratedTerminal",
"envFile": "${workspaceFolder}/.env",
},
{
"type": "node",
"request": "launch",
"name": "Debug gitea operator",
"runtimeExecutable": "npm",
"runtimeArgs": ["run-script", "operator:gitea-dev"],
"cwd": "${workspaceRoot}",
"console": "integratedTerminal",
"envFile": "${workspaceFolder}/.env",
},
{
"type": "node",
"request": "launch",
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,8 @@
"tasks:wait-for": "node dist/tasks/otomi/wait-for.js",
"operator:secrets-dev": "NODE_TLS_REJECT_UNAUTHORIZED=0 ts-node-dev ./src/operator/secrets.ts",
"operator:secrets": "node dist/operator/secrets.js",
"operator:gitea-dev": "NODE_TLS_REJECT_UNAUTHORIZED=0 ts-node-dev ./src/operator/gitea.ts",
"operator:gitea": "node dist/operator/gitea.js",
"test": "NODE_ENV=test mocha -r ts-node/register -r ts-custom-error --exit src/**/*.test.*"
},
"standard-version": {
Expand Down
13 changes: 13 additions & 0 deletions src/operator/gitea.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { expect } from 'chai'
import { buildTeamString } from './gitea'

describe('giteaOperator', () => {
const teamNames = ['team-demo', 'team-demo2', 'team-demo3']
it('should create a valid group mapping string with all the teams', () => {
const mappingString = buildTeamString(teamNames)
expect(mappingString).to.be.equal(
'{"team-demo":{"otomi":["otomi-viewer","team-demo"]},"team-demo2":{"otomi":["otomi-viewer","team-demo2"]},"team-demo3":{"otomi":["otomi-viewer","team-demo3"]}}',
)
expect(mappingString).to.not.contain('team-admin')
})
})
130 changes: 130 additions & 0 deletions src/operator/gitea.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import Operator from '@dot-i/k8s-operator'
import * as k8s from '@kubernetes/client-node'
import { KubeConfig } from '@kubernetes/client-node'
import stream from 'stream'

interface groupMapping {
[key: string]: {
otomi: string[]
}
}

const kc = new KubeConfig()
// loadFromCluster when deploying on cluster
// loadFromDefault when locally connecting to cluster
kc.loadFromCluster()
const k8sApi = kc.makeApiClient(k8s.CoreV1Api)

export function buildTeamString(teamNames: any[]): string {
if (teamNames === undefined) return '{}'
const teamObject: groupMapping = {}
teamNames.forEach((teamName: string) => {
teamObject[teamName] = { otomi: ['otomi-viewer', teamName] }
})
return JSON.stringify(teamObject)
}

async function execGiteaCLICommand(podNamespace: string, podName: string) {
try {
console.debug('Finding namespaces')
let namespaces: any
try {
namespaces = (await k8sApi.listNamespace(undefined, undefined, undefined, undefined, 'type=team')).body
} catch (error) {
console.debug('No namespaces found, exited with error:', error)
throw error
}
console.debug('Filtering namespaces with "team-" prefix')
let teamNamespaces: any
try {
teamNamespaces = namespaces.items.map((namespace) => namespace.metadata?.name)
} catch (error) {
console.debug('Teamnamespaces exited with error:', error)
throw error
}
if (teamNamespaces.length > 0) {
const teamNamespaceString = buildTeamString(teamNamespaces)
const execCommand = [
'sh',
'-c',
`AUTH_ID=$(gitea admin auth list --vertical-bars | grep -E "\\|otomi-idp\\s+\\|" | grep -iE "\\|OAuth2\\s+\\|" | awk -F " " '{print $1}' | tr -d '\n') && gitea admin auth update-oauth --id "$AUTH_ID" --group-team-map '${teamNamespaceString}'`,
]
if (podNamespace && podName) {
const exec = new k8s.Exec(kc)
// Run gitea CLI command to update the gitea oauth group mapping
await exec
.exec(
podNamespace,
podName,
'gitea',
execCommand,
process.stdout as stream.Writable,
process.stderr as stream.Writable,
process.stdin as stream.Readable,
false,
(status: k8s.V1Status) => {
console.log('Exited with status:')
console.log(JSON.stringify(status, null, 2))
},
)
.catch((error) => {
console.debug('Error occurred during exec:', error)
throw error
})
}
} else {
console.debug('No team namespaces found')
}
} catch (error) {
console.debug(`Error updating IDP group mapping: ${error.message}`)
throw error
}
}

async function runExecCommand() {
try {
await execGiteaCLICommand('gitea', 'gitea-0')
} catch (error) {
console.debug('Error could not run exec command', error)
console.debug('Retrying in 30 seconds')
await new Promise((resolve) => setTimeout(resolve, 30000))
console.log('Retrying to run exec command')
await runExecCommand()
}
}

export default class MyOperator extends Operator {
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
protected async init() {
// Watch all namespaces
try {
await this.watchResource('', 'v1', 'namespaces', async (e) => {
const { object }: { object: k8s.V1Pod } = e
const { metadata } = object
// Check if namespace starts with prefix 'team-'
if (metadata && !metadata.name?.startsWith('team-')) return
if (metadata && metadata.name === 'team-admin') return
await runExecCommand()
})
} catch (error) {
console.debug(error)
}
}
}

async function main(): Promise<void> {
const operator = new MyOperator()
console.info(`Listening to team namespace changes in all namespaces`)
console.info('Setting up namespace prefix filter to "team-"')
await operator.start()
const exit = (reason: string) => {
operator.stop()
process.exit(0)
}

process.on('SIGTERM', () => exit('SIGTERM')).on('SIGINT', () => exit('SIGINT'))
}

if (typeof require !== 'undefined' && require.main === module) {
main()
}

0 comments on commit e38d297

Please sign in to comment.