Skip to content

Commit cb6750e

Browse files
committed
adds minimal support for attaching detected ACM clusters
1 parent 797fc20 commit cb6750e

File tree

4 files changed

+261
-0
lines changed

4 files changed

+261
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ dist/
44
.DS_Store
55
.env
66
gui_test_screenshots
7+
.nodeenv

src/components/AttachMenu.tsx

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,13 @@ import ResourceIcon from './ResourceIcon';
4141

4242
const ALERTS_ENDPOINT = '/api/prometheus/api/v1/rules?type=alert';
4343

44+
// Managed clusters have an additional info object that lives in a namespace whose name matches the cluster name
45+
const fetchManagedClusterInfo = async (clusterName: string): Promise<K8sResourceKind> => {
46+
const endpoint = `/api/kubernetes/apis/internal.open-cluster-management.io/v1beta1/namespaces/${clusterName}/managedclusterinfos/${clusterName}`;
47+
const response = await consoleFetchJSON(endpoint, 'get', getRequestInitWithAuthHeader());
48+
return response;
49+
};
50+
4451
// Sanity check on the YAML file size
4552
const MAX_FILE_SIZE_KB = 500;
4653

@@ -194,6 +201,56 @@ const AttachMenu: React.FC = () => {
194201
setError(t('Error fetching alerting rules: {{err}}', { err }));
195202
setLoaded();
196203
});
204+
} else if (
205+
// Only show this attachment option when the object in play is a ManagedCluster
206+
kind === 'cluster.open-cluster-management.io~v1~ManagedCluster' &&
207+
attachmentType === AttachmentTypes.YAML
208+
) {
209+
setLoading();
210+
211+
// First attach the ManagedCluster object
212+
if (context) {
213+
const clusterData = cloneDeep(context);
214+
delete clusterData.metadata.managedFields;
215+
try {
216+
const clusterYaml = dumpYAML(clusterData, { lineWidth: -1 }).trim();
217+
dispatch(
218+
attachmentSet(AttachmentTypes.YAML, kind, name, undefined, namespace, clusterYaml),
219+
);
220+
} catch (e) {
221+
setError(t('Error converting ManagedCluster to YAML: {{e}}', { e }));
222+
setLoaded();
223+
return;
224+
}
225+
}
226+
227+
// Then fetch and attach the ManagedClusterInfo object
228+
fetchManagedClusterInfo(name)
229+
.then((clusterInfo) => {
230+
const data = cloneDeep(clusterInfo);
231+
delete data.metadata.managedFields;
232+
try {
233+
const yaml = dumpYAML(data, { lineWidth: -1 }).trim();
234+
dispatch(
235+
attachmentSet(
236+
AttachmentTypes.YAML,
237+
'ManagedClusterInfo',
238+
name,
239+
undefined,
240+
name,
241+
yaml,
242+
),
243+
);
244+
close();
245+
} catch (e) {
246+
setError(t('Error converting ManagedClusterInfo to YAML: {{e}}', { e }));
247+
}
248+
setLoaded();
249+
})
250+
.catch((err) => {
251+
setError(t('Error fetching cluster info: {{err}}', { err }));
252+
setLoaded();
253+
});
197254
} else if (
198255
context &&
199256
(attachmentType === AttachmentTypes.YAML || attachmentType === AttachmentTypes.YAMLFiltered)
@@ -335,6 +392,11 @@ const AttachMenu: React.FC = () => {
335392
<SelectOption value={AttachmentTypes.YAML}>
336393
<FileCodeIcon /> {t('Alert')} {isLoading && <Spinner size="md" />}
337394
</SelectOption>
395+
) : kind === 'cluster.open-cluster-management.io~v1~ManagedCluster' ? (
396+
<SelectOption value={AttachmentTypes.YAML}>
397+
<TaskIcon /> {t('Attach cluster info')}
398+
{isLoading && <Spinner size="md" />}
399+
</SelectOption>
338400
) : (
339401
<>
340402
{isResourceContext && (

src/hooks/useLocationContext.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,27 @@ export const useLocationContext = () => {
6666
}
6767
}
6868
}
69+
70+
urlMatches = path.match(
71+
// This is what ACM cluster URLs look like:
72+
// http://something.tld/multicloud/infrastructure/clusters/details/aks-central/aks-central/overview
73+
// they are not namespaced and the resource name is repeated
74+
new RegExp(
75+
`/multicloud/infrastructure/clusters/details/(${resourceName})/${resourceName}/overview`,
76+
),
77+
);
78+
if (urlMatches) {
79+
// The k8s object for the cluster is not in the URL path, so we have to directly check if we are looking
80+
// at a cluster object here
81+
const key = 'cluster.open-cluster-management.io~v1~ManagedCluster';
82+
83+
if (models[key]) {
84+
setKind(key);
85+
setName(urlMatches[1]);
86+
setNamespace(undefined);
87+
return;
88+
}
89+
}
6990
}
7091

7192
if (new RegExp('^/monitoring/alerts/[0-9]+').test(path)) {

tests/tests/lightspeed-install.cy.ts

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -657,4 +657,181 @@ metadata:
657657
{ force: true },
658658
);
659659
});
660+
661+
it('Test attach cluster info for ManagedCluster', () => {
662+
// Mock ManagedCluster details page
663+
cy.visit(
664+
'/k8s/ns/test-cluster/cluster.open-cluster-management.io~v1~ManagedCluster/test-cluster',
665+
);
666+
cy.get(mainButton).click();
667+
cy.get(popover).should('exist');
668+
669+
// Test that the attach menu shows "Attach cluster info" option for ManagedCluster
670+
cy.get(attachMenuButton).click();
671+
cy.get(attachMenu)
672+
.should('include.text', 'Attach cluster info')
673+
.should('include.text', 'Upload from computer')
674+
.should('not.include.text', 'Full YAML file')
675+
.should('not.include.text', 'Filtered YAML')
676+
.should('not.include.text', 'Events')
677+
.should('not.include.text', 'Logs');
678+
679+
// Mock the API calls for ManagedCluster and ManagedClusterInfo
680+
cy.intercept(
681+
'GET',
682+
'/api/kubernetes/apis/cluster.open-cluster-management.io/v1/managedclusters/test-cluster',
683+
{
684+
statusCode: 200,
685+
body: {
686+
kind: 'ManagedCluster',
687+
apiVersion: 'cluster.open-cluster-management.io/v1',
688+
metadata: {
689+
name: 'test-cluster',
690+
namespace: 'test-cluster',
691+
},
692+
spec: {
693+
hubAcceptsClient: true,
694+
},
695+
status: {
696+
conditions: [
697+
{
698+
type: 'ManagedClusterConditionAvailable',
699+
status: 'True',
700+
},
701+
],
702+
},
703+
},
704+
},
705+
).as('getManagedCluster');
706+
707+
cy.intercept(
708+
'GET',
709+
'/api/kubernetes/apis/internal.open-cluster-management.io/v1beta1/namespaces/test-cluster/managedclusterinfos/test-cluster',
710+
{
711+
statusCode: 200,
712+
body: {
713+
kind: 'ManagedClusterInfo',
714+
apiVersion: 'internal.open-cluster-management.io/v1beta1',
715+
metadata: {
716+
name: 'test-cluster',
717+
namespace: 'test-cluster',
718+
},
719+
status: {
720+
distributionInfo: {
721+
type: 'OCP',
722+
ocp: {
723+
version: '4.14.0',
724+
},
725+
},
726+
nodeList: [
727+
{
728+
name: 'master-0',
729+
conditions: [
730+
{
731+
type: 'Ready',
732+
status: 'True',
733+
},
734+
],
735+
},
736+
],
737+
},
738+
},
739+
},
740+
).as('getManagedClusterInfo');
741+
742+
// Click "Attach cluster info" button
743+
cy.get(attachMenu).find('button').contains('Attach cluster info').click();
744+
745+
// Wait for both API calls
746+
cy.wait('@getManagedCluster');
747+
cy.wait('@getManagedClusterInfo');
748+
749+
// Verify that both ManagedCluster and ManagedClusterInfo attachments are added
750+
cy.get(attachments)
751+
.should('include.text', 'test-cluster')
752+
.should('include.text', 'YAML')
753+
.find('button')
754+
.should('have.length', 2); // Should have both ManagedCluster and ManagedClusterInfo attachments
755+
756+
// Test the ManagedCluster attachment preview
757+
cy.get(attachments).find('button').contains('test-cluster').first().click();
758+
cy.get(modal)
759+
.should('include.text', 'Preview attachment')
760+
.should('include.text', 'test-cluster')
761+
.should('include.text', 'kind: ManagedCluster')
762+
.should('include.text', 'apiVersion: cluster.open-cluster-management.io/v1')
763+
.find('button')
764+
.contains('Dismiss')
765+
.click();
766+
767+
// Test the ManagedClusterInfo attachment preview
768+
cy.get(attachments).find('button').contains('test-cluster').last().click();
769+
cy.get(modal)
770+
.should('include.text', 'Preview attachment')
771+
.should('include.text', 'test-cluster')
772+
.should('include.text', 'kind: ManagedClusterInfo')
773+
.should('include.text', 'apiVersion: internal.open-cluster-management.io/v1beta1')
774+
.should('include.text', 'distributionInfo')
775+
.find('button')
776+
.contains('Dismiss')
777+
.click();
778+
779+
// Test submitting a prompt with cluster attachments
780+
cy.interceptQuery('queryStub', PROMPT_SUBMITTED, null, [
781+
{ attachmentType: 'yaml', contentType: 'application/yaml' },
782+
{ attachmentType: 'yaml', contentType: 'application/yaml' },
783+
]);
784+
cy.get(promptInput).type(`${PROMPT_SUBMITTED}{enter}`);
785+
cy.wait('@queryStub');
786+
});
787+
788+
it('Test ManagedCluster attachment error handling', () => {
789+
// Mock ManagedCluster details page
790+
cy.visit(
791+
'/k8s/ns/test-cluster/cluster.open-cluster-management.io~v1~ManagedCluster/test-cluster',
792+
);
793+
cy.get(mainButton).click();
794+
cy.get(popover).should('exist');
795+
796+
// Mock successful ManagedCluster API call but failed ManagedClusterInfo call
797+
cy.intercept(
798+
'GET',
799+
'/api/kubernetes/apis/cluster.open-cluster-management.io/v1/managedclusters/test-cluster',
800+
{
801+
statusCode: 200,
802+
body: {
803+
kind: 'ManagedCluster',
804+
apiVersion: 'cluster.open-cluster-management.io/v1',
805+
metadata: {
806+
name: 'test-cluster',
807+
namespace: 'test-cluster',
808+
},
809+
},
810+
},
811+
).as('getManagedCluster');
812+
813+
cy.intercept(
814+
'GET',
815+
'/api/kubernetes/apis/internal.open-cluster-management.io/v1beta1/namespaces/test-cluster/managedclusterinfos/test-cluster',
816+
{
817+
statusCode: 404,
818+
body: {
819+
kind: 'Status',
820+
message:
821+
'managedclusterinfos.internal.open-cluster-management.io "test-cluster" not found',
822+
},
823+
},
824+
).as('getManagedClusterInfoError');
825+
826+
// Click "Attach cluster info" button
827+
cy.get(attachMenuButton).click();
828+
cy.get(attachMenu).find('button').contains('Attach cluster info').click();
829+
830+
// Wait for API calls
831+
cy.wait('@getManagedCluster');
832+
cy.wait('@getManagedClusterInfoError');
833+
834+
// Verify error is displayed
835+
cy.get(attachMenu).should('include.text', 'Error fetching cluster info');
836+
});
660837
});

0 commit comments

Comments
 (0)