diff --git a/packages/amazon/src/aws.module.ts b/packages/amazon/src/aws.module.ts index d06b6dc7c21..1c0aca5aa88 100644 --- a/packages/amazon/src/aws.module.ts +++ b/packages/amazon/src/aws.module.ts @@ -26,22 +26,27 @@ import { AWS_LOAD_BALANCER_MODULE } from './loadBalancer/loadBalancer.module'; import amazonLogo from './logo/amazon.logo.svg'; import { AMAZON_PIPELINE_STAGES_BAKE_AWSBAKESTAGE } from './pipeline/stages/bake/awsBakeStage'; import { AMAZON_PIPELINE_STAGES_CLONESERVERGROUP_AWSCLONESERVERGROUPSTAGE } from './pipeline/stages/cloneServerGroup/awsCloneServerGroupStage'; +import { AMAZON_PIPELINE_STAGES_LAMBDA_DELETE } from './pipeline/stages/deleteLambda'; import { CLOUD_FORMATION_CHANGE_SET_INFO } from './pipeline/stages/deployCloudFormation/CloudFormationChangeSetInfo'; import { CLOUDFORMATION_TEMPLATE_ENTRY } from './pipeline/stages/deployCloudFormation/cloudFormationTemplateEntry.component'; import { DEPLOY_CLOUDFORMATION_STACK_STAGE } from './pipeline/stages/deployCloudFormation/deployCloudFormationStackStage'; import { AWS_EVALUATE_CLOUD_FORMATION_CHANGE_SET_EXECUTION_SERVICE } from './pipeline/stages/deployCloudFormation/evaluateCloudFormationChangeSetExecution.service'; +import { AMAZON_PIPELINE_STAGES_LAMBDA_DEPLOY } from './pipeline/stages/deployLambda'; import { AMAZON_PIPELINE_STAGES_DESTROYASG_AWSDESTROYASGSTAGE } from './pipeline/stages/destroyAsg/awsDestroyAsgStage'; import { AMAZON_PIPELINE_STAGES_DISABLEASG_AWSDISABLEASGSTAGE } from './pipeline/stages/disableAsg/awsDisableAsgStage'; import { AMAZON_PIPELINE_STAGES_DISABLECLUSTER_AWSDISABLECLUSTERSTAGE } from './pipeline/stages/disableCluster/awsDisableClusterStage'; import { AMAZON_PIPELINE_STAGES_ENABLEASG_AWSENABLEASGSTAGE } from './pipeline/stages/enableAsg/awsEnableAsgStage'; import { AMAZON_PIPELINE_STAGES_FINDAMI_AWSFINDAMISTAGE } from './pipeline/stages/findAmi/awsFindAmiStage'; import { AMAZON_PIPELINE_STAGES_FINDIMAGEFROMTAGS_AWSFINDIMAGEFROMTAGSSTAGE } from './pipeline/stages/findImageFromTags/awsFindImageFromTagsStage'; +import { AMAZON_PIPELINE_STAGES_LAMBDA_INVOKE } from './pipeline/stages/invokeLambda'; import { AMAZON_PIPELINE_STAGES_MODIFYSCALINGPROCESS_MODIFYSCALINGPROCESSSTAGE } from './pipeline/stages/modifyScalingProcess/modifyScalingProcessStage'; import { AMAZON_PIPELINE_STAGES_RESIZEASG_AWSRESIZEASGSTAGE } from './pipeline/stages/resizeAsg/awsResizeAsgStage'; import { AMAZON_PIPELINE_STAGES_ROLLBACKCLUSTER_AWSROLLBACKCLUSTERSTAGE } from './pipeline/stages/rollbackCluster/awsRollbackClusterStage'; +import { AMAZON_PIPELINE_STAGES_LAMBDA_ROUTE } from './pipeline/stages/routeLambda'; import { AMAZON_PIPELINE_STAGES_SCALEDOWNCLUSTER_AWSSCALEDOWNCLUSTERSTAGE } from './pipeline/stages/scaleDownCluster/awsScaleDownClusterStage'; import { AMAZON_PIPELINE_STAGES_SHRINKCLUSTER_AWSSHRINKCLUSTERSTAGE } from './pipeline/stages/shrinkCluster/awsShrinkClusterStage'; import { AMAZON_PIPELINE_STAGES_TAGIMAGE_AWSTAGIMAGESTAGE } from './pipeline/stages/tagImage/awsTagImageStage'; +import { AMAZON_PIPELINE_STAGES_LAMBDA_UPDATE } from './pipeline/stages/updateCodeLambda'; import { AWS_REACT_MODULE } from './reactShims/aws.react.module'; import { AMAZON_SEARCH_SEARCHRESULTFORMATTER } from './search/searchResultFormatter'; import { AWS_SECURITY_GROUP_MODULE } from './securityGroup/securityGroup.module'; @@ -114,6 +119,11 @@ module(AMAZON_MODULE, [ INSTANCE_SECURITY_GROUPS_COMPONENT, INSTANCE_DNS_COMPONENT, AMAZON_INSTANCE_INFORMATION_COMPONENT, + AMAZON_PIPELINE_STAGES_LAMBDA_DELETE, + AMAZON_PIPELINE_STAGES_LAMBDA_DEPLOY, + AMAZON_PIPELINE_STAGES_LAMBDA_INVOKE, + AMAZON_PIPELINE_STAGES_LAMBDA_UPDATE, + AMAZON_PIPELINE_STAGES_LAMBDA_ROUTE, ]).config(() => { CloudProviderRegistry.registerProvider('aws', { name: 'Amazon', diff --git a/packages/amazon/src/pipeline/stages/deleteLambda/index.ts b/packages/amazon/src/pipeline/stages/deleteLambda/index.ts index a2bcdd62906..9df8bc99929 100644 --- a/packages/amazon/src/pipeline/stages/deleteLambda/index.ts +++ b/packages/amazon/src/pipeline/stages/deleteLambda/index.ts @@ -1,11 +1,17 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +import { module } from 'angular'; + import { Registry, SETTINGS } from '@spinnaker/core'; -import { lambdaDeleteStage } from './LambdaDeleteStage'; +import { lambdaDeleteStage } from './LambdaDeleteStage'; export * from './LambdaDeleteStage'; -if (SETTINGS.feature.lambdaAdditionalStages) { - Registry.pipeline.registerStage(lambdaDeleteStage); -} +export const AMAZON_PIPELINE_STAGES_LAMBDA_DELETE = 'spinnaker.amazon.pipeline.stage.Aws.LambdaDeleteStage'; + +module(AMAZON_PIPELINE_STAGES_LAMBDA_DELETE, []).config(function () { + if (SETTINGS.feature.lambdaAdditionalStages) { + Registry.pipeline.registerStage(lambdaDeleteStage); + } +}); diff --git a/packages/amazon/src/pipeline/stages/deployLambda/components/AwsLambdaFunctionStageForm.tsx b/packages/amazon/src/pipeline/stages/deployLambda/components/AwsLambdaFunctionStageForm.tsx index bbe837f855b..9e03752a57b 100644 --- a/packages/amazon/src/pipeline/stages/deployLambda/components/AwsLambdaFunctionStageForm.tsx +++ b/packages/amazon/src/pipeline/stages/deployLambda/components/AwsLambdaFunctionStageForm.tsx @@ -15,7 +15,6 @@ import { TetheredCreatable, TextInput, } from '@spinnaker/core'; -import { NumberConcurrencyInput } from '@spinnaker/core/dist/presentation/forms/inputs/NumberConcurrencyInput'; import { BasicSettingsForm, ExecutionRoleForm, LambdaAtEdgeForm, NetworkForm, TriggerEventsForm } from './index'; @@ -96,7 +95,7 @@ export function AwsLambdaFunctionStageForm(props: IFormikStageConfigInjectedProp help={ } - input={(props) => } + input={(props) => } /> ) : ( - + ) } required={false} diff --git a/packages/amazon/src/pipeline/stages/routeLambda/index.ts b/packages/amazon/src/pipeline/stages/routeLambda/index.ts index 154d3d60cb2..3ae71566143 100644 --- a/packages/amazon/src/pipeline/stages/routeLambda/index.ts +++ b/packages/amazon/src/pipeline/stages/routeLambda/index.ts @@ -1,11 +1,18 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +import { module } from 'angular'; + import { Registry, SETTINGS } from '@spinnaker/core'; + import { lambdaRouteStage } from './LambdaRouteStage'; export * from './LambdaRouteStage'; -if (SETTINGS.feature.lambdaAdditionalStages) { - Registry.pipeline.registerStage(lambdaRouteStage); -} +export const AMAZON_PIPELINE_STAGES_LAMBDA_ROUTE = 'spinnaker.amazon.pipeline.stage.Aws.LambdaTrafficRoutingStage'; + +module(AMAZON_PIPELINE_STAGES_LAMBDA_ROUTE, []).config(function () { + if (SETTINGS.feature.lambdaAdditionalStages) { + Registry.pipeline.registerStage(lambdaRouteStage); + } +}); diff --git a/packages/amazon/src/pipeline/stages/updateCodeLambda/LambdaUpdateCodeStage.less b/packages/amazon/src/pipeline/stages/updateCodeLambda/LambdaUpdateCodeStage.less new file mode 100644 index 00000000000..35112591771 --- /dev/null +++ b/packages/amazon/src/pipeline/stages/updateCodeLambda/LambdaUpdateCodeStage.less @@ -0,0 +1,5 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +.LambdaCodeUpdateStageConfig { +} diff --git a/packages/amazon/src/pipeline/stages/updateCodeLambda/index.ts b/packages/amazon/src/pipeline/stages/updateCodeLambda/index.ts index f8f0dccc76a..65297db27d3 100644 --- a/packages/amazon/src/pipeline/stages/updateCodeLambda/index.ts +++ b/packages/amazon/src/pipeline/stages/updateCodeLambda/index.ts @@ -1,11 +1,18 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +import { module } from 'angular'; + import { Registry, SETTINGS } from '@spinnaker/core'; + import { lambdaUpdateCodeStage } from './LambdaUpdateCodeStage'; export * from './LambdaUpdateCodeStage'; -if (SETTINGS.feature.lambdaAdditionalStages) { - Registry.pipeline.registerStage(lambdaUpdateCodeStage); -} +export const AMAZON_PIPELINE_STAGES_LAMBDA_UPDATE = 'spinnaker.amazon.pipeline.stage.Aws.LambdaUpdateCodeStage'; + +module(AMAZON_PIPELINE_STAGES_LAMBDA_UPDATE, []).config(function () { + if (SETTINGS.feature.lambdaAdditionalStages) { + Registry.pipeline.registerStage(lambdaUpdateCodeStage); + } +}); diff --git a/packages/core/src/config/settings.ts b/packages/core/src/config/settings.ts index 95c2d60cbed..0b009d37e29 100644 --- a/packages/core/src/config/settings.ts +++ b/packages/core/src/config/settings.ts @@ -141,6 +141,7 @@ export interface ISpinnakerSettings { }; stashTriggerInfo?: string; pollSchedule: number; + tasksViewLimitPerPage: number; providers?: { [key: string]: IProviderSettings; // allows custom providers not typed in here (good for testing too) }; diff --git a/packages/core/src/deploymentStrategy/strategies/redblack/AdditionalFields.tsx b/packages/core/src/deploymentStrategy/strategies/redblack/AdditionalFields.tsx index 1142429eb42..b243a78d22c 100644 --- a/packages/core/src/deploymentStrategy/strategies/redblack/AdditionalFields.tsx +++ b/packages/core/src/deploymentStrategy/strategies/redblack/AdditionalFields.tsx @@ -8,72 +8,101 @@ export interface IRedBlackStrategyAdditionalFieldsProps extends IDeploymentStrat command: IRedBlackCommand; } -export const AdditionalFields = ({ command, onChange }: IRedBlackStrategyAdditionalFieldsProps) => ( -
-
- -
-
- -
-
- - onChange('maxRemainingAsgs', e.target.value)} - min="2" - /> -
-
- - onChange('delayBeforeDisableSec', e.target.value)} - placeholder="0" - /> - seconds -
- {command.scaleDown && ( -
- - onChange('delayBeforeScaleDownSec', e.target.value)} - placeholder="0" - /> - seconds +export class AdditionalFields extends React.Component { + private rollbackOnFailureChange = (e: React.ChangeEvent) => { + this.props.command.rollback.onFailure = e.target.checked; + this.forceUpdate(); + }; + + private scaleDownChange = (e: React.ChangeEvent) => { + this.props.command.scaleDown = e.target.checked; + this.forceUpdate(); + }; + + private maxRemainingAsgsChange = (e: React.ChangeEvent) => { + this.props.command.maxRemainingAsgs = parseInt(e.target.value, 10); + this.forceUpdate(); + }; + + private delayBeforeDisableSecChange = (e: React.ChangeEvent) => { + this.props.command.delayBeforeDisableSec = parseInt(e.target.value, 10); + this.forceUpdate(); + }; + + private delayBeforeScaleDownSecChange = (e: React.ChangeEvent) => { + this.props.command.delayBeforeScaleDownSec = parseInt(e.target.value, 10); + this.forceUpdate(); + }; + + public render() { + const { command } = this.props; + return ( +
+
+ +
+
+ +
+
+ + +
+
+ + + seconds +
+ {command.scaleDown && ( +
+ + + seconds +
+ )}
- )} -
-); + ); + } +} diff --git a/packages/core/src/pipeline/config/services/PipelineConfigService.ts b/packages/core/src/pipeline/config/services/PipelineConfigService.ts index 0d32a9943ae..8fd8c6d5331 100644 --- a/packages/core/src/pipeline/config/services/PipelineConfigService.ts +++ b/packages/core/src/pipeline/config/services/PipelineConfigService.ts @@ -142,6 +142,9 @@ export class PipelineConfigService { private static groupStagesByRequisiteStageRefIds(pipeline: IPipeline) { return pipeline.stages.reduce((acc, obj) => { const parent = obj['refId']; + if (obj['requisiteStageRefIds'] === undefined) { + obj['requisiteStageRefIds'] = []; + } obj.requisiteStageRefIds.forEach((child) => { const values = acc.get(child); if (values && values.length) { diff --git a/packages/core/src/pipeline/config/stages/overrideTimeout/OverrideTimeout.tsx b/packages/core/src/pipeline/config/stages/overrideTimeout/OverrideTimeout.tsx index 8b31a7e9696..841092e09d1 100644 --- a/packages/core/src/pipeline/config/stages/overrideTimeout/OverrideTimeout.tsx +++ b/packages/core/src/pipeline/config/stages/overrideTimeout/OverrideTimeout.tsx @@ -1,4 +1,4 @@ -import { get } from 'lodash'; +import { get, isNumber } from 'lodash'; import { Duration } from 'luxon'; import React from 'react'; @@ -10,7 +10,7 @@ const { useEffect, useState } = React; export interface IOverrideTimeoutConfigProps { stageConfig: IStageConfig; - stageTimeoutMs: number; + stageTimeoutMs: number | any; updateStageField: (changes: Partial) => void; } @@ -41,8 +41,13 @@ export const OverrideTimeout = (props: IOverrideTimeoutConfigProps) => { }, [props.stageTimeoutMs]); const stageChanged = () => { - if (props.stageTimeoutMs !== undefined) { + if (props.stageTimeoutMs !== undefined && !isExpression) { enableTimeout(); + } else if (props.stageTimeoutMs !== undefined && isExpression) { + setOverrideTimeout(true); + props.updateStageField({ + stageTimeoutMs: props.stageTimeoutMs || null, + }); } else { clearTimeout(); } @@ -74,6 +79,10 @@ export const OverrideTimeout = (props: IOverrideTimeoutConfigProps) => { }; const isConfigurable = !!get(props.stageConfig, 'supportsCustomTimeout'); + const isExpression = + props.stageTimeoutMs !== undefined && props.stageTimeoutMs !== null && !isNumber(props.stageTimeoutMs) + ? props.stageTimeoutMs.includes('${') + : false; if (isConfigurable) { return ( @@ -94,7 +103,7 @@ export const OverrideTimeout = (props: IOverrideTimeoutConfigProps) => {
- {overrideTimeout && ( + {overrideTimeout && !isExpression && (
@@ -123,6 +132,17 @@ export const OverrideTimeout = (props: IOverrideTimeoutConfigProps) => {
)} + {overrideTimeout && isExpression && ( +
+
+
+ + Resolved at runtime from expression: {props.stageTimeoutMs} + +
+
+
+ )} ); } else { diff --git a/packages/core/src/pipeline/config/validation/PipelineConfigValidator.ts b/packages/core/src/pipeline/config/validation/PipelineConfigValidator.ts index 19c2f7f3dcc..2e37c59b415 100644 --- a/packages/core/src/pipeline/config/validation/PipelineConfigValidator.ts +++ b/packages/core/src/pipeline/config/validation/PipelineConfigValidator.ts @@ -54,6 +54,10 @@ export interface ICustomValidator extends IStageOrTriggerValidator, IValidatorCo [k: string]: any; } +function isNumberOrSpel(valInput: any) { + return (isNumber(valInput) && valInput > 0) || (typeof valInput === 'string' && valInput.includes('${')); +} + export class PipelineConfigValidator { private static validators: Map = new Map(); private static validationStream: Subject = new Subject(); @@ -151,7 +155,7 @@ export class PipelineConfigValidator { ); } - if (stage.stageTimeoutMs !== undefined && !(isNumber(stage.stageTimeoutMs) && stage.stageTimeoutMs > 0)) { + if (stage.stageTimeoutMs !== undefined && !isNumberOrSpel(stage.stageTimeoutMs)) { stageValidations.set(stage, [ ...(stageValidations.get(stage) || []), 'Stage is configured to fail after a specific amount of time, but no time is set.', diff --git a/packages/core/src/presentation/forms/inputs/NumberConcurrencyInput.tsx b/packages/core/src/presentation/forms/inputs/NumberConcurrencyInput.tsx deleted file mode 100644 index 81970637415..00000000000 --- a/packages/core/src/presentation/forms/inputs/NumberConcurrencyInput.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import React from 'react'; - -import { useInternalValidator } from './hooks'; -import type { IFormInputProps, OmitControlledInputPropsFrom } from './interface'; -import { orEmptyString, validationClassName } from './utils'; -import type { IValidator } from '../validation'; -import { composeValidators, Validators } from '../validation'; - -interface INumberInputProps extends IFormInputProps, OmitControlledInputPropsFrom> { - inputClassName?: string; -} - -const isNumber = (val: any): val is number => typeof val === 'number'; - -export function NumberConcurrencyInput(props: INumberInputProps) { - const { value, validation, inputClassName, ...otherProps } = props; - - const minMaxValidator: IValidator = (val: any, label?: string) => { - const minValidator = isNumber(props.min) ? Validators.minValue(props.min) : undefined; - const maxValidator = isNumber(props.max) ? Validators.maxValue(props.max) : undefined; - const validator = composeValidators([minValidator, maxValidator]); - return validator ? validator(val, label) : null; - }; - - useInternalValidator(validation, minMaxValidator); - - const className = `NumberInput form-control ${orEmptyString(inputClassName)} ${validationClassName(validation)}`; - return ; -} diff --git a/packages/core/src/task/task.dataSource.js b/packages/core/src/task/task.dataSource.js index 03f9ce7dc54..515e1a9c771 100644 --- a/packages/core/src/task/task.dataSource.js +++ b/packages/core/src/task/task.dataSource.js @@ -2,6 +2,7 @@ import * as angular from 'angular'; import { ApplicationDataSourceRegistry } from '../application/service/ApplicationDataSourceRegistry'; import { CLUSTER_SERVICE } from '../cluster/cluster.service'; +import { SETTINGS } from '../config'; import { TaskReader } from './task.read.service'; export const CORE_TASK_TASK_DATASOURCE = 'spinnaker.core.task.dataSource'; @@ -14,8 +15,23 @@ angular.module(CORE_TASK_TASK_DATASOURCE, [CLUSTER_SERVICE]).run([ return $q.when(angular.isArray(tasks) ? tasks : []); }; - const loadTasks = (application) => { - return TaskReader.getTasks(application.name); + const loadPaginatedTasks = async (application, page = 1) => { + let limitPerPage = SETTINGS.tasksViewLimitPerPage; + const tasks = await TaskReader.getTasks(application.name, [], limitPerPage, page); + if (tasks.length === limitPerPage) { + return tasks.concat(await loadPaginatedTasks(application, page + 1)); + } else { + return tasks; + } + }; + + const loadTasks = (application, page = 1) => { + let limitPerPage = SETTINGS.tasksViewLimitPerPage; + if (limitPerPage === undefined) { + return TaskReader.getTasks(application.name); + } else { + return loadPaginatedTasks(application, page); + } }; const loadRunningTasks = (application) => { diff --git a/packages/core/src/task/task.read.service.ts b/packages/core/src/task/task.read.service.ts index efb273b3edc..14662fc5ac6 100644 --- a/packages/core/src/task/task.read.service.ts +++ b/packages/core/src/task/task.read.service.ts @@ -8,10 +8,19 @@ import { OrchestratedItemTransformer } from '../orchestratedItem/orchestratedIte export class TaskReader { private static activeStatuses: string[] = ['RUNNING', 'SUSPENDED', 'NOT_STARTED']; - public static getTasks(applicationName: string, statuses: string[] = []): PromiseLike { + public static getTasks( + applicationName: string, + statuses: string[] = [], + limitPerPage: number = null, + page: number = null, + ): PromiseLike { return REST('/applications') .path(applicationName, 'tasks') - .query({ statuses: statuses.join(',') }) + .query({ + statuses: statuses.join(','), + limit: limitPerPage, + page: page, + }) .get() .then((tasks: ITask[]) => { tasks.forEach((task) => this.setTaskProperties(task)); diff --git a/yarn.lock b/yarn.lock index 3d3034a9dea..c75f48e9d1a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18292,10 +18292,10 @@ vite-plugin-svgr@^0.3.0: dependencies: "@svgr/core" "^5.5.0" -vite@2.9.16: - version "2.9.16" - resolved "https://registry.yarnpkg.com/vite/-/vite-2.9.16.tgz#daf7ba50f5cc37a7bf51b118ba06bc36e97898e9" - integrity sha512-X+6q8KPyeuBvTQV8AVSnKDvXoBMnTx8zxh54sOwmmuOdxkjMmEJXH2UEchA+vTMps1xw9vL64uwJOWryULg7nA== +vite@2.9.18: + version "2.9.18" + resolved "https://registry.yarnpkg.com/vite/-/vite-2.9.18.tgz#74e2a83b29da81e602dac4c293312cc575f091c7" + integrity sha512-sAOqI5wNM9QvSEE70W3UGMdT8cyEn0+PmJMTFvTB8wB0YbYUWw3gUbY62AOyrXosGieF2htmeLATvNxpv/zNyQ== dependencies: esbuild "^0.14.27" postcss "^8.4.13"