@spinnaker/amazon 0.13.3 → 0.13.5

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 (23) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/index.js +1 -1
  3. package/dist/index.js.map +1 -1
  4. package/dist/serverGroup/configure/serverGroupConfiguration.service.d.ts +1 -0
  5. package/dist/serverGroup/configure/wizard/instanceType/InstanceTypeWarning.d.ts +6 -0
  6. package/dist/serverGroup/configure/wizard/instanceType/advancedMode/AdvancedModeSelector.d.ts +1 -0
  7. package/dist/serverGroup/configure/wizard/instanceType/advancedMode/InstanceTypeTable.d.ts +3 -0
  8. package/dist/serverGroup/configure/wizard/instanceType/simpleMode/SimpleModeSelector.d.ts +1 -0
  9. package/package.json +3 -3
  10. package/src/pipeline/stages/rollbackCluster/awsRollbackClusterStage.js +3 -1
  11. package/src/pipeline/stages/rollbackCluster/rollbackClusterStage.html +11 -0
  12. package/src/serverGroup/AvailabilityZoneSelector.tsx +6 -7
  13. package/src/serverGroup/configure/serverGroupConfiguration.service.ts +23 -1
  14. package/src/serverGroup/configure/wizard/instanceType/InstanceTypeSelector.tsx +64 -30
  15. package/src/serverGroup/configure/wizard/instanceType/InstanceTypeWarning.tsx +44 -0
  16. package/src/serverGroup/configure/wizard/instanceType/advancedMode/AdvancedModeSelector.tsx +3 -0
  17. package/src/serverGroup/configure/wizard/instanceType/advancedMode/AmazonInstanceTypeInfoRenderer.tsx +14 -14
  18. package/src/serverGroup/configure/wizard/instanceType/advancedMode/InstanceTypeTable.tsx +6 -0
  19. package/src/serverGroup/configure/wizard/instanceType/simpleMode/SimpleModeSelector.tsx +3 -0
  20. package/src/serverGroup/configure/wizard/pages/ServerGroupBasicSettings.tsx +3 -0
  21. package/src/serverGroup/configure/wizard/pages/ServerGroupInstanceType.tsx +13 -3
  22. package/src/serverGroup/details/sections/LaunchConfigDetailsSection.tsx +6 -2
  23. package/src/serverGroup/serverGroup.transformer.ts +4 -0
@@ -4,6 +4,7 @@ import type { IAmazonInstanceType } from '../../instance/awsInstanceType.service
4
4
  export declare type IBlockDeviceMappingSource = 'source' | 'ami' | 'default';
5
5
  export interface IAmazonServerGroupCommandDirty extends IServerGroupCommandDirty {
6
6
  targetGroups?: string[];
7
+ launchTemplateOverridesForInstanceType?: IAmazonInstanceTypeOverride[];
7
8
  }
8
9
  export interface IAmazonServerGroupCommandResult extends IServerGroupCommandResult {
9
10
  dirty: IAmazonServerGroupCommandDirty;
@@ -0,0 +1,6 @@
1
+ /// <reference types="react" />
2
+ import type { IAmazonServerGroupCommandDirty } from '../../serverGroupConfiguration.service';
3
+ export declare function InstanceTypeWarning(props: {
4
+ dirty: IAmazonServerGroupCommandDirty;
5
+ clearWarnings: () => void;
6
+ }): JSX.Element;
@@ -6,6 +6,7 @@ export interface IAdvancedModeSelectorProps {
6
6
  formik: FormikProps<IAmazonServerGroupCommand>;
7
7
  instanceTypeDetails: IAmazonInstanceTypeCategory[];
8
8
  setUnlimitedCpuCredits: (unlimitedCpuCredits: boolean | undefined) => void;
9
+ clearWarnings: () => void;
9
10
  }
10
11
  /**
11
12
  * Note: Launch templates support is expected to be enabled if this component is rendered.
@@ -2,6 +2,7 @@
2
2
  import type { IAmazonInstanceTypeCategory } from '../../../../../instance/awsInstanceType.service';
3
3
  import type { IAmazonInstanceType } from '../../../../../instance/awsInstanceType.service';
4
4
  import type { IAmazonInstanceTypeOverride } from '../../../serverGroupConfiguration.service';
5
+ import type { IAmazonServerGroupCommandViewState } from '../../../serverGroupConfiguration.service';
5
6
  import './advancedMode.less';
6
7
  export interface IInstanceTypeTableProps {
7
8
  currentProfile: string;
@@ -11,5 +12,7 @@ export interface IInstanceTypeTableProps {
11
12
  availableInstanceTypesList: IAmazonInstanceType[];
12
13
  handleInstanceTypesChange: (instanceTypes: IAmazonInstanceTypeOverride[]) => void;
13
14
  setUnlimitedCpuCredits: (unlimitedCpuCredits: boolean | undefined) => void;
15
+ viewState: IAmazonServerGroupCommandViewState;
16
+ clearWarnings: () => void;
14
17
  }
15
18
  export declare function InstanceTypeTable(props: IInstanceTypeTableProps): JSX.Element;
@@ -4,5 +4,6 @@ export interface ISimpleModeSelectorProps {
4
4
  command: IAmazonServerGroupCommand;
5
5
  setUnlimitedCpuCredits: (unlimitedCpuCredits: boolean | undefined) => void;
6
6
  setFieldValue: (field: keyof IAmazonServerGroupCommand, value: any, shouldValidate?: boolean) => void;
7
+ clearWarnings: () => void;
7
8
  }
8
9
  export declare function SimpleModeSelector(props: ISimpleModeSelectorProps): JSX.Element;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@spinnaker/amazon",
3
3
  "license": "Apache-2.0",
4
- "version": "0.13.3",
4
+ "version": "0.13.5",
5
5
  "module": "dist/index.js",
6
6
  "typings": "dist/index.d.ts",
7
7
  "scripts": {
@@ -13,7 +13,7 @@
13
13
  "lib": "npm run build"
14
14
  },
15
15
  "dependencies": {
16
- "@spinnaker/core": "^0.22.2",
16
+ "@spinnaker/core": "^0.23.0",
17
17
  "@uirouter/angularjs": "1.0.26",
18
18
  "@uirouter/core": "6.0.8",
19
19
  "@uirouter/react": "1.0.7",
@@ -55,5 +55,5 @@
55
55
  "shx": "0.3.3",
56
56
  "typescript": "4.3.5"
57
57
  },
58
- "gitHead": "587b5a7a5fe2121cd93bed86e5e83795ee401be0"
58
+ "gitHead": "57a7190ca71a316ec8739dc25f6b5703a14fb3e5"
59
59
  }
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { module } from 'angular';
4
4
 
5
- import { AccountService, Registry } from '@spinnaker/core';
5
+ import { AccountService, Registry, SETTINGS } from '@spinnaker/core';
6
6
 
7
7
  export const AMAZON_PIPELINE_STAGES_ROLLBACKCLUSTER_AWSROLLBACKCLUSTERSTAGE =
8
8
  'spinnaker.amazon.pipeline.stage.rollbackClusterStage';
@@ -60,5 +60,7 @@ module(AMAZON_PIPELINE_STAGES_ROLLBACKCLUSTER_AWSROLLBACKCLUSTERSTAGE, [])
60
60
  if (!stage.regions.length && $scope.application.defaultRegions.aws) {
61
61
  stage.regions.push($scope.application.defaultRegions.aws);
62
62
  }
63
+
64
+ $scope.viewState.dynamicRollbackTimeout = SETTINGS.feature.dynamicRollbackTimeout;
63
65
  },
64
66
  ]);
@@ -28,5 +28,16 @@
28
28
  />
29
29
  percent of instances are healthy.
30
30
  </div>
31
+ <div class="col-sm-10 col-sm-offset-2" ng-if="viewState.dynamicRollbackTimeout">
32
+ Rollback Timeout is
33
+ <input
34
+ type="number"
35
+ min="0"
36
+ max="100"
37
+ ng-model="stage.rollbackTimeout"
38
+ class="form-control input-sm inline-number"
39
+ />
40
+ minutes.
41
+ </div>
31
42
  </div>
32
43
  </div>
@@ -27,23 +27,22 @@ export class AvailabilityZoneSelector extends React.Component<
27
27
  usePreferredZones: props.usePreferredZones || !props.selectedZones || props.selectedZones.length === 0,
28
28
  };
29
29
 
30
- this.setDefaultZones(props);
30
+ this.setDefaultZones(this.state.usePreferredZones, props);
31
31
  }
32
32
 
33
33
  public componentWillReceiveProps(nextProps: IAvailabilityZoneSelectorProps): void {
34
34
  if (nextProps.region !== this.props.region || nextProps.credentials !== this.props.credentials) {
35
- this.setDefaultZones(nextProps);
35
+ this.setDefaultZones(this.state.usePreferredZones, nextProps);
36
36
  }
37
37
  }
38
38
 
39
- private setDefaultZones(props: IAvailabilityZoneSelectorProps) {
40
- const { credentials, onChange, region } = props;
41
- const { usePreferredZones } = this.state;
39
+ private setDefaultZones(usePreferredZones: boolean, props: IAvailabilityZoneSelectorProps) {
40
+ const { credentials, region } = props;
42
41
 
43
42
  AccountService.getAvailabilityZonesForAccountAndRegion('aws', credentials, region).then((preferredZones) => {
44
43
  this.setState({ defaultZones: preferredZones });
45
44
  if (usePreferredZones && preferredZones) {
46
- onChange(preferredZones.slice());
45
+ props.onChange(preferredZones.slice());
47
46
  }
48
47
  });
49
48
  }
@@ -53,7 +52,7 @@ export class AvailabilityZoneSelector extends React.Component<
53
52
  this.setState({ usePreferredZones });
54
53
 
55
54
  if (usePreferredZones) {
56
- this.setDefaultZones(this.props);
55
+ this.setDefaultZones(usePreferredZones, this.props);
57
56
  }
58
57
  };
59
58
 
@@ -3,6 +3,7 @@ import {
3
3
  chain,
4
4
  clone,
5
5
  cloneDeep,
6
+ difference,
6
7
  extend,
7
8
  find,
8
9
  flatten,
@@ -61,6 +62,7 @@ export type IBlockDeviceMappingSource = 'source' | 'ami' | 'default';
61
62
 
62
63
  export interface IAmazonServerGroupCommandDirty extends IServerGroupCommandDirty {
63
64
  targetGroups?: string[];
65
+ launchTemplateOverridesForInstanceType?: IAmazonInstanceTypeOverride[];
64
66
  }
65
67
 
66
68
  export interface IAmazonServerGroupCommandResult extends IServerGroupCommandResult {
@@ -376,11 +378,13 @@ export class AwsServerGroupConfigurationService {
376
378
 
377
379
  public configureInstanceTypes(command: IAmazonServerGroupCommand): IServerGroupCommandResult {
378
380
  const result: IAmazonServerGroupCommandResult = { dirty: {} };
381
+
379
382
  if (command.region && (command.virtualizationType || command.viewState.disableImageSelection)) {
380
383
  let filteredTypesInfo: IAmazonInstanceType[] = this.awsInstanceTypeService.getAvailableTypesForRegions(
381
384
  command.backingData.instanceTypesInfo,
382
385
  [command.region],
383
386
  );
387
+
384
388
  if (command.virtualizationType || command.amiArchitecture) {
385
389
  filteredTypesInfo = this.awsInstanceTypeService.filterInstanceTypes(
386
390
  filteredTypesInfo,
@@ -389,18 +393,35 @@ export class AwsServerGroupConfigurationService {
389
393
  command.amiArchitecture,
390
394
  );
391
395
  }
392
-
393
396
  const filteredTypes: string[] = map(filteredTypesInfo, 'name');
397
+
398
+ // handle incompatibility for single instance type case
394
399
  if (command.instanceType && !filteredTypes.includes(command.instanceType)) {
395
400
  result.dirty.instanceType = command.instanceType;
396
401
  command.instanceType = null;
397
402
  }
403
+
404
+ // handle incompatibility for multiple instance types case
405
+ const multipleInstanceTypes: string[] = map(command.launchTemplateOverridesForInstanceType, 'instanceType');
406
+ const validInstanceTypes: string[] = intersection(multipleInstanceTypes, filteredTypes);
407
+ const invalidInstanceTypes: string[] = difference(multipleInstanceTypes, validInstanceTypes);
408
+
409
+ if (command.launchTemplateOverridesForInstanceType && invalidInstanceTypes.length > 0) {
410
+ result.dirty.launchTemplateOverridesForInstanceType = command.launchTemplateOverridesForInstanceType.filter(
411
+ (it) => invalidInstanceTypes.includes(it.instanceType),
412
+ );
413
+ command.launchTemplateOverridesForInstanceType = command.launchTemplateOverridesForInstanceType.filter((it) =>
414
+ validInstanceTypes.includes(it.instanceType),
415
+ );
416
+ }
417
+
398
418
  command.backingData.filtered.instanceTypes = filteredTypes;
399
419
  command.backingData.filtered.instanceTypesInfo = filteredTypesInfo;
400
420
  } else {
401
421
  command.backingData.filtered.instanceTypes = [];
402
422
  command.backingData.filtered.instanceTypesInfo = [];
403
423
  }
424
+
404
425
  extend(command.viewState.dirty, result.dirty);
405
426
  return result;
406
427
  }
@@ -613,6 +634,7 @@ export class AwsServerGroupConfigurationService {
613
634
  });
614
635
  command.vpcId = subnet ? subnet.vpcId : null;
615
636
  }
637
+
616
638
  extend(result.dirty, this.configureInstanceTypes(command).dirty);
617
639
  return result;
618
640
  }
@@ -16,51 +16,83 @@ export interface IInstanceTypeSelectorProps {
16
16
 
17
17
  export function InstanceTypeSelector(props: IInstanceTypeSelectorProps) {
18
18
  const { instanceTypeDetails } = props;
19
- const { values: command, setFieldValue } = props.formik;
19
+ const { values, setFieldValue } = props.formik;
20
20
  const isLaunchTemplatesEnabled = AWSProviderSettings.serverGroups?.enableLaunchTemplates;
21
21
 
22
- const useSimpleMode = command.viewState.useSimpleInstanceTypeSelector;
23
- const [unlimitedCpuCredits, setUnlimitedCpuCredits] = useState(command.unlimitedCpuCredits);
22
+ const useSimpleMode = values.viewState.useSimpleInstanceTypeSelector;
23
+ const [unlimitedCpuCredits, setUnlimitedCpuCredits] = useState(values.unlimitedCpuCredits);
24
24
 
25
25
  useEffect(() => {
26
- if (command.unlimitedCpuCredits !== unlimitedCpuCredits) {
26
+ if (values.unlimitedCpuCredits !== unlimitedCpuCredits) {
27
27
  setFieldValue('unlimitedCpuCredits', unlimitedCpuCredits);
28
28
  }
29
29
  }, [unlimitedCpuCredits]);
30
30
 
31
+ const clearWarnings = () => {
32
+ const { formik } = props;
33
+
34
+ // clear for both keys to support consistency between simple and advanced modes
35
+ formik.values.viewState.dirty['instanceType'] = null;
36
+ formik.values.viewState.dirty['launchTemplateOverridesForInstanceType'] = null;
37
+
38
+ formik.validateForm();
39
+ };
40
+
31
41
  const handleModeChange = (useSimpleModeNew: boolean) => {
32
42
  if (useSimpleMode !== useSimpleModeNew) {
33
43
  setFieldValue('viewState', {
34
- ...command.viewState,
44
+ ...values.viewState,
35
45
  useSimpleInstanceTypeSelector: useSimpleModeNew,
36
46
  });
37
47
 
38
48
  // update selected instance type(s) if mode changed.
39
49
  // Simple mode uses command.instanceType to track selected type. Advanced mode uses command.launchTemplateOverridesForInstanceType to track selected types.
40
- const multipleInstanceTypesInProps = command.launchTemplateOverridesForInstanceType;
41
- const singleInstanceTypeInProps = command.instanceType;
42
-
43
- const toSimple = useSimpleModeNew && multipleInstanceTypesInProps?.length;
44
- const toAdvanced = !useSimpleModeNew && singleInstanceTypeInProps;
45
- if (toSimple) {
46
- const highestPriorityNum = Math.min(...multipleInstanceTypesInProps.map((it) => it.priority));
47
- const instanceTypeWithHighestPriority = multipleInstanceTypesInProps.find(
48
- (it) => it.priority === highestPriorityNum,
49
- ).instanceType;
50
-
51
- setFieldValue('instanceType', instanceTypeWithHighestPriority);
52
- setFieldValue('launchTemplateOverridesForInstanceType', []);
53
- command.instanceTypeChanged(command);
54
- } else if (toAdvanced) {
55
- const instanceTypes: IAmazonInstanceTypeOverride[] = [
56
- {
57
- instanceType: singleInstanceTypeInProps,
58
- priority: 1,
59
- },
60
- ];
61
- setFieldValue('instanceType', undefined);
62
- setFieldValue('launchTemplateOverridesForInstanceType', instanceTypes);
63
- command.launchTemplateOverridesChanged(command);
50
+ if (useSimpleModeNew) {
51
+ const multipleInstanceTypesInProps = values.launchTemplateOverridesForInstanceType;
52
+ const dirtyMultipleInstanceTypesInProps = values.viewState.dirty.launchTemplateOverridesForInstanceType;
53
+
54
+ if (multipleInstanceTypesInProps?.length) {
55
+ const instanceTypeWithHighestPriority = multipleInstanceTypesInProps.reduce((prev, current) => {
56
+ return prev.priority < current.priority ? prev : current;
57
+ }).instanceType;
58
+ setFieldValue('instanceType', instanceTypeWithHighestPriority);
59
+ setFieldValue('launchTemplateOverridesForInstanceType', []);
60
+ values.instanceTypeChanged(values);
61
+ }
62
+
63
+ if (dirtyMultipleInstanceTypesInProps?.length) {
64
+ const instanceTypeWithHighestPriorityDirty = dirtyMultipleInstanceTypesInProps.reduce((prev, current) => {
65
+ return prev.priority < current.priority ? prev : current;
66
+ }).instanceType;
67
+ setFieldValue('viewState.dirty.instanceType', instanceTypeWithHighestPriorityDirty);
68
+ setFieldValue('viewState.dirty.launchTemplateOverridesForInstanceType', []);
69
+ }
70
+ } else if (!useSimpleModeNew) {
71
+ const singleInstanceTypeInProps = values.instanceType;
72
+ const dirtySingleInstanceTypeInProps = values.viewState.dirty.instanceType;
73
+
74
+ if (singleInstanceTypeInProps) {
75
+ const instanceTypes: IAmazonInstanceTypeOverride[] = [
76
+ {
77
+ instanceType: singleInstanceTypeInProps,
78
+ priority: 1,
79
+ },
80
+ ];
81
+ setFieldValue('instanceType', undefined);
82
+ setFieldValue('launchTemplateOverridesForInstanceType', instanceTypes);
83
+ values.launchTemplateOverridesChanged(values);
84
+ }
85
+
86
+ if (dirtySingleInstanceTypeInProps) {
87
+ const dirtyInstanceTypes: IAmazonInstanceTypeOverride[] = [
88
+ {
89
+ instanceType: dirtySingleInstanceTypeInProps,
90
+ priority: 1,
91
+ },
92
+ ];
93
+ setFieldValue('viewState.dirty.instanceType', undefined);
94
+ setFieldValue('viewState.dirty.launchTemplateOverridesForInstanceType', dirtyInstanceTypes);
95
+ }
64
96
  }
65
97
  }
66
98
  };
@@ -86,6 +118,7 @@ export function InstanceTypeSelector(props: IInstanceTypeSelectorProps) {
86
118
  formik={props.formik}
87
119
  instanceTypeDetails={instanceTypeDetails}
88
120
  setUnlimitedCpuCredits={setUnlimitedCpuCredits}
121
+ clearWarnings={clearWarnings}
89
122
  />
90
123
  </div>
91
124
  );
@@ -124,9 +157,10 @@ export function InstanceTypeSelector(props: IInstanceTypeSelectorProps) {
124
157
  )}
125
158
  </div>
126
159
  <SimpleModeSelector
127
- command={command}
160
+ command={values}
128
161
  setUnlimitedCpuCredits={setUnlimitedCpuCredits}
129
162
  setFieldValue={setFieldValue}
163
+ clearWarnings={clearWarnings}
130
164
  />
131
165
  </div>
132
166
  );
@@ -0,0 +1,44 @@
1
+ import React from 'react';
2
+
3
+ import type { IAmazonServerGroupCommandDirty } from '../../serverGroupConfiguration.service';
4
+
5
+ export function InstanceTypeWarning(props: { dirty: IAmazonServerGroupCommandDirty; clearWarnings: () => void }) {
6
+ if (
7
+ props.dirty.instanceType ||
8
+ (props.dirty.launchTemplateOverridesForInstanceType &&
9
+ props.dirty.launchTemplateOverridesForInstanceType.length > 0)
10
+ ) {
11
+ return (
12
+ <div className="col-md-12">
13
+ <div className="alert alert-warning">
14
+ <p>
15
+ <i className="fa fa-exclamation-triangle" />
16
+ {props.dirty.instanceType &&
17
+ 'The following instance type was found incompatible with the selected image/region and was removed:'}
18
+ {props.dirty.launchTemplateOverridesForInstanceType &&
19
+ props.dirty.launchTemplateOverridesForInstanceType.length > 0 &&
20
+ 'The following instance type(s) were found incompatible with the selected image/region and were removed:'}
21
+ </p>
22
+ <ul>
23
+ {props.dirty.instanceType && <li key={props.dirty.instanceType}>{props.dirty.instanceType}</li>}
24
+ {props.dirty.launchTemplateOverridesForInstanceType &&
25
+ props.dirty.launchTemplateOverridesForInstanceType.length > 0 &&
26
+ props.dirty.launchTemplateOverridesForInstanceType.map((it) => (
27
+ <li key={it.instanceType}>
28
+ {it.instanceType}
29
+ {it.weightedCapacity ? ' with weight ' + it.weightedCapacity : ''}
30
+ </li>
31
+ ))}
32
+ </ul>
33
+ <p className="text-right">
34
+ <a className="btn btn-sm btn-default dirty-flag-dismiss clickable" onClick={() => props.clearWarnings()}>
35
+ Okay
36
+ </a>
37
+ </p>
38
+ </div>
39
+ </div>
40
+ );
41
+ } else {
42
+ return null;
43
+ }
44
+ }
@@ -15,6 +15,7 @@ export interface IAdvancedModeSelectorProps {
15
15
  formik: FormikProps<IAmazonServerGroupCommand>;
16
16
  instanceTypeDetails: IAmazonInstanceTypeCategory[];
17
17
  setUnlimitedCpuCredits: (unlimitedCpuCredits: boolean | undefined) => void;
18
+ clearWarnings: () => void;
18
19
  }
19
20
 
20
21
  /**
@@ -93,6 +94,8 @@ export function AdvancedModeSelector(props: IAdvancedModeSelectorProps) {
93
94
  }
94
95
  handleInstanceTypesChange={handleInstanceTypesChange}
95
96
  setUnlimitedCpuCredits={setUnlimitedCpuCredits}
97
+ viewState={command.viewState}
98
+ clearWarnings={props.clearWarnings}
96
99
  />
97
100
  </div>
98
101
  );
@@ -4,50 +4,50 @@ import React from 'react';
4
4
  import type { IAmazonInstanceType } from '../../../../../instance/awsInstanceType.service';
5
5
 
6
6
  export function AmazonInstanceTypeInfoRenderer(props: { instanceType: IAmazonInstanceType }) {
7
- const spotSupport = props.instanceType.supportedUsageClasses.includes('spot') ? '| SPOT supported' : '';
8
- const CpuMem = `${props.instanceType.defaultVCpus} vCPU | ${props.instanceType.memoryInGiB} Gib Memory ${spotSupport}`;
9
- const instanceStorageInfo = props.instanceType.instanceStorageSupported && (
7
+ const spotSupport = props.instanceType?.supportedUsageClasses?.includes('spot') ? '| SPOT supported' : '';
8
+ const CpuMem = `${props.instanceType?.defaultVCpus} vCPU | ${props.instanceType?.memoryInGiB} Gib Memory ${spotSupport}`;
9
+ const instanceStorageInfo = props.instanceType?.instanceStorageSupported && (
10
10
  <span>
11
11
  <br />
12
12
  <span className={`select-option-label-attributes`}>
13
- {`Instance Storage: ${_.toUpper(props.instanceType.instanceStorageInfo.storageTypes)} | ${
14
- props.instanceType.instanceStorageInfo.totalSizeInGB
13
+ {`Instance Storage: ${_.toUpper(props.instanceType?.instanceStorageInfo?.storageTypes)} | ${
14
+ props.instanceType?.instanceStorageInfo?.totalSizeInGB
15
15
  } Gib total size`}
16
16
  </span>
17
17
  </span>
18
18
  );
19
- const ebsInfo = props.instanceType.ebsInfo && (
19
+ const ebsInfo = props.instanceType?.ebsInfo && (
20
20
  <span>
21
21
  <br />
22
22
  <span className={`select-option-label-attributes`}>
23
- {`EBS: optimization ${props.instanceType.ebsInfo.ebsOptimizedSupport} | NVMe ${props.instanceType.ebsInfo.nvmeSupport} | Encryption ${props.instanceType.ebsInfo.encryptionSupport}`}
23
+ {`EBS: optimization ${props.instanceType?.ebsInfo?.ebsOptimizedSupport} | NVMe ${props.instanceType?.ebsInfo?.nvmeSupport} | Encryption ${props.instanceType?.ebsInfo?.encryptionSupport}`}
24
24
  </span>
25
25
  </span>
26
26
  );
27
- const gpuInfo = props.instanceType.gpuInfo && (
27
+ const gpuInfo = props.instanceType?.gpuInfo && (
28
28
  <span>
29
29
  <br />
30
30
  <span className={`select-option-label-attributes`}>
31
- {`GPU: ${props.instanceType.gpuInfo.totalGpuMemoryInMiB} MiB total memory`}
32
- {props.instanceType.gpuInfo.gpus.map(
31
+ {`GPU: ${props.instanceType?.gpuInfo?.totalGpuMemoryInMiB} MiB total memory`}
32
+ {props.instanceType?.gpuInfo?.gpus.map(
33
33
  (g) => ` | ${g.count} ${g.manufacturer} ${g.name} GPUs, size: ${g.gpuSizeInMiB} MiB`,
34
34
  )}
35
35
  </span>
36
36
  </span>
37
37
  );
38
- const generationInfo = typeof props.instanceType.currentGeneration !== 'undefined' &&
39
- props.instanceType.currentGeneration !== null && (
38
+ const generationInfo = typeof props.instanceType?.currentGeneration !== 'undefined' &&
39
+ props.instanceType?.currentGeneration !== null && (
40
40
  <span>
41
41
  <br />
42
42
  <span className={`select-option-label-attributes`}>
43
- {props.instanceType.currentGeneration ? 'Current Generation' : 'Previous Generation'}
43
+ {props.instanceType?.currentGeneration ? 'Current Generation' : 'Previous Generation'}
44
44
  </span>
45
45
  </span>
46
46
  );
47
47
 
48
48
  return (
49
49
  <span>
50
- <span className={`select-option-label`}>{props.instanceType.name}</span>
50
+ <span className={`select-option-label`}>{props.instanceType?.name}</span>
51
51
  <br />
52
52
  <span className={`select-option-label-attributes`}>{CpuMem}</span>
53
53
  {instanceStorageInfo}
@@ -6,10 +6,12 @@ import { CpuCreditsToggle } from '../CpuCreditsToggle';
6
6
  import { InstanceTypeTableBody } from './InstanceTypeTableBody';
7
7
  import { Header, Heading } from './InstanceTypeTableParts';
8
8
  import { Footer } from './InstanceTypeTableParts';
9
+ import { InstanceTypeWarning } from '../InstanceTypeWarning';
9
10
  import { AWSProviderSettings } from '../../../../../aws.settings';
10
11
  import type { IAmazonInstanceTypeCategory } from '../../../../../instance/awsInstanceType.service';
11
12
  import type { IAmazonInstanceType } from '../../../../../instance/awsInstanceType.service';
12
13
  import type { IAmazonInstanceTypeOverride } from '../../../serverGroupConfiguration.service';
14
+ import type { IAmazonServerGroupCommandViewState } from '../../../serverGroupConfiguration.service';
13
15
 
14
16
  import './advancedMode.less';
15
17
 
@@ -21,6 +23,8 @@ export interface IInstanceTypeTableProps {
21
23
  availableInstanceTypesList: IAmazonInstanceType[];
22
24
  handleInstanceTypesChange: (instanceTypes: IAmazonInstanceTypeOverride[]) => void;
23
25
  setUnlimitedCpuCredits: (unlimitedCpuCredits: boolean | undefined) => void;
26
+ viewState: IAmazonServerGroupCommandViewState;
27
+ clearWarnings: () => void;
24
28
  }
25
29
 
26
30
  export function InstanceTypeTable(props: IInstanceTypeTableProps) {
@@ -96,6 +100,7 @@ export function InstanceTypeTable(props: IInstanceTypeTableProps) {
96
100
  profileLabel={label}
97
101
  profileDescriptionArr={descriptionListOverride ? descriptionListOverride : families.map((f) => f.description)}
98
102
  />
103
+ <InstanceTypeWarning dirty={props.viewState.dirty} clearWarnings={props.clearWarnings} />
99
104
  {isCpuCreditsEnabled && cpuCreditsToggle}
100
105
  <table className="table table-hover">
101
106
  <Header isCustom={isCustom} showCpuCredits={showCpuCredits} />
@@ -115,6 +120,7 @@ export function InstanceTypeTable(props: IInstanceTypeTableProps) {
115
120
  return (
116
121
  <div className={'row sub-section'}>
117
122
  <Heading isCustom={isCustom} />
123
+ <InstanceTypeWarning dirty={props.viewState.dirty} clearWarnings={props.clearWarnings} />
118
124
  {isCpuCreditsEnabled && cpuCreditsToggle}
119
125
  <table className="table table-hover">
120
126
  <Header isCustom={isCustom} />
@@ -3,6 +3,7 @@ import React from 'react';
3
3
  import { NgReact } from '@spinnaker/core';
4
4
 
5
5
  import { CpuCreditsToggle } from '../CpuCreditsToggle';
6
+ import { InstanceTypeWarning } from '../InstanceTypeWarning';
6
7
  import { AWSProviderSettings } from '../../../../../aws.settings';
7
8
  import type { IAmazonServerGroupCommand } from '../../../serverGroupConfiguration.service';
8
9
 
@@ -10,6 +11,7 @@ export interface ISimpleModeSelectorProps {
10
11
  command: IAmazonServerGroupCommand;
11
12
  setUnlimitedCpuCredits: (unlimitedCpuCredits: boolean | undefined) => void;
12
13
  setFieldValue: (field: keyof IAmazonServerGroupCommand, value: any, shouldValidate?: boolean) => void;
14
+ clearWarnings: () => void;
13
15
  }
14
16
 
15
17
  export function SimpleModeSelector(props: ISimpleModeSelectorProps) {
@@ -41,6 +43,7 @@ export function SimpleModeSelector(props: ISimpleModeSelectorProps) {
41
43
  onTypeChanged={instanceTypeChanged}
42
44
  onProfileChanged={instanceProfileChanged}
43
45
  />
46
+ <InstanceTypeWarning dirty={command.viewState.dirty} clearWarnings={props.clearWarnings} />
44
47
  <div style={{ padding: '0 15px' }}>
45
48
  {command.viewState.instanceProfile && command.viewState.instanceProfile !== 'custom' && (
46
49
  <InstanceTypeSelector command={command} onTypeChanged={instanceTypeChanged} />
@@ -98,6 +98,9 @@ export class ServerGroupBasicSettings
98
98
  setFieldValue('associateIPv6Address', false);
99
99
  }
100
100
  }
101
+
102
+ // an image change might clear the single or multiple instance types previously selected with the image, validate the form to surface related errors.
103
+ this.props.formik.validateForm();
101
104
  };
102
105
 
103
106
  private accountUpdated = (account: string): void => {
@@ -31,12 +31,22 @@ export class ServerGroupInstanceType
31
31
  const errors: FormikErrors<IAmazonServerGroupCommand> = {};
32
32
 
33
33
  if (values.viewState.useSimpleInstanceTypeSelector) {
34
- if (!values.instanceType) {
34
+ // For the clone scenario, when values.instanceType is cleared for various reasons (see serverGroupConfiguration.service.ts),
35
+ // the previously selected instance type might be cleared/ unselected to fail-fast and this might look like unexpected behavior. But, there is always a reason when this happens.
36
+ // The error handling below adds transparency around what changed and why.
37
+ if (values.viewState.dirty.instanceType) {
38
+ errors.instanceType = 'You must confirm removed instance type and pick a compatible instance type.';
39
+ } else if (!values.instanceType) {
35
40
  errors.instanceType = 'Instance Type required.';
36
41
  }
37
42
  } else {
38
- const advancedModeErrors = this.validateAdvancedModeFields(values, errors);
39
- Object.assign(errors, { ...advancedModeErrors });
43
+ const prevInstanceTypes = values.viewState.dirty.launchTemplateOverridesForInstanceType;
44
+ if (prevInstanceTypes && prevInstanceTypes.length > 0) {
45
+ errors.instanceType = 'You must confirm removed instance types and/or pick more instance types.';
46
+ } else {
47
+ const advancedModeErrors = this.validateAdvancedModeFields(values, errors);
48
+ Object.assign(errors, { ...advancedModeErrors });
49
+ }
40
50
  }
41
51
 
42
52
  return errors;
@@ -58,8 +58,12 @@ export class LaunchConfigDetailsSection extends React.Component<
58
58
  <dt>IAM Profile</dt>
59
59
  <dd>{launchConfig.iamInstanceProfile}</dd>
60
60
 
61
- <dt>Instance Monitoring</dt>
62
- <dd>{launchConfig.instanceMonitoring.enabled ? 'enabled' : 'disabled'}</dd>
61
+ {launchConfig.instanceMonitoring && (
62
+ <>
63
+ <dt>Instance Monitoring</dt>
64
+ <dd>{launchConfig.instanceMonitoring.enabled ? 'enabled' : 'disabled'}</dd>
65
+ </>
66
+ )}
63
67
 
64
68
  {launchConfig.spotPrice && <dt>Spot Price</dt>}
65
69
  {launchConfig.spotPrice && <dd>{launchConfig.spotPrice}</dd>}
@@ -96,6 +96,10 @@ export class AwsServerGroupTransformer {
96
96
  deployConfig.targetGroups = base.targetGroups || [];
97
97
  deployConfig.account = deployConfig.credentials;
98
98
  deployConfig.subnetType = deployConfig.subnetType || '';
99
+ deployConfig.instanceType =
100
+ deployConfig.instanceType ||
101
+ (deployConfig.launchTemplateOverridesForInstanceType &&
102
+ deployConfig.launchTemplateOverridesForInstanceType[0].instanceType); // populate instanceType for backwards compatibility
99
103
 
100
104
  if (base.viewState.mode !== 'clone') {
101
105
  delete deployConfig.source;