Skip to content
Merged
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
3 changes: 3 additions & 0 deletions src/openapi/api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1991,6 +1991,7 @@ paths:
post:
operationId: getWorkloadCatalog
x-eov-operation-handler: v1/workloadCatalog
x-aclSchema: Workload
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why schema is called workload if the resource is tWorkloadCatalog?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WorkloadCatalog is part of the Workload workflow

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean we inherit the same ACL from Workload ? If so please add a comment. Otherwise it looks like a mistake.

description: Get workload catalog from a repository
requestBody:
content:
Expand All @@ -2015,6 +2016,7 @@ paths:
get:
operationId: getHelmChartContent
x-eov-operation-handler: v1/helmChartContent
x-aclSchema: Workload
parameters:
- name: url
in: query
Expand All @@ -2041,6 +2043,7 @@ paths:
post:
operationId: createWorkloadCatalog
x-eov-operation-handler: v1/createWorkloadCatalog
x-aclSchema: Workload
description: Create workload catalog from a repository
requestBody:
content:
Expand Down
19 changes: 15 additions & 4 deletions src/otomi-stack.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { CoreV1Api, KubeConfig, User as k8sUser, V1ObjectReference } from '@kubernetes/client-node'
import { CoreV1Api, User as k8sUser, KubeConfig, V1ObjectReference } from '@kubernetes/client-node'
import Debug from 'debug'

import { getRegions, ObjectStorageKeyRegions } from '@linode/api-v4'
Expand Down Expand Up @@ -132,7 +132,13 @@ import { getAplObjectFromV1, getV1MergeObject, getV1ObjectFromApl } from './util
import { getSealedSecretsPEM, sealedSecretManifest } from './utils/sealedSecretUtils'
import { getKeycloakUsers, isValidUsername } from './utils/userUtils'
import { defineClusterId, ObjectStorageClient } from './utils/wizardUtils'
import { fetchChartYaml, fetchWorkloadCatalog, NewHelmChartValues, sparseCloneChart } from './utils/workloadUtils'
import {
fetchChartYaml,
fetchWorkloadCatalog,
isInteralGiteaURL,
NewHelmChartValues,
sparseCloneChart,
} from './utils/workloadUtils'

interface ExcludedApp extends App {
managed: boolean
Expand Down Expand Up @@ -1208,6 +1214,7 @@ export default class OtomiStack {
if (!codeRepoName) return ['HEAD']
const coderepo = this.getCodeRepo(teamId, codeRepoName)
const { repositoryUrl, secret: secretName } = coderepo
const { cluster } = this.getSettings(['cluster'])
try {
let sshPrivateKey = '',
username = '',
Expand All @@ -1222,7 +1229,8 @@ export default class OtomiStack {

const isPrivate = !!secretName
const isSSH = !!sshPrivateKey
const repoUrl = repositoryUrl.startsWith('https://gitea')

const repoUrl = isInteralGiteaURL(repositoryUrl, cluster?.domainSuffix)
? repositoryUrl
: normalizeRepoUrl(repositoryUrl, isPrivate, isSSH)

Expand Down Expand Up @@ -1564,8 +1572,9 @@ export default class OtomiStack {

if (!url) throw new OtomiError(400, 'Helm chart catalog URL is not set')

const { cluster } = this.getSettings(['cluster'])
try {
const { helmCharts, catalog } = await fetchWorkloadCatalog(url, helmChartsDir, teamId)
const { helmCharts, catalog } = await fetchWorkloadCatalog(url, helmChartsDir, teamId, cluster?.domainSuffix)
return { url, helmCharts, catalog }
} catch (error) {
debug('Error fetching workload catalog')
Expand All @@ -1586,6 +1595,7 @@ export default class OtomiStack {
const localHelmChartsDir = `/tmp/otomi/charts/${uuid}`
const helmChartCatalogUrl = env.HELM_CHART_CATALOG
const { user, email } = this.git
const { cluster } = this.getSettings(['cluster'])

try {
await sparseCloneChart(
Expand All @@ -1597,6 +1607,7 @@ export default class OtomiStack {
chartTargetDirName,
chartIcon,
allowTeams,
cluster?.domainSuffix,
)
return true
} catch (error) {
Expand Down
42 changes: 41 additions & 1 deletion src/utils/workloadUtils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,40 @@ describe('detectGitProvider', () => {
})
})

// ----------------------------------------------------------------
// Tests for isInteralGiteaURL
describe('isInteralGiteaURL', () => {
it('returns true for a valid internal gitea URL', () => {
const result = workloadUtils.isInteralGiteaURL('https://gitea.cluster.local/my-org/my-repo', 'cluster.local')

expect(result).toBe(true)
})

it('returns false for a non-gitea hostname', () => {
const result = workloadUtils.isInteralGiteaURL('https://github.com/my-org/my-repo', 'cluster.local')

expect(result).toBe(false)
})

it('returns false when clusterDomainSuffix is missing', () => {
const result = workloadUtils.isInteralGiteaURL('https://gitea.cluster.local/my-org/my-repo')

expect(result).toBe(false)
})

it('returns false when URL hostname does not exactly match', () => {
const result = workloadUtils.isInteralGiteaURL('https://gitea.other.local/my-org/my-repo', 'cluster.local')

expect(result).toBe(false)
})

it('returns false for an invalid URL', () => {
const result = workloadUtils.isInteralGiteaURL('not-a-real-url', 'cluster.local')

expect(result).toBe(false)
})
})

// ----------------------------------------------------------------
// Tests for getGitCloneUrl
describe('getGitCloneUrl', () => {
Expand Down Expand Up @@ -350,6 +384,7 @@ describe('sparseCloneChart', () => {
const chartTargetDirName = 'cassandra'
const chartIcon = 'https://example.com/icon.png'
const allowTeams = true
const clusterDomainSuffix = 'example.com'

beforeEach(() => {
jest.clearAllMocks()
Expand Down Expand Up @@ -389,6 +424,7 @@ describe('sparseCloneChart', () => {
chartTargetDirName,
chartIcon,
allowTeams,
clusterDomainSuffix,
)

expect(result).toBe(true)
Expand Down Expand Up @@ -430,6 +466,7 @@ describe('sparseCloneChart', () => {
}
;(simpleGit as jest.Mock).mockReturnValue(mockGit)
jest.spyOn(workloadUtils, 'isGiteaURL').mockImplementation(() => true)
jest.spyOn(workloadUtils, 'isInteralGiteaURL').mockImplementation(() => true)

await sparseCloneChart(
gitRepositoryUrl,
Expand All @@ -440,6 +477,7 @@ describe('sparseCloneChart', () => {
chartTargetDirName,
chartIcon,
allowTeams,
clusterDomainSuffix,
)

// Check that clone was called with encoded URL
Expand Down Expand Up @@ -597,7 +635,9 @@ describe('fetchWorkloadCatalog', () => {
}
;(simpleGit as jest.Mock).mockReturnValue(mockGit)

const result = await fetchWorkloadCatalog(url, helmChartsDir, 'admin')
jest.spyOn(workloadUtils, 'isInteralGiteaURL').mockReturnValue(true)

const result = await fetchWorkloadCatalog(url, helmChartsDir, 'admin', 'example.com')

expect(fs.mkdirSync).toHaveBeenCalledWith(helmChartsDir, { recursive: true })
expect(mockGit.clone).toHaveBeenCalledWith(
Expand Down
23 changes: 20 additions & 3 deletions src/utils/workloadUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,16 @@ export function isGiteaURL(url: string) {
return giteaPattern.test(hostname)
}

export function isInteralGiteaURL(repositoryUrl: string, clusterDomainSuffix?: string) {
if (!clusterDomainSuffix) return false
try {
const url = new URL(repositoryUrl)
return url.hostname === `gitea.${clusterDomainSuffix}`
} catch {
return false
}
}

export function detectGitProvider(url) {
if (!url || typeof url !== 'string') return null

Expand Down Expand Up @@ -258,6 +268,7 @@ export class chartRepo {
* @param allowTeams - Boolean indicating if teams are allowed to use the chart.
* If false, the key is set to [].
* If true, the key is set to null.
* @param clusterDomainSuffix - domainSuffix set in cluster settings, used to check if URL is an interal Gitea URL
*/
export async function sparseCloneChart(
gitRepositoryUrl: string,
Expand All @@ -268,6 +279,7 @@ export async function sparseCloneChart(
chartTargetDirName: string,
chartIcon?: string,
allowTeams?: boolean,
clusterDomainSuffix?: string,
): Promise<boolean> {
const details = detectGitProvider(gitRepositoryUrl)
if (!details) return false
Expand All @@ -278,7 +290,7 @@ export async function sparseCloneChart(

if (!existsSync(localHelmChartsDir)) mkdirSync(localHelmChartsDir, { recursive: true })
let gitUrl = helmChartCatalogUrl
if (isGiteaURL(helmChartCatalogUrl)) {
if (isInteralGiteaURL(helmChartCatalogUrl, clusterDomainSuffix)) {
const [protocol, bareUrl] = helmChartCatalogUrl.split('://')
const encodedUser = encodeURIComponent(process.env.GIT_USER as string)
const encodedPassword = encodeURIComponent(process.env.GIT_PASSWORD as string)
Expand Down Expand Up @@ -318,10 +330,15 @@ export async function sparseCloneChart(
return true
}

export async function fetchWorkloadCatalog(url: string, helmChartsDir: string, teamId: string): Promise<Promise<any>> {
export async function fetchWorkloadCatalog(
url: string,
helmChartsDir: string,
teamId: string,
clusterDomainSuffix?: string,
): Promise<Promise<any>> {
if (!existsSync(helmChartsDir)) mkdirSync(helmChartsDir, { recursive: true })
let gitUrl = url
if (isGiteaURL(url)) {
if (isInteralGiteaURL(url, clusterDomainSuffix)) {
const [protocol, bareUrl] = url.split('://')
const encodedUser = encodeURIComponent(process.env.GIT_USER as string)
const encodedPassword = encodeURIComponent(process.env.GIT_PASSWORD as string)
Expand Down
Loading