Skip to content
Open
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
7 changes: 7 additions & 0 deletions osac-ui/libs/i18n/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"Allocated": "Allocated",
"An external IP is required": "An external IP is required",
"An external IP pool is required": "An external IP pool is required",
"API endpoint": "API endpoint",
"API URL": "API URL",
"Architecture": "Architecture",
"Architecture is required": "Architecture is required",
Expand All @@ -66,8 +67,10 @@
"Auto attach external IP": "Auto attach external IP",
"Auto External IP Attachment": "Auto External IP Attachment",
"Auto-provisioned": "Auto-provisioned",
"Auto-provisioned resources": "Auto-provisioned resources",
"Automatically provision external IPs for the cluster API and ingress endpoints.": "Automatically provision external IPs for the cluster API and ingress endpoints.",
"Available": "Available",
"Awaiting provisioning": "Awaiting provisioning",
"Back": "Back",
"Backend": "Backend",
"Backend associations": "Backend associations",
Expand Down Expand Up @@ -334,6 +337,8 @@
"External IP pool overview": "External IP pool overview",
"External IP pools": "External IP pools",
"External IPs": "External IPs",
"ExternalIP": "ExternalIP",
"ExternalIPAttachment": "ExternalIPAttachment",
"Failed": "Failed",
"Failed to attach external IP": "Failed to attach external IP",
"Failed to connect to the console.": "Failed to connect to the console.",
Expand Down Expand Up @@ -463,6 +468,7 @@
"Inbound Rules": "Inbound Rules",
"Infrastructure": "Infrastructure",
"Infrastructure Networking": "Infrastructure Networking",
"Ingress endpoint": "Ingress endpoint",
"Instance type": "Instance type",
"Instance type is required": "Instance type is required",
"Instance types": "Instance types",
Expand Down Expand Up @@ -551,6 +557,7 @@
"No": "No",
"No accelerators added.": "No accelerators added.",
"No additional disks added.": "No additional disks added.",
"No auto-provisioned resources.": "No auto-provisioned resources.",
"No bare metal instance types yet.": "No bare metal instance types yet.",
"No bare metal instances match your search.": "No bare metal instances match your search.",
"No bare metal instances yet.": "No bare metal instances yet.",
Expand Down
1 change: 1 addition & 0 deletions osac-ui/libs/types/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export * from './osac/public/v1/external_ip_type_pb.js'
export * from './osac/public/v1/external_ips_service_pb.js'
export * from './osac/public/v1/external_ip_attachment_type_pb.js'
export * from './osac/public/v1/external_ip_attachments_service_pb.js'
export * from './osac/public/v1/external_ip_attribution_type_pb.js'

export * from './osac/public/v1/nat_gateway_type_pb.js'
export * from './osac/public/v1/nat_gateways_service_pb.js'
Expand Down
19 changes: 19 additions & 0 deletions osac-ui/libs/ui-components/src/api/v1/cluster.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import { keepPreviousData, useMutation } from '@tanstack/react-query';
import { Cluster, ClusterSchema, Clusters, Secrets } from '@osac/types';
import { useProjectFilterQuery } from '@osac/ui-components/hooks/use-project-filter-query';

import { useExternalIPAttachments, useExternalIPs } from './external-ip';
import { useApiFetch } from '../api-context';
import { type CelFilter, escapeCelStringLiteral } from '../cel';
import { apiQueryKey } from '../types';
import { type ApiQueryClient, useApiQuery, useApiQueryClient } from '../use-api-query';

Expand Down Expand Up @@ -138,3 +140,20 @@ export const useFetchClusterPassword = (passwordSecretId: string) => {

return { password, isPending, error, retry };
};

const autoCreatedForFilter = (clusterId: string): CelFilter =>
`this.metadata.labels["auto-created-for"] == "${escapeCelStringLiteral(clusterId)}"` as CelFilter;

export const useClusterAutoProvisionedResources = (clusterId: string, enabled: boolean) => {
const filter = autoCreatedForFilter(clusterId);

const externalIpsQuery = useExternalIPs({ filter, limit: 2 }, { enabled });
const externalIpAttachmentsQuery = useExternalIPAttachments({ filter, limit: 2 }, { enabled });

return {
externalIps: externalIpsQuery.data ?? [],
externalIpAttachments: externalIpAttachmentsQuery.data ?? [],
isLoading: externalIpsQuery.isLoading || externalIpAttachmentsQuery.isLoading,
error: externalIpsQuery.error ?? externalIpAttachmentsQuery.error,
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import React, { type ReactNode, createElement } from 'react';
import { create } from '@bufbuild/protobuf';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { render, screen } from '@testing-library/react';
import { describe, expect, it } from 'vitest';

import {
ClusterSchema,
type ExternalIP,
type ExternalIPAttachment,
ExternalIPAttachmentEndpoint,
ExternalIPAttachmentSchema,
ExternalIPAttachmentState,
ExternalIPSchema,
ExternalIPState,
} from '@osac/types';

import ClusterAutoProvisionedResources from './ClusterAutoProvisionedResources';
import { ClusterNetworkingDetailsTab } from './ClusterNetworkingDetailsTab';
import { ApiProvider } from '../../../api/api-context';
import { createMockConnectTransport } from '../../../test-utils/createMockConnectTransport';

const CLUSTER_ID = 'cl-auto-1';

const makeExternalIp = (
overrides: Partial<{ id: string; name: string; state: ExternalIPState; address: string }> = {},
): ExternalIP =>
create(ExternalIPSchema, {
id: overrides.id ?? 'eip-1',
metadata: {
name: overrides.name ?? 'auto-eip-api',
labels: { 'auto-created-for': CLUSTER_ID },
},
status: {
state: overrides.state ?? ExternalIPState.EXTERNAL_IP_STATE_ALLOCATED,
address: overrides.address ?? '198.51.100.1',
},
});

const makeAttachment = (
overrides: Partial<{
id: string;
name: string;
state: ExternalIPAttachmentState;
endpoint: ExternalIPAttachmentEndpoint;
address: string;
}> = {},
): ExternalIPAttachment =>
create(ExternalIPAttachmentSchema, {
id: overrides.id ?? 'eipa-1',
metadata: {
name: overrides.name ?? 'auto-eipa-api',
labels: { 'auto-created-for': CLUSTER_ID },
},
spec: {
targetEndpoint:
overrides.endpoint ?? ExternalIPAttachmentEndpoint.EXTERNAL_IP_ATTACHMENT_ENDPOINT_API,
},
status: {
state: overrides.state ?? ExternalIPAttachmentState.EXTERNAL_IP_ATTACHMENT_STATE_READY,
externalIpAddress: overrides.address ?? '198.51.100.1',
},
});

const renderWithProviders = (
ui: React.ReactElement,
fixtures: {
externalIps?: ExternalIP[];
externalIpAttachments?: ExternalIPAttachment[];
} = {},
) => {
const transport = createMockConnectTransport(fixtures);
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false }, mutations: { retry: false } },
});
const wrapper = ({ children }: { children: ReactNode }) =>
createElement(
ApiProvider,
{ transport } as React.ComponentProps<typeof ApiProvider>,
createElement(QueryClientProvider, { client: queryClient }, children),
);
return render(ui, { wrapper });
};

describe('ClusterAutoProvisionedResources', () => {
it('is not rendered when autoExternalIpAttachment is false', () => {
const cluster = create(ClusterSchema, {
id: CLUSTER_ID,
spec: { autoExternalIpAttachment: false },
});

renderWithProviders(<ClusterNetworkingDetailsTab cluster={cluster} />);

expect(screen.queryByText('Auto-provisioned resources')).not.toBeInTheDocument();
});

it('is not rendered when autoExternalIpAttachment is undefined', () => {
const cluster = create(ClusterSchema, {
id: CLUSTER_ID,
});

renderWithProviders(<ClusterNetworkingDetailsTab cluster={cluster} />);

expect(screen.queryByText('Auto-provisioned resources')).not.toBeInTheDocument();
});

it('shows auto-provisioned ExternalIPs and ExternalIPAttachments', async () => {
const eip = makeExternalIp();
const eipa = makeAttachment();

renderWithProviders(<ClusterAutoProvisionedResources clusterId={CLUSTER_ID} />, {
externalIps: [eip],
externalIpAttachments: [eipa],
});

expect(await screen.findByText('auto-eip-api')).toBeInTheDocument();
expect(screen.getByText('auto-eipa-api')).toBeInTheDocument();
expect(screen.getByText('ExternalIP')).toBeInTheDocument();
expect(screen.getByText('ExternalIPAttachment')).toBeInTheDocument();
});

it('shows correct status labels for resources', async () => {
const eip = makeExternalIp({
state: ExternalIPState.EXTERNAL_IP_STATE_ALLOCATED,
});
const eipa = makeAttachment({
state: ExternalIPAttachmentState.EXTERNAL_IP_ATTACHMENT_STATE_PENDING,
});

renderWithProviders(<ClusterAutoProvisionedResources clusterId={CLUSTER_ID} />, {
externalIps: [eip],
externalIpAttachments: [eipa],
});

expect(await screen.findByText('Allocated')).toBeInTheDocument();
expect(screen.getByText('Pending')).toBeInTheDocument();
});

it('shows endpoint labels for attachments', async () => {
const eipaApi = makeAttachment({
id: 'eipa-api',
name: 'att-api',
endpoint: ExternalIPAttachmentEndpoint.EXTERNAL_IP_ATTACHMENT_ENDPOINT_API,
});
const eipaIngress = makeAttachment({
id: 'eipa-ingress',
name: 'att-ingress',
endpoint: ExternalIPAttachmentEndpoint.EXTERNAL_IP_ATTACHMENT_ENDPOINT_INGRESS,
});

renderWithProviders(<ClusterAutoProvisionedResources clusterId={CLUSTER_ID} />, {
externalIpAttachments: [eipaApi, eipaIngress],
});

expect(await screen.findByText('API')).toBeInTheDocument();
expect(screen.getByText('Ingress')).toBeInTheDocument();
});

it('shows empty state message when no auto-provisioned resources exist', async () => {
renderWithProviders(<ClusterAutoProvisionedResources clusterId={CLUSTER_ID} />, {
externalIps: [],
externalIpAttachments: [],
});

expect(await screen.findByText('No auto-provisioned resources.')).toBeInTheDocument();
});
});
Loading
Loading