Skip to content

Commit 6a2aba7

Browse files
feat: sparse checkout for chart (#942)
* feat: sparse checkout for chart * fix: cleanup * fix: remove files --------- Co-authored-by: svcAPLBot <174728082+svcAPLBot@users.noreply.github.com>
1 parent 54fdf6c commit 6a2aba7

4 files changed

Lines changed: 175 additions & 3 deletions

File tree

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import Debug from 'debug'
2+
import { Response } from 'express'
3+
import { OpenApiRequestExt } from 'src/otomi-models'
4+
5+
const debug = Debug('otomi:api:v2:catalogs:charts:chartname')
6+
7+
/**
8+
* GET /v2/catalogs/{catalogId}/charts/{chartName}
9+
* Get a single chart for a specific catalog
10+
*/
11+
export const getAplCatalogsChart = async (req: OpenApiRequestExt, res: Response): Promise<void> => {
12+
const { catalogId, chartName } = req.params
13+
debug(`getAplCatalogChart(${catalogId}, ${chartName})`)
14+
15+
const data = await req.otomi.getAplCatalogChart(decodeURIComponent(catalogId), decodeURIComponent(chartName))
16+
17+
res.json(data)
18+
}

src/openapi/api.yaml

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1993,7 +1993,31 @@ paths:
19931993
application/json:
19941994
schema:
19951995
$ref: '#/components/schemas/OpenApiValidationError'
1996-
1996+
'/v2/catalogs/{catalogId}/charts/{chartName}':
1997+
parameters:
1998+
- $ref: '#/components/parameters/catalogParams'
1999+
- name: chartName
2000+
in: path
2001+
required: true
2002+
schema:
2003+
type: string
2004+
description: Name of the chart to fetch
2005+
get:
2006+
operationId: getAplCatalogsChart
2007+
x-eov-operation-handler: v2/catalogs/{catalogId}/charts/{chartName}
2008+
description: Get a single chart for a specific catalog
2009+
x-aclSchema: AplCatalog
2010+
responses:
2011+
'200':
2012+
description: Successfully obtained app catalog chart
2013+
content:
2014+
application/json:
2015+
schema:
2016+
$ref: '#/components/schemas/AplCatalogChartResponse'
2017+
'400':
2018+
$ref: '#/components/responses/BadRequest'
2019+
'404':
2020+
$ref: '#/components/responses/NotFound'
19972021
/v1/coderepos:
19982022
get:
19992023
operationId: getAllCodeRepos

src/otomi-stack.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ import { defineClusterId, ObjectStorageClient } from './utils/wizardUtils'
138138
import {
139139
fetchChartYaml,
140140
fetchWorkloadCatalog,
141+
fetchWorkloadCatalogChart,
141142
isInteralGiteaURL,
142143
NewHelmChartValues,
143144
sparseCloneChart,
@@ -1722,6 +1723,37 @@ export default class OtomiStack {
17221723
return { ...charts, branch }
17231724
}
17241725

1726+
async getAplCatalogChart(
1727+
name: string,
1728+
chartName: string,
1729+
): Promise<{ url: string; branch: string; chart: any | null; chartsPath?: string }> {
1730+
const catalog = this.getAplCatalog(name)
1731+
const { repositoryUrl, branch, chartsPath } = catalog.spec
1732+
const { cluster } = this.getSettings(['cluster'])
1733+
1734+
const uuid = uuidv4()
1735+
const helmChartsDir = `/tmp/otomi/charts/${name}/${branch}/chart/${uuid}`
1736+
1737+
try {
1738+
const chart = await fetchWorkloadCatalogChart(
1739+
repositoryUrl,
1740+
helmChartsDir,
1741+
chartName,
1742+
branch,
1743+
cluster?.domainSuffix,
1744+
undefined,
1745+
chartsPath as string | undefined,
1746+
)
1747+
1748+
return { url: repositoryUrl, branch, chart, chartsPath }
1749+
} catch (error) {
1750+
debug(`Error fetching workload chart '${chartName}': ${error.message}`)
1751+
return { url: repositoryUrl, branch, chart: null, chartsPath }
1752+
} finally {
1753+
if (existsSync(helmChartsDir)) rmSync(helmChartsDir, { recursive: true, force: true })
1754+
}
1755+
}
1756+
17251757
async getHelmChartContent(url: string): Promise<any> {
17261758
return await fetchChartYaml(url)
17271759
}

src/utils/workloadUtils.ts

Lines changed: 100 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import axios from 'axios'
22
import Debug from 'debug'
3-
import { existsSync, lstatSync, mkdirSync, renameSync, rmSync } from 'fs'
3+
import { existsSync, lstatSync, mkdirSync, mkdtempSync, renameSync, rmSync } from 'fs'
44
import { readFile } from 'fs-extra'
55
import { readdir, writeFile } from 'fs/promises'
6-
import path from 'path'
6+
import { tmpdir } from 'os'
7+
import path, { join } from 'path'
78
import simpleGit, { SimpleGit } from 'simple-git'
89
import { safeReadTextFile } from 'src/utils'
910
import { cleanEnv, GIT_PROVIDER_URL_PATTERNS } from 'src/validators'
@@ -339,6 +340,40 @@ export async function sparseCloneChart(
339340
return true
340341
}
341342

343+
export async function sparseCheckoutPath(
344+
gitCloneUrl: string,
345+
ref: string,
346+
sparsePath: string,
347+
targetBaseDir: string,
348+
targetDirName: string,
349+
): Promise<{ success: true; checkoutPath: string } | { success: false; error: string }> {
350+
if (!existsSync(targetBaseDir)) mkdirSync(targetBaseDir, { recursive: true })
351+
352+
const tempCloneDir = mkdtempSync(join(tmpdir(), 'sparse-checkout-'))
353+
const finalDestinationPath = join(targetBaseDir, targetDirName)
354+
355+
try {
356+
rmSync(finalDestinationPath, { recursive: true, force: true })
357+
358+
const normalizedSparsePath = sparsePath.replace(/^\/+/, '').replace(/\/+$/, '')
359+
const refAndPath = `${ref}/${normalizedSparsePath}`
360+
361+
const repo = new chartRepo(tempCloneDir, gitCloneUrl)
362+
await repo.cloneSingleChart(refAndPath, finalDestinationPath)
363+
364+
rmSync(join(finalDestinationPath, '.git'), { recursive: true, force: true })
365+
366+
return { success: true, checkoutPath: finalDestinationPath }
367+
} catch (error) {
368+
return {
369+
success: false,
370+
error: error instanceof Error ? error.message : 'Unknown sparse checkout error.',
371+
}
372+
} finally {
373+
rmSync(tempCloneDir, { recursive: true, force: true })
374+
}
375+
}
376+
342377
/**
343378
* Encodes Git credentials into the URL for internal Gitea repositories
344379
*/
@@ -534,3 +569,66 @@ export async function fetchWorkloadCatalog(
534569

535570
return { helmCharts, catalog }
536571
}
572+
573+
export async function fetchWorkloadCatalogChart(
574+
url: string,
575+
helmChartsDir: string,
576+
chartName: string,
577+
branch: string = 'main',
578+
clusterDomainSuffix?: string,
579+
teamId?: string,
580+
chartsPath?: string,
581+
): Promise<any | null> {
582+
const resolvedHelmChartsDir = path.resolve(helmChartsDir)
583+
584+
if (!existsSync(resolvedHelmChartsDir)) {
585+
mkdirSync(resolvedHelmChartsDir, { recursive: true })
586+
}
587+
588+
const gitUrl = encodeGitCredentials(url, clusterDomainSuffix)
589+
590+
const sparsePath = chartsPath ? `${chartsPath}/${chartName}` : chartName
591+
const checkoutResult = await sparseCheckoutPath(gitUrl, branch, sparsePath, resolvedHelmChartsDir, chartName)
592+
593+
if (!checkoutResult.success) {
594+
debug(`Sparse checkout failed for chart '${chartName}' from '${url}': ${checkoutResult.error}`)
595+
return null
596+
}
597+
598+
const chartDir = checkoutResult.checkoutPath
599+
600+
try {
601+
const values = await safeReadTextFile(chartDir, 'values.yaml')
602+
603+
let valuesSchema = '{}'
604+
try {
605+
const schemaContent = await safeReadTextFile(chartDir, 'values.schema.json')
606+
valuesSchema = schemaContent || '{}'
607+
} catch {
608+
// optional
609+
}
610+
611+
const chartYaml = await safeReadTextFile(chartDir, 'Chart.yaml')
612+
const chartMetadata = YAML.parse(chartYaml)
613+
614+
let readme = 'There is no `README` for this chart.'
615+
try {
616+
readme = await safeReadTextFile(chartDir, 'README.md')
617+
} catch {
618+
// optional
619+
}
620+
621+
return {
622+
name: chartName,
623+
values: values || '{}',
624+
valuesSchema,
625+
icon: chartMetadata?.icon,
626+
chartVersion: chartMetadata?.version,
627+
chartDescription: chartMetadata?.description,
628+
readme,
629+
}
630+
} catch (error) {
631+
debug(`Error parsing chart '${chartName}' in '${chartDir}': ${error.message}`)
632+
return null
633+
}
634+
}

0 commit comments

Comments
 (0)