@spinnaker/core 0.28.0 → 0.29.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 (27) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dist/config/settings.d.ts +8 -0
  3. package/dist/index.js +36 -36
  4. package/dist/index.js.map +1 -1
  5. package/dist/notification/selector/types/cdevents/CDEventsNotificationType.d.ts +5 -0
  6. package/dist/notification/selector/types/cdevents/cdevents.notification.d.ts +2 -0
  7. package/dist/notification/selector/types/index.d.ts +1 -0
  8. package/dist/pipeline/config/stages/overrideTimeout/OverrideTimeout.d.ts +1 -1
  9. package/dist/presentation/forms/validation/validators.d.ts +2 -0
  10. package/dist/task/task.read.service.d.ts +1 -1
  11. package/package.json +2 -2
  12. package/src/config/settings.ts +6 -0
  13. package/src/manifest/stage/JobStageExecutionLogs.tsx +2 -2
  14. package/src/notification/modal/NotificationDetails.tsx +4 -1
  15. package/src/notification/notification.types.ts +2 -0
  16. package/src/notification/selector/types/cdevents/CDEventsNotificationType.tsx +36 -0
  17. package/src/notification/selector/types/cdevents/cdevents.notification.ts +8 -0
  18. package/src/notification/selector/types/index.ts +1 -0
  19. package/src/pipeline/config/services/PipelineConfigService.ts +3 -0
  20. package/src/pipeline/config/stages/overrideTimeout/OverrideTimeout.tsx +24 -4
  21. package/src/pipeline/config/stages/wait/waitStage.ts +1 -0
  22. package/src/pipeline/config/validation/PipelineConfigValidator.ts +5 -1
  23. package/src/presentation/forms/validation/validators.ts +24 -1
  24. package/src/task/task.dataSource.js +18 -2
  25. package/src/task/task.read.service.ts +11 -2
  26. package/dist/presentation/forms/inputs/NumberConcurrencyInput.d.ts +0 -7
  27. package/src/presentation/forms/inputs/NumberConcurrencyInput.tsx +0 -29
@@ -0,0 +1,5 @@
1
+ import React from 'react';
2
+ import type { INotificationTypeCustomConfig } from '../../../../domain';
3
+ export declare class CDEventsNotificationType extends React.Component<INotificationTypeCustomConfig> {
4
+ render(): JSX.Element;
5
+ }
@@ -0,0 +1,2 @@
1
+ import type { INotificationTypeConfig } from '../../../../domain';
2
+ export declare const cdEventsNotification: INotificationTypeConfig;
@@ -5,3 +5,4 @@ export * from './microsoftteams/MicrosoftTeamsNotificationType';
5
5
  export * from './pubsub/PubsubNotificationType';
6
6
  export * from './slack/SlackNotificationType';
7
7
  export * from './sms/SmsNotificationType';
8
+ export * from './cdevents/CDEventsNotificationType';
@@ -2,7 +2,7 @@
2
2
  import type { IStage } from '../../../../domain';
3
3
  export interface IOverrideTimeoutConfigProps {
4
4
  stageConfig: IStageConfig;
5
- stageTimeoutMs: number;
5
+ stageTimeoutMs: number | any;
6
6
  updateStageField: (changes: Partial<IStage>) => void;
7
7
  }
8
8
  interface IStageConfig {
@@ -10,6 +10,8 @@ export declare const Validators: {
10
10
  arrayNotEmpty: (message?: string) => IValidator;
11
11
  checkBetween: (fieldName: string, min: number, max: number) => IValidator;
12
12
  emailValue: (message?: string) => IValidator;
13
+ cdeventsTypeValue: (message?: string) => IValidator;
14
+ urlValue: (message?: string) => IValidator;
13
15
  isNum: (message?: string) => IValidator;
14
16
  isRequired: (message?: string) => IValidator;
15
17
  isValidJson: (message?: string) => IValidator;
@@ -2,7 +2,7 @@ import type { Subject } from 'rxjs';
2
2
  import type { ITask } from '../domain';
3
3
  export declare class TaskReader {
4
4
  private static activeStatuses;
5
- static getTasks(applicationName: string, statuses?: string[]): PromiseLike<ITask[]>;
5
+ static getTasks(applicationName: string, statuses?: string[], limitPerPage?: number, page?: number): PromiseLike<ITask[]>;
6
6
  static getRunningTasks(applicationName: string): PromiseLike<ITask[]>;
7
7
  static getTask(taskId: string): PromiseLike<ITask>;
8
8
  static waitUntilTaskMatches(task: ITask, closure: (task: ITask) => boolean, failureClosure?: (task: ITask) => boolean, interval?: number, notifier?: Subject<void>): PromiseLike<ITask>;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@spinnaker/core",
3
3
  "license": "Apache-2.0",
4
- "version": "0.28.0",
4
+ "version": "0.29.0",
5
5
  "module": "dist/index.js",
6
6
  "typings": "dist/index.d.ts",
7
7
  "publishConfig": {
@@ -123,5 +123,5 @@
123
123
  "shx": "0.3.3",
124
124
  "typescript": "4.3.5"
125
125
  },
126
- "gitHead": "20d6ca89f39a5f44a802a1ccaffd669587450d76"
126
+ "gitHead": "082a084908e13888d640e88a5bd6e1571479de14"
127
127
  }
@@ -21,6 +21,7 @@ export interface INotificationSettings {
21
21
  pubsub: { enabled: boolean };
22
22
  slack: { botName: string; enabled: boolean };
23
23
  sms: { enabled: boolean };
24
+ cdevents: { enabled: boolean };
24
25
  }
25
26
 
26
27
  export interface IFeatures {
@@ -141,6 +142,7 @@ export interface ISpinnakerSettings {
141
142
  };
142
143
  stashTriggerInfo?: string;
143
144
  pollSchedule: number;
145
+ tasksViewLimitPerPage: number;
144
146
  providers?: {
145
147
  [key: string]: IProviderSettings; // allows custom providers not typed in here (good for testing too)
146
148
  };
@@ -152,6 +154,10 @@ export interface ISpinnakerSettings {
152
154
  useClassicFirewallLabels: boolean;
153
155
  kubernetesAdHocInfraWritesEnabled: boolean;
154
156
  changelogUrl: string;
157
+ cdevents?: {
158
+ validUrlPattern: string;
159
+ validCDEvent: string;
160
+ };
155
161
  }
156
162
 
157
163
  export const SETTINGS: ISpinnakerSettings = (window as any).spinnakerSettings || {};
@@ -1,4 +1,4 @@
1
- import { isEmpty, template } from 'lodash';
1
+ import { template } from 'lodash';
2
2
  import React from 'react';
3
3
  import { from as observableFrom, Subject } from 'rxjs';
4
4
  import { takeUntil } from 'rxjs/operators';
@@ -52,7 +52,7 @@ export class JobStageExecutionLogs extends React.Component<IJobStageExecutionLog
52
52
  const { manifest } = this.state;
53
53
  const { externalLink, podNamesProviders, location, account } = this.props;
54
54
  // prefer links to external logging platforms
55
- if (!isEmpty(manifest) && externalLink) {
55
+ if (externalLink) {
56
56
  return (
57
57
  <a target="_blank" href={this.renderExternalLink(externalLink, manifest)}>
58
58
  Console Output (External)
@@ -54,7 +54,10 @@ export class NotificationDetails extends React.Component<INotificationDetailsPro
54
54
  }
55
55
 
56
56
  private renderCustomMessage = (type: string, whenOption: string): React.ReactNode => {
57
- if (whenOption !== 'manualJudgment' && ['email', 'slack', 'googlechat', 'microsoftteams'].includes(type)) {
57
+ if (
58
+ whenOption !== 'manualJudgment' &&
59
+ ['email', 'slack', 'googlechat', 'microsoftteams', 'cdevents'].includes(type)
60
+ ) {
58
61
  return (
59
62
  <FormikFormField
60
63
  name={`message["${whenOption}"].text`}
@@ -6,6 +6,7 @@ import type { INotificationTypeConfig } from '../domain';
6
6
  import { Registry } from '../registry';
7
7
 
8
8
  import { bearyChatNotification } from './selector/types/bearychat/beary.notification';
9
+ import { cdEventsNotification } from './selector/types/cdevents/cdevents.notification';
9
10
  import { emailNotification } from './selector/types/email/email.notification';
10
11
  import { githubstatusNotification } from './selector/types/githubstatus/githubstatus.notification';
11
12
  import { googlechatNotification } from './selector/types/googlechat/googlechat.notification';
@@ -23,6 +24,7 @@ import { smsNotification } from './selector/types/sms/sms.notification';
23
24
  pubsubNotification,
24
25
  slackNotification,
25
26
  smsNotification,
27
+ cdEventsNotification,
26
28
  ].forEach((config: INotificationTypeConfig) => {
27
29
  if (SETTINGS.notifications) {
28
30
  const notificationSetting: { enabled: boolean; botName?: string } =
@@ -0,0 +1,36 @@
1
+ import React from 'react';
2
+
3
+ import type { INotificationTypeCustomConfig } from '../../../../domain';
4
+ import { FormikFormField, TextInput, Validators } from '../../../../presentation';
5
+
6
+ export class CDEventsNotificationType extends React.Component<INotificationTypeCustomConfig> {
7
+ public render() {
8
+ const { fieldName } = this.props;
9
+ return (
10
+ <>
11
+ <FormikFormField
12
+ label="Events Broker URL"
13
+ name={fieldName ? `${fieldName}.address` : 'address'}
14
+ validate={Validators.skipIfSpel(Validators.urlValue('Please enter a valid URL'))}
15
+ input={(props) => (
16
+ <TextInput
17
+ inputClassName={'form-control input-sm'}
18
+ {...props}
19
+ placeholder="Enter an events message broker URL"
20
+ />
21
+ )}
22
+ required={true}
23
+ />
24
+ <FormikFormField
25
+ label="CDEvents Type"
26
+ name={fieldName ? `${fieldName}.cdEventsType` : 'cdEventsType'}
27
+ validate={Validators.skipIfSpel(Validators.cdeventsTypeValue('Please enter a valid CDEvents Type'))}
28
+ input={(props) => (
29
+ <TextInput inputClassName={'form-control input-sm'} {...props} placeholder="Enter a CDEvents type" />
30
+ )}
31
+ required={true}
32
+ />
33
+ </>
34
+ );
35
+ }
36
+ }
@@ -0,0 +1,8 @@
1
+ import { CDEventsNotificationType } from './CDEventsNotificationType';
2
+ import type { INotificationTypeConfig } from '../../../../domain';
3
+
4
+ export const cdEventsNotification: INotificationTypeConfig = {
5
+ component: CDEventsNotificationType,
6
+ key: 'cdevents',
7
+ label: 'CDEvents',
8
+ };
@@ -5,3 +5,4 @@ export * from './microsoftteams/MicrosoftTeamsNotificationType';
5
5
  export * from './pubsub/PubsubNotificationType';
6
6
  export * from './slack/SlackNotificationType';
7
7
  export * from './sms/SmsNotificationType';
8
+ export * from './cdevents/CDEventsNotificationType';
@@ -142,6 +142,9 @@ export class PipelineConfigService {
142
142
  private static groupStagesByRequisiteStageRefIds(pipeline: IPipeline) {
143
143
  return pipeline.stages.reduce((acc, obj) => {
144
144
  const parent = obj['refId'];
145
+ if (obj['requisiteStageRefIds'] === undefined) {
146
+ obj['requisiteStageRefIds'] = [];
147
+ }
145
148
  obj.requisiteStageRefIds.forEach((child) => {
146
149
  const values = acc.get(child);
147
150
  if (values && values.length) {
@@ -1,4 +1,4 @@
1
- import { get } from 'lodash';
1
+ import { get, isNumber } from 'lodash';
2
2
  import { Duration } from 'luxon';
3
3
  import React from 'react';
4
4
 
@@ -10,7 +10,7 @@ const { useEffect, useState } = React;
10
10
 
11
11
  export interface IOverrideTimeoutConfigProps {
12
12
  stageConfig: IStageConfig;
13
- stageTimeoutMs: number;
13
+ stageTimeoutMs: number | any;
14
14
  updateStageField: (changes: Partial<IStage>) => void;
15
15
  }
16
16
 
@@ -41,8 +41,13 @@ export const OverrideTimeout = (props: IOverrideTimeoutConfigProps) => {
41
41
  }, [props.stageTimeoutMs]);
42
42
 
43
43
  const stageChanged = () => {
44
- if (props.stageTimeoutMs !== undefined) {
44
+ if (props.stageTimeoutMs !== undefined && !isExpression) {
45
45
  enableTimeout();
46
+ } else if (props.stageTimeoutMs !== undefined && isExpression) {
47
+ setOverrideTimeout(true);
48
+ props.updateStageField({
49
+ stageTimeoutMs: props.stageTimeoutMs || null,
50
+ });
46
51
  } else {
47
52
  clearTimeout();
48
53
  }
@@ -74,6 +79,10 @@ export const OverrideTimeout = (props: IOverrideTimeoutConfigProps) => {
74
79
  };
75
80
 
76
81
  const isConfigurable = !!get(props.stageConfig, 'supportsCustomTimeout');
82
+ const isExpression =
83
+ props.stageTimeoutMs !== undefined && props.stageTimeoutMs !== null && !isNumber(props.stageTimeoutMs)
84
+ ? props.stageTimeoutMs.includes('${')
85
+ : false;
77
86
 
78
87
  if (isConfigurable) {
79
88
  return (
@@ -94,7 +103,7 @@ export const OverrideTimeout = (props: IOverrideTimeoutConfigProps) => {
94
103
  </div>
95
104
  </div>
96
105
  </div>
97
- {overrideTimeout && (
106
+ {overrideTimeout && !isExpression && (
98
107
  <div>
99
108
  <div className="form-group form-inline">
100
109
  <div className="col-md-9 col-md-offset-1 checkbox-padding">
@@ -123,6 +132,17 @@ export const OverrideTimeout = (props: IOverrideTimeoutConfigProps) => {
123
132
  </div>
124
133
  </div>
125
134
  )}
135
+ {overrideTimeout && isExpression && (
136
+ <div className="form-group">
137
+ <div className="col-md-9 col-md-offset-1">
138
+ <div className="sm-control-field">
139
+ <span>
140
+ Resolved at runtime from expression: <code>{props.stageTimeoutMs}</code>
141
+ </span>
142
+ </div>
143
+ </div>
144
+ </div>
145
+ )}
126
146
  </>
127
147
  );
128
148
  } else {
@@ -8,6 +8,7 @@ Registry.pipeline.registerStage({
8
8
  label: 'Wait',
9
9
  description: 'Waits a specified period of time',
10
10
  key: 'wait',
11
+ restartable: true,
11
12
  component: WaitStageConfig,
12
13
  executionDetailsSections: [WaitExecutionDetails, ExecutionDetailsTasks],
13
14
  executionLabelComponent: WaitExecutionLabel,
@@ -54,6 +54,10 @@ export interface ICustomValidator extends IStageOrTriggerValidator, IValidatorCo
54
54
  [k: string]: any;
55
55
  }
56
56
 
57
+ function isNumberOrSpel(valInput: any) {
58
+ return (isNumber(valInput) && valInput > 0) || (typeof valInput === 'string' && valInput.includes('${'));
59
+ }
60
+
57
61
  export class PipelineConfigValidator {
58
62
  private static validators: Map<string, IStageOrTriggerValidator> = new Map();
59
63
  private static validationStream: Subject<IPipelineValidationResults> = new Subject();
@@ -151,7 +155,7 @@ export class PipelineConfigValidator {
151
155
  );
152
156
  }
153
157
 
154
- if (stage.stageTimeoutMs !== undefined && !(isNumber(stage.stageTimeoutMs) && stage.stageTimeoutMs > 0)) {
158
+ if (stage.stageTimeoutMs !== undefined && !isNumberOrSpel(stage.stageTimeoutMs)) {
155
159
  stageValidations.set(stage, [
156
160
  ...(stageValidations.get(stage) || []),
157
161
  'Stage is configured to fail after a specific amount of time, but no time is set.',
@@ -1,6 +1,7 @@
1
1
  import { isNumber } from 'lodash';
2
- import { robotToHuman } from '../../robotToHumanFilter/robotToHuman.filter';
3
2
 
3
+ import { SETTINGS } from '../../../config/settings';
4
+ import { robotToHuman } from '../../robotToHumanFilter/robotToHuman.filter';
4
5
  import type { IValidator } from './validation';
5
6
 
6
7
  const THIS_FIELD = 'This field';
@@ -9,6 +10,12 @@ const VALID_EMAIL_REGEX = new RegExp(
9
10
  '^(([^<>()\\[\\]\\\\.,;:\\s@"]+(\\.[^<>()\\[\\]\\\\.,;:\\s@"]+)*)|(".+"))@((\\[[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}])|(([a-zA-Z\\-0-9]+\\.)+[a-zA-Z]{2,}))$',
10
11
  );
11
12
 
13
+ const urlPattern = SETTINGS.cdevents?.validUrlPattern ?? '^https?://.+$';
14
+ const VALID_URL = new RegExp(urlPattern);
15
+
16
+ const cdeventPattern = SETTINGS.cdevents?.validCDEvent ?? '^dev\\.cdevents\\.[^.]+\\.[^.]+$';
17
+ const VALID_CDEVENT_REGEX = new RegExp(cdeventPattern);
18
+
12
19
  const emailValue = (message?: string): IValidator => {
13
20
  return function emailValue(val: string, label = THIS_FIELD) {
14
21
  message = message || `${label} is not a valid email address.`;
@@ -16,6 +23,20 @@ const emailValue = (message?: string): IValidator => {
16
23
  };
17
24
  };
18
25
 
26
+ const urlValue = (message?: string): IValidator => {
27
+ return function urlValue(val: string, label = THIS_FIELD) {
28
+ message = message || `${label} is not a valid URL.`;
29
+ return val && !VALID_URL.test(val) && message;
30
+ };
31
+ };
32
+
33
+ const cdeventsTypeValue = (message?: string): IValidator => {
34
+ return function cdeventsTypeValue(val: string, label = THIS_FIELD) {
35
+ message = message || `${label} is not a valid CDEvents Type.`;
36
+ return val && !VALID_CDEVENT_REGEX.test(val) && message;
37
+ };
38
+ };
39
+
19
40
  const isRequired = (message?: string): IValidator => {
20
41
  return function isRequired(val: any, label = THIS_FIELD) {
21
42
  message = message || `${label} is required.`;
@@ -138,6 +159,8 @@ export const Validators = {
138
159
  arrayNotEmpty,
139
160
  checkBetween,
140
161
  emailValue,
162
+ cdeventsTypeValue,
163
+ urlValue,
141
164
  isNum,
142
165
  isRequired,
143
166
  isValidJson,
@@ -2,6 +2,7 @@ import * as angular from 'angular';
2
2
 
3
3
  import { ApplicationDataSourceRegistry } from '../application/service/ApplicationDataSourceRegistry';
4
4
  import { CLUSTER_SERVICE } from '../cluster/cluster.service';
5
+ import { SETTINGS } from '../config';
5
6
  import { TaskReader } from './task.read.service';
6
7
 
7
8
  export const CORE_TASK_TASK_DATASOURCE = 'spinnaker.core.task.dataSource';
@@ -14,8 +15,23 @@ angular.module(CORE_TASK_TASK_DATASOURCE, [CLUSTER_SERVICE]).run([
14
15
  return $q.when(angular.isArray(tasks) ? tasks : []);
15
16
  };
16
17
 
17
- const loadTasks = (application) => {
18
- return TaskReader.getTasks(application.name);
18
+ const loadPaginatedTasks = async (application, page = 1) => {
19
+ let limitPerPage = SETTINGS.tasksViewLimitPerPage;
20
+ const tasks = await TaskReader.getTasks(application.name, [], limitPerPage, page);
21
+ if (tasks.length === limitPerPage) {
22
+ return tasks.concat(await loadPaginatedTasks(application, page + 1));
23
+ } else {
24
+ return tasks;
25
+ }
26
+ };
27
+
28
+ const loadTasks = (application, page = 1) => {
29
+ let limitPerPage = SETTINGS.tasksViewLimitPerPage;
30
+ if (limitPerPage === undefined) {
31
+ return TaskReader.getTasks(application.name);
32
+ } else {
33
+ return loadPaginatedTasks(application, page);
34
+ }
19
35
  };
20
36
 
21
37
  const loadRunningTasks = (application) => {
@@ -8,10 +8,19 @@ import { OrchestratedItemTransformer } from '../orchestratedItem/orchestratedIte
8
8
  export class TaskReader {
9
9
  private static activeStatuses: string[] = ['RUNNING', 'SUSPENDED', 'NOT_STARTED'];
10
10
 
11
- public static getTasks(applicationName: string, statuses: string[] = []): PromiseLike<ITask[]> {
11
+ public static getTasks(
12
+ applicationName: string,
13
+ statuses: string[] = [],
14
+ limitPerPage: number = null,
15
+ page: number = null,
16
+ ): PromiseLike<ITask[]> {
12
17
  return REST('/applications')
13
18
  .path(applicationName, 'tasks')
14
- .query({ statuses: statuses.join(',') })
19
+ .query({
20
+ statuses: statuses.join(','),
21
+ limit: limitPerPage,
22
+ page: page,
23
+ })
15
24
  .get()
16
25
  .then((tasks: ITask[]) => {
17
26
  tasks.forEach((task) => this.setTaskProperties(task));
@@ -1,7 +0,0 @@
1
- import React from 'react';
2
- import type { IFormInputProps, OmitControlledInputPropsFrom } from './interface';
3
- interface INumberInputProps extends IFormInputProps, OmitControlledInputPropsFrom<React.InputHTMLAttributes<any>> {
4
- inputClassName?: string;
5
- }
6
- export declare function NumberConcurrencyInput(props: INumberInputProps): JSX.Element;
7
- export {};
@@ -1,29 +0,0 @@
1
- import React from 'react';
2
-
3
- import { useInternalValidator } from './hooks';
4
- import type { IFormInputProps, OmitControlledInputPropsFrom } from './interface';
5
- import { orEmptyString, validationClassName } from './utils';
6
- import type { IValidator } from '../validation';
7
- import { composeValidators, Validators } from '../validation';
8
-
9
- interface INumberInputProps extends IFormInputProps, OmitControlledInputPropsFrom<React.InputHTMLAttributes<any>> {
10
- inputClassName?: string;
11
- }
12
-
13
- const isNumber = (val: any): val is number => typeof val === 'number';
14
-
15
- export function NumberConcurrencyInput(props: INumberInputProps) {
16
- const { value, validation, inputClassName, ...otherProps } = props;
17
-
18
- const minMaxValidator: IValidator = (val: any, label?: string) => {
19
- const minValidator = isNumber(props.min) ? Validators.minValue(props.min) : undefined;
20
- const maxValidator = isNumber(props.max) ? Validators.maxValue(props.max) : undefined;
21
- const validator = composeValidators([minValidator, maxValidator]);
22
- return validator ? validator(val, label) : null;
23
- };
24
-
25
- useInternalValidator(validation, minMaxValidator);
26
-
27
- const className = `NumberInput form-control ${orEmptyString(inputClassName)} ${validationClassName(validation)}`;
28
- return <input className={className} type="number" value={orEmptyString(value)} {...otherProps} />;
29
- }