@spinnaker/kubernetes 2026.2.2 → 2026.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +1 -2
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/instance/details/KubernetesInstanceDetails.d.ts +54 -0
- package/dist/instance/details/kubernetesInstanceDetails.utils.d.ts +13 -0
- package/dist/instance/index.d.ts +1 -1
- package/dist/kubernetes.module.d.ts +5 -1
- package/dist/loadBalancer/transformer.d.ts +1 -1
- package/dist/manifest/AnnotationCustomSections.d.ts +3 -2
- package/dist/manifest/ManifestKindSearch.d.ts +1 -1
- package/dist/manifest/delete/Delete.d.ts +0 -1
- package/dist/manifest/delete/DeleteModal.d.ts +1 -1
- package/dist/manifest/delete/delete.types.d.ts +4 -0
- package/dist/manifest/index.d.ts +3 -6
- package/dist/manifest/manifest.service.d.ts +5 -0
- package/dist/manifest/manifestCommandBuilder.service.d.ts +1 -1
- package/dist/manifest/rollout/PauseRollout.d.ts +0 -1
- package/dist/manifest/rollout/ResumeRollout.d.ts +0 -1
- package/dist/manifest/rollout/RollingRestart.d.ts +4 -4
- package/dist/manifest/rollout/UndoRollout.d.ts +0 -1
- package/dist/manifest/scale/Scale.d.ts +1 -2
- package/dist/manifest/scale/ScaleModal.d.ts +3 -3
- package/dist/manifest/scale/ScaleSettingsForm.d.ts +7 -1
- package/dist/manifest/selector/ManifestSelector.d.ts +1 -1
- package/dist/manifest/traffic/ManifestTrafficService.d.ts +2 -2
- package/dist/pipelines/stages/deleteManifest/DeleteManifestOptionsForm.d.ts +1 -1
- package/dist/pipelines/stages/deployManifest/manifestStatus/DeployStatus.d.ts +1 -0
- package/dist/pipelines/stages/deployManifest/manifestStatus/ManifestDetailsLink.d.ts +6 -3
- package/dist/pipelines/stages/traffic/disableManifest.stage.d.ts +2 -1
- package/dist/pipelines/stages/traffic/enableManifest.stage.d.ts +2 -1
- package/dist/rawResource/index.d.ts +3 -1
- package/dist/rawResource/rawResource.dataSource.d.ts +1 -1
- package/dist/rawResource/rawResource.states.d.ts +4 -1
- package/dist/resources/resources.state.d.ts +2 -1
- package/dist/securityGroup/details/KubernetesSecurityGroupDetails.d.ts +49 -0
- package/dist/securityGroup/index.d.ts +3 -1
- package/dist/securityGroup/transformer.d.ts +4 -1
- package/dist/serverGroup/serverGroupCommandBuilder.d.ts +1 -1
- package/dist/serverGroup/serverGroupTransformer.d.ts +1 -1
- package/dist/serverGroupManager/details/ServerGroupManagerDetails.d.ts +7 -3
- package/package.json +3 -12
- package/src/help/kubernetes.help.ts +2 -0
- package/src/index.ts +2 -2
- package/src/instance/details/KubernetesInstanceDetails.spec.tsx +451 -0
- package/src/instance/details/KubernetesInstanceDetails.tsx +342 -0
- package/src/instance/details/kubernetesInstanceDetails.utils.ts +77 -0
- package/src/instance/index.ts +1 -1
- package/src/kubernetes.module.spec.ts +25 -0
- package/src/kubernetes.module.ts +23 -87
- package/src/loadBalancer/details/KubernetesLoadBalancerActions.tsx +30 -35
- package/src/loadBalancer/details/sections/LoadBalancerAnnotationCustomSection.tsx +2 -3
- package/src/loadBalancer/transformer.ts +1 -1
- package/src/manifest/AnnotationCustomSections.spec.tsx +178 -0
- package/src/manifest/AnnotationCustomSections.tsx +179 -2
- package/src/manifest/ManifestKindSearch.tsx +1 -1
- package/src/manifest/delete/Delete.tsx +0 -9
- package/src/manifest/delete/DeleteModal.tsx +69 -72
- package/src/manifest/delete/delete.types.ts +4 -0
- package/src/manifest/index.ts +3 -6
- package/src/manifest/manifest.service.ts +18 -1
- package/src/manifest/manifestCommandBuilder.service.spec.ts +41 -0
- package/src/manifest/manifestCommandBuilder.service.ts +45 -59
- package/src/manifest/rollout/PauseRollout.tsx +1 -9
- package/src/manifest/rollout/ResumeRollout.tsx +2 -10
- package/src/manifest/rollout/RollingRestart.tsx +16 -22
- package/src/manifest/rollout/UndoRollout.tsx +1 -10
- package/src/manifest/rollout/UndoRolloutModal.tsx +5 -1
- package/src/manifest/scale/Scale.tsx +12 -11
- package/src/manifest/scale/ScaleModal.tsx +43 -31
- package/src/manifest/scale/ScaleSettingsForm.tsx +8 -1
- package/src/manifest/selector/ManifestSelector.spec.tsx +89 -46
- package/src/manifest/selector/ManifestSelector.tsx +4 -5
- package/src/manifest/traffic/ManifestTrafficService.ts +2 -2
- package/src/manifest/wizard/ManifestWizard.tsx +1 -1
- package/src/pipelines/stages/deleteManifest/DeleteManifestOptionsForm.tsx +1 -1
- package/src/pipelines/stages/deleteManifest/DeleteManifestStageConfig.tsx +1 -1
- package/src/pipelines/stages/deployManifest/DeployManifestStageForm.tsx +16 -0
- package/src/pipelines/stages/deployManifest/ManifestCopier.spec.ts +52 -77
- package/src/pipelines/stages/deployManifest/ManifestDeploymentOptions.spec.tsx +2 -0
- package/src/pipelines/stages/deployManifest/manifestStatus/DeployStatus.tsx +39 -4
- package/src/pipelines/stages/deployManifest/manifestStatus/ManifestDetailsLink.spec.tsx +35 -0
- package/src/pipelines/stages/deployManifest/manifestStatus/ManifestDetailsLink.tsx +10 -5
- package/src/pipelines/stages/traffic/ManifestTrafficStageRegistration.spec.ts +34 -0
- package/src/pipelines/stages/traffic/disableManifest.stage.ts +17 -19
- package/src/pipelines/stages/traffic/enableManifest.stage.ts +17 -19
- package/src/rawResource/index.ts +18 -1
- package/src/rawResource/rawResource.dataSource.ts +5 -8
- package/src/rawResource/rawResource.states.ts +50 -51
- package/src/resources/resources.state.spec.ts +138 -0
- package/src/resources/resources.state.ts +30 -35
- package/src/securityGroup/details/KubernetesSecurityGroupDetails.spec.tsx +292 -0
- package/src/securityGroup/details/KubernetesSecurityGroupDetails.tsx +304 -0
- package/src/securityGroup/index.ts +3 -1
- package/src/securityGroup/transformer.ts +2 -10
- package/src/serverGroup/details/KubernetesServerGroupActions.tsx +31 -61
- package/src/serverGroup/details/kubernetesServerGroupDetailsGetter.ts +1 -1
- package/src/serverGroup/details/sections/ServerGroupAnnotationCustomSection.tsx +2 -3
- package/src/serverGroup/serverGroupCommandBuilder.ts +1 -1
- package/src/serverGroup/serverGroupTransformer.ts +1 -1
- package/src/serverGroupManager/details/ServerGroupManagerActions.tsx +48 -128
- package/src/serverGroupManager/details/ServerGroupManagerDetails.spec.ts +12 -0
- package/src/serverGroupManager/details/ServerGroupManagerDetails.tsx +19 -8
- package/src/serverGroupManager/details/sections/ServerGroupManagerAnnotationCustomSection.tsx +2 -3
- package/src/serverGroupManager/details/useKubernetesServerGroupManagerDetails.spec.tsx +112 -0
- package/src/serverGroupManager/details/useKubernetesServerGroupManagerDetails.tsx +50 -29
- package/dist/instance/details/details.controller.d.ts +0 -1
- package/dist/manifest/annotationCustomSections.component.d.ts +0 -3
- package/dist/manifest/delete/delete.controller.d.ts +0 -12
- package/dist/manifest/delete/deleteOptionsForm.component.d.ts +0 -1
- package/dist/manifest/editor/json/jsonEditor.component.d.ts +0 -1
- package/dist/manifest/manifestEvents.component.d.ts +0 -1
- package/dist/manifest/manifestImageDetails.component.d.ts +0 -1
- package/dist/manifest/manifestLabels.component.d.ts +0 -1
- package/dist/manifest/manifestQos.component.d.ts +0 -1
- package/dist/manifest/manifestResources.component.d.ts +0 -1
- package/dist/manifest/rollout/pause.controller.d.ts +0 -1
- package/dist/manifest/rollout/resume.controller.d.ts +0 -1
- package/dist/manifest/rollout/undo.controller.d.ts +0 -5
- package/dist/manifest/scale/scale.controller.d.ts +0 -8
- package/dist/manifest/scale/scaleSettingsForm.component.d.ts +0 -1
- package/dist/manifest/selector/selector.component.d.ts +0 -1
- package/dist/manifest/status/condition.component.d.ts +0 -1
- package/dist/manifest/status/status.component.d.ts +0 -1
- package/dist/rawResource/rawResource.module.d.ts +0 -1
- package/dist/reactShims/index.d.ts +0 -1
- package/dist/reactShims/kubernetes.react.injector.d.ts +0 -11
- package/dist/reactShims/kubernetes.react.module.d.ts +0 -1
- package/dist/securityGroup/details/details.controller.d.ts +0 -1
- package/src/instance/details/details.controller.ts +0 -186
- package/src/instance/details/details.html +0 -80
- package/src/manifest/annotationCustomSections.component.ts +0 -114
- package/src/manifest/delete/delete.controller.ts +0 -80
- package/src/manifest/delete/delete.html +0 -42
- package/src/manifest/delete/deleteOptionsForm.component.ts +0 -52
- package/src/manifest/editor/json/jsonEditor.component.ts +0 -12
- package/src/manifest/manifestEvents.component.ts +0 -12
- package/src/manifest/manifestImageDetails.component.ts +0 -12
- package/src/manifest/manifestLabels.component.ts +0 -12
- package/src/manifest/manifestQos.component.ts +0 -12
- package/src/manifest/manifestResources.component.ts +0 -12
- package/src/manifest/rollout/pause.controller.ts +0 -66
- package/src/manifest/rollout/pause.html +0 -28
- package/src/manifest/rollout/resume.controller.ts +0 -66
- package/src/manifest/rollout/resume.html +0 -28
- package/src/manifest/rollout/undo.controller.ts +0 -74
- package/src/manifest/rollout/undo.html +0 -44
- package/src/manifest/scale/scale.controller.ts +0 -75
- package/src/manifest/scale/scale.html +0 -32
- package/src/manifest/scale/scaleSettingsForm.component.ts +0 -11
- package/src/manifest/selector/selector.component.ts +0 -17
- package/src/manifest/status/condition.component.ts +0 -12
- package/src/manifest/status/status.component.ts +0 -11
- package/src/rawResource/rawResource.module.ts +0 -8
- package/src/reactShims/index.ts +0 -1
- package/src/reactShims/kubernetes.react.injector.ts +0 -30
- package/src/reactShims/kubernetes.react.module.ts +0 -12
- package/src/securityGroup/details/details.controller.ts +0 -118
- package/src/securityGroup/details/details.html +0 -91
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import type { IKubernetesLoadBalancerDetailsSectionProps } from './IKubernetesLoadBalancerDetailsSectionProps';
|
|
3
|
-
import {
|
|
3
|
+
import { AnnotationCustomSections } from '../../../manifest/AnnotationCustomSections';
|
|
4
4
|
|
|
5
5
|
export function LoadBalancerAnnotationCustomSection({ loadBalancer }: IKubernetesLoadBalancerDetailsSectionProps) {
|
|
6
6
|
const { manifest } = loadBalancer;
|
|
7
|
-
|
|
8
|
-
return <KubernetesAnnotationCustomSections manifest={manifest.manifest} resource={loadBalancer} />;
|
|
7
|
+
return <AnnotationCustomSections manifest={manifest.manifest} resource={loadBalancer} />;
|
|
9
8
|
}
|
|
@@ -5,7 +5,7 @@ import type { IInstanceCounts, IServerGroup } from '@spinnaker/core';
|
|
|
5
5
|
import type { IKubernetesLoadBalancer } from '../interfaces';
|
|
6
6
|
|
|
7
7
|
export class KubernetesLoadBalancerTransformer {
|
|
8
|
-
public normalizeLoadBalancer(loadBalancer: IKubernetesLoadBalancer):
|
|
8
|
+
public normalizeLoadBalancer(loadBalancer: IKubernetesLoadBalancer): Promise<IKubernetesLoadBalancer> {
|
|
9
9
|
loadBalancer.provider = loadBalancer.type;
|
|
10
10
|
loadBalancer.instances = [];
|
|
11
11
|
loadBalancer.instanceCounts = this.buildInstanceCounts(loadBalancer.serverGroups);
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { mount } from 'enzyme';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
import { AnnotationCustomSections } from './AnnotationCustomSections';
|
|
5
|
+
|
|
6
|
+
describe('<AnnotationCustomSections />', () => {
|
|
7
|
+
it('renders text annotations under section groups', () => {
|
|
8
|
+
const wrapper = mount(
|
|
9
|
+
<AnnotationCustomSections
|
|
10
|
+
manifest={manifestWithAnnotations({
|
|
11
|
+
'deployment-details.details.spinnaker.io/owner-name': 'Delivery Platform',
|
|
12
|
+
'support.details.spinnaker.io/contact': 'On call',
|
|
13
|
+
})}
|
|
14
|
+
resource={resource()}
|
|
15
|
+
/>,
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
expect(wrapper.find('h4').at(0).text()).toContain('deployment details');
|
|
19
|
+
expect(wrapper.find('h4').at(1).text()).toContain('support');
|
|
20
|
+
expect(wrapper.text()).toContain('owner name');
|
|
21
|
+
expect(wrapper.text()).toContain('Delivery Platform');
|
|
22
|
+
expect(wrapper.text()).toContain('contact');
|
|
23
|
+
expect(wrapper.text()).toContain('On call');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('sanitizes HTML annotations while enforcing rel on targeted links', () => {
|
|
27
|
+
const wrapper = mount(
|
|
28
|
+
<AnnotationCustomSections
|
|
29
|
+
manifest={manifestWithAnnotations({
|
|
30
|
+
'links.details.html.spinnaker.io/runbook':
|
|
31
|
+
'<a href="https://example.com/runbook" target="_blank" onclick="alert(1)">Runbook</a><a href="https://example.com/dashboard" target="dashboard">Dashboard</a><script>alert(2)</script>',
|
|
32
|
+
})}
|
|
33
|
+
resource={resource()}
|
|
34
|
+
/>,
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
const html = wrapper.html();
|
|
38
|
+
expect((html.match(/target="_blank"/g) || []).length).toBe(2);
|
|
39
|
+
expect(html).not.toContain('target="dashboard"');
|
|
40
|
+
expect(html).toContain('rel="noopener noreferrer"');
|
|
41
|
+
expect((html.match(/rel="noopener noreferrer"/g) || []).length).toBe(2);
|
|
42
|
+
expect(html).toContain('Runbook');
|
|
43
|
+
expect(html).toContain('Dashboard');
|
|
44
|
+
expect(html).not.toContain('onclick');
|
|
45
|
+
expect(html).not.toContain('<script>');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('escapes interpolated resource values in HTML annotations', () => {
|
|
49
|
+
const wrapper = mount(
|
|
50
|
+
<AnnotationCustomSections
|
|
51
|
+
manifest={manifestWithAnnotations({
|
|
52
|
+
'summary.details.html.spinnaker.io/name': '<strong>Owner:</strong> {{ displayName }}',
|
|
53
|
+
})}
|
|
54
|
+
resource={resource({
|
|
55
|
+
displayName: '<a href="https://evil.example" target="_blank">Evil</a>',
|
|
56
|
+
})}
|
|
57
|
+
/>,
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
const html = wrapper.html();
|
|
61
|
+
expect(html).toContain('<strong>Owner:</strong>');
|
|
62
|
+
expect(html).toContain('<a href="https://evil.example" target="_blank">Evil</a>');
|
|
63
|
+
expect(wrapper.find('a[href="https://evil.example"]').exists()).toBe(false);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('interpolates path placeholders against resource and manifest values', () => {
|
|
67
|
+
const wrapper = mount(
|
|
68
|
+
<AnnotationCustomSections
|
|
69
|
+
manifest={manifestWithAnnotations({
|
|
70
|
+
'summary.details.spinnaker.io/name': '{{ displayName }} in {{ namespace }} from {{ manifest.metadata.name }}',
|
|
71
|
+
})}
|
|
72
|
+
resource={resource({ displayName: 'frontend', namespace: 'production' })}
|
|
73
|
+
/>,
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
expect(wrapper.text()).toContain('frontend in production from frontend-manifest');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('leaves non-path expression placeholders unresolved', () => {
|
|
80
|
+
const wrapper = mount(
|
|
81
|
+
<AnnotationCustomSections
|
|
82
|
+
manifest={manifestWithAnnotations({
|
|
83
|
+
'summary.details.spinnaker.io/name': '{{ displayName | uppercase }} {{ getDisplayName() }}',
|
|
84
|
+
})}
|
|
85
|
+
resource={resource({
|
|
86
|
+
displayName: 'frontend',
|
|
87
|
+
getDisplayName: () => 'frontend',
|
|
88
|
+
})}
|
|
89
|
+
/>,
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
expect(wrapper.text()).toContain('{{ displayName | uppercase }} {{ getDisplayName() }}');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('leaves prototype-chain path placeholders unresolved', () => {
|
|
96
|
+
const wrapper = mount(
|
|
97
|
+
<AnnotationCustomSections
|
|
98
|
+
manifest={manifestWithAnnotations({
|
|
99
|
+
'summary.details.spinnaker.io/name':
|
|
100
|
+
'{{ constructor }} {{ resource.constructor }} {{ resource["constructor"] }} {{ resource.__proto__ }} {{ resource.prototype }} {{ displayName }}',
|
|
101
|
+
})}
|
|
102
|
+
resource={resource({ displayName: 'frontend' })}
|
|
103
|
+
/>,
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
const text = wrapper.text();
|
|
107
|
+
expect(text).toContain(
|
|
108
|
+
'{{ constructor }} {{ resource.constructor }} {{ resource["constructor"] }} {{ resource.__proto__ }} {{ resource.prototype }} frontend',
|
|
109
|
+
);
|
|
110
|
+
expect(text).not.toContain('function Object');
|
|
111
|
+
expect(text).not.toContain('[object Object]');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('treats inherited function path values as missing', () => {
|
|
115
|
+
const wrapper = mount(
|
|
116
|
+
<AnnotationCustomSections
|
|
117
|
+
manifest={manifestWithAnnotations({
|
|
118
|
+
'summary.details.spinnaker.io/name': 'before {{ toString }} {{ resource.toString }} after {{ displayName }}',
|
|
119
|
+
})}
|
|
120
|
+
resource={resource({ displayName: 'frontend' })}
|
|
121
|
+
/>,
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
const text = wrapper.text();
|
|
125
|
+
expect(text).toContain('before after frontend');
|
|
126
|
+
expect(text).not.toContain('function toString');
|
|
127
|
+
expect(text).not.toContain('[native code]');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('strips target attributes from sanitized non-anchor elements', () => {
|
|
131
|
+
const wrapper = mount(
|
|
132
|
+
<AnnotationCustomSections
|
|
133
|
+
manifest={manifestWithAnnotations({
|
|
134
|
+
'links.details.html.spinnaker.io/runbook':
|
|
135
|
+
'<div target="_blank">Container</div><a href="https://example.com/runbook" target="_blank">Runbook</a>',
|
|
136
|
+
})}
|
|
137
|
+
resource={resource()}
|
|
138
|
+
/>,
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
const html = wrapper.html();
|
|
142
|
+
expect(html).toContain('<div>Container</div>');
|
|
143
|
+
expect(html).toContain('target="_blank"');
|
|
144
|
+
expect(html).toContain('rel="noopener noreferrer"');
|
|
145
|
+
expect(html).not.toContain('<div target="_blank">');
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('renders text entries before HTML entries within the same section', () => {
|
|
149
|
+
const wrapper = mount(
|
|
150
|
+
<AnnotationCustomSections
|
|
151
|
+
manifest={manifestWithAnnotations({
|
|
152
|
+
'runbook.details.html.spinnaker.io/link': '<a href="https://example.com" target="_blank">Runbook</a>',
|
|
153
|
+
'runbook.details.spinnaker.io/summary': 'Read this first',
|
|
154
|
+
})}
|
|
155
|
+
resource={resource()}
|
|
156
|
+
/>,
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
const sectionText = wrapper.find('.content-body').at(0).text();
|
|
160
|
+
expect(sectionText.indexOf('Read this first')).toBeLessThan(sectionText.indexOf('Runbook'));
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const manifestWithAnnotations = (annotations: { [key: string]: string }) => ({
|
|
165
|
+
metadata: {
|
|
166
|
+
name: 'frontend-manifest',
|
|
167
|
+
annotations,
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const resource = (overrides: any = {}) =>
|
|
172
|
+
({
|
|
173
|
+
apiVersion: 'apps/v1',
|
|
174
|
+
displayName: 'frontend',
|
|
175
|
+
kind: 'Deployment',
|
|
176
|
+
namespace: 'default',
|
|
177
|
+
...overrides,
|
|
178
|
+
} as any);
|
|
@@ -1,6 +1,183 @@
|
|
|
1
|
-
import
|
|
1
|
+
import DOMPurify from 'dompurify';
|
|
2
|
+
import { get, partition } from 'lodash';
|
|
3
|
+
import React from 'react';
|
|
4
|
+
|
|
5
|
+
import { CollapsibleSection } from '@spinnaker/core';
|
|
6
|
+
|
|
7
|
+
const PATH_EXPRESSION_PATTERN = /^[A-Za-z_$][\w$]*(?:(?:\.[A-Za-z_$][\w$]*)|\[(?:\d+|'[^']+'|"[^"]+")\])*$/;
|
|
8
|
+
const BLOCKED_PATH_SEGMENT_PATTERN = /(?:^|\.)(?:constructor|__proto__|prototype)(?=\.|\[|$)|\[(?:"(?:constructor|__proto__|prototype)"|'(?:constructor|__proto__|prototype)')\]/;
|
|
2
9
|
|
|
3
10
|
export interface IAnnotationCustomSectionsProps {
|
|
4
11
|
manifest: any;
|
|
5
|
-
resource:
|
|
12
|
+
resource: any;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface IAnnotationsMap {
|
|
16
|
+
[key: string]: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface ICustomSection {
|
|
20
|
+
title: string;
|
|
21
|
+
key: string;
|
|
22
|
+
isHtml: boolean;
|
|
23
|
+
content: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface ICustomSectionMap {
|
|
27
|
+
[key: string]: ICustomSection[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface IParsedAnnotationKey {
|
|
31
|
+
title: string;
|
|
32
|
+
key: string;
|
|
33
|
+
isHtml: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function AnnotationCustomSections({ manifest, resource }: IAnnotationCustomSectionsProps) {
|
|
37
|
+
const customSections = buildAnnotationSections(manifest?.metadata?.annotations, resource, manifest);
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<>
|
|
41
|
+
{Object.entries(customSections).map(([section, entries]) => (
|
|
42
|
+
<CollapsibleSection key={section} heading={section} defaultExpanded={true}>
|
|
43
|
+
{entries.map((entry, index) =>
|
|
44
|
+
entry.isHtml ? (
|
|
45
|
+
<div
|
|
46
|
+
key={`${entry.key}-${index}`}
|
|
47
|
+
dangerouslySetInnerHTML={{
|
|
48
|
+
__html: entry.content,
|
|
49
|
+
}}
|
|
50
|
+
/>
|
|
51
|
+
) : (
|
|
52
|
+
<div key={`${entry.key}-${index}`}>
|
|
53
|
+
{entry.key && <span style={{ fontWeight: 'bold' }}>{entry.key}</span>} <span>{entry.content}</span>
|
|
54
|
+
</div>
|
|
55
|
+
),
|
|
56
|
+
)}
|
|
57
|
+
</CollapsibleSection>
|
|
58
|
+
))}
|
|
59
|
+
</>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function buildAnnotationSections(
|
|
64
|
+
annotations: IAnnotationsMap | undefined,
|
|
65
|
+
resource: any,
|
|
66
|
+
manifest: any,
|
|
67
|
+
): ICustomSectionMap {
|
|
68
|
+
if (annotations == null) {
|
|
69
|
+
return {};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const customSections = Object.keys(annotations).reduce((memo: ICustomSectionMap, annotationKey: string) => {
|
|
73
|
+
const entry = annotationToEntry(annotations[annotationKey], annotationKey, resource, manifest);
|
|
74
|
+
if (entry != null && entry.title) {
|
|
75
|
+
memo[entry.title] = memo[entry.title] || [];
|
|
76
|
+
memo[entry.title].push(entry);
|
|
77
|
+
}
|
|
78
|
+
return memo;
|
|
79
|
+
}, {});
|
|
80
|
+
|
|
81
|
+
return Object.keys(customSections).reduce((memo: ICustomSectionMap, sectionTitle: string) => {
|
|
82
|
+
const [htmlEntries, textEntries] = partition(customSections[sectionTitle], (section) => section.isHtml);
|
|
83
|
+
memo[sectionTitle] = textEntries.concat(htmlEntries);
|
|
84
|
+
return memo;
|
|
85
|
+
}, {});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function annotationToEntry(
|
|
89
|
+
annotationContent: string,
|
|
90
|
+
annotationKey: string,
|
|
91
|
+
resource: any,
|
|
92
|
+
manifest: any,
|
|
93
|
+
): ICustomSection | null {
|
|
94
|
+
const parsed = parseAnnotationKey(annotationKey);
|
|
95
|
+
if (parsed == null) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const content =
|
|
100
|
+
resource && annotationContent.includes('{{')
|
|
101
|
+
? interpolateAnnotation(annotationContent, resource, manifest, parsed.isHtml)
|
|
102
|
+
: annotationContent;
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
title: parsed.title.replace(/-/g, ' ').trim(),
|
|
106
|
+
key: parsed.key.replace(/-/g, ' ').trim(),
|
|
107
|
+
content: parsed.isHtml ? sanitizeContent(content) : content,
|
|
108
|
+
isHtml: parsed.isHtml,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function parseAnnotationKey(annotationKey: string): IParsedAnnotationKey | null {
|
|
113
|
+
const keyParts = /([^.]+)\.details\.(html\.)?spinnaker\.io(?:\/(.*))?/.exec(annotationKey);
|
|
114
|
+
if (keyParts == null || keyParts.length !== 4) {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
title: keyParts[1] || '',
|
|
119
|
+
key: keyParts[3] || '',
|
|
120
|
+
isHtml: !!keyParts[2],
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function interpolateAnnotation(content: string, resource: any, manifest: any, escapeValues = false): string {
|
|
125
|
+
const context = { ...resource, resource, manifest };
|
|
126
|
+
return content.replace(/{{\s*([^}]+?)\s*}}/g, (match, path: string) => {
|
|
127
|
+
const expression = path.trim();
|
|
128
|
+
if (!isPathExpression(expression)) {
|
|
129
|
+
return match;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const value = get(context, expression);
|
|
133
|
+
if (value == null || typeof value === 'function') {
|
|
134
|
+
return '';
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const stringValue = String(value);
|
|
138
|
+
return escapeValues ? escapeHtml(stringValue) : stringValue;
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function isPathExpression(expression: string): boolean {
|
|
143
|
+
return PATH_EXPRESSION_PATTERN.test(expression) && !BLOCKED_PATH_SEGMENT_PATTERN.test(expression);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function escapeHtml(value: string): string {
|
|
147
|
+
return value.replace(/[&<>"']/g, (character) => {
|
|
148
|
+
switch (character) {
|
|
149
|
+
case '&':
|
|
150
|
+
return '&';
|
|
151
|
+
case '<':
|
|
152
|
+
return '<';
|
|
153
|
+
case '>':
|
|
154
|
+
return '>';
|
|
155
|
+
case '"':
|
|
156
|
+
return '"';
|
|
157
|
+
case "'":
|
|
158
|
+
return ''';
|
|
159
|
+
default:
|
|
160
|
+
return character;
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function sanitizeContent(unsanitized: string): string {
|
|
166
|
+
const sanitized = DOMPurify.sanitize(unsanitized, {
|
|
167
|
+
ADD_ATTR: ['target'],
|
|
168
|
+
});
|
|
169
|
+
const container = document.createElement('div');
|
|
170
|
+
container.innerHTML = sanitized;
|
|
171
|
+
container.querySelectorAll('[target]').forEach((element) => {
|
|
172
|
+
if (element.tagName !== 'A') {
|
|
173
|
+
element.removeAttribute('target');
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const rel = new Set((element.getAttribute('rel') || '').split(/\s+/).filter(Boolean));
|
|
178
|
+
rel.add('noopener');
|
|
179
|
+
rel.add('noreferrer');
|
|
180
|
+
element.setAttribute('rel', Array.from(rel).join(' '));
|
|
181
|
+
});
|
|
182
|
+
return container.innerHTML;
|
|
6
183
|
}
|
|
@@ -13,7 +13,7 @@ interface IManifestKindSearchResults {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
export class ManifestKindSearchService {
|
|
16
|
-
public static search(kind: string, namespace: string, account: string):
|
|
16
|
+
public static search(kind: string, namespace: string, account: string): Promise<IManifestKindSearchResults[]> {
|
|
17
17
|
return SearchService.search<IManifestKindSearchResults>({
|
|
18
18
|
q: namespace,
|
|
19
19
|
type: kind,
|
|
@@ -1,10 +1,7 @@
|
|
|
1
|
-
import { module } from 'angular';
|
|
2
1
|
import React from 'react';
|
|
3
2
|
import { MenuItem } from 'react-bootstrap';
|
|
4
|
-
import { react2angular } from 'react2angular';
|
|
5
3
|
|
|
6
4
|
import type { Application } from '@spinnaker/core';
|
|
7
|
-
import { withErrorBoundary } from '@spinnaker/core';
|
|
8
5
|
import { useModal } from '@spinnaker/core';
|
|
9
6
|
|
|
10
7
|
import { DeleteModal } from './DeleteModal';
|
|
@@ -26,9 +23,3 @@ export function Delete(props: IDeleteProps) {
|
|
|
26
23
|
</>
|
|
27
24
|
);
|
|
28
25
|
}
|
|
29
|
-
|
|
30
|
-
export const KUBERNETES_DELETE = 'spinnaker.kubernetes.delete';
|
|
31
|
-
module(KUBERNETES_DELETE, []).component(
|
|
32
|
-
'kubernetesDelete',
|
|
33
|
-
react2angular(withErrorBoundary(Delete, 'kubernetesDelete'), ['application', 'resource', 'manifestController']),
|
|
34
|
-
);
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { UISref } from '@uirouter/react';
|
|
2
|
-
import { UIRouterContextComponent } from '@uirouter/react-hybrid';
|
|
3
2
|
import type { FormikProps } from 'formik';
|
|
4
3
|
import { Form } from 'formik';
|
|
5
4
|
import React, { useState } from 'react';
|
|
@@ -19,7 +18,7 @@ import {
|
|
|
19
18
|
ValidationMessage,
|
|
20
19
|
} from '@spinnaker/core';
|
|
21
20
|
|
|
22
|
-
import type { IDeleteOptions } from './delete.
|
|
21
|
+
import type { IDeleteOptions } from './delete.types';
|
|
23
22
|
import type { IAnyKubernetesResource } from '../../interfaces';
|
|
24
23
|
import DeleteManifestOptionsForm from '../../pipelines/stages/deleteManifest/DeleteManifestOptionsForm';
|
|
25
24
|
|
|
@@ -76,77 +75,75 @@ export function DeleteModal({
|
|
|
76
75
|
};
|
|
77
76
|
|
|
78
77
|
return (
|
|
79
|
-
<
|
|
80
|
-
<
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
<Modal.
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
>
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
78
|
+
<Modal show={isOpen} onHide={dismissModal}>
|
|
79
|
+
<TaskMonitorWrapper monitor={taskMonitor} />
|
|
80
|
+
<SpinFormik<IKubernetesDeleteValues>
|
|
81
|
+
initialValues={initialValues}
|
|
82
|
+
onSubmit={submit}
|
|
83
|
+
render={(formik) => (
|
|
84
|
+
<>
|
|
85
|
+
<ModalClose dismiss={dismissModal} />
|
|
86
|
+
<Modal.Header>
|
|
87
|
+
<Modal.Title>
|
|
88
|
+
Delete {robotToHuman(resource.name)} in {resource.namespace}
|
|
89
|
+
</Modal.Title>
|
|
90
|
+
</Modal.Header>
|
|
91
|
+
{manifestController && (
|
|
92
|
+
<div className="alert alert-warning">
|
|
93
|
+
Manifest is controlled by{' '}
|
|
94
|
+
<UISref
|
|
95
|
+
to="^.serverGroupManager"
|
|
96
|
+
params={{
|
|
97
|
+
accountId: resource.account,
|
|
98
|
+
region: resource.region,
|
|
99
|
+
serverGroupManager: manifestController,
|
|
100
|
+
provider: 'kubernetes',
|
|
101
|
+
}}
|
|
102
|
+
>
|
|
103
|
+
<a> {robotToHuman(manifestController)} </a>
|
|
104
|
+
</UISref>{' '}
|
|
105
|
+
and may be recreated after deletion.
|
|
106
|
+
</div>
|
|
107
|
+
)}
|
|
108
|
+
<Modal.Body>
|
|
109
|
+
<Form className="form-horizontal">
|
|
110
|
+
<DeleteManifestOptionsForm
|
|
111
|
+
onOptionsChange={(options: IDeleteOptions) => onOptionChange(formik, options)}
|
|
112
|
+
options={{
|
|
113
|
+
gracePeriodSeconds: formik.values.gracePeriodSeconds,
|
|
114
|
+
cascading: formik.values.cascading,
|
|
115
|
+
}}
|
|
116
|
+
/>
|
|
117
|
+
<TaskReason reason={formik.values.reason} onChange={(val) => formik.setFieldValue('reason', val)} />
|
|
118
|
+
</Form>
|
|
119
|
+
{formik.status?.error && (
|
|
120
|
+
<div className="sp-margin-xl-top">
|
|
121
|
+
<ValidationMessage
|
|
122
|
+
type="error"
|
|
123
|
+
message={
|
|
124
|
+
<span className="flex-container-v">
|
|
125
|
+
<span className="text-bold">Something went wrong:</span>
|
|
126
|
+
{formik.status.error.message && <span>{formik.status.error.message}</span>}
|
|
127
|
+
</span>
|
|
128
|
+
}
|
|
129
|
+
/>
|
|
108
130
|
</div>
|
|
109
131
|
)}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
<span className="flex-container-v">
|
|
127
|
-
<span className="text-bold">Something went wrong:</span>
|
|
128
|
-
{formik.status.error.message && <span>{formik.status.error.message}</span>}
|
|
129
|
-
</span>
|
|
130
|
-
}
|
|
131
|
-
/>
|
|
132
|
-
</div>
|
|
133
|
-
)}
|
|
134
|
-
</Modal.Body>
|
|
135
|
-
<Modal.Footer>
|
|
136
|
-
<UserVerification account={resource.account} onValidChange={setVerified} />
|
|
137
|
-
<Button onClick={dismissModal}>Cancel</Button>
|
|
138
|
-
<SubmitButton
|
|
139
|
-
onClick={() => submit(formik.values)}
|
|
140
|
-
isDisabled={!formik.isValid || formik.isSubmitting || !verified}
|
|
141
|
-
isFormSubmit={true}
|
|
142
|
-
submitting={formik.isSubmitting}
|
|
143
|
-
label={`Delete ${resource.name}`}
|
|
144
|
-
/>
|
|
145
|
-
</Modal.Footer>
|
|
146
|
-
</>
|
|
147
|
-
)}
|
|
148
|
-
/>
|
|
149
|
-
</Modal>
|
|
150
|
-
</UIRouterContextComponent>
|
|
132
|
+
</Modal.Body>
|
|
133
|
+
<Modal.Footer>
|
|
134
|
+
<UserVerification account={resource.account} onValidChange={setVerified} />
|
|
135
|
+
<Button onClick={dismissModal}>Cancel</Button>
|
|
136
|
+
<SubmitButton
|
|
137
|
+
onClick={() => submit(formik.values)}
|
|
138
|
+
isDisabled={!formik.isValid || formik.isSubmitting || !verified}
|
|
139
|
+
isFormSubmit={true}
|
|
140
|
+
submitting={formik.isSubmitting}
|
|
141
|
+
label={`Delete ${resource.name}`}
|
|
142
|
+
/>
|
|
143
|
+
</Modal.Footer>
|
|
144
|
+
</>
|
|
145
|
+
)}
|
|
146
|
+
/>
|
|
147
|
+
</Modal>
|
|
151
148
|
);
|
|
152
149
|
}
|
package/src/manifest/index.ts
CHANGED
|
@@ -2,9 +2,6 @@ export * from './manifest.service';
|
|
|
2
2
|
export * from './manifestCommandBuilder.service';
|
|
3
3
|
export * from './IManifestCoordinates';
|
|
4
4
|
export * from './artifact/ManifestArtifact';
|
|
5
|
-
export * from './delete/delete.
|
|
6
|
-
export * from './
|
|
7
|
-
export * from './
|
|
8
|
-
export * from './scale/scale.controller';
|
|
9
|
-
export * from './status/condition.component';
|
|
10
|
-
export * from './status/status.component';
|
|
5
|
+
export * from './delete/delete.types';
|
|
6
|
+
export * from './scale/ScaleSettingsForm';
|
|
7
|
+
export * from './status/ManifestCondition';
|
|
@@ -10,6 +10,23 @@ export interface IStageManifest {
|
|
|
10
10
|
};
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
// A manifest that the artifact-store entity storage feature has replaced with a
|
|
14
|
+
// reference, rather than expanding it back into a real manifest before deck ever
|
|
15
|
+
// sees it (e.g. gate isn't configured to expand these on read).
|
|
16
|
+
export interface IStoredManifestReference {
|
|
17
|
+
type: string;
|
|
18
|
+
reference: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function isStoredManifestReference(manifest: unknown): manifest is IStoredManifestReference {
|
|
22
|
+
return (
|
|
23
|
+
!!manifest &&
|
|
24
|
+
typeof manifest === 'object' &&
|
|
25
|
+
(manifest as IStoredManifestReference).type === 'remote/map/base64' &&
|
|
26
|
+
typeof (manifest as IStoredManifestReference).reference === 'string'
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
13
30
|
export interface IManifestParams {
|
|
14
31
|
account: string;
|
|
15
32
|
location: string;
|
|
@@ -21,7 +38,7 @@ export type IManifestCallback = (manifest: IManifest) => void;
|
|
|
21
38
|
export class KubernetesManifestService {
|
|
22
39
|
public static subscribe(app: Application, params: IManifestParams, fn: IManifestCallback): () => void {
|
|
23
40
|
KubernetesManifestService.updateManifest(params, fn);
|
|
24
|
-
return app.onRefresh(
|
|
41
|
+
return app.onRefresh(() => KubernetesManifestService.updateManifest(params, fn));
|
|
25
42
|
}
|
|
26
43
|
|
|
27
44
|
private static updateManifest(params: IManifestParams, fn: IManifestCallback) {
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { AccountService } from '@spinnaker/core';
|
|
2
|
+
|
|
3
|
+
import { KubernetesManifestCommandBuilder } from './manifestCommandBuilder.service';
|
|
4
|
+
|
|
5
|
+
describe('KubernetesManifestCommandBuilder', () => {
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
spyOn(AccountService, 'getArtifactAccounts').and.returnValue(
|
|
8
|
+
Promise.resolve([{ name: 'artifact-account' }]) as any,
|
|
9
|
+
);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('uses the source account when it is available', async () => {
|
|
13
|
+
spyOn(AccountService, 'getAllAccountDetailsForProvider').and.returnValue(
|
|
14
|
+
Promise.resolve([{ name: 'fallback-account' }, { name: 'source-account' }]) as any,
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
const result = await KubernetesManifestCommandBuilder.buildNewManifestCommand(
|
|
18
|
+
{ name: 'frontend' } as any,
|
|
19
|
+
{ kind: 'Deployment' },
|
|
20
|
+
undefined,
|
|
21
|
+
'source-account',
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
expect(result.command.account).toBe('source-account');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('falls back to the first account when the source account is unavailable', async () => {
|
|
28
|
+
spyOn(AccountService, 'getAllAccountDetailsForProvider').and.returnValue(
|
|
29
|
+
Promise.resolve([{ name: 'fallback-account' }]) as any,
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
const result = await KubernetesManifestCommandBuilder.buildNewManifestCommand(
|
|
33
|
+
{ name: 'frontend' } as any,
|
|
34
|
+
{ kind: 'Deployment' },
|
|
35
|
+
undefined,
|
|
36
|
+
'missing-account',
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
expect(result.command.account).toBe('fallback-account');
|
|
40
|
+
});
|
|
41
|
+
});
|