Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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
Original file line number Diff line number Diff line change
Expand Up @@ -1158,7 +1158,7 @@ Topology Data Model Factory Extension
| ---- | ---------- | -------- | ----------- |
| `id` | `string` | no | Unique ID for the factory. |
| `priority` | `number` | no | Priority for the factory |
| `resources` | `WatchK8sResourcesGeneric` | yes | Resources to be fetched from useK8sWatchResources hook. |
| `resources` | `WatchK8sResourcesGeneric \| CodeRef<() => Promise<WatchK8sResourcesGeneric>>` | yes | Resources to be fetched from useK8sWatchResources hook. |
| `workloadKeys` | `string[]` | yes | Keys in resources containing workloads. |
| `getDataModel` | `CodeRef<TopologyDataModelGetter>` | yes | Getter for the data model factory |
| `isResourceDepicted` | `CodeRef<TopologyDataModelDepicted>` | yes | Getter for function to determine if a resource is depicted by this model factory |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export type TopologyDataModelFactory = ExtensionDeclaration<
/** Priority for the factory */
priority: number;
/** Resources to be fetched from useK8sWatchResources hook. */
resources?: WatchK8sResourcesGeneric;
resources?: WatchK8sResourcesGeneric | CodeRef<() => Promise<WatchK8sResourcesGeneric>>;
/** Keys in resources containing workloads. */
workloadKeys?: string[];
/** Getter for the data model factory */
Expand Down
107 changes: 80 additions & 27 deletions frontend/packages/knative-plugin/src/utils/get-knative-resources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,7 @@ import {
KafkaConnectionModel,
} from '../models';
import { Traffic } from '../types';
import {
getDynamicEventSourcesWatchers,
getDynamicEventingChannelWatchers,
} from './fetch-dynamic-eventsources-utils';
import { fetchEventSourcesCrd, fetchChannelsCrd } from './fetch-dynamic-eventsources-utils';

export type KnativeItem = {
revisions?: K8sResourceKind[];
Expand Down Expand Up @@ -438,31 +435,87 @@ export const getSinkableResources = (namespace: string): FirehoseResource[] => {
: [];
};

export const getKnativeServingResources = (namespace: string) => {
return {
...knativeServingResourcesRevisionWatchers(namespace),
...knativeServingResourcesConfigurationsWatchers(namespace),
...knativeServingResourcesRoutesWatchers(namespace),
...knativeServingResourcesServicesWatchers(namespace),
...knativeCamelDomainMappingResourceWatchers(namespace),
};
};
export const getKnativeServingResources = () =>
Promise.resolve({
revisions: {
model: { group: 'serving.knative.dev', version: 'v1', kind: 'Revision' },
opts: { isList: true, optional: true, namespaced: true },
},
configurations: {
model: { group: 'serving.knative.dev', version: 'v1', kind: 'Configuration' },
opts: { isList: true, optional: true, namespaced: true },
},
ksroutes: {
model: { group: 'serving.knative.dev', version: 'v1', kind: 'Route' },
opts: { isList: true, optional: true, namespaced: true },
},
ksservices: {
model: { group: 'serving.knative.dev', version: 'v1', kind: 'Service' },
opts: { isList: true, optional: true, namespaced: true },
},
domainmappings: {
model: { group: 'serving.knative.dev', version: 'v1beta1', kind: 'DomainMapping' },
opts: { isList: true, optional: true, namespaced: true },
},
});

export const getKnativeEventingResources = async () => {
// Fetch dynamic event sources and channels at runtime
const eventSourceModels = await fetchEventSourcesCrd();
const eventingChannels = await fetchChannelsCrd();

const dynamicEventSources = eventSourceModels.reduce((acc, model) => {
const ref = referenceForModel(model);
acc[ref] = {
model: { group: model.apiGroup, version: model.apiVersion, kind: model.kind },
opts: { isList: true, optional: true, namespaced: true },
};
return acc;
}, {});

const dynamicChannels = eventingChannels.reduce((acc, model) => {
const ref = referenceForModel(model);
acc[ref] = {
model: { group: model.apiGroup, version: model.apiVersion, kind: model.kind },
opts: { isList: true, optional: true, namespaced: true },
};
return acc;
}, {});

export const getKnativeEventingResources = (namespace: string) => {
return {
...knativeEventingResourcesSubscriptionWatchers(namespace),
...getDynamicEventSourcesWatchers(namespace),
...getDynamicEventingChannelWatchers(namespace),
...knativeEventingBrokerResourceWatchers(namespace),
...knativeEventingTriggerResourceWatchers(namespace),
eventingsubscription: {
model: { group: 'messaging.knative.dev', version: 'v1', kind: 'Subscription' },
opts: { isList: true, optional: true, namespaced: true },
},
brokers: {
model: { group: 'eventing.knative.dev', version: 'v1', kind: 'Broker' },
opts: { isList: true, optional: true, namespaced: true },
},
triggers: {
model: { group: 'eventing.knative.dev', version: 'v1', kind: 'Trigger' },
opts: { isList: true, optional: true, namespaced: true },
},
...dynamicEventSources,
...dynamicChannels,
};
};

export const getKnativeEventingKameletsResources = (namespace: string) => {
return {
...knativeCamelIntegrationsResourceWatchers(namespace),
...knativeCamelKameletBindingResourceWatchers(namespace),
...knativeCamelDomainMappingResourceWatchers(namespace),
...knativeCamelKameletResourceWatchers(namespace),
};
};
export const getKnativeEventingKameletsResources = () =>
Promise.resolve({
integrations: {
model: { group: 'camel.apache.org', version: 'v1', kind: 'Integration' },
opts: { isList: true, optional: true, namespaced: true },
},
kameletbindings: {
model: { group: 'camel.apache.org', version: 'v1alpha1', kind: 'KameletBinding' },
opts: { isList: true, optional: true, namespaced: true },
},
domainmappings: {
model: { group: 'serving.knative.dev', version: 'v1beta1', kind: 'DomainMapping' },
opts: { isList: true, optional: true, namespaced: true },
},
kamelets: {
model: { group: 'camel.apache.org', version: 'v1alpha1', kind: 'Kamelet' },
opts: { isList: true, optional: true, namespaced: true },
},
});
Original file line number Diff line number Diff line change
@@ -1,29 +1,98 @@
import * as React from 'react';
import { useContext, useRef, useEffect, useMemo } from 'react';
import {
WatchK8sResources,
WatchK8sResourcesGeneric,
WatchK8sResource,
} from '@console/dynamic-plugin-sdk';
import { referenceForModel, modelForGroupKind } from '@console/internal/module/k8s';
import { useDeepCompareMemoize } from '@console/shared';
import { TopologyDataModelFactory } from '../extensions/topology';
import { useResolvedResources } from '../hooks/useTopologyDataModelFactory';
import { ModelContext, ExtensibleModel, ModelExtensionContext } from './ModelContext';

interface DataModelExtensionProps {
dataModelFactory: TopologyDataModelFactory['properties'];
}

/**
* Converts a single resource from WatchK8sResourcesGeneric format to WatchK8sResource format.
* This handles the namespace injection and model reference resolution.
*/
const convertGenericResource = (
namespace: string,
model?: { group?: string; version?: string; kind: string },
opts?: Partial<WatchK8sResource>,
): WatchK8sResource | null => {
if (!model) {
return { namespace, ...opts };
}

// Try to find the internal model
const internalModel = modelForGroupKind(model.group, model.kind);
if (!internalModel) {
// CRD not found - log warning and skip
// eslint-disable-next-line no-console
console.warn(
`Could not find model (CRD) for group "${model.group}" and kind "${model.kind}". Resource will be skipped.`,
);
return null;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this needed to work? my understanding was that WatchK8sResourcesGeneric allows you to provide all the options that WatchK8sResource does


const reference = referenceForModel(internalModel);
return { namespace, kind: reference, ...opts };
};

const DataModelExtension: React.FC<DataModelExtensionProps> = ({ dataModelFactory }) => {
const dataModelContext = React.useContext<ExtensibleModel>(ModelContext);
const { id, priority, resources } = dataModelFactory;
const dataModelContext = useContext<ExtensibleModel>(ModelContext);
const { id, priority, resources: rawResources } = dataModelFactory;
const workloadKeys = useDeepCompareMemoize(dataModelFactory.workloadKeys);
const extensionContext = React.useRef<ModelExtensionContext>({
const { resolved: resolvedResources, isGeneric } = useResolvedResources(
rawResources,
dataModelContext.namespace,
);

// Convert WatchK8sResourcesGeneric to WatchK8sResources if needed
const finalResources = useMemo<WatchK8sResources<any> | undefined>(() => {
if (!resolvedResources) {
return undefined;
}

if (isGeneric) {
// Resources are in WatchK8sResourcesGeneric format, need to be converted
const genericResources = resolvedResources as WatchK8sResourcesGeneric;
const converted: WatchK8sResources<any> = {};

Object.entries(genericResources).forEach(([key, resource]) => {
const flattenedResource = convertGenericResource(
dataModelContext.namespace,
resource?.model,
resource?.opts,
);
if (flattenedResource) {
converted[key] = flattenedResource;
}
});

return converted;
}

// Already in the correct format
return resolvedResources as WatchK8sResources<any>;
}, [resolvedResources, isGeneric, dataModelContext.namespace]);

const extensionContext = useRef<ModelExtensionContext>({
priority,
workloadKeys,
resources,
resources: undefined,
});

React.useEffect(() => {
useEffect(() => {
const storedContext = dataModelContext.getExtension(id);
if (!storedContext) {
extensionContext.current = {
priority,
workloadKeys,
resources,
resources: finalResources,
};
dataModelContext.updateExtension(id, extensionContext.current);

Expand Down Expand Up @@ -73,7 +142,7 @@ const DataModelExtension: React.FC<DataModelExtensionProps> = ({ dataModelFactor
dataModelContext.updateExtension(id, extensionContext.current);
}
}
}, [dataModelContext, dataModelFactory, id, priority, resources, workloadKeys]);
}, [dataModelContext, dataModelFactory, id, priority, finalResources, workloadKeys]);

return null;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
referenceForExtensionModel,
referenceForModel,
} from '@console/internal/module/k8s';
import { LoadedExtension, useExtensions } from '@console/plugin-sdk/src';
import { LoadedExtension, useExtensions } from '@console/plugin-sdk';
import { isTopologyDataModelFactory, TopologyDataModelFactory } from '../extensions/topology';
import DataModelExtension from './DataModelExtension';
import { ModelContext, ExtensibleModel } from './ModelContext';
Expand Down Expand Up @@ -58,12 +58,27 @@ export const getNamespacedDynamicModelFactories = (
extensions: LoadedExtension<DynamicTopologyDataModelFactory>[],
) =>
extensions.map((extension) => {
const { resources } = extension.properties;

// If resources is a CodeRef (function), keep it as-is for resolution in DataModelExtension
if (typeof resources === 'function') {
return {
...extension,
properties: {
...extension.properties,
// Keep the original CodeRef - it will be resolved by useResolvedResources hook
resources,
},
};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

doesn't this just return extension?

}

// Static WatchK8sResourcesGeneric - convert to function format for legacy compatibility
return {
...extension,
properties: {
...extension.properties,
resources: (namespace: string) =>
Object.entries(extension.properties.resources || {}).reduce((acc, [key, resource]) => {
Object.entries(resources || {}).reduce((acc, [key, resource]: [string, any]) => {
const flattenedResource = flattenResource(
namespace,
extension,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {

export type ModelExtensionContext = {
priority: number;
resources?: (namespace: string) => WatchK8sResources<any>;
resources?: WatchK8sResources<any>;
workloadKeys?: string[];
dataModelGetter?: TopologyDataModelGetter;
dataModelDepicter?: TopologyDataModelDepicted;
Expand Down Expand Up @@ -98,7 +98,7 @@ export class ExtensibleModel {
const extensionKeys = Object.keys(this.extensions);
this.watchedResources = extensionKeys.reduce((acc, key) => {
if (this.extensions[key].resources) {
const resList = this.extensions[key].resources(this.namespace);
const resList = this.extensions[key].resources;
Object.keys(resList).forEach((resKey) => {
if (!acc[resKey]) {
acc[resKey] = resList[resKey];
Expand Down
Loading