Skip to content
4 changes: 2 additions & 2 deletions src/openapi/definitions.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -939,8 +939,8 @@ replicas:
title: Number of replicas
default: 1
repoUrl:
description: Path to a remote git repo without protocol. Will use https to access.
pattern: '^(?:(?:https://)?[A-Za-z0-9.-]+\.[A-Za-z]{2,}|git@[A-Za-z0-9.-]+\.[A-Za-z]{2,}:)/[A-Za-z0-9_.-]+(?:/[A-Za-z0-9_.-]+)+(?:\.git)?$'
description: Path to a remote git repo. Will use https to access when protocol is omitted.
pattern: '^(?:https://)?[A-Za-z0-9.-]+\.[A-Za-z]{2,}(?:/[A-Za-z0-9_.-]+){2,}(?:\.git)?/?$'
type: string
x-message: a valid git repo URL
example: github.com/example/repo
Expand Down
81 changes: 75 additions & 6 deletions src/otomi-stack.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1135,34 +1135,100 @@ describe('APL code repositories tests', () => {
expect(otomiStack.doDeleteDeployment).toHaveBeenCalled()
})

test('should create an external public code repository', async () => {
test.each([
Comment thread
dennisvankekem marked this conversation as resolved.
{
input: 'github.com/github-samples/pets-workshop',
expected: 'https://github.com/github-samples/pets-workshop.git',
},
{
input: 'https://github.com/github-samples/pets-workshop',
expected: 'https://github.com/github-samples/pets-workshop.git',
},
{
input: 'https://github.com/github-samples/pets-workshop/',
expected: 'https://github.com/github-samples/pets-workshop.git',
},
{
input: 'https://github.com/github-samples/pets-workshop.git',
expected: 'https://github.com/github-samples/pets-workshop.git',
},
{
input: 'https://github.com/github-samples/pets-workshop.git/',
expected: 'https://github.com/github-samples/pets-workshop.git',
},
])('should create an external public code repository and normalize url: $input', async ({ input, expected }) => {
const codeRepo = await otomiStack.createAplCodeRepo('demo', {
metadata: {
name: 'ext-pub-1',
},
spec: {
gitService: 'github',
repositoryUrl: 'https://github.test.com',
repositoryUrl: input,
},
kind: 'AplTeamCodeRepo',
})

expect(codeRepo.metadata.name).toBe('ext-pub-1')
expect(codeRepo.metadata.labels['apl.io/teamId']).toBe('demo')
expect(codeRepo.spec.gitService).toBe('github')
expect(codeRepo.spec.repositoryUrl).toBe('https://github.test.com')
expect(codeRepo.spec.repositoryUrl).toBe(expected)
expect(codeRepo.spec.private).toBeFalsy()
expect(codeRepo.spec.secret).toBeUndefined()

const stored = otomiStack.fileStore.getTeamResource('AplTeamCodeRepo', 'demo', 'ext-pub-1')

expect(stored).toBeDefined()
expect(stored?.spec.gitService).toBe('github')
expect(stored?.spec.repositoryUrl).toBe('https://github.test.com')
expect(stored?.spec.repositoryUrl).toBe(expected)
expect(stored?.spec.secret).toBeUndefined()

expect(otomiStack.doDeployment).toHaveBeenCalled()
})

test.each(['ssh://git@github.com/github-samples/pets-workshop.git'])(
'should reject unsupported ssh repository url: %s',
async (repositoryUrl) => {
await expect(
otomiStack.createAplCodeRepo('demo', {
metadata: {
name: 'ext-pub-1',
},
spec: {
gitService: 'github',
repositoryUrl,
},
kind: 'AplTeamCodeRepo',
}),
).rejects.toThrow()
},
)

test('should reject duplicate external repository url after normalization', async () => {
await otomiStack.createAplCodeRepo('demo', {
metadata: {
name: 'ext-pub-1',
},
spec: {
gitService: 'github',
repositoryUrl: 'https://github.com/github-samples/pets-workshop/',
},
kind: 'AplTeamCodeRepo',
})

await expect(
otomiStack.createAplCodeRepo('demo', {
metadata: {
name: 'ext-pub-2',
},
spec: {
gitService: 'github',
repositoryUrl: 'https://github.com/github-samples/pets-workshop',
},
kind: 'AplTeamCodeRepo',
}),
).rejects.toThrow('Code repository URL already exists')
})

test('should get an existing external public code repository', () => {
const extPubRepo: AplCodeRepoResponse = {
kind: 'AplTeamCodeRepo',
Expand Down Expand Up @@ -1256,7 +1322,7 @@ describe('APL code repositories tests', () => {
},
spec: {
gitService: 'github',
repositoryUrl: 'https://github.test.com',
repositoryUrl: 'https://github.com/github-samples/pets-workshop/',
private: true,
secret: 'test',
},
Expand All @@ -1266,12 +1332,15 @@ describe('APL code repositories tests', () => {
expect(codeRepo.metadata.name).toBe('ext-priv-1')
expect(codeRepo.metadata.labels['apl.io/teamId']).toBe('demo')
expect(codeRepo.spec.gitService).toBe('github')
expect(codeRepo.spec.repositoryUrl).toBe('https://github.test.com')
expect(codeRepo.spec.repositoryUrl).toBe('https://github.com/github-samples/pets-workshop.git')
expect(codeRepo.spec.private).toBe(true)
expect(codeRepo.spec.secret).toBe('test')

const stored = otomiStack.fileStore.getTeamResource('AplTeamCodeRepo', 'demo', 'ext-priv-1')

expect(stored).toBeDefined()
expect(stored?.spec.gitService).toBe('github')
expect(stored?.spec.repositoryUrl).toBe('https://github.com/github-samples/pets-workshop.git')
expect(stored?.spec.private).toBe(true)
expect(stored?.spec.secret).toBe('test')

Expand Down
63 changes: 55 additions & 8 deletions src/otomi-stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1441,18 +1441,65 @@ export default class OtomiStack {
}

async createAplCodeRepo(teamId: string, data: AplCodeRepoRequest): Promise<AplCodeRepoResponse> {
// Check if URL already exists
const existingRepos = this.getTeamAplCodeRepos(teamId)
const allRepoUrls = existingRepos.map((repo) => repo.spec.repositoryUrl) || []
if (allRepoUrls.includes(data.spec.repositoryUrl)) throw new AlreadyExists('Code repository URL already exists')
const allNames = existingRepos.map((repo) => repo.metadata.name) || []
if (allNames.includes(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 isExternalRepo = data.spec.gitService !== 'gitea'

const repositoryUrl = isExternalRepo
? normalizeRepoUrl(
data.spec.repositoryUrl,
data.spec.private ?? false,
false, // SSH is not allowed
)
: data.spec.repositoryUrl.trim().replace(/\/$/, '')
Comment thread
dennisvankekem marked this conversation as resolved.

if (!repositoryUrl) {
throw new Error('Invalid repository URL')
}
Comment thread
dennisvankekem marked this conversation as resolved.

const normalizeForComparison = (repo: AplCodeRepoResponse) => {
if (repo.spec.gitService === 'gitea') {
return repo.spec.repositoryUrl.trim().replace(/\/$/, '')
}

return normalizeRepoUrl(repo.spec.repositoryUrl, repo.spec.private ?? false, false)
}

const allRepoUrls = existingRepos
.map(normalizeForComparison)
.filter((repoUrl): repoUrl is string => Boolean(repoUrl))

if (allRepoUrls.includes(repositoryUrl)) {
Comment thread
dennisvankekem marked this conversation as resolved.
throw new AlreadyExists('Code repository URL already exists')
}

const allNames = existingRepos.map((repo) => repo.metadata.name)

if (allNames.includes(data.metadata.name)) {
throw new AlreadyExists('Code repo name already exists')
}

const sanitizedData: AplCodeRepoRequest = {
...data,
spec: {
...data.spec,
repositoryUrl,
},
}

if (!sanitizedData.spec.private) {
unset(sanitizedData.spec, 'secret')
}

if (sanitizedData.spec.gitService === 'gitea') {
unset(sanitizedData.spec, 'private')
}

const teamObject = toTeamObject(teamId, sanitizedData)
const aplRecord = await this.saveTeamConfigItem(teamObject)

await this.doDeployment(aplRecord)

return aplRecord.content as AplCodeRepoResponse
}

Expand Down
Loading