1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-07-25 00:09:40 +02:00

feat(oci): oci helm support [r8s-361] (#787)

This commit is contained in:
Ali 2025-07-13 10:37:43 +12:00 committed by GitHub
parent b6a6ce9aaf
commit 2697d6c5d7
80 changed files with 4264 additions and 812 deletions

View file

@ -1,6 +1,5 @@
import { useQueries } from '@tanstack/react-query';
import { compact, flatMap } from 'lodash';
import { useMemo } from 'react';
import { compact } from 'lodash';
import { useQuery } from '@tanstack/react-query';
import axios from '@/portainer/services/axios';
import { withGlobalError } from '@/react-tools/react-query';
@ -8,45 +7,27 @@ import { withGlobalError } from '@/react-tools/react-query';
import { Chart, HelmChartsResponse } from '../types';
/**
* React hook to fetch helm charts from the provided repositories
* Charts from each repository are loaded independently, allowing the UI
* to show charts as they become available instead of waiting for all
* repositories to load
* React hook to fetch helm charts from the provided HTTP repository.
* Charts are loaded from the specified repository URL.
*
* @param userId User ID
* @param repositories List of repository URLs to fetch charts from
* @param repository Repository URL to fetch charts from
* @param enabled Flag indicating if the query should be enabled
* @returns Query result containing helm charts
*/
export function useHelmChartList(userId: number, repositories: string[] = []) {
// Fetch charts from each repository in parallel as separate queries
const chartQueries = useQueries({
queries: useMemo(
() =>
repositories.map((repo) => ({
queryKey: [userId, repo, 'helm-charts'],
queryFn: () => getChartsFromRepo(repo),
enabled: !!userId && repositories.length > 0,
// one request takes a long time, so fail early to get feedback to the user faster
retries: false,
...withGlobalError(`Unable to retrieve Helm charts from ${repo}`),
})),
[repositories, userId]
),
export function useHelmHTTPChartList(
userId: number,
repository: string,
enabled: boolean
) {
return useQuery({
queryKey: [userId, repository, 'helm-charts'],
queryFn: () => getChartsFromRepo(repository),
enabled: !!userId && !!repository && enabled,
// one request takes a long time, so fail early to get feedback to the user faster
retry: false,
...withGlobalError(`Unable to retrieve Helm charts from ${repository}`),
});
// Combine the results for easier consumption by components
const allCharts = useMemo(
() => flatMap(compact(chartQueries.map((q) => q.data))),
[chartQueries]
);
return {
// Data from all repositories that have loaded so far
data: allCharts,
// Overall loading state
isInitialLoading: chartQueries.some((q) => q.isInitialLoading),
// Overall error state
isError: chartQueries.some((q) => q.isError),
};
}
async function getChartsFromRepo(repo: string): Promise<Chart[]> {

View file

@ -4,8 +4,11 @@ import axios, { parseAxiosError } from '@/portainer/services/axios';
import { withGlobalError } from '@/react-tools/react-query';
type Params = {
/** The name of the chart to get the values for */
chart: string;
/** The repository URL or registry ID */
repo: string;
/** The version of the chart to get the values for */
version?: string;
};
@ -16,18 +19,26 @@ async function getHelmChartValues(params: Params) {
});
return response.data;
} catch (err) {
throw parseAxiosError(err as Error, 'Unable to get Helm chart values');
throw parseAxiosError(err, 'Unable to get Helm chart values');
}
}
export function useHelmChartValues(params: Params) {
export function useHelmChartValues(params: Params, isLatestVersion = false) {
const hasValidRepoUrl = !!params.repo;
return useQuery({
queryKey: ['helm-chart-values', params.repo, params.chart, params.version],
queryKey: [
'helm-chart-values',
params.repo,
params.chart,
// if the latest version is fetched, use the latest version key to cache the latest version
isLatestVersion ? 'latest' : params.version,
],
queryFn: () => getHelmChartValues(params),
enabled: !!params.chart && !!params.repo,
enabled: !!params.chart && hasValidRepoUrl,
select: (data) => ({
values: data,
}),
retry: 1,
staleTime: 60 * 1000 * 20, // 60 minutes, because values are not expected to change often
...withGlobalError('Unable to get Helm chart values'),
});

View file

@ -1,43 +0,0 @@
import { useQuery } from '@tanstack/react-query';
import { compact } from 'lodash';
import axios, { parseAxiosError } from '@/portainer/services/axios';
import { UserId } from '@/portainer/users/types';
import { withGlobalError } from '@/react-tools/react-query';
import { useCurrentUser } from '@/react/hooks/useUser';
import { HelmRegistriesResponse } from '../types';
/**
* Hook to fetch all Helm registries for the current user
*/
export function useHelmRegistries() {
const { user } = useCurrentUser();
return useQuery(
['helm', 'registries'],
async () => getHelmRegistries(user.Id),
{
enabled: !!user.Id,
...withGlobalError('Unable to retrieve helm registries'),
}
);
}
/**
* Get Helm registries for user
*/
async function getHelmRegistries(userId: UserId) {
try {
const { data } = await axios.get<HelmRegistriesResponse>(
`users/${userId}/helm/repositories`
);
const repos = compact([
// compact will remove the global repository if it's empty
data.GlobalRepository.toLowerCase(),
...data.UserRepositories.map((repo) => repo.URL.toLowerCase()),
]);
return [...new Set(repos)];
} catch (err) {
throw parseAxiosError(err, 'Unable to retrieve helm repositories for user');
}
}

View file

@ -21,6 +21,10 @@ export interface ChartVersion {
AppVersion?: string;
}
type RepoSource = {
repo?: string;
};
/**
* React hook to get a list of available versions for a chart from specified repositories
*
@ -32,21 +36,21 @@ export interface ChartVersion {
export function useHelmRepoVersions(
chart: string,
staleTime: number,
repositories: string[] = [],
repoSources: RepoSource[] = [],
useCache: boolean = true
) {
// Fetch versions from each repository in parallel as separate queries
const versionQueries = useQueries({
queries: useMemo(
() =>
repositories.map((repo) => ({
repoSources.map(({ repo }) => ({
queryKey: ['helm', 'repositories', chart, repo, useCache],
queryFn: () => getSearchHelmRepo(repo, chart, useCache),
enabled: !!chart && repositories.length > 0,
queryFn: () => getSearchHelmRepo({ repo, chart, useCache }),
enabled: !!chart && repoSources.length > 0,
staleTime,
...withGlobalError(`Unable to retrieve versions from ${repo}`),
})),
[repositories, chart, staleTime, useCache]
[repoSources, chart, staleTime, useCache]
),
});
@ -58,30 +62,35 @@ export function useHelmRepoVersions(
return {
data: allVersions,
isInitialLoading: versionQueries.some((q) => q.isLoading),
isInitialLoading: versionQueries.some((q) => q.isInitialLoading),
isError: versionQueries.some((q) => q.isError),
isFetching: versionQueries.some((q) => q.isFetching),
refetch: () => Promise.all(versionQueries.map((q) => q.refetch())),
};
}
type SearchRepoParams = {
repo?: string;
chart: string;
useCache?: boolean;
};
/**
* Get Helm repositories for user
*/
async function getSearchHelmRepo(
repo: string,
chart: string,
useCache: boolean = true
params: SearchRepoParams
): Promise<ChartVersion[]> {
try {
const { data } = await axios.get<HelmSearch>(`templates/helm`, {
params: { repo, chart, useCache },
params,
});
const versions = data.entries[chart];
// if separated by '/', take the last part
const chartKey = params.chart.split('/').pop() || params.chart;
const versions = data.entries[chartKey];
return (
versions?.map((v) => ({
Chart: chart,
Repo: repo,
Repo: params.repo ?? '',
Version: v.version,
AppVersion: v.appVersion,
})) ?? []

View file

@ -0,0 +1,84 @@
import { useQuery } from '@tanstack/react-query';
import { compact } from 'lodash';
import axios, { parseAxiosError } from '@/portainer/services/axios';
import { UserId } from '@/portainer/users/types';
import { withGlobalError } from '@/react-tools/react-query';
import { useCurrentUser } from '@/react/hooks/useUser';
import { Option } from '@/react/components/form-components/PortainerSelect';
import { HelmRegistriesResponse } from '../types';
import { RepoValue } from '../components/HelmRegistrySelect';
/**
* Hook to fetch all Helm registries for the current user
*/
export function useUserHelmRepositories<T = string[]>({
select,
}: {
select?: (registries: string[]) => T;
} = {}) {
const { user } = useCurrentUser();
return useQuery(
['helm', 'registries'],
async () => getUserHelmRepositories(user.Id),
{
enabled: !!user.Id,
select,
...withGlobalError('Unable to retrieve helm registries'),
}
);
}
export function useHelmRepoOptions() {
return useUserHelmRepositories({
select: (registries) => {
const repoOptions = registries
.map<Option<RepoValue>>((registry) => ({
label: registry,
value: {
repoUrl: registry,
isOCI: false,
name: registry,
},
}))
.sort((a, b) => a.label.localeCompare(b.label));
return [
{
label: 'Helm Repositories',
options: repoOptions,
},
{
label: 'OCI Registries',
options: [
{
label:
'Installing from an OCI registry is a Portainer Business Feature',
value: {},
disabled: true,
},
],
},
];
},
});
}
/**
* Get Helm repositories for user
*/
async function getUserHelmRepositories(userId: UserId) {
try {
const { data } = await axios.get<HelmRegistriesResponse>(
`users/${userId}/helm/repositories`
);
// compact will remove the global repository if it's empty
const repos = compact([
data.GlobalRepository.toLowerCase(),
...data.UserRepositories.map((repo) => repo.URL.toLowerCase()),
]);
return [...new Set(repos)];
} catch (err) {
throw parseAxiosError(err, 'Unable to retrieve helm repositories for user');
}
}