@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.
Files changed (158) hide show
  1. package/dist/index.d.ts +1 -2
  2. package/dist/index.js +1 -1
  3. package/dist/index.js.map +1 -1
  4. package/dist/instance/details/KubernetesInstanceDetails.d.ts +54 -0
  5. package/dist/instance/details/kubernetesInstanceDetails.utils.d.ts +13 -0
  6. package/dist/instance/index.d.ts +1 -1
  7. package/dist/kubernetes.module.d.ts +5 -1
  8. package/dist/loadBalancer/transformer.d.ts +1 -1
  9. package/dist/manifest/AnnotationCustomSections.d.ts +3 -2
  10. package/dist/manifest/ManifestKindSearch.d.ts +1 -1
  11. package/dist/manifest/delete/Delete.d.ts +0 -1
  12. package/dist/manifest/delete/DeleteModal.d.ts +1 -1
  13. package/dist/manifest/delete/delete.types.d.ts +4 -0
  14. package/dist/manifest/index.d.ts +3 -6
  15. package/dist/manifest/manifest.service.d.ts +5 -0
  16. package/dist/manifest/manifestCommandBuilder.service.d.ts +1 -1
  17. package/dist/manifest/rollout/PauseRollout.d.ts +0 -1
  18. package/dist/manifest/rollout/ResumeRollout.d.ts +0 -1
  19. package/dist/manifest/rollout/RollingRestart.d.ts +4 -4
  20. package/dist/manifest/rollout/UndoRollout.d.ts +0 -1
  21. package/dist/manifest/scale/Scale.d.ts +1 -2
  22. package/dist/manifest/scale/ScaleModal.d.ts +3 -3
  23. package/dist/manifest/scale/ScaleSettingsForm.d.ts +7 -1
  24. package/dist/manifest/selector/ManifestSelector.d.ts +1 -1
  25. package/dist/manifest/traffic/ManifestTrafficService.d.ts +2 -2
  26. package/dist/pipelines/stages/deleteManifest/DeleteManifestOptionsForm.d.ts +1 -1
  27. package/dist/pipelines/stages/deployManifest/manifestStatus/DeployStatus.d.ts +1 -0
  28. package/dist/pipelines/stages/deployManifest/manifestStatus/ManifestDetailsLink.d.ts +6 -3
  29. package/dist/pipelines/stages/traffic/disableManifest.stage.d.ts +2 -1
  30. package/dist/pipelines/stages/traffic/enableManifest.stage.d.ts +2 -1
  31. package/dist/rawResource/index.d.ts +3 -1
  32. package/dist/rawResource/rawResource.dataSource.d.ts +1 -1
  33. package/dist/rawResource/rawResource.states.d.ts +4 -1
  34. package/dist/resources/resources.state.d.ts +2 -1
  35. package/dist/securityGroup/details/KubernetesSecurityGroupDetails.d.ts +49 -0
  36. package/dist/securityGroup/index.d.ts +3 -1
  37. package/dist/securityGroup/transformer.d.ts +4 -1
  38. package/dist/serverGroup/serverGroupCommandBuilder.d.ts +1 -1
  39. package/dist/serverGroup/serverGroupTransformer.d.ts +1 -1
  40. package/dist/serverGroupManager/details/ServerGroupManagerDetails.d.ts +7 -3
  41. package/package.json +3 -12
  42. package/src/help/kubernetes.help.ts +2 -0
  43. package/src/index.ts +2 -2
  44. package/src/instance/details/KubernetesInstanceDetails.spec.tsx +451 -0
  45. package/src/instance/details/KubernetesInstanceDetails.tsx +342 -0
  46. package/src/instance/details/kubernetesInstanceDetails.utils.ts +77 -0
  47. package/src/instance/index.ts +1 -1
  48. package/src/kubernetes.module.spec.ts +25 -0
  49. package/src/kubernetes.module.ts +23 -87
  50. package/src/loadBalancer/details/KubernetesLoadBalancerActions.tsx +30 -35
  51. package/src/loadBalancer/details/sections/LoadBalancerAnnotationCustomSection.tsx +2 -3
  52. package/src/loadBalancer/transformer.ts +1 -1
  53. package/src/manifest/AnnotationCustomSections.spec.tsx +178 -0
  54. package/src/manifest/AnnotationCustomSections.tsx +179 -2
  55. package/src/manifest/ManifestKindSearch.tsx +1 -1
  56. package/src/manifest/delete/Delete.tsx +0 -9
  57. package/src/manifest/delete/DeleteModal.tsx +69 -72
  58. package/src/manifest/delete/delete.types.ts +4 -0
  59. package/src/manifest/index.ts +3 -6
  60. package/src/manifest/manifest.service.ts +18 -1
  61. package/src/manifest/manifestCommandBuilder.service.spec.ts +41 -0
  62. package/src/manifest/manifestCommandBuilder.service.ts +45 -59
  63. package/src/manifest/rollout/PauseRollout.tsx +1 -9
  64. package/src/manifest/rollout/ResumeRollout.tsx +2 -10
  65. package/src/manifest/rollout/RollingRestart.tsx +16 -22
  66. package/src/manifest/rollout/UndoRollout.tsx +1 -10
  67. package/src/manifest/rollout/UndoRolloutModal.tsx +5 -1
  68. package/src/manifest/scale/Scale.tsx +12 -11
  69. package/src/manifest/scale/ScaleModal.tsx +43 -31
  70. package/src/manifest/scale/ScaleSettingsForm.tsx +8 -1
  71. package/src/manifest/selector/ManifestSelector.spec.tsx +89 -46
  72. package/src/manifest/selector/ManifestSelector.tsx +4 -5
  73. package/src/manifest/traffic/ManifestTrafficService.ts +2 -2
  74. package/src/manifest/wizard/ManifestWizard.tsx +1 -1
  75. package/src/pipelines/stages/deleteManifest/DeleteManifestOptionsForm.tsx +1 -1
  76. package/src/pipelines/stages/deleteManifest/DeleteManifestStageConfig.tsx +1 -1
  77. package/src/pipelines/stages/deployManifest/DeployManifestStageForm.tsx +16 -0
  78. package/src/pipelines/stages/deployManifest/ManifestCopier.spec.ts +52 -77
  79. package/src/pipelines/stages/deployManifest/ManifestDeploymentOptions.spec.tsx +2 -0
  80. package/src/pipelines/stages/deployManifest/manifestStatus/DeployStatus.tsx +39 -4
  81. package/src/pipelines/stages/deployManifest/manifestStatus/ManifestDetailsLink.spec.tsx +35 -0
  82. package/src/pipelines/stages/deployManifest/manifestStatus/ManifestDetailsLink.tsx +10 -5
  83. package/src/pipelines/stages/traffic/ManifestTrafficStageRegistration.spec.ts +34 -0
  84. package/src/pipelines/stages/traffic/disableManifest.stage.ts +17 -19
  85. package/src/pipelines/stages/traffic/enableManifest.stage.ts +17 -19
  86. package/src/rawResource/index.ts +18 -1
  87. package/src/rawResource/rawResource.dataSource.ts +5 -8
  88. package/src/rawResource/rawResource.states.ts +50 -51
  89. package/src/resources/resources.state.spec.ts +138 -0
  90. package/src/resources/resources.state.ts +30 -35
  91. package/src/securityGroup/details/KubernetesSecurityGroupDetails.spec.tsx +292 -0
  92. package/src/securityGroup/details/KubernetesSecurityGroupDetails.tsx +304 -0
  93. package/src/securityGroup/index.ts +3 -1
  94. package/src/securityGroup/transformer.ts +2 -10
  95. package/src/serverGroup/details/KubernetesServerGroupActions.tsx +31 -61
  96. package/src/serverGroup/details/kubernetesServerGroupDetailsGetter.ts +1 -1
  97. package/src/serverGroup/details/sections/ServerGroupAnnotationCustomSection.tsx +2 -3
  98. package/src/serverGroup/serverGroupCommandBuilder.ts +1 -1
  99. package/src/serverGroup/serverGroupTransformer.ts +1 -1
  100. package/src/serverGroupManager/details/ServerGroupManagerActions.tsx +48 -128
  101. package/src/serverGroupManager/details/ServerGroupManagerDetails.spec.ts +12 -0
  102. package/src/serverGroupManager/details/ServerGroupManagerDetails.tsx +19 -8
  103. package/src/serverGroupManager/details/sections/ServerGroupManagerAnnotationCustomSection.tsx +2 -3
  104. package/src/serverGroupManager/details/useKubernetesServerGroupManagerDetails.spec.tsx +112 -0
  105. package/src/serverGroupManager/details/useKubernetesServerGroupManagerDetails.tsx +50 -29
  106. package/dist/instance/details/details.controller.d.ts +0 -1
  107. package/dist/manifest/annotationCustomSections.component.d.ts +0 -3
  108. package/dist/manifest/delete/delete.controller.d.ts +0 -12
  109. package/dist/manifest/delete/deleteOptionsForm.component.d.ts +0 -1
  110. package/dist/manifest/editor/json/jsonEditor.component.d.ts +0 -1
  111. package/dist/manifest/manifestEvents.component.d.ts +0 -1
  112. package/dist/manifest/manifestImageDetails.component.d.ts +0 -1
  113. package/dist/manifest/manifestLabels.component.d.ts +0 -1
  114. package/dist/manifest/manifestQos.component.d.ts +0 -1
  115. package/dist/manifest/manifestResources.component.d.ts +0 -1
  116. package/dist/manifest/rollout/pause.controller.d.ts +0 -1
  117. package/dist/manifest/rollout/resume.controller.d.ts +0 -1
  118. package/dist/manifest/rollout/undo.controller.d.ts +0 -5
  119. package/dist/manifest/scale/scale.controller.d.ts +0 -8
  120. package/dist/manifest/scale/scaleSettingsForm.component.d.ts +0 -1
  121. package/dist/manifest/selector/selector.component.d.ts +0 -1
  122. package/dist/manifest/status/condition.component.d.ts +0 -1
  123. package/dist/manifest/status/status.component.d.ts +0 -1
  124. package/dist/rawResource/rawResource.module.d.ts +0 -1
  125. package/dist/reactShims/index.d.ts +0 -1
  126. package/dist/reactShims/kubernetes.react.injector.d.ts +0 -11
  127. package/dist/reactShims/kubernetes.react.module.d.ts +0 -1
  128. package/dist/securityGroup/details/details.controller.d.ts +0 -1
  129. package/src/instance/details/details.controller.ts +0 -186
  130. package/src/instance/details/details.html +0 -80
  131. package/src/manifest/annotationCustomSections.component.ts +0 -114
  132. package/src/manifest/delete/delete.controller.ts +0 -80
  133. package/src/manifest/delete/delete.html +0 -42
  134. package/src/manifest/delete/deleteOptionsForm.component.ts +0 -52
  135. package/src/manifest/editor/json/jsonEditor.component.ts +0 -12
  136. package/src/manifest/manifestEvents.component.ts +0 -12
  137. package/src/manifest/manifestImageDetails.component.ts +0 -12
  138. package/src/manifest/manifestLabels.component.ts +0 -12
  139. package/src/manifest/manifestQos.component.ts +0 -12
  140. package/src/manifest/manifestResources.component.ts +0 -12
  141. package/src/manifest/rollout/pause.controller.ts +0 -66
  142. package/src/manifest/rollout/pause.html +0 -28
  143. package/src/manifest/rollout/resume.controller.ts +0 -66
  144. package/src/manifest/rollout/resume.html +0 -28
  145. package/src/manifest/rollout/undo.controller.ts +0 -74
  146. package/src/manifest/rollout/undo.html +0 -44
  147. package/src/manifest/scale/scale.controller.ts +0 -75
  148. package/src/manifest/scale/scale.html +0 -32
  149. package/src/manifest/scale/scaleSettingsForm.component.ts +0 -11
  150. package/src/manifest/selector/selector.component.ts +0 -17
  151. package/src/manifest/status/condition.component.ts +0 -12
  152. package/src/manifest/status/status.component.ts +0 -11
  153. package/src/rawResource/rawResource.module.ts +0 -8
  154. package/src/reactShims/index.ts +0 -1
  155. package/src/reactShims/kubernetes.react.injector.ts +0 -30
  156. package/src/reactShims/kubernetes.react.module.ts +0 -12
  157. package/src/securityGroup/details/details.controller.ts +0 -118
  158. 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 { KubernetesReactInjector } from '../../../reactShims';
3
+ import { AnnotationCustomSections } from '../../../manifest/AnnotationCustomSections';
4
4
 
5
5
  export function LoadBalancerAnnotationCustomSection({ loadBalancer }: IKubernetesLoadBalancerDetailsSectionProps) {
6
6
  const { manifest } = loadBalancer;
7
- const { KubernetesAnnotationCustomSections } = KubernetesReactInjector;
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): PromiseLike<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('&lt;a href="https://evil.example" target="_blank"&gt;Evil&lt;/a&gt;');
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 type { IKubernetesResource } from '../interfaces';
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: IKubernetesResource;
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 '&amp;';
151
+ case '<':
152
+ return '&lt;';
153
+ case '>':
154
+ return '&gt;';
155
+ case '"':
156
+ return '&quot;';
157
+ case "'":
158
+ return '&#39;';
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): PromiseLike<IManifestKindSearchResults[]> {
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.controller';
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
- <UIRouterContextComponent>
80
- <Modal show={isOpen} onHide={dismissModal}>
81
- <TaskMonitorWrapper monitor={taskMonitor} />
82
- <SpinFormik<IKubernetesDeleteValues>
83
- initialValues={initialValues}
84
- onSubmit={submit}
85
- render={(formik) => (
86
- <>
87
- <ModalClose dismiss={dismissModal} />
88
- <Modal.Header>
89
- <Modal.Title>
90
- Delete {robotToHuman(resource.name)} in {resource.namespace}
91
- </Modal.Title>
92
- </Modal.Header>
93
- {manifestController && (
94
- <div className="alert alert-warning">
95
- Manifest is controlled by{' '}
96
- <UISref
97
- to="^.serverGroupManager"
98
- params={{
99
- accountId: resource.account,
100
- region: resource.region,
101
- serverGroupManager: manifestController,
102
- provider: 'kubernetes',
103
- }}
104
- >
105
- <a> {robotToHuman(manifestController)} </a>
106
- </UISref>{' '}
107
- and may be recreated after deletion.
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
- <Modal.Body>
111
- <Form className="form-horizontal">
112
- <DeleteManifestOptionsForm
113
- onOptionsChange={(options: IDeleteOptions) => onOptionChange(formik, options)}
114
- options={{
115
- gracePeriodSeconds: formik.values.gracePeriodSeconds,
116
- cascading: formik.values.cascading,
117
- }}
118
- />
119
- <TaskReason reason={formik.values.reason} onChange={(val) => formik.setFieldValue('reason', val)} />
120
- </Form>
121
- {formik.status?.error && (
122
- <div className="sp-margin-xl-top">
123
- <ValidationMessage
124
- type="error"
125
- message={
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
  }
@@ -0,0 +1,4 @@
1
+ export interface IDeleteOptions {
2
+ gracePeriodSeconds?: number;
3
+ cascading: boolean;
4
+ }
@@ -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.controller';
6
- export * from './rollout/pause.controller';
7
- export * from './rollout/undo.controller';
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(null, () => KubernetesManifestService.updateManifest(params, fn));
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
+ });