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:
parent
b6a6ce9aaf
commit
2697d6c5d7
80 changed files with 4264 additions and 812 deletions
|
@ -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[]> {
|
||||
|
|
|
@ -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'),
|
||||
});
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
})) ?? []
|
||||
|
|
84
app/react/kubernetes/helm/queries/useHelmRepositories.ts
Normal file
84
app/react/kubernetes/helm/queries/useHelmRepositories.ts
Normal 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');
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue