Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions src/api.authz.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -680,7 +680,7 @@ describe('API authz tests', () => {
secret: 'demo',
}
test('team member can create its own codeRepo', async () => {
jest.spyOn(otomiStack, 'createCodeRepo').mockResolvedValue({} as CodeRepo)
jest.spyOn(otomiStack.codeRepos, 'createV1').mockResolvedValue({} as CodeRepo)
await agent
.post(`/v1/teams/${teamId}/coderepos`)
.send(data)
Expand All @@ -689,15 +689,15 @@ describe('API authz tests', () => {
})

test('team member can read its own codeRepo', async () => {
jest.spyOn(otomiStack, 'getCodeRepo').mockResolvedValue({} as never)
jest.spyOn(otomiStack.codeRepos, 'getV1').mockReturnValue({} as CodeRepo)
await agent
.get(`/v1/teams/${teamId}/coderepos/my-uuid`)
.set('Authorization', `Bearer ${teamMemberToken}`)
.expect(200)
})

test('team member can update its own codeRepo', async () => {
jest.spyOn(otomiStack, 'editCodeRepo').mockResolvedValue({} as CodeRepo)
jest.spyOn(otomiStack.codeRepos, 'editV1').mockResolvedValue({} as CodeRepo)

await agent
.put(`/v1/teams/${teamId}/coderepos/my-uuid`)
Expand All @@ -707,7 +707,7 @@ describe('API authz tests', () => {
})

test('team member can delete its own codeRepo', async () => {
jest.spyOn(otomiStack, 'deleteCodeRepo').mockResolvedValue()
jest.spyOn(otomiStack.codeRepos, 'delete').mockResolvedValue()

await agent
.delete(`/v1/teams/${teamId}/coderepos/my-uuid`)
Expand Down
2 changes: 1 addition & 1 deletion src/api/v1/coderepos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@ const debug = Debug('otomi:api:v1:codeRepos')
*/
export const getAllCodeRepos = (req: OpenApiRequestExt, res: Response): void => {
debug('getAllCodeRepos')
const v = req.otomi.getAllCodeRepos()
const v = req.otomi.codeRepos.getAll()
res.json(v)
}
4 changes: 2 additions & 2 deletions src/api/v1/teams/{teamId}/coderepos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const debug = Debug('otomi:api:v1:teams:codeRepos')
export const getTeamCodeRepos = (req: OpenApiRequestExt, res: Response): void => {
const { teamId } = req.params
debug(`getTeamCodeRepos(${teamId})`)
const v = req.otomi.getTeamCodeRepos(teamId)
const v = req.otomi.codeRepos.getByTeam(teamId)
res.json(v)
}

Expand All @@ -22,6 +22,6 @@ export const getTeamCodeRepos = (req: OpenApiRequestExt, res: Response): void =>
export const createCodeRepo = async (req: OpenApiRequestExt, res: Response): Promise<void> => {
const { teamId } = req.params
debug(`createCodeRepos(${teamId}, ...)`)
const v = await req.otomi.createCodeRepo(teamId, req.body as CodeRepo)
const v = await req.otomi.codeRepos.createV1(teamId, req.body as CodeRepo)
res.json(v)
}
6 changes: 3 additions & 3 deletions src/api/v1/teams/{teamId}/coderepos/{codeRepositoryName}.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const debug = Debug('otomi:api:v1:teams:codeRepos')
export const getCodeRepo = (req: OpenApiRequestExt, res: Response): void => {
const { teamId, codeRepositoryName } = req.params
debug(`getCodeRepo(${codeRepositoryName})`)
const data = req.otomi.getCodeRepo(decodeURIComponent(teamId), decodeURIComponent(codeRepositoryName))
const data = req.otomi.codeRepos.getV1(decodeURIComponent(teamId), decodeURIComponent(codeRepositoryName))
res.json(data)
}

Expand All @@ -22,7 +22,7 @@ export const getCodeRepo = (req: OpenApiRequestExt, res: Response): void => {
export const editCodeRepo = async (req: OpenApiRequestExt, res: Response): Promise<void> => {
const { teamId, codeRepositoryName } = req.params
debug(`editCodeRepo(${codeRepositoryName})`)
const data = await req.otomi.editCodeRepo(decodeURIComponent(teamId), decodeURIComponent(codeRepositoryName), {
const data = await req.otomi.codeRepos.editV1(decodeURIComponent(teamId), decodeURIComponent(codeRepositoryName), {
...req.body,
} as CodeRepo)
res.json(data)
Expand All @@ -35,6 +35,6 @@ export const editCodeRepo = async (req: OpenApiRequestExt, res: Response): Promi
export const deleteCodeRepo = async (req: OpenApiRequestExt, res: Response): Promise<void> => {
const { teamId, codeRepositoryName } = req.params
debug(`deleteCodeRepo(${codeRepositoryName})`)
await req.otomi.deleteCodeRepo(decodeURIComponent(teamId), decodeURIComponent(codeRepositoryName))
await req.otomi.codeRepos.delete(decodeURIComponent(teamId), decodeURIComponent(codeRepositoryName))
res.json({})
}
2 changes: 1 addition & 1 deletion src/api/v2/coderepos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@ const debug = Debug('otomi:api:v2:codeRepos')
*/
export const getAllAplCodeRepos = (req: OpenApiRequestExt, res: Response): void => {
debug('getAllCodeRepos')
const v = req.otomi.getAllAplCodeRepos()
const v = req.otomi.codeRepos.getAllApl()
res.json(v)
}
4 changes: 2 additions & 2 deletions src/api/v2/teams/{teamId}/coderepos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const debug = Debug('otomi:api:v2:teams:codeRepos')
export const getTeamAplCodeRepos = (req: OpenApiRequestExt, res: Response): void => {
const { teamId } = req.params
debug(`getTeamCodeRepos(${teamId})`)
const v = req.otomi.getTeamAplCodeRepos(decodeURIComponent(teamId))
const v = req.otomi.codeRepos.getByTeamApl(decodeURIComponent(teamId))
res.json(v)
}

Expand All @@ -22,6 +22,6 @@ export const getTeamAplCodeRepos = (req: OpenApiRequestExt, res: Response): void
export const createAplCodeRepo = async (req: OpenApiRequestExt, res: Response): Promise<void> => {
const { teamId } = req.params
debug(`createCodeRepos(${teamId}, ...)`)
const v = await req.otomi.createAplCodeRepo(decodeURIComponent(teamId), req.body as AplCodeRepoRequest)
const v = await req.otomi.codeRepos.create(decodeURIComponent(teamId), req.body as AplCodeRepoRequest)
res.json(v)
}
8 changes: 4 additions & 4 deletions src/api/v2/teams/{teamId}/coderepos/{codeRepositoryName}.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const debug = Debug('otomi:api:v2:teams:codeRepos')
export const getAplCodeRepo = (req: OpenApiRequestExt, res: Response): void => {
const { teamId, codeRepositoryName } = req.params
debug(`getCodeRepo(${codeRepositoryName})`)
const data = req.otomi.getAplCodeRepo(decodeURIComponent(teamId), decodeURIComponent(codeRepositoryName))
const data = req.otomi.codeRepos.get(decodeURIComponent(teamId), decodeURIComponent(codeRepositoryName))
res.json(data)
}

Expand All @@ -22,7 +22,7 @@ export const getAplCodeRepo = (req: OpenApiRequestExt, res: Response): void => {
export const editAplCodeRepo = async (req: OpenApiRequestExt, res: Response): Promise<void> => {
const { teamId, codeRepositoryName } = req.params
debug(`editCodeRepo(${codeRepositoryName})`)
const data = await req.otomi.editAplCodeRepo(
const data = await req.otomi.codeRepos.edit(
decodeURIComponent(teamId),
decodeURIComponent(codeRepositoryName),
req.body as AplCodeRepoRequest,
Expand All @@ -37,7 +37,7 @@ export const editAplCodeRepo = async (req: OpenApiRequestExt, res: Response): Pr
export const patchAplCodeRepo = async (req: OpenApiRequestExt, res: Response): Promise<void> => {
const { teamId, codeRepositoryName } = req.params
debug(`editCodeRepo(${codeRepositoryName}, patch)`)
const data = await req.otomi.editAplCodeRepo(
const data = await req.otomi.codeRepos.edit(
decodeURIComponent(teamId),
decodeURIComponent(codeRepositoryName),
req.body as DeepPartial<AplCodeRepoRequest>,
Expand All @@ -53,6 +53,6 @@ export const patchAplCodeRepo = async (req: OpenApiRequestExt, res: Response): P
export const deleteAplCodeRepo = async (req: OpenApiRequestExt, res: Response): Promise<void> => {
const { teamId, codeRepositoryName } = req.params
debug(`deleteCodeRepo(${codeRepositoryName})`)
await req.otomi.deleteCodeRepo(decodeURIComponent(teamId), decodeURIComponent(codeRepositoryName))
await req.otomi.codeRepos.delete(decodeURIComponent(teamId), decodeURIComponent(codeRepositoryName))
res.json({})
}
82 changes: 82 additions & 0 deletions src/deployer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import Debug from 'debug'
import { FileStore } from 'src/fileStore/file-store'
import { Git } from 'src/git'
import { cleanSession, getSessionStack } from 'src/middleware'
import { AplKind, AplRecord, AplTeamObject } from 'src/otomi-models'
import { getSanitizedErrorMessage } from 'src/utils'

const debug = Debug('otomi:deployer')

export class Deployer {
constructor(
private git: Git,
private fileStore: FileStore,
private editor: string | undefined,
private sessionId: string,
) {}

async saveTeamConfigItem(aplTeamObject: AplTeamObject): Promise<AplRecord> {
debug(
`Saving ${aplTeamObject.kind} ${aplTeamObject.metadata.name} for team ${aplTeamObject.metadata.labels['apl.io/teamId']}`,
)
const filePath = this.fileStore.setTeamResource(aplTeamObject)
await this.git.writeFile(filePath, aplTeamObject)
return { filePath, content: aplTeamObject }
}

async deleteTeamConfigItem(kind: AplKind, teamId: string, name: string): Promise<string> {
debug(`Removing ${kind} ${name} for team ${teamId}`)
const filePath = this.fileStore.deleteTeamResource(kind, teamId, name)
await this.git.removeFile(filePath)
return filePath
}

async doDeployments(aplRecords: AplRecord[], encryptSecrets = true, files?: string[]): Promise<void> {
const rootStack = await getSessionStack()
try {
await this.git.save(this.editor!, encryptSecrets, files)
await rootStack.git.git.pull()
for (const aplRecord of aplRecords) {
rootStack.fileStore.set(aplRecord.filePath, aplRecord.content)
}
debug(`Updated root stack values with ${this.sessionId} changes`)
} catch (e) {
e.message = getSanitizedErrorMessage(e)
throw e
} finally {
await cleanSession(this.sessionId)
}
}

async doDeployment(aplRecord: AplRecord, encryptSecrets = true, files?: string[]): Promise<void> {
const rootStack = await getSessionStack()
try {
await this.git.save(this.editor!, encryptSecrets, files)
await rootStack.git.git.pull()
rootStack.fileStore.set(aplRecord.filePath, aplRecord.content)
debug(`Updated root stack values with ${this.sessionId} changes`)
} catch (e) {
e.message = getSanitizedErrorMessage(e)
throw e
} finally {
await cleanSession(this.sessionId)
}
}

async doDeleteDeployment(filePaths: string[]): Promise<void> {
const rootStack = await getSessionStack()
try {
await this.git.save(this.editor!, false)
await rootStack.git.git.pull()
for (const filePath of filePaths) {
rootStack.fileStore.delete(filePath)
}
debug(`Updated root stack values with ${this.sessionId} changes`)
} catch (e) {
e.message = getSanitizedErrorMessage(e)
throw e
} finally {
await cleanSession(this.sessionId)
}
}
}
98 changes: 98 additions & 0 deletions src/domains/code-repos.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { cloneDeep, merge, unset } from 'lodash'
import { Deployer } from 'src/deployer'
import { AlreadyExists, NotExistError } from 'src/error'
import { FileStore } from 'src/fileStore/file-store'
import {
AplCodeRepoRequest,
AplCodeRepoResponse,
buildTeamObject,
CodeRepo,
DeepPartial,
toTeamObject,
} from 'src/otomi-models'
import { getAplObjectFromV1, getV1MergeObject, getV1ObjectFromApl } from 'src/utils/manifests'

export class CodeRepos {
constructor(
private fileStore: FileStore,
private deployer: Deployer,
) {}

getAll(): CodeRepo[] {
return this.getAllApl().map((codeRepo) => getV1ObjectFromApl(codeRepo) as CodeRepo)
}

getAllApl(): AplCodeRepoResponse[] {
const files = this.fileStore.getAllTeamResourcesByKind('AplTeamCodeRepo')
return Array.from(files.values()) as AplCodeRepoResponse[]
}

getByTeam(teamId: string): CodeRepo[] {
return this.getByTeamApl(teamId).map((codeRepo) => getV1ObjectFromApl(codeRepo) as CodeRepo)
}

getByTeamApl(teamId: string): AplCodeRepoResponse[] {
const files = this.fileStore.getTeamResourcesByKindAndTeamId('AplTeamCodeRepo', teamId)
return Array.from(files.values()) as AplCodeRepoResponse[]
}

getV1(teamId: string, name: string): CodeRepo {
return getV1ObjectFromApl(this.get(teamId, name)) as CodeRepo
}

get(teamId: string, name: string): AplCodeRepoResponse {
const codeRepo = this.fileStore.getTeamResource('AplTeamCodeRepo', teamId, name)
if (!codeRepo) throw new NotExistError(`Code repo ${name} not found in team ${teamId}`)
return codeRepo as AplCodeRepoResponse
}

async createV1(teamId: string, data: CodeRepo): Promise<CodeRepo> {
return getV1ObjectFromApl(
await this.create(teamId, getAplObjectFromV1('AplTeamCodeRepo', data) as AplCodeRepoRequest),
) as CodeRepo
}

async create(teamId: string, data: AplCodeRepoRequest): Promise<AplCodeRepoResponse> {
const existingRepos = this.getByTeamApl(teamId)
if (existingRepos.some((repo) => repo.spec.repositoryUrl === data.spec.repositoryUrl))
throw new AlreadyExists('Code repository URL already exists')
if (existingRepos.some((repo) => repo.metadata.name === data.metadata.name))
throw new AlreadyExists('Code repo name already exists')
if (!data.spec.private) unset(data.spec, 'secret')
if (data.spec.gitService === 'gitea') unset(data.spec, 'private')

const teamObject = toTeamObject(teamId, data)
const aplRecord = await this.deployer.saveTeamConfigItem(teamObject)
await this.deployer.doDeployment(aplRecord, false)
return aplRecord.content as AplCodeRepoResponse
}

async editV1(teamId: string, name: string, data: CodeRepo): Promise<CodeRepo> {
return getV1ObjectFromApl(
await this.edit(teamId, name, getV1MergeObject(data) as DeepPartial<AplCodeRepoRequest>),
) as CodeRepo
}

async edit(
teamId: string,
name: string,
data: DeepPartial<AplCodeRepoRequest>,
patch = false,
): Promise<AplCodeRepoResponse> {
if (!data.spec?.private) unset(data.spec, 'secret')
if (data.spec?.gitService === 'gitea') unset(data.spec, 'private')

const existing = this.get(teamId, name)
const updatedSpec = patch ? merge(cloneDeep(existing.spec), data.spec) : { ...existing.spec, ...data.spec }
const teamObject = buildTeamObject(existing, updatedSpec)

const aplRecord = await this.deployer.saveTeamConfigItem(teamObject)
await this.deployer.doDeployment(aplRecord, false)
return aplRecord.content as AplCodeRepoResponse
}

async delete(teamId: string, name: string): Promise<void> {
const filePath = await this.deployer.deleteTeamConfigItem('AplTeamCodeRepo', teamId, name)
await this.deployer.doDeleteDeployment([filePath])
}
}
2 changes: 1 addition & 1 deletion src/middleware/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import Debug from 'debug'
import { RequestHandler } from 'express'
import { remove } from 'fs-extra'
import http from 'http'
import { cloneDeep } from 'lodash'
import { join } from 'path'
import { Server } from 'socket.io'
import { ApiNotReadyError } from 'src/error'
Expand Down Expand Up @@ -43,6 +42,7 @@ export const setSessionStack = async (editor: string, sessionId: string): Promis
sessions[sessionId] = new OtomiStack(editor, sessionId)
await sessions[sessionId].initGitWorktree(readOnlyStack.git)
sessions[sessionId].fileStore.copyFrom(readOnlyStack.fileStore)
sessions[sessionId].initDomain()
} else sessions[sessionId].sessionId = sessionId
return sessions[sessionId]
}
Expand Down
Loading
Loading