diff --git a/src/openapi/api.yaml b/src/openapi/api.yaml index 376c00e8..3b7fb7d0 100644 --- a/src/openapi/api.yaml +++ b/src/openapi/api.yaml @@ -1991,6 +1991,7 @@ paths: post: operationId: getWorkloadCatalog x-eov-operation-handler: v1/workloadCatalog + x-aclSchema: Workload description: Get workload catalog from a repository requestBody: content: @@ -2015,6 +2016,7 @@ paths: get: operationId: getHelmChartContent x-eov-operation-handler: v1/helmChartContent + x-aclSchema: Workload parameters: - name: url in: query @@ -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: diff --git a/src/otomi-stack.ts b/src/otomi-stack.ts index 2a375fd7..eca37fe9 100644 --- a/src/otomi-stack.ts +++ b/src/otomi-stack.ts @@ -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' @@ -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 @@ -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 = '', @@ -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) @@ -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') @@ -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( @@ -1597,6 +1607,7 @@ export default class OtomiStack { chartTargetDirName, chartIcon, allowTeams, + cluster?.domainSuffix, ) return true } catch (error) { diff --git a/src/utils/workloadUtils.test.ts b/src/utils/workloadUtils.test.ts index 7cd04953..0ee2c3b7 100644 --- a/src/utils/workloadUtils.test.ts +++ b/src/utils/workloadUtils.test.ts @@ -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', () => { @@ -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() @@ -389,6 +424,7 @@ describe('sparseCloneChart', () => { chartTargetDirName, chartIcon, allowTeams, + clusterDomainSuffix, ) expect(result).toBe(true) @@ -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, @@ -440,6 +477,7 @@ describe('sparseCloneChart', () => { chartTargetDirName, chartIcon, allowTeams, + clusterDomainSuffix, ) // Check that clone was called with encoded URL @@ -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( diff --git a/src/utils/workloadUtils.ts b/src/utils/workloadUtils.ts index 13dadd86..9fb3d67a 100644 --- a/src/utils/workloadUtils.ts +++ b/src/utils/workloadUtils.ts @@ -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 @@ -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, @@ -268,6 +279,7 @@ export async function sparseCloneChart( chartTargetDirName: string, chartIcon?: string, allowTeams?: boolean, + clusterDomainSuffix?: string, ): Promise { const details = detectGitProvider(gitRepositoryUrl) if (!details) return false @@ -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) @@ -318,10 +330,15 @@ export async function sparseCloneChart( return true } -export async function fetchWorkloadCatalog(url: string, helmChartsDir: string, teamId: string): Promise> { +export async function fetchWorkloadCatalog( + url: string, + helmChartsDir: string, + teamId: string, + clusterDomainSuffix?: string, +): Promise> { 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)