From 9c8a83aa256432814eb04685ecad258f6e5c6407 Mon Sep 17 00:00:00 2001 From: pankhuri srivastava Date: Wed, 8 Jun 2022 18:47:26 +0530 Subject: [PATCH] added cloudrun changes --- halconfig/settings.js | 8 + karma-shim.js | 3 + karma.conf.js | 2 + package.json | 1 + packages/app/src/app.ts | 2 + packages/app/src/settings.js | 8 + packages/cloudrun/.npmignore | 4 + packages/cloudrun/package.json | 53 ++++ packages/cloudrun/src/cloudrun.module.ts | 68 +++++ packages/cloudrun/src/cloudrun.settings.ts | 14 ++ .../cloudrun/src/common/cloudrunHealth.ts | 3 + ...onditionalDescriptionListItem.component.ts | 37 +++ .../src/common/domain/ICloudrunInstance.ts | 21 ++ .../common/domain/ICloudrunLoadBalancer.ts | 18 ++ packages/cloudrun/src/common/domain/index.ts | 2 + .../common/loadBalancerMessage.component.html | 31 +++ .../common/loadBalancerMessage.component.ts | 13 + packages/cloudrun/src/help/cloudrun.help.ts | 67 +++++ packages/cloudrun/src/index.ts | 3 + .../instance/details/details.controller.ts | 113 +++++++++ .../src/instance/details/details.html | 71 ++++++ packages/cloudrun/src/interfaces/index.ts | 1 + .../src/interfaces/infrastructure.types.ts | 23 ++ .../allocationConfigurationRow.component.ts | 70 ++++++ .../wizard/basicSettings.component.html | 43 ++++ .../wizard/basicSettings.component.ts | 88 +++++++ ...eAllocationConfigurationRow.component.html | 27 ++ ...ageAllocationConfigurationRow.component.ts | 88 +++++++ .../configure/wizard/wizard.controller.ts | 157 ++++++++++++ .../loadBalancer/configure/wizard/wizard.html | 39 +++ .../loadBalancer/configure/wizard/wizard.less | 17 ++ .../details/details.controller.ts | 140 +++++++++++ .../src/loadBalancer/details/details.html | 81 ++++++ packages/cloudrun/src/loadBalancer/index.ts | 3 + .../loadBalancer/loadBalancerTransformer.ts | 175 +++++++++++++ packages/cloudrun/src/logo/cloudrun.icon.svg | 64 +++++ packages/cloudrun/src/logo/cloudrun.logo.less | 6 + packages/cloudrun/src/logo/cloudrun.logo.png | Bin 0 -> 3516 bytes .../cloudrun/src/pipeline/pipeline.module.ts | 6 + .../cloudrunEditLoadBalancerStage.ts | 74 ++++++ .../editLoadBalancerExecutionDetails.html | 33 +++ .../editLoadBalancerStage.html | 51 ++++ .../loadBalancerChoice.modal.controller.ts | 65 +++++ .../loadBalancerChoice.modal.html | 53 ++++ .../serverGroupCommandBuilder.service.ts | 236 ++++++++++++++++++ .../configure/wizard/BasicSettings.tsx | 109 ++++++++ .../configure/wizard/ConfigFiles.tsx | 98 ++++++++ .../configure/wizard/serverGroupWizard.tsx | 135 ++++++++++ .../serverGroup/details/details.controller.ts | 234 +++++++++++++++++ .../src/serverGroup/details/details.html | 88 +++++++ packages/cloudrun/src/serverGroup/index.ts | 3 + .../serverGroupTransformer.service.ts | 65 +++++ packages/cloudrun/tsconfig.json | 10 + scripts/buildModules.js | 1 + scripts/build_order.sh | 1 + scripts/bumpPackage.js | 1 + tsconfig.json | 3 + 57 files changed, 2830 insertions(+) create mode 100644 packages/cloudrun/.npmignore create mode 100644 packages/cloudrun/package.json create mode 100644 packages/cloudrun/src/cloudrun.module.ts create mode 100644 packages/cloudrun/src/cloudrun.settings.ts create mode 100644 packages/cloudrun/src/common/cloudrunHealth.ts create mode 100644 packages/cloudrun/src/common/conditionalDescriptionListItem.component.ts create mode 100644 packages/cloudrun/src/common/domain/ICloudrunInstance.ts create mode 100644 packages/cloudrun/src/common/domain/ICloudrunLoadBalancer.ts create mode 100644 packages/cloudrun/src/common/domain/index.ts create mode 100644 packages/cloudrun/src/common/loadBalancerMessage.component.html create mode 100644 packages/cloudrun/src/common/loadBalancerMessage.component.ts create mode 100644 packages/cloudrun/src/help/cloudrun.help.ts create mode 100644 packages/cloudrun/src/index.ts create mode 100644 packages/cloudrun/src/instance/details/details.controller.ts create mode 100644 packages/cloudrun/src/instance/details/details.html create mode 100644 packages/cloudrun/src/interfaces/index.ts create mode 100644 packages/cloudrun/src/interfaces/infrastructure.types.ts create mode 100644 packages/cloudrun/src/loadBalancer/configure/wizard/allocationConfigurationRow.component.ts create mode 100644 packages/cloudrun/src/loadBalancer/configure/wizard/basicSettings.component.html create mode 100644 packages/cloudrun/src/loadBalancer/configure/wizard/basicSettings.component.ts create mode 100644 packages/cloudrun/src/loadBalancer/configure/wizard/stageAllocationConfigurationRow.component.html create mode 100644 packages/cloudrun/src/loadBalancer/configure/wizard/stageAllocationConfigurationRow.component.ts create mode 100644 packages/cloudrun/src/loadBalancer/configure/wizard/wizard.controller.ts create mode 100644 packages/cloudrun/src/loadBalancer/configure/wizard/wizard.html create mode 100644 packages/cloudrun/src/loadBalancer/configure/wizard/wizard.less create mode 100644 packages/cloudrun/src/loadBalancer/details/details.controller.ts create mode 100644 packages/cloudrun/src/loadBalancer/details/details.html create mode 100644 packages/cloudrun/src/loadBalancer/index.ts create mode 100644 packages/cloudrun/src/loadBalancer/loadBalancerTransformer.ts create mode 100644 packages/cloudrun/src/logo/cloudrun.icon.svg create mode 100644 packages/cloudrun/src/logo/cloudrun.logo.less create mode 100644 packages/cloudrun/src/logo/cloudrun.logo.png create mode 100644 packages/cloudrun/src/pipeline/pipeline.module.ts create mode 100644 packages/cloudrun/src/pipeline/stages/editLoadBalancer/cloudrunEditLoadBalancerStage.ts create mode 100644 packages/cloudrun/src/pipeline/stages/editLoadBalancer/editLoadBalancerExecutionDetails.html create mode 100644 packages/cloudrun/src/pipeline/stages/editLoadBalancer/editLoadBalancerStage.html create mode 100644 packages/cloudrun/src/pipeline/stages/editLoadBalancer/loadBalancerChoice.modal.controller.ts create mode 100644 packages/cloudrun/src/pipeline/stages/editLoadBalancer/loadBalancerChoice.modal.html create mode 100644 packages/cloudrun/src/serverGroup/configure/serverGroupCommandBuilder.service.ts create mode 100644 packages/cloudrun/src/serverGroup/configure/wizard/BasicSettings.tsx create mode 100644 packages/cloudrun/src/serverGroup/configure/wizard/ConfigFiles.tsx create mode 100644 packages/cloudrun/src/serverGroup/configure/wizard/serverGroupWizard.tsx create mode 100644 packages/cloudrun/src/serverGroup/details/details.controller.ts create mode 100644 packages/cloudrun/src/serverGroup/details/details.html create mode 100644 packages/cloudrun/src/serverGroup/index.ts create mode 100644 packages/cloudrun/src/serverGroup/serverGroupTransformer.service.ts create mode 100644 packages/cloudrun/tsconfig.json diff --git a/halconfig/settings.js b/halconfig/settings.js index bf64cee85b8..823d6d6b164 100644 --- a/halconfig/settings.js +++ b/halconfig/settings.js @@ -37,6 +37,13 @@ var appengine = { account: '{%appengine.default.account%}', }, }; + +var cloudrun = { + defaults: { + account: '{%cloudrun.default.account%}', + }, +}; + var oracle = { defaults: { account: '{%oracle.default.account%}', @@ -147,6 +154,7 @@ window.spinnakerSettings = { gce: gce, huaweicloud: huaweicloud, kubernetes: {}, + cloudrun: {}, oracle: oracle, tencentcloud: tencentcloud, }, diff --git a/karma-shim.js b/karma-shim.js index 6aa2110839b..4b17f53bade 100644 --- a/karma-shim.js +++ b/karma-shim.js @@ -63,3 +63,6 @@ testContext.keys().forEach(testContext); testContext = require.context('./packages/titus/src', true, /\.spec\.(js|ts|tsx)$/); testContext.keys().forEach(testContext); + +testContext = require.context('./packages/cloudrun/src', true, /\.spec\.(js|ts|tsx)$/); +testContext.keys().forEach(testContext); diff --git a/karma.conf.js b/karma.conf.js index c5252fd14bd..47ce72bce4f 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -52,6 +52,8 @@ const webpackConfig = { '@spinnaker/tencentcloud': path.resolve(`${MODULES_ROOT}/tencentcloud/src`), titus: path.resolve(`${MODULES_ROOT}/titus/src`), '@spinnaker/titus': path.resolve(`${MODULES_ROOT}/titus/src`), + cloudrun: path.resolve(`${MODULES_ROOT}/cloudrun/src`), + '@spinnaker/cloudrun': path.resolve(`${MODULES_ROOT}/cloudrun/src`), }, }, plugins: [new ForkTsCheckerWebpackPlugin({ checkSyntacticErrors: true })], diff --git a/package.json b/package.json index 9b50d6be674..a9e4e66c274 100644 --- a/package.json +++ b/package.json @@ -81,6 +81,7 @@ "workspaces": [ "packages/app", "packages/amazon", + "packages/cloudrun", "packages/appengine", "packages/azure", "packages/cloudfoundry", diff --git a/packages/app/src/app.ts b/packages/app/src/app.ts index 86c6d8fa28d..6a919e4bf26 100644 --- a/packages/app/src/app.ts +++ b/packages/app/src/app.ts @@ -11,6 +11,7 @@ import { AZURE_MODULE } from '@spinnaker/azure'; import { GOOGLE_MODULE } from '@spinnaker/google'; import { CANARY_MODULE } from './canary/canary.module'; import { KUBERNETES_MODULE } from '@spinnaker/kubernetes'; +import { CLOUDRUN_MODULE } from '@spinnaker/cloudrun'; import { ORACLE_MODULE } from '@spinnaker/oracle'; import { KAYENTA_MODULE } from '@spinnaker/kayenta'; import { TITUS_MODULE } from '@spinnaker/titus'; @@ -23,6 +24,7 @@ module('netflix.spinnaker', [ AZURE_MODULE, GOOGLE_MODULE, ECS_MODULE, + CLOUDRUN_MODULE, DOCKER_MODULE, ORACLE_MODULE, APPENGINE_MODULE, diff --git a/packages/app/src/settings.js b/packages/app/src/settings.js index 6e25d5fe98d..4867b46bb86 100644 --- a/packages/app/src/settings.js +++ b/packages/app/src/settings.js @@ -94,6 +94,7 @@ window.spinnakerSettings = { 'gce', 'huaweicloud', 'kubernetes', + 'cloudrun', 'oracle', 'tencentcloud', ], @@ -174,6 +175,12 @@ window.spinnakerSettings = { account: 'my-appengine-account', }, }, + + cloudrun: { + defaults: { + account: 'my-cloudrun-account', + }, + }, aws: { defaults: { account: 'test', @@ -247,6 +254,7 @@ window.spinnakerSettings = { }, }, kubernetes: {}, + oracle: { defaults: { account: 'DEFAULT', diff --git a/packages/cloudrun/.npmignore b/packages/cloudrun/.npmignore new file mode 100644 index 00000000000..7879f9c4b4a --- /dev/null +++ b/packages/cloudrun/.npmignore @@ -0,0 +1,4 @@ +yalc.* +.* +tsconfig.json +webpack.config.js diff --git a/packages/cloudrun/package.json b/packages/cloudrun/package.json new file mode 100644 index 00000000000..824c3f09530 --- /dev/null +++ b/packages/cloudrun/package.json @@ -0,0 +1,53 @@ +{ + "name": "@spinnaker/cloudrun", + "license": "Apache-2.0", + "version": "0.2.13", + "module": "dist/index.js", + "typings": "dist/index.d.ts", + "scripts": { + "clean": "shx rm -rf dist", + "prepublishOnly": "npm run build", + "build": "npm run clean && spinnaker-scripts build", + "dev": "spinnaker-scripts start", + "dev:push": "spinnaker-scripts start --push", + "lib": "npm run build" + }, + "dependencies": { + "@spinnaker/core": "^0.19.4", + "@uirouter/angularjs": "1.0.26", + "@uirouter/react": "1.0.7", + "angular": "1.6.10", + "angular-ui-bootstrap": "2.5.0", + "brace": "0.11.1", + "dompurify": "2.0.17", + "enzyme": "3.10.0", + "formik": "1.5.1", + "js-yaml": "3.13.1", + "lodash": "4.17.21", + "luxon": "1.23.0", + "ngimport": "0.6.1", + "react": "16.14.0", + "react-ace": "6.4.0", + "react-bootstrap": "0.32.1", + "react-ga": "2.4.1", + "react-select": "1.2.1", + "react2angular": "3.2.1", + "rxjs": "6.6.7" + }, + "devDependencies": { + "@spinnaker/eslint-plugin": "^3.0.1", + "@spinnaker/scripts": "^0.2.5", + "@types/angular": "1.6.26", + "@types/angular-ui-bootstrap": "0.13.41", + "@types/dompurify": "0.0.29", + "@types/enzyme": "3.10.3", + "@types/js-yaml": "3.5.30", + "@types/lodash": "4.14.64", + "@types/luxon": "1.11.1", + "@types/react": "16.14.10", + "@types/react-bootstrap": "0.32.5", + "@types/react-select": "1.3.4", + "shx": "0.3.3", + "typescript": "4.3.5" + } +} diff --git a/packages/cloudrun/src/cloudrun.module.ts b/packages/cloudrun/src/cloudrun.module.ts new file mode 100644 index 00000000000..4a559cd60bc --- /dev/null +++ b/packages/cloudrun/src/cloudrun.module.ts @@ -0,0 +1,68 @@ +import { module } from 'angular'; + +import { CloudProviderRegistry } from '@spinnaker/core'; + +import { CLOUDRUN_LOAD_BALANCER_CREATE_MESSAGE } from './common/loadBalancerMessage.component'; +import './help/cloudrun.help'; +import { CLOUDRUN_INSTANCE_DETAILS_CTRL } from './instance/details/details.controller'; +import { CLOUDRUN_ALLOCATION_CONFIGURATION_ROW } from './loadBalancer/configure/wizard/allocationConfigurationRow.component'; +import { CLOUDRUN_LOAD_BALANCER_BASIC_SETTINGS } from './loadBalancer/configure/wizard/basicSettings.component'; +import { CLOUDRUN_STAGE_ALLOCATION_CONFIGURATION_ROW } from './loadBalancer/configure/wizard/stageAllocationConfigurationRow.component'; +import { CLOUDRUN_LOAD_BALANCER_WIZARD_CTRL } from './loadBalancer/configure/wizard/wizard.controller'; +import { CLOUDRUN_LOAD_BALANCER_DETAILS_CTRL } from './loadBalancer/details/details.controller'; +import { CLOUDRUN_LOAD_BALANCER_TRANSFORMER } from './loadBalancer/loadBalancerTransformer'; +import logo from './logo/cloudrun.logo.png'; +import { CLOUDRUN_PIPELINE_MODULE } from './pipeline/pipeline.module'; +import { CLOUDRUN_SERVER_GROUP_COMMAND_BUILDER } from './serverGroup/configure/serverGroupCommandBuilder.service'; +import { ServerGroupWizard } from './serverGroup/configure/wizard/serverGroupWizard'; +import { CLOUDRUN_SERVER_GROUP_DETAILS_CTRL } from './serverGroup/details/details.controller'; +import { CLOUDRUN_SERVER_GROUP_TRANSFORMER } from './serverGroup/serverGroupTransformer.service'; + +import './logo/cloudrun.logo.less'; + +export const CLOUDRUN_MODULE = 'spinnaker.cloudrun'; + +const requires = [ + CLOUDRUN_SERVER_GROUP_COMMAND_BUILDER, + CLOUDRUN_SERVER_GROUP_DETAILS_CTRL, + CLOUDRUN_SERVER_GROUP_TRANSFORMER, + CLOUDRUN_LOAD_BALANCER_TRANSFORMER, + CLOUDRUN_LOAD_BALANCER_DETAILS_CTRL, + CLOUDRUN_LOAD_BALANCER_WIZARD_CTRL, + CLOUDRUN_LOAD_BALANCER_CREATE_MESSAGE, + CLOUDRUN_ALLOCATION_CONFIGURATION_ROW, + CLOUDRUN_LOAD_BALANCER_BASIC_SETTINGS, + CLOUDRUN_STAGE_ALLOCATION_CONFIGURATION_ROW, + CLOUDRUN_PIPELINE_MODULE, + CLOUDRUN_INSTANCE_DETAILS_CTRL, +]; + +module(CLOUDRUN_MODULE, requires).config(() => { + CloudProviderRegistry.registerProvider('cloudrun', { + name: 'cloudrun', + //adHocInfrastructureWritesEnabled: SETTINGS.cloudrunAdHocInfraWritesEnabled, + logo: { + path: logo, + }, + + instance: { + detailsTemplateUrl: require('./instance/details/details.html'), + detailsController: 'cloudrunInstanceDetailsCtrl', + }, + serverGroup: { + CloneServerGroupModal: ServerGroupWizard, + commandBuilder: 'cloudrunV2ServerGroupCommandBuilder', + detailsController: 'cloudrunV2ServerGroupDetailsCtrl', + detailsTemplateUrl: require('./serverGroup/details/details.html'), + transformer: 'cloudrunV2ServerGroupTransformer', + }, + + loadBalancer: { + transformer: 'cloudrunLoadBalancerTransformer', + createLoadBalancerTemplateUrl: require('./loadBalancer/configure/wizard/wizard.html'), + createLoadBalancerController: 'cloudrunLoadBalancerWizardCtrl', + detailsTemplateUrl: require('./loadBalancer/details/details.html'), + detailsController: 'cloudrunLoadBalancerDetailsCtrl', + }, + }); +}); diff --git a/packages/cloudrun/src/cloudrun.settings.ts b/packages/cloudrun/src/cloudrun.settings.ts new file mode 100644 index 00000000000..f3b8cc0b080 --- /dev/null +++ b/packages/cloudrun/src/cloudrun.settings.ts @@ -0,0 +1,14 @@ +import type { IProviderSettings } from '@spinnaker/core'; +import { SETTINGS } from '@spinnaker/core'; + +export interface ICloudrunProviderSettings extends IProviderSettings { + defaults: { + account?: string; + }; +} + +export const CloudrunProviderSettings: ICloudrunProviderSettings = (SETTINGS.providers + .cloudrun as ICloudrunProviderSettings) || { defaults: {} }; +if (CloudrunProviderSettings) { + CloudrunProviderSettings.resetToOriginal = SETTINGS.resetProvider('cloudrun'); +} diff --git a/packages/cloudrun/src/common/cloudrunHealth.ts b/packages/cloudrun/src/common/cloudrunHealth.ts new file mode 100644 index 00000000000..44305da87ed --- /dev/null +++ b/packages/cloudrun/src/common/cloudrunHealth.ts @@ -0,0 +1,3 @@ +export class CloudrunHealth { + public static PLATFORM = 'Cloudrun Service'; +} diff --git a/packages/cloudrun/src/common/conditionalDescriptionListItem.component.ts b/packages/cloudrun/src/common/conditionalDescriptionListItem.component.ts new file mode 100644 index 00000000000..59f72a77066 --- /dev/null +++ b/packages/cloudrun/src/common/conditionalDescriptionListItem.component.ts @@ -0,0 +1,37 @@ +import type { IComponentOptions, IController, IFilterService } from 'angular'; +import { module } from 'angular'; + +class CloudrunConditionalDescriptionListItemCtrl implements IController { + public label: string; + public key: string; + public component: any; + + public static $inject = ['$filter']; + constructor(private $filter: IFilterService) {} + + public $onInit(): void { + if (!this.label) { + this.label = this.$filter('robotToHuman')(this.key); + } + } +} + +const cloudrunConditionalDescriptionListItem: IComponentOptions = { + bindings: { label: '@', key: '@', component: '<' }, + transclude: { + keyLabel: '?keyText', + valueLabel: '?valueLabel', + }, + template: ` +
{{$ctrl.label}}
+
{{$ctrl.component[$ctrl.key]}}
+ `, + controller: CloudrunConditionalDescriptionListItemCtrl, +}; + +export const CLOUDRUN_CONDITIONAL_DESCRIPTION_LIST_ITEM = 'spinnaker.cloudrun.conditionalDescriptionListItem'; + +module(CLOUDRUN_CONDITIONAL_DESCRIPTION_LIST_ITEM, []).component( + 'cloudrunConditionalDtDd', + cloudrunConditionalDescriptionListItem, +); diff --git a/packages/cloudrun/src/common/domain/ICloudrunInstance.ts b/packages/cloudrun/src/common/domain/ICloudrunInstance.ts new file mode 100644 index 00000000000..d2805b27fca --- /dev/null +++ b/packages/cloudrun/src/common/domain/ICloudrunInstance.ts @@ -0,0 +1,21 @@ +import type { IInstance } from '@spinnaker/core'; + +export interface ICloudrunInstance extends IInstance { + name: string; + id: string; + account?: string; + region?: string; + instanceStatus: 'DYNAMIC' | 'RESIDENT' | 'UNKNOWN'; + launchTime: number; + loadBalancers: string[]; + serverGroup: string; + vmDebugEnabled: boolean; + vmName: string; + vmStatus: string; + vmZoneName: string; + qps: number; + healthState: string; + cloudProvider: string; + errors: number; + averageLatency: number; +} diff --git a/packages/cloudrun/src/common/domain/ICloudrunLoadBalancer.ts b/packages/cloudrun/src/common/domain/ICloudrunLoadBalancer.ts new file mode 100644 index 00000000000..a8845c5aef4 --- /dev/null +++ b/packages/cloudrun/src/common/domain/ICloudrunLoadBalancer.ts @@ -0,0 +1,18 @@ +import type { ILoadBalancer } from '@spinnaker/core'; + +export interface ICloudrunLoadBalancer extends ILoadBalancer { + credentials?: string; + split?: ICloudrunTrafficSplit; + migrateTraffic: boolean; + dispatchRules?: ICloudrunDispatchRule[]; +} + +export interface ICloudrunTrafficSplit { + trafficTargets: [{ revisionName: string; percent: number }]; +} + +export interface ICloudrunDispatchRule { + domain: string; + path: string; + service: string; +} diff --git a/packages/cloudrun/src/common/domain/index.ts b/packages/cloudrun/src/common/domain/index.ts new file mode 100644 index 00000000000..0f4f8e82793 --- /dev/null +++ b/packages/cloudrun/src/common/domain/index.ts @@ -0,0 +1,2 @@ +export * from './ICloudrunLoadBalancer'; +export * from './ICloudrunInstance'; diff --git a/packages/cloudrun/src/common/loadBalancerMessage.component.html b/packages/cloudrun/src/common/loadBalancerMessage.component.html new file mode 100644 index 00000000000..035cc21727a --- /dev/null +++ b/packages/cloudrun/src/common/loadBalancerMessage.component.html @@ -0,0 +1,31 @@ +
+
+
+

+ Spinnaker cannot create a load balancer for Cloudrun. + A Spinnaker load balancer maps to an Cloudrun service, which is specified in a version's + app.yaml. +

+

For example, the following app.yaml

+
+  			runtime: python27
+  			api_version: 1
+        …
+  			service: mobile
+        …
+  		
+

+ deploys to the mobile service. If you do not specify a service, your version will be deployed to + the default service. +

+

+ If a service does not exist when a version is deployed, it will be created. It will then be editable as a load + balancer within Spinnaker. +

+

+ Cloudrun Documentation +

+
+
+
diff --git a/packages/cloudrun/src/common/loadBalancerMessage.component.ts b/packages/cloudrun/src/common/loadBalancerMessage.component.ts new file mode 100644 index 00000000000..9787e473fb9 --- /dev/null +++ b/packages/cloudrun/src/common/loadBalancerMessage.component.ts @@ -0,0 +1,13 @@ +import { module } from 'angular'; + +const cloudrunLoadBalancerMessageComponent: ng.IComponentOptions = { + bindings: { showCreateMessage: '<', columnOffset: '@', columns: '@' }, + templateUrl: require('./loadBalancerMessage.component.html'), +}; + +export const CLOUDRUN_LOAD_BALANCER_CREATE_MESSAGE = 'spinnaker.cloudrun.loadBalancer.createMessage.component'; + +module(CLOUDRUN_LOAD_BALANCER_CREATE_MESSAGE, []).component( + 'cloudrunLoadBalancerMessage', + cloudrunLoadBalancerMessageComponent, +); diff --git a/packages/cloudrun/src/help/cloudrun.help.ts b/packages/cloudrun/src/help/cloudrun.help.ts new file mode 100644 index 00000000000..18bc8da1670 --- /dev/null +++ b/packages/cloudrun/src/help/cloudrun.help.ts @@ -0,0 +1,67 @@ +import { HelpContentsRegistry } from '@spinnaker/core'; + +const helpContents = [ + { + key: 'cloudrun.serverGroup.stack', + value: + '(Optional) Stack is one of the core naming components of a cluster, used to create vertical stacks of dependent services for integration testing.', + }, + + { + key: 'cloudrun.serverGroup.file', + value: `
+    apiVersion: serving.knative.dev/v1
+    kind: Service
+    metadata:
+        name: spinappcloud1
+        namespace: '135005621049'
+        labels:
+            cloud.googleapis.com/location: us-central1
+    annotations:
+        run.googleapis.com/client-name: cloud-console
+        serving.knative.dev/creator: kiran@opsmx.io
+        serving.knative.dev/lastModifier: kiran@opsmx.io
+        client.knative.dev/user-image: us-docker.pkg.dev/cloudrun/container/hello
+        run.googleapis.com/ingress-status: all
+    spec:
+        template:
+        metagoogleapis.com/ingress: all
+        run.data:
+        name: spinappcloud1
+    annotations:
+        run.googleapis.com/client-name: cloud-console
+        autoscaling.knative.dev/minScale: '1'
+        autoscaling.knative.dev/maxScale: '3'
+    spec:
+        containerConcurrency: 80
+        timeoutSeconds: 200
+        serviceAccountName:spinnaker-cloudrun-account@my-orbit-project-71824.iam.gserviceaccount.com
+        containers:
+           - image:us-docker.pkg.dev/cloudrun/container/hello
+        ports:
+           - name: http1
+        containerPort: 8080
+        resources:
+        limits:
+        cpu: 1000m
+        memory: 256Mi  
+
+ `, + }, + + { + key: 'cloudrun.serverGroup.detail', + value: + ' (Optional) Detail is a string of free-form alphanumeric characters and hyphens to describe any other variables.', + }, + { + key: 'cloudrun.serverGroup.configFiles', + value: `

The contents of a Cloudrun config file (e.g., an app.yaml

`, + }, + { + key: 'cloudrun.loadBalancer.allocations', + value: 'An allocation is the percent of traffic directed to a server group.', + }, +]; + +helpContents.forEach((entry) => HelpContentsRegistry.register(entry.key, entry.value)); diff --git a/packages/cloudrun/src/index.ts b/packages/cloudrun/src/index.ts new file mode 100644 index 00000000000..e0595fbf9f4 --- /dev/null +++ b/packages/cloudrun/src/index.ts @@ -0,0 +1,3 @@ +export * from './cloudrun.module'; +export * from './serverGroup'; +export * from './loadBalancer'; diff --git a/packages/cloudrun/src/instance/details/details.controller.ts b/packages/cloudrun/src/instance/details/details.controller.ts new file mode 100644 index 00000000000..1d6e1769d42 --- /dev/null +++ b/packages/cloudrun/src/instance/details/details.controller.ts @@ -0,0 +1,113 @@ +import type { IController, IQService } from 'angular'; +import { module } from 'angular'; +import { cloneDeep, flattenDeep } from 'lodash'; + +import type { Application, ILoadBalancer } from '@spinnaker/core'; +import { ConfirmationModalService, InstanceReader, InstanceWriter, RecentHistoryService } from '@spinnaker/core'; +import type { ICloudrunInstance } from '../../common/domain'; + +interface InstanceFromStateParams { + instanceId: string; +} + +interface InstanceManager { + account: string; + region: string; + category: string; // e.g., serverGroup, loadBalancer. + name: string; // Parent resource name, not instance name. + instances: ICloudrunInstance[]; +} + +class CloudrunInstanceDetailsController implements IController { + public state = { loading: true }; + public instance: ICloudrunInstance; + public instanceIdNotFound: string; + public upToolTip = "An Cloudrun instance is 'Up' if a load balancer is directing traffic to its server group."; + public outOfServiceToolTip = ` + An Cloudrun instance is 'Out Of Service' if no load balancers are directing traffic to its server group.`; + + public static $inject = ['$q', 'app', 'instance']; + + constructor(private $q: IQService, private app: Application, instance: InstanceFromStateParams) { + this.app + .ready() + .then(() => this.retrieveInstance(instance)) + .then((instanceDetails) => { + this.instance = instanceDetails; + this.state.loading = false; + }) + .catch(() => { + this.instanceIdNotFound = instance.instanceId; + this.state.loading = false; + }); + } + + public terminateInstance(): void { + const instance = cloneDeep(this.instance) as any; + const shortName = `${this.instance.name.substring(0, 10)}...`; + instance.placement = {}; + instance.instanceId = instance.name; + + const taskMonitor = { + application: this.app, + title: 'Terminating ' + shortName, + onTaskComplete() { + if (this.$state.includes('**.instanceDetails', { instanceId: instance.name })) { + this.$state.go('^'); + } + }, + }; + + const submitMethod = () => { + return InstanceWriter.terminateInstance(instance, this.app, { cloudProvider: 'cloudrun' }); + }; + + ConfirmationModalService.confirm({ + header: 'Really terminate ' + shortName + '?', + buttonText: 'Terminate ' + shortName, + account: instance.account, + taskMonitorConfig: taskMonitor, + submitMethod, + }); + } + + private retrieveInstance(instance: InstanceFromStateParams): PromiseLike { + const instanceLocatorPredicate = (dataSource: InstanceManager) => { + return dataSource.instances.some((possibleMatch) => possibleMatch.id === instance.instanceId); + }; + + const dataSources: InstanceManager[] = flattenDeep([ + this.app.getDataSource('serverGroups').data, + this.app.getDataSource('loadBalancers').data, + this.app.getDataSource('loadBalancers').data.map((loadBalancer: ILoadBalancer) => loadBalancer.serverGroups), + ]); + + const instanceManager = dataSources.find(instanceLocatorPredicate); + + if (instanceManager) { + const recentHistoryExtraData: { [key: string]: string } = { + region: instanceManager.region, + account: instanceManager.account, + }; + if (instanceManager.category === 'serverGroup') { + recentHistoryExtraData.serverGroup = instanceManager.name; + } + RecentHistoryService.addExtraDataToLatest('instances', recentHistoryExtraData); + + return InstanceReader.getInstanceDetails( + instanceManager.account, + instanceManager.region, + instance.instanceId, + ).then((instanceDetails: ICloudrunInstance) => { + instanceDetails.account = instanceManager.account; + instanceDetails.region = instanceManager.region; + return instanceDetails; + }); + } else { + return this.$q.reject(); + } + } +} + +export const CLOUDRUN_INSTANCE_DETAILS_CTRL = 'spinnaker.cloudrun.instanceDetails.controller'; +module(CLOUDRUN_INSTANCE_DETAILS_CTRL, []).controller('cloudrunInstanceDetailsCtrl', CloudrunInstanceDetailsController); diff --git a/packages/cloudrun/src/instance/details/details.html b/packages/cloudrun/src/instance/details/details.html new file mode 100644 index 00000000000..26cee0e5d83 --- /dev/null +++ b/packages/cloudrun/src/instance/details/details.html @@ -0,0 +1,71 @@ +
+
+ +
+
+ +
+
+
+
+ +
+
Launched
+
{{ctrl.instance.launchTime | timestamp}}
+
In
+
{{}}
+
Server Group
+
+ {{ctrl.instance.serverGroup}} +
+
Region
+
{{ctrl.instance.region}}
+ +
+
+ +
+
Load Balancer
+
+ + + {{ctrl.instance.loadBalancers[0]}} + +
+
+
+
+
+
+
+

Instance not found.

+
+
+
+
diff --git a/packages/cloudrun/src/interfaces/index.ts b/packages/cloudrun/src/interfaces/index.ts new file mode 100644 index 00000000000..14605457055 --- /dev/null +++ b/packages/cloudrun/src/interfaces/index.ts @@ -0,0 +1 @@ +export * from './infrastructure.types'; diff --git a/packages/cloudrun/src/interfaces/infrastructure.types.ts b/packages/cloudrun/src/interfaces/infrastructure.types.ts new file mode 100644 index 00000000000..4b4acb2689b --- /dev/null +++ b/packages/cloudrun/src/interfaces/infrastructure.types.ts @@ -0,0 +1,23 @@ +import type { IInstance, ILoadBalancer, IMoniker, IServerGroup, IServerGroupManager } from '@spinnaker/core'; + +export interface ICloudrunResource { + apiVersion: string; + createdTime?: number; + displayName: string; + kind: string; + namespace: string; +} + +export interface ICloudrunInstance extends IInstance, ICloudrunResource { + humanReadableName: string; + moniker: IMoniker; + publicDnsName?: string; +} + +export interface ICloudrunLoadBalancer extends ILoadBalancer, ICloudrunResource {} + +export interface ICloudrunServerGroup extends IServerGroup, ICloudrunResource { + disabled: boolean; +} + +export interface ICloudrunServerGroupManager extends IServerGroupManager, ICloudrunResource {} diff --git a/packages/cloudrun/src/loadBalancer/configure/wizard/allocationConfigurationRow.component.ts b/packages/cloudrun/src/loadBalancer/configure/wizard/allocationConfigurationRow.component.ts new file mode 100644 index 00000000000..5189b4c47cc --- /dev/null +++ b/packages/cloudrun/src/loadBalancer/configure/wizard/allocationConfigurationRow.component.ts @@ -0,0 +1,70 @@ +import type { IComponentOptions, IController } from 'angular'; +import { module } from 'angular'; +import { uniq } from 'lodash'; + +import type { ICloudrunAllocationDescription } from '../../loadBalancerTransformer'; + +class CloudrunAllocationConfigurationRowCtrl implements IController { + public allocationDescription: ICloudrunAllocationDescription; + public serverGroupOptions: string[]; + + public getServerGroupOptions(): string[] { + if (this.allocationDescription.revisionName) { + return uniq(this.serverGroupOptions.concat(this.allocationDescription.revisionName)); + } else { + return this.serverGroupOptions; + } + } +} + +const cloudrunAllocationConfigurationRowComponent: IComponentOptions = { + bindings: { + allocationDescription: '<', + removeAllocation: '&', + serverGroupOptions: '<', + onAllocationChange: '&', + }, + template: ` +
+
+
+ + + {{$select.selected}} + + +
+
+
+
+
+
+ + % +
+
+
+ + + +
+
+
+ `, + controller: CloudrunAllocationConfigurationRowCtrl, +}; + +export const CLOUDRUN_ALLOCATION_CONFIGURATION_ROW = 'spinnaker.cloudrun.allocationConfigurationRow.component'; + +module(CLOUDRUN_ALLOCATION_CONFIGURATION_ROW, []).component( + 'cloudrunAllocationConfigurationRow', + cloudrunAllocationConfigurationRowComponent, +); diff --git a/packages/cloudrun/src/loadBalancer/configure/wizard/basicSettings.component.html b/packages/cloudrun/src/loadBalancer/configure/wizard/basicSettings.component.html new file mode 100644 index 00000000000..97668ba57ef --- /dev/null +++ b/packages/cloudrun/src/loadBalancer/configure/wizard/basicSettings.component.html @@ -0,0 +1,43 @@ + +
+
+
+ Allocations + +
+
+
+ + +
+
+ + +
+ +
+
+
+
+

Allocations must sum to 100%.

+
+
+
+
diff --git a/packages/cloudrun/src/loadBalancer/configure/wizard/basicSettings.component.ts b/packages/cloudrun/src/loadBalancer/configure/wizard/basicSettings.component.ts new file mode 100644 index 00000000000..630c6e289c0 --- /dev/null +++ b/packages/cloudrun/src/loadBalancer/configure/wizard/basicSettings.component.ts @@ -0,0 +1,88 @@ +import type { IController } from 'angular'; +import { module } from 'angular'; +import { difference } from 'lodash'; + +import type { CloudrunLoadBalancerUpsertDescription } from '../../loadBalancerTransformer'; + +class CloudrunLoadBalancerSettingsController implements IController { + public loadBalancer: CloudrunLoadBalancerUpsertDescription; + public serverGroupOptions: string[]; + public forPipelineConfig: boolean; + + public $onInit(): void { + this.updateServerGroupOptions(); + } + + public addAllocation(): void { + const remainingServerGroups = this.serverGroupsWithoutAllocation(); + if (remainingServerGroups.length) { + this.loadBalancer.splitDescription.allocationDescriptions.push({ + revisionName: remainingServerGroups[0], + percent: 0, + // locatorType: 'fromExisting', + }); + this.updateServerGroupOptions(); + } else if (this.forPipelineConfig) { + this.loadBalancer.splitDescription.allocationDescriptions.push({ + percent: 0, + // locatorType: 'text', + revisionName: '', + }); + } + } + + public removeAllocation(index: number): void { + this.loadBalancer.splitDescription.allocationDescriptions.splice(index, 1); + this.updateServerGroupOptions(); + } + + public allocationIsInvalid(): boolean { + return ( + this.loadBalancer.splitDescription.allocationDescriptions.reduce( + (sum, allocationDescription) => sum + allocationDescription.percent, + 0, + ) !== 100 + ); + } + + public updateServerGroupOptions(): void { + this.serverGroupOptions = this.serverGroupsWithoutAllocation(); + } + + public showAddButton(): boolean { + if (this.forPipelineConfig) { + return true; + } else { + return this.serverGroupsWithoutAllocation().length > 0; + } + } + + public initializeAsTextInput(serverGroupName: string): boolean { + if (this.forPipelineConfig) { + return !this.loadBalancer.serverGroups.map((serverGroup) => serverGroup.name).includes(serverGroupName); + } else { + return false; + } + } + + private serverGroupsWithoutAllocation(): string[] { + const serverGroupsWithAllocation = this.loadBalancer.splitDescription.allocationDescriptions.map( + (description) => description.revisionName, + ); + const allServerGroups = this.loadBalancer.serverGroups.map((serverGroup) => serverGroup.name); + return difference(allServerGroups, serverGroupsWithAllocation); + } +} + +const cloudrunLoadBalancerSettingsComponent: ng.IComponentOptions = { + bindings: { loadBalancer: '=', forPipelineConfig: '<', application: '<' }, + controller: CloudrunLoadBalancerSettingsController, + templateUrl: require('./basicSettings.component.html'), +}; + +export const CLOUDRUN_LOAD_BALANCER_BASIC_SETTINGS = 'spinnaker.cloudrun.loadBalancerSettings.component'; + +module(CLOUDRUN_LOAD_BALANCER_BASIC_SETTINGS, []).component( + 'cloudrunLoadBalancerBasicSettings', + cloudrunLoadBalancerSettingsComponent, +); diff --git a/packages/cloudrun/src/loadBalancer/configure/wizard/stageAllocationConfigurationRow.component.html b/packages/cloudrun/src/loadBalancer/configure/wizard/stageAllocationConfigurationRow.component.html new file mode 100644 index 00000000000..46dea2edcd1 --- /dev/null +++ b/packages/cloudrun/src/loadBalancer/configure/wizard/stageAllocationConfigurationRow.component.html @@ -0,0 +1,27 @@ +
+
+
+ + +
+
+
+ + % +
+
+
+ + + +
+
+
diff --git a/packages/cloudrun/src/loadBalancer/configure/wizard/stageAllocationConfigurationRow.component.ts b/packages/cloudrun/src/loadBalancer/configure/wizard/stageAllocationConfigurationRow.component.ts new file mode 100644 index 00000000000..f4d12cb4759 --- /dev/null +++ b/packages/cloudrun/src/loadBalancer/configure/wizard/stageAllocationConfigurationRow.component.ts @@ -0,0 +1,88 @@ +import type { IComponentOptions, IController } from 'angular'; +import { module } from 'angular'; +import { uniq } from 'lodash'; + +import type { Application } from '@spinnaker/core'; +import { AppListExtractor, StageConstants } from '@spinnaker/core'; + +import type { ICloudrunAllocationDescription } from '../../loadBalancerTransformer'; + +class CloudrunStageAllocationLabelCtrl implements IController { + public inputViewValue: string; + private allocationDescription: ICloudrunAllocationDescription; + + /* private static mapTargetCoordinateToLabel(targetCoordinate: string): string { + const target = StageConstants.TARGET_LIST.find((t) => t.val === targetCoordinate); + if (target) { + return target.label; + } else { + return null; + } + } */ + + public $doCheck(): void { + this.setInputViewValue(); + } + + private setInputViewValue(): void { + this.inputViewValue = this.allocationDescription.revisionName; + } +} + +const cloudrunStageAllocationLabel: IComponentOptions = { + bindings: { allocationDescription: '<' }, + controller: CloudrunStageAllocationLabelCtrl, + template: ``, +}; + +class CloudrunStageAllocationConfigurationRowCtrl implements IController { + public allocationDescription: ICloudrunAllocationDescription; + public serverGroupOptions: string[]; + public targets = StageConstants.TARGET_LIST; + public clusterList: string[]; + public onAllocationChange: Function; + private application: Application; + private region: string; + private account: string; + + public $onInit() { + const clusterFilter = AppListExtractor.clusterFilterForCredentialsAndRegion(this.account, this.region); + this.clusterList = AppListExtractor.getClusters([this.application], clusterFilter); + } + + public getServerGroupOptions(): string[] { + if (this.allocationDescription.revisionName) { + return uniq(this.serverGroupOptions.concat(this.allocationDescription.revisionName)); + } else { + return this.serverGroupOptions; + } + } + + public onLocatorTypeChange(): void { + // Prevents pipeline expressions (or non-existent server groups) from entering the dropdown. + if (!this.serverGroupOptions.includes(this.allocationDescription.revisionName)) { + delete this.allocationDescription.revisionName; + } + this.onAllocationChange(); + } +} + +const cloudrunStageAllocationConfigurationRow: IComponentOptions = { + bindings: { + application: '<', + region: '@', + account: '@', + allocationDescription: '<', + removeAllocation: '&', + serverGroupOptions: '<', + onAllocationChange: '&', + }, + controller: CloudrunStageAllocationConfigurationRowCtrl, + templateUrl: require('./stageAllocationConfigurationRow.component.html'), +}; + +export const CLOUDRUN_STAGE_ALLOCATION_CONFIGURATION_ROW = + 'spinnaker.cloudrun.stageAllocationConfigurationRow.component'; +module(CLOUDRUN_STAGE_ALLOCATION_CONFIGURATION_ROW, []) + .component('cloudrunStageAllocationConfigurationRow', cloudrunStageAllocationConfigurationRow) + .component('cloudrunStageAllocationLabel', cloudrunStageAllocationLabel); diff --git a/packages/cloudrun/src/loadBalancer/configure/wizard/wizard.controller.ts b/packages/cloudrun/src/loadBalancer/configure/wizard/wizard.controller.ts new file mode 100644 index 00000000000..38ed17f38b6 --- /dev/null +++ b/packages/cloudrun/src/loadBalancer/configure/wizard/wizard.controller.ts @@ -0,0 +1,157 @@ +import type { StateService } from '@uirouter/angularjs'; +import type { IController } from 'angular'; +import { module } from 'angular'; +import type { IModalServiceInstance } from 'angular-ui-bootstrap'; +import { cloneDeep } from 'lodash'; + +import type { Application } from '@spinnaker/core'; +import { LoadBalancerWriter, TaskMonitor } from '@spinnaker/core'; + +import type { CloudrunLoadBalancerTransformer, ICloudrunTrafficSplitDescription } from '../../loadBalancerTransformer'; +import { CloudrunLoadBalancerUpsertDescription } from '../../loadBalancerTransformer'; + +import './wizard.less'; + +class CloudrunLoadBalancerWizardController implements IController { + public state = { loading: true }; + public loadBalancer: CloudrunLoadBalancerUpsertDescription; + public heading: string; + public submitButtonLabel: string; + public taskMonitor: TaskMonitor; + + public static $inject = [ + '$scope', + '$state', + '$uibModalInstance', + 'application', + 'loadBalancer', + 'isNew', + 'forPipelineConfig', + 'cloudrunLoadBalancerTransformer', + 'wizardSubFormValidation', + ]; + constructor( + public $scope: ng.IScope, + private $state: StateService, + private $uibModalInstance: IModalServiceInstance, + private application: Application, + loadBalancer: CloudrunLoadBalancerUpsertDescription, + public isNew: boolean, + private forPipelineConfig: boolean, + private cloudrunLoadBalancerTransformer: CloudrunLoadBalancerTransformer, + private wizardSubFormValidation: any, + ) { + this.submitButtonLabel = this.forPipelineConfig ? 'Done' : 'Update'; + + if (this.isNew) { + this.heading = 'Create New Load Balancer'; + } else { + this.heading = `Edit ${[ + loadBalancer.name, + loadBalancer.region, + loadBalancer.account || loadBalancer.credentials, + ].join(':')}`; + this.cloudrunLoadBalancerTransformer + .convertLoadBalancerForEditing(loadBalancer, application) + .then((convertedLoadBalancer) => { + this.loadBalancer = this.cloudrunLoadBalancerTransformer.convertLoadBalancerToUpsertDescription( + convertedLoadBalancer, + ); + if (loadBalancer.split && !this.loadBalancer.splitDescription) { + this.loadBalancer.splitDescription = CloudrunLoadBalancerUpsertDescription.convertTrafficSplitToTrafficSplitDescription( + loadBalancer.split, + ); + } else { + this.loadBalancer.splitDescription = loadBalancer.splitDescription; + } + this.loadBalancer.mapAllocationsToPercentages(); + this.setTaskMonitor(); + this.initializeFormValidation(); + this.state.loading = false; + }); + } + } + + public submit(): any { + const description = cloneDeep(this.loadBalancer); + description.mapAllocationsToPercentages(); + delete description.serverGroups; + + if (this.forPipelineConfig) { + return this.$uibModalInstance.close(description); + } else { + return this.taskMonitor.submit(() => { + return LoadBalancerWriter.upsertLoadBalancer(description, this.application, 'Update'); + }); + } + } + + public cancel(): void { + this.$uibModalInstance.dismiss(); + } + + public showSubmitButton(): boolean { + return this.wizardSubFormValidation.subFormsAreValid(); + } + + private setTaskMonitor(): void { + this.taskMonitor = new TaskMonitor({ + application: this.application, + title: 'Updating your load balancer', + modalInstance: this.$uibModalInstance, + onTaskComplete: () => this.onTaskComplete(), + }); + } + + private initializeFormValidation(): void { + this.wizardSubFormValidation.config({ form: 'form', scope: this.$scope }).register({ + page: 'basic-settings', + subForm: 'basicSettingsForm', + validators: [ + { + watchString: 'ctrl.loadBalancer.splitDescription', + validator: (splitDescription: ICloudrunTrafficSplitDescription): boolean => { + return ( + splitDescription.allocationDescriptions.reduce((sum, description) => sum + description.percent, 0) === 100 + ); + }, + watchDeep: true, + }, + ], + }); + } + + private onTaskComplete(): void { + this.application.getDataSource('loadBalancers').refresh(); + this.application.getDataSource('loadBalancers').onNextRefresh(this.$scope, () => this.onApplicationRefresh()); + } + + private onApplicationRefresh(): void { + // If the user has already closed the modal, do not navigate to the new details view + if ((this.$scope as any).$$destroyed) { + // $$destroyed is not in the ng.IScope interface + return; + } + + this.$uibModalInstance.dismiss(); + const newStateParams = { + name: this.loadBalancer.name, + accountId: this.loadBalancer.credentials, + region: this.loadBalancer.region, + provider: 'cloudrun', + }; + + if (!this.$state.includes('**.loadBalancerDetails')) { + this.$state.go('.loadBalancerDetails', newStateParams); + } else { + this.$state.go('^.loadBalancerDetails', newStateParams); + } + } +} + +export const CLOUDRUN_LOAD_BALANCER_WIZARD_CTRL = 'spinnaker.cloudrun.loadBalancer.wizard.controller'; + +module(CLOUDRUN_LOAD_BALANCER_WIZARD_CTRL, []).controller( + 'cloudrunLoadBalancerWizardCtrl', + CloudrunLoadBalancerWizardController, +); diff --git a/packages/cloudrun/src/loadBalancer/configure/wizard/wizard.html b/packages/cloudrun/src/loadBalancer/configure/wizard/wizard.html new file mode 100644 index 00000000000..970d993560d --- /dev/null +++ b/packages/cloudrun/src/loadBalancer/configure/wizard/wizard.html @@ -0,0 +1,39 @@ +
+
+ +
+ +
+ + + +
+
+ + +
diff --git a/packages/cloudrun/src/loadBalancer/configure/wizard/wizard.less b/packages/cloudrun/src/loadBalancer/configure/wizard/wizard.less new file mode 100644 index 00000000000..597e3f8d63b --- /dev/null +++ b/packages/cloudrun/src/loadBalancer/configure/wizard/wizard.less @@ -0,0 +1,17 @@ +appengine-load-balancer-basic-settings { + a.btn.btn-link { + padding: 0; + } + + .form-group { + margin-top: 0.4rem; + } +} + +appengine-load-balancer-advanced-settings { + .checkbox { + input[type='checkbox'] { + margin: 0 0 0.1rem 0; + } + } +} diff --git a/packages/cloudrun/src/loadBalancer/details/details.controller.ts b/packages/cloudrun/src/loadBalancer/details/details.controller.ts new file mode 100644 index 00000000000..d0555e70b45 --- /dev/null +++ b/packages/cloudrun/src/loadBalancer/details/details.controller.ts @@ -0,0 +1,140 @@ +import type { StateService } from '@uirouter/angularjs'; +import type { IController, IScope } from 'angular'; +import { module } from 'angular'; +import type { IModalService } from 'angular-ui-bootstrap'; +import { cloneDeep } from 'lodash'; + +import type { Application, ILoadBalancer, ILoadBalancerDeleteCommand } from '@spinnaker/core'; +import { ConfirmationModalService, LoadBalancerWriter } from '@spinnaker/core'; +import type { ICloudrunLoadBalancer } from '../../common/domain/index'; + +interface ILoadBalancerFromStateParams { + accountId: string; + region: string; + name: string; +} + +class CloudrunLoadBalancerDetailsController implements IController { + public state = { loading: true }; + private loadBalancerFromParams: ILoadBalancerFromStateParams; + public loadBalancer: ICloudrunLoadBalancer; + + public static $inject = ['$uibModal', '$state', '$scope', 'loadBalancer', 'app']; + constructor( + private $uibModal: IModalService, + private $state: StateService, + private $scope: IScope, + loadBalancer: ILoadBalancerFromStateParams, + private app: Application, + ) { + this.loadBalancerFromParams = loadBalancer; + this.app + .getDataSource('loadBalancers') + .ready() + .then(() => this.extractLoadBalancer()); + } + + // edit loadbalancer to change traffic + + public editLoadBalancer(): void { + this.$uibModal.open({ + templateUrl: require('../configure/wizard/wizard.html'), + controller: 'cloudrunLoadBalancerWizardCtrl as ctrl', + size: 'lg', + resolve: { + application: () => this.app, + loadBalancer: () => cloneDeep(this.loadBalancer), + isNew: () => false, + forPipelineConfig: () => false, + }, + }); + } + + private extractLoadBalancer(): void { + this.loadBalancer = this.app.getDataSource('loadBalancers').data.find((test: ILoadBalancer) => { + return test.name === this.loadBalancerFromParams.name && test.account === this.loadBalancerFromParams.accountId; + }) as ICloudrunLoadBalancer; + + if (this.loadBalancer) { + this.state.loading = false; + this.app.getDataSource('loadBalancers').onRefresh(this.$scope, () => this.extractLoadBalancer()); + } else { + this.autoClose(); + } + } + + public deleteLoadBalancer(): void { + const taskMonitor = { + application: this.app, + title: 'Deleting ' + this.loadBalancer.name, + }; + + const submitMethod = () => { + const loadBalancer: ILoadBalancerDeleteCommand = { + cloudProvider: this.loadBalancer.cloudProvider, + loadBalancerName: this.loadBalancer.name, + credentials: this.loadBalancer.account, + }; + return LoadBalancerWriter.deleteLoadBalancer(loadBalancer, this.app); + }; + + ConfirmationModalService.confirm({ + header: 'Really delete ' + this.loadBalancer.name + '?', + buttonText: 'Delete ' + this.loadBalancer.name, + body: this.getConfirmationModalBodyHtml(), + account: this.loadBalancer.account, + taskMonitorConfig: taskMonitor, + submitMethod, + }); + } + + public canDeleteLoadBalancer(): boolean { + return this.loadBalancer.name !== 'default'; + } + + private getConfirmationModalBodyHtml(): string { + const serverGroupNames = this.loadBalancer.serverGroups.map((serverGroup) => serverGroup.name); + const hasAny = serverGroupNames ? serverGroupNames.length > 0 : false; + const hasMoreThanOne = serverGroupNames ? serverGroupNames.length > 1 : false; + + // HTML accepted by the confirmationModalService is static (i.e., not managed by angular). + if (hasAny) { + if (hasMoreThanOne) { + const listOfServerGroupNames = serverGroupNames.map((name) => `
  • ${name}
  • `).join(''); + return `
    +

    + Deleting ${this.loadBalancer.name} will destroy the following server groups: +

      + ${listOfServerGroupNames} +
    +

    +
    + `; + } else { + return `
    +

    + Deleting ${this.loadBalancer.name} will destroy ${serverGroupNames[0]}. +

    +
    + `; + } + } else { + return null; + } + } + + private autoClose(): void { + if (this.$scope.$$destroyed) { + return; + } else { + this.$state.params.allowModalToStayOpen = true; + this.$state.go('^', null, { location: 'replace' }); + } + } +} + +export const CLOUDRUN_LOAD_BALANCER_DETAILS_CTRL = 'spinnaker.cloudrun.loadBalancerDetails.controller'; +module(CLOUDRUN_LOAD_BALANCER_DETAILS_CTRL, []).controller( + 'cloudrunLoadBalancerDetailsCtrl', + CloudrunLoadBalancerDetailsController, +); diff --git a/packages/cloudrun/src/loadBalancer/details/details.html b/packages/cloudrun/src/loadBalancer/details/details.html new file mode 100644 index 00000000000..7a56e549982 --- /dev/null +++ b/packages/cloudrun/src/loadBalancer/details/details.html @@ -0,0 +1,81 @@ +
    +
    +
    + + + +
    +
    + +
    +
    + +
    +
    + + + +
    +
    + +

    {{ctrl.loadBalancer.name}}

    +
    +
    +
    + +
    +
    +
    + +
    + +
    +
    In
    +
    +
    Region
    +
    {{ctrl.loadBalancer.region}}
    +
    Server Groups
    +
    + +
    +
    +
    + +
    +
      +
    • + {{trafficTarget.revisionName}}:{{trafficTarget.percent}} +
    • +
    +
    +
    +
    +
    diff --git a/packages/cloudrun/src/loadBalancer/index.ts b/packages/cloudrun/src/loadBalancer/index.ts new file mode 100644 index 00000000000..d2cf8b08313 --- /dev/null +++ b/packages/cloudrun/src/loadBalancer/index.ts @@ -0,0 +1,3 @@ +export * from './loadBalancerTransformer'; +export * from './details/details.controller'; +export * from './configure/wizard/wizard.controller'; diff --git a/packages/cloudrun/src/loadBalancer/loadBalancerTransformer.ts b/packages/cloudrun/src/loadBalancer/loadBalancerTransformer.ts new file mode 100644 index 00000000000..734a889062a --- /dev/null +++ b/packages/cloudrun/src/loadBalancer/loadBalancerTransformer.ts @@ -0,0 +1,175 @@ +/* eslint-disable no-console */ +import { module } from 'angular'; +import { camelCase, chain, cloneDeep, filter, get, has, reduce } from 'lodash'; + +import type { + Application, + IInstance, + IInstanceCounts, + ILoadBalancer, + ILoadBalancerUpsertCommand, + IServerGroup, +} from '@spinnaker/core'; +//import type { ICloudrunLoadBalancer, ICloudrunTrafficSplit, ShardBy } from '../common/domain/index'; +import type { ICloudrunLoadBalancer, ICloudrunTrafficSplit } from '../common/domain/index'; + +export interface ICloudrunAllocationDescription { + //serverGroupName?: string; + revisionName?: string; + target?: string; + cluster?: string; + //allocation: number; + percent: number; + // locatorType: 'fromExisting' | 'targetCoordinate' | 'text'; +} + +export interface ICloudrunTrafficSplitDescription { + //shardBy: ShardBy; + allocationDescriptions: ICloudrunAllocationDescription[]; +} + +export class CloudrunLoadBalancerUpsertDescription implements ILoadBalancerUpsertCommand, ICloudrunLoadBalancer { + public credentials: string; + public account: string; + public loadBalancerName: string; + public name: string; + public splitDescription: ICloudrunTrafficSplitDescription; + public split?: ICloudrunTrafficSplit; + public migrateTraffic: boolean; + public region: string; + public cloudProvider: string; + public serverGroups?: any[]; + + public static convertTrafficSplitToTrafficSplitDescription( + split: ICloudrunTrafficSplit, + ): ICloudrunTrafficSplitDescription { + const allocationDescriptions = reduce( + split.trafficTargets, + (acc: any, trafficTarget: any) => { + const { revisionName, percent } = trafficTarget; + //console.log("trafficTarget",trafficTarget,acc, revisionName, percent) + return acc.concat({ percent, revisionName, locatorType: 'fromExisting' }); + }, + [], + ); + // console.log(allocationDescriptions) + return { allocationDescriptions }; + } + + constructor(loadBalancer: ICloudrunLoadBalancer) { + this.credentials = loadBalancer.account || loadBalancer.credentials; + this.account = this.credentials; + this.cloudProvider = loadBalancer.cloudProvider; + this.loadBalancerName = loadBalancer.name; + this.name = loadBalancer.name; + this.region = loadBalancer.region; + this.migrateTraffic = loadBalancer.migrateTraffic || false; + this.serverGroups = loadBalancer.serverGroups; + } + + public mapAllocationsToDecimals() { + this.splitDescription.allocationDescriptions.forEach((description) => { + description.percent = description.percent / 100; + }); + } + + public mapAllocationsToPercentages() { + this.splitDescription.allocationDescriptions.forEach((description) => { + // An allocation percent has at most one decimal place. + description.percent = Math.round(description.percent); + }); + } +} + +export class CloudrunLoadBalancerTransformer { + public static $inject = ['$q']; + constructor(private $q: ng.IQService) {} + public normalizeLoadBalancer(loadBalancer: ILoadBalancer): PromiseLike { + loadBalancer.provider = loadBalancer.type; + loadBalancer.instanceCounts = this.buildInstanceCounts(loadBalancer.serverGroups); + loadBalancer.instances = []; + loadBalancer.serverGroups.forEach((serverGroup) => { + serverGroup.account = loadBalancer.account; + serverGroup.region = loadBalancer.region; + + if (serverGroup.detachedInstances) { + serverGroup.detachedInstances = (serverGroup.detachedInstances as any).map((id: string) => ({ id })); + } + serverGroup.instances = serverGroup.instances + .concat(serverGroup.detachedInstances || []) + .map((instance: any) => this.transformInstance(instance, loadBalancer)); + }); + + const activeServerGroups = filter(loadBalancer.serverGroups, { isDisabled: false }); + loadBalancer.instances = chain(activeServerGroups).map('instances').flatten().value() as IInstance[]; + return this.$q.resolve(loadBalancer); + } + + public convertLoadBalancerForEditing( + loadBalancer: ICloudrunLoadBalancer, + application: Application, + ): PromiseLike { + return application + .getDataSource('loadBalancers') + .ready() + .then(() => { + const upToDateLoadBalancer = application + .getDataSource('loadBalancers') + .data.find((candidate: ILoadBalancer) => { + return ( + candidate.name === loadBalancer.name && + (candidate.account === loadBalancer.account || candidate.account === loadBalancer.credentials) + ); + }); + + if (upToDateLoadBalancer) { + loadBalancer.serverGroups = cloneDeep(upToDateLoadBalancer.serverGroups); + } + return loadBalancer; + }); + } + + public convertLoadBalancerToUpsertDescription( + loadBalancer: ICloudrunLoadBalancer, + ): CloudrunLoadBalancerUpsertDescription { + return new CloudrunLoadBalancerUpsertDescription(loadBalancer); + } + + private buildInstanceCounts(serverGroups: IServerGroup[]): IInstanceCounts { + const instanceCounts: IInstanceCounts = chain(serverGroups) + .map('instances') + .flatten() + .reduce( + (acc: IInstanceCounts, instance: any) => { + if (has(instance, 'health.state')) { + acc[camelCase(instance.health.state)]++; + } + return acc; + }, + { up: 0, down: 0, outOfService: 0, succeeded: 0, failed: 0, starting: 0, unknown: 0 }, + ) + .value(); + + instanceCounts.outOfService += chain(serverGroups).map('detachedInstances').flatten().value().length; + return instanceCounts; + } + + private transformInstance(instance: any, loadBalancer: ILoadBalancer) { + instance.provider = loadBalancer.type; + instance.account = loadBalancer.account; + instance.region = loadBalancer.region; + instance.loadBalancers = [loadBalancer.name]; + const health = instance.health || {}; + instance.healthState = get(instance, 'health.state') || 'OutOfService'; + instance.health = [health]; + + return instance as IInstance; + } +} + +export const CLOUDRUN_LOAD_BALANCER_TRANSFORMER = 'spinnaker.cloudrun.loadBalancer.transformer.service'; + +module(CLOUDRUN_LOAD_BALANCER_TRANSFORMER, []).service( + 'cloudrunLoadBalancerTransformer', + CloudrunLoadBalancerTransformer, +); diff --git a/packages/cloudrun/src/logo/cloudrun.icon.svg b/packages/cloudrun/src/logo/cloudrun.icon.svg new file mode 100644 index 00000000000..b0c46ab8ed7 --- /dev/null +++ b/packages/cloudrun/src/logo/cloudrun.icon.svg @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/cloudrun/src/logo/cloudrun.logo.less b/packages/cloudrun/src/logo/cloudrun.logo.less new file mode 100644 index 00000000000..b0fe8736def --- /dev/null +++ b/packages/cloudrun/src/logo/cloudrun.logo.less @@ -0,0 +1,6 @@ +.cloud-provider-logo { + .icon-cloudrun { + mask-image: url('cloudrun.icon.svg'); + background-color: #4285f4; + } +} diff --git a/packages/cloudrun/src/logo/cloudrun.logo.png b/packages/cloudrun/src/logo/cloudrun.logo.png new file mode 100644 index 0000000000000000000000000000000000000000..935da7b1a9edcbf9f5cbed55faf9e876a61cea99 GIT binary patch literal 3516 zcmV;t4MXyYP)Er!PNUfgYhkN;=j@KK7;f(fb_P>@-2MxJ$&LhdgD5Q^fGtiC2`^T`~K$c z{qOYtUX}Hfw)ph-{nO$6+~@tZ%=*gO`-rah*W>(3iuB*;`v3p@Z=d#|zWIHp_pr$N zW|Zi9q3TG31D+s_a#e^?#)6ZkXhx!1-#N_JpkWld|r%$n0yE=o4$;j;`#> z*YB>v>y@$SE`Dgh000cnNkldw-(37KTAO6p9F_wXLl^)7txGcK4jQeE)BD zKnP|*Lb7rJJU`c;?mg<`8}hCsYo(6+{~pfJU%+}C2@gMxgohtT!o!av;o--Tusaqf zpV#*%R^wux4Mw=wEzS_z?|W~A^PV2SW`xV02v@-{fH~rieh8OC0p4T7-x83@qWrpc3X0t}GM24J)DJ%>J=Ucs1lSQ$}=alQ0_^ zFOLA}`#8e$Uej$(L?`wLhjog5*drX+DM*AU$G{K{?i5u7;ldK(;7&mT0KOARI%Qa6Xc0hJ<;-?;{$dd4zYS3CH3Tb$GIC3|Yd_I0Xr5 z$B-l(kyDV6bqqPeY&JFt_Vgd=nc5;BevpX_5&U#}E^a(kV!IV#g2?j%`iR41L0pIt2-_ zV~7Yx>l7qB(XNz;a5HWRB^*OQIAW(DA##jv!ZEHXnjvzGZo*MJ1qp#;bP|r;DM+Xj zj@&6obUQ{D;pm-$M5kkP5suy|NOU?z2jO=7PC=r}G5CbpV8vbd=yD7`;XybBiH=<< zE+N(_-nc)==35VgA;1$Nox)px@qUtU@!kHU%y$ePA=W8o*Y3@REu*(~&+P9La2 zpSQG%>TZxLn=-N;gGIPjvaeuYNWXf3@8H$&c4drMj=>^SJg1;f*5Uq6Kz9*Xl#%He z3_{g41=vkkgEs*`u0h-zs*NNAqI`% zGhbTDR2jQcEkcdmC+~uKpHqKAA%@LH(5-T8iew(o-suNrYhhAtItC=vJg0z@^-Db2t60N7PU!i_f{abafP~kYyH6h7j{IB< z(^^H6eXrB>mHx49WHMn0j2Q z(1|w4zO#Cm7Ai(0`+likwI(CAlB)@|or1hK{gpo`!fBm-Kkoyyc(oAS0}Q zX+mhgDaruZ_vwC^=7N#m^){tnO`qxCo=^0vxr}tK zcgZ>5!A3o?RfOhFwBhFZ12s%133=$g5`fr{@ym>i#4$?3mBG_5umt@BhUrW7>J5dVe)!bwUv=sC8MkdO8O@r0K_W>^%2@0pA(!5 z3p?u|`_AAnMcnsbLdNLQ*;>#LX5YXWl4l-<9T~%|b8h33N05D=GPCcVWDKqyE4S)^ z?7Oi@#M~`I%jl@dxOA-4Fy``o6YHA znmqCVgz+m@_#zuSGY!17;*6eZ>f{@(;53;|@Ly$V#Vwo&6ej$npTkt~dhXY9AYi{q zK9|~Xe8;0PK$acpl!sUHdVRiazRM&|9&b_jDC zU%?Ld0^$)jR#s2&gc9VXDk;rtb1jMaCR-~jV3?MIwFN#cZ3u`&46N*8&;_$DI7G~6 zQsm{MZ2@=jP5dQxQIvic)@=G_j|g0W4;D{P`T`;lcQal=Xu6XUl6O{(6c&{KqE+xOSRgzBCmaD~5yN^%*ly{Kyq+(qNS}(>; zIR>om1P7$4#N}hzyR?9vi0x_QMm~X^h;&+IxPlfI%7aa| z1QhAD0yBk*5Wy~r;VNjDb;C`zu2Uy<0a=KkWdK&PYww+cOGe-(TigM-))26b7=kls zv)u@AuN!Q#C7{c~q<}5N03?TIJ4)aQ{v-1Qzb&9}8G%EDJADm8UB>`5&~THDmvx&E z5FmDU1!+14TpEH+wwQAuvrbG1NFzp#eS}tbo)#g_r=4^4qV3bXE0BS{P+;tvLmx!YS{y&jNoGVMb^&KjF} z1CQF@Ql)AxZ(o2JCR9_bH38wr2ndxNgUY&}Q&`5mKajB~U_MBhsW1C(rGFG0qt3b; z0v3d|fOe+P>QmaD%O_mnr_;LQ?{C_y!zX1>Rt22IFKwsb6S|-1g<{o%&=#;RgR&}M zKFu(kqJt3c7-bn$^9d{oSJnmOI7JuX33jq64rEvYRtGYc1!UxfB0{WV6dQO<0e87# zE+F$inG1e}0GIF*Ct`Rqf8zAPq^nckvVc6N=p=OSQH~M(@;NHSPw%z`>{BxvJg4X;bnkG8->h@+gXa_iLTtg?aEM%|5D{V>V=zRnQ-}!BYm9*qyKBih z36YL56e8a#goH@P7zok-x09ipaDiT9^pDu(6kRZcg*2gJQ;l^* zsls?ELRYaJ+AgACO@Sj+atzCeGEN~+sOA`U5oMeLL71rlT0~4lYYHTxdK;!KMCpcN zSwi(TOgo5jPJtp+Z^N`={oT7yN`x9sH5OEqbP61yMv1g>L|Lc65$d#Innsj$3M8RU z8>U%AX{SIEYBlZ~MMPaafg#jt+&73Q?-Xc4t;T(Gh?tIWB%xj#rYS^(Q{V~pnrh5g zKgKEWgqn@}MpW$V5Ys2j^qOi6AfoRILlWv<^`ei6zN83CsC(6mE+Wn;ln8aNdeKEh zUxtPz)Nb6@LR7fV4o|qy?fKR~L?xukg!=8m>WEW`TNw-xx}VyJiq?<5M@yAZ|FU#7 z6B2PVdH|v8)lb63Lx11Xx%p;p$oT$gCI4R1BfHx q;m48i@Z(5$_;DmW{5TRGe*6#LZXKwBoJ7z70000 this.$scope.application, + }, + }) + .result.then((newLoadBalancer: ILoadBalancer) => { + this.$scope.stage.loadBalancers.push(newLoadBalancer); + }) + .catch(() => {}); + } + + public editLoadBalancer(index: number) { + const config = CloudProviderRegistry.getValue('cloudrun', 'loadBalancer'); + this.$uibModal + .open({ + templateUrl: config.createLoadBalancerTemplateUrl, + controller: `${config.createLoadBalancerController} as ctrl`, + size: 'lg', + resolve: { + application: () => this.$scope.application, + loadBalancer: () => cloneDeep(this.$scope.stage.loadBalancers[index]), + isNew: () => false, + forPipelineConfig: () => true, + }, + }) + .result.then((updatedLoadBalancer: ILoadBalancer) => { + this.$scope.stage.loadBalancers[index] = updatedLoadBalancer; + }) + .catch(() => {}); + } + + public removeLoadBalancer(index: number): void { + this.$scope.stage.loadBalancers.splice(index, 1); + } +} + +export const CLOUDRUN_EDIT_LOAD_BALANCER_STAGE = 'spinnaker.cloudrun.pipeline.stage.editLoadBalancerStage'; +module(CLOUDRUN_EDIT_LOAD_BALANCER_STAGE, [CLOUDRUN_LOAD_BALANCER_CHOICE_MODAL_CTRL]) + .config(() => { + Registry.pipeline.registerStage({ + label: 'Edit Load Balancer', + description: 'Edits a load balancer', + key: 'upsertAppEngineLoadBalancers', + cloudProvider: 'cloudrun', + templateUrl: require('./editLoadBalancerStage.html'), + executionDetailsUrl: require('./editLoadBalancerExecutionDetails.html'), + executionConfigSections: ['editLoadBalancerConfig', 'taskStatus'], + controller: 'cloudrunEditLoadBalancerStageCtrl', + controllerAs: 'editLoadBalancerStageCtrl', + validators: [], + }); + }) + .controller('cloudrunEditLoadBalancerStageCtrl', CloudrunEditLoadBalancerStageCtrl); diff --git a/packages/cloudrun/src/pipeline/stages/editLoadBalancer/editLoadBalancerExecutionDetails.html b/packages/cloudrun/src/pipeline/stages/editLoadBalancer/editLoadBalancerExecutionDetails.html new file mode 100644 index 00000000000..1ee8b1224b4 --- /dev/null +++ b/packages/cloudrun/src/pipeline/stages/editLoadBalancer/editLoadBalancerExecutionDetails.html @@ -0,0 +1,33 @@ +
    + +
    +
    +
    + + + + + + + + + + + + + + + +
    AccountNameRegion
    + + {{ loadBalancer.name }}{{ loadBalancer.region }}
    +
    +
    + +
    +
    +
    + +
    +
    +
    diff --git a/packages/cloudrun/src/pipeline/stages/editLoadBalancer/editLoadBalancerStage.html b/packages/cloudrun/src/pipeline/stages/editLoadBalancer/editLoadBalancerStage.html new file mode 100644 index 00000000000..0772bd9d571 --- /dev/null +++ b/packages/cloudrun/src/pipeline/stages/editLoadBalancer/editLoadBalancerStage.html @@ -0,0 +1,51 @@ +
    +
    +
    +

    Load Balancers

    +
    +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + +
    AccountNameRegionActions
    + + {{ loadBalancer.name }}{{ loadBalancer.region }} + + + + + +
    + +
    +
    +
    +
    diff --git a/packages/cloudrun/src/pipeline/stages/editLoadBalancer/loadBalancerChoice.modal.controller.ts b/packages/cloudrun/src/pipeline/stages/editLoadBalancer/loadBalancerChoice.modal.controller.ts new file mode 100644 index 00000000000..7daa4cd2194 --- /dev/null +++ b/packages/cloudrun/src/pipeline/stages/editLoadBalancer/loadBalancerChoice.modal.controller.ts @@ -0,0 +1,65 @@ +import type { IController } from 'angular'; +import { module } from 'angular'; +import type { IModalService, IModalServiceInstance } from 'angular-ui-bootstrap'; +import { cloneDeep } from 'lodash'; + +import type { Application, ILoadBalancer } from '@spinnaker/core'; +import { CloudProviderRegistry } from '@spinnaker/core'; + +class CloudrunLoadBalancerChoiceModalCtrl implements IController { + public state = { loading: true }; + public loadBalancers: ILoadBalancer[]; + public selectedLoadBalancer: ILoadBalancer; + + public static $inject = ['$uibModal', '$uibModalInstance', 'application']; + constructor( + private $uibModal: IModalService, + private $uibModalInstance: IModalServiceInstance, + private application: Application, + ) { + this.initialize(); + } + + public submit(): void { + const config = CloudProviderRegistry.getValue('cloudrun', 'loadBalancer'); + const updatedLoadBalancerPromise = this.$uibModal.open({ + templateUrl: config.createLoadBalancerTemplateUrl, + controller: `${config.createLoadBalancerController} as ctrl`, + size: 'lg', + resolve: { + application: () => this.application, + loadBalancer: () => cloneDeep(this.selectedLoadBalancer), + isNew: () => false, + forPipelineConfig: () => true, + }, + }).result; + + this.$uibModalInstance.close(updatedLoadBalancerPromise); + } + + public cancel(): void { + this.$uibModalInstance.dismiss(); + } + + private initialize(): void { + this.application + .getDataSource('loadBalancers') + .ready() + .then(() => { + this.loadBalancers = (this.application.loadBalancers.data as ILoadBalancer[]).filter( + (candidate) => candidate.cloudProvider === 'cloudrun', + ); + + if (this.loadBalancers.length) { + this.selectedLoadBalancer = this.loadBalancers[0]; + } + this.state.loading = false; + }); + } +} + +export const CLOUDRUN_LOAD_BALANCER_CHOICE_MODAL_CTRL = 'spinnaker.Cloudrun.loadBalancerChoiceModal.controller'; +module(CLOUDRUN_LOAD_BALANCER_CHOICE_MODAL_CTRL, []).controller( + 'cloudrunLoadBalancerChoiceModelCtrl', + CloudrunLoadBalancerChoiceModalCtrl, +); diff --git a/packages/cloudrun/src/pipeline/stages/editLoadBalancer/loadBalancerChoice.modal.html b/packages/cloudrun/src/pipeline/stages/editLoadBalancer/loadBalancerChoice.modal.html new file mode 100644 index 00000000000..1ca07afd15d --- /dev/null +++ b/packages/cloudrun/src/pipeline/stages/editLoadBalancer/loadBalancerChoice.modal.html @@ -0,0 +1,53 @@ +
    + + + + + +
    diff --git a/packages/cloudrun/src/serverGroup/configure/serverGroupCommandBuilder.service.ts b/packages/cloudrun/src/serverGroup/configure/serverGroupCommandBuilder.service.ts new file mode 100644 index 00000000000..c39fe198199 --- /dev/null +++ b/packages/cloudrun/src/serverGroup/configure/serverGroupCommandBuilder.service.ts @@ -0,0 +1,236 @@ +import { module } from 'angular'; +import { cloneDeep } from 'lodash'; +import { $q } from 'ngimport'; + +import type { + Application, + IAccountDetails, + IMoniker, + IPipeline, + IServerGroupCommand, + IServerGroupCommandViewState, + IStage, +} from '@spinnaker/core'; +import { AccountService } from '@spinnaker/core'; + +import { CloudrunProviderSettings } from '../../cloudrun.settings'; + +export enum ServerGroupSource { + TEXT = 'text', + ARTIFACT = 'artifact', +} + +export interface ICloudrunServerGroupCommandData { + command: ICloudrunServerGroupCommand; + metadata: ICloudrunServerGroupCommandMetadata; +} + +export interface ICloudrunServerGroupCommand extends Omit { + application?: string; + stack?: string; + account: string; + configFiles: string[]; + freeFormDetails: string; + region: string; + regions: []; + isNew?: boolean; + cloudProvider: string; + provider: string; + selectedProvider: string; + manifest: any; // deprecated + manifests: any[]; + relationships: ICloudrunServerGroupSpinnakerRelationships; + moniker: IMoniker; + manifestArtifactId?: string; + manifestArtifactAccount?: string; + source: ServerGroupSource; + versioned?: boolean; + gitCredentialType?: string; + viewState: IServerGroupCommandViewState; + mode: string; + credentials: string; + sourceType: string; + configArtifacts: any[]; + interestingHealthProviderNames: []; + fromArtifact: boolean; +} + +export interface IViewState { + mode: string; + submitButtonLabel: string; + disableStrategySelection: boolean; + stage?: IStage; + pipeline?: IPipeline; +} + +export interface ICloudrunServerGroupCommandMetadata { + backingData: any; +} + +export interface ICloudrunServerGroupSpinnakerRelationships { + loadBalancers?: string[]; + securityGroups?: string[]; +} + +const getSubmitButtonLabel = (mode: string): string => { + switch (mode) { + case 'createPipeline': + return 'Add'; + case 'editPipeline': + return 'Done'; + default: + return 'Create'; + } +}; + +export class CloudrunV2ServerGroupCommandBuilder { + // new add servergroup + public buildNewServerGroupCommand(app: Application): PromiseLike { + return CloudrunServerGroupCommandBuilder.buildNewServerGroupCommand(app); + } + + // add servergroup from deploy stage of pipeline + public buildNewServerGroupCommandForPipeline(_stage: IStage, pipeline: IPipeline) { + return CloudrunServerGroupCommandBuilder.buildNewServerGroupCommandForPipeline(_stage, pipeline); + } +} + +export class CloudrunServerGroupCommandBuilder { + public static $inject = ['$q']; + + // TODO(lwander) add explanatory error messages + public static ServerGroupCommandIsValid(command: ICloudrunServerGroupCommand): boolean { + if (!command.moniker) { + return false; + } + + if (!command.moniker.app) { + return false; + } + + return true; + } + + public static copyAndCleanCommand(input: ICloudrunServerGroupCommand): ICloudrunServerGroupCommand { + const command = cloneDeep(input); + return command; + } + + // deploy stage : construct servergroup command + public static buildNewServerGroupCommandForPipeline(stage: IStage, pipeline: IPipeline): any { + const command: any = this.buildNewServerGroupCommand({ name: pipeline.application } as Application); + command.viewState = { + ...command.viewState, + pipeline, + requiresTemplateSelection: true, + stage, + }; + // command.isNew = false + return command; + } + + public static buildServerGroupCommandFromPipeline( + app: Application, + cluster: ICloudrunServerGroupCommand, + _stage: IStage, + pipeline: IPipeline, + ): PromiseLike { + return CloudrunServerGroupCommandBuilder.buildNewServerGroupCommand(app, 'cloudrun', 'create').then( + (command: ICloudrunServerGroupCommandData) => { + command = { + ...command, + ...cluster, + backingData: { + ...command.metadata.backingData.backingData, + // triggerOptions: AppengineServerGroupCommandBuilder.getTriggerOptions(pipeline), + //expectedArtifacts: CloudrunServerGroupCommandBuilder.getExpectedArtifacts(pipeline), + }, + credentials: cluster.account || command.metadata.backingData.credentials, + viewState: { + ...command.metadata.backingData.viewState, + stage: _stage, + pipeline, + }, + //isNew : false + } as ICloudrunServerGroupCommandData; + return command; + }, + ); + } + + public static getCredentials(accounts: IAccountDetails[]): string { + const accountNames: string[] = (accounts || []).map((account) => account.name); + const defaultCredentials: string = CloudrunProviderSettings.defaults.account; + + return accountNames.includes(defaultCredentials) ? defaultCredentials : accountNames[0]; + } + + // new servergroup command + public static buildNewServerGroupCommand( + app: Application, + sourceAccount?: string, + mode = 'create', + ): PromiseLike { + const dataToFetch = { + accounts: AccountService.getAllAccountDetailsForProvider('cloudrun'), + artifactAccounts: AccountService.getArtifactAccounts(), + }; + + // TODO(dpeach): if no callers of this method are Angular controllers, + // $q.all may be safely replaced with Promise.all. + return $q.all(dataToFetch).then((backingData: { accounts: IAccountDetails[] }) => { + const { accounts } = backingData; + + // const credentials = this.getCredentials(accounts); + /* .then((backingData: { accounts: IAccountDetails[]; artifactAccounts: IArtifactAccount[]}) => { + const { accounts, artifactAccounts } = backingData; */ + + const account = accounts.some((a) => a.name === sourceAccount) + ? accounts.find((a) => a.name === sourceAccount).name + : accounts.length + ? accounts[0].name + : null; + const viewState: IViewState = { + mode, + submitButtonLabel: getSubmitButtonLabel(mode), + disableStrategySelection: mode === 'create', + }; + + //TODO : needs to be modified to [] at the time of integration + const regions = backingData.accounts.some((a) => a.name === sourceAccount) + ? accounts.find((a) => a.name === sourceAccount).regions + : []; + const credentials = account ? account : this.getCredentials(accounts); + const cloudProvider = 'cloudrun'; + return { + command: { + application: app.name, + configFiles: [''], + cloudProvider, + selectedProvider: cloudProvider, + provider: cloudProvider, + regions, + credentials, + gitCredentialType: 'NONE', + manifest: null, + sourceType: 'git', + configArtifacts: [], + interestingHealthProviderNames: [], + fromArtifact: false, + account, + viewState, + }, + metadata: { + backingData, + }, + } as ICloudrunServerGroupCommandData; + }); + } +} + +export const CLOUDRUN_SERVER_GROUP_COMMAND_BUILDER = 'spinnaker.cloudrun.serverGroup.commandBuilder.service'; + +module(CLOUDRUN_SERVER_GROUP_COMMAND_BUILDER, []).service( + 'cloudrunV2ServerGroupCommandBuilder', + CloudrunV2ServerGroupCommandBuilder, +); diff --git a/packages/cloudrun/src/serverGroup/configure/wizard/BasicSettings.tsx b/packages/cloudrun/src/serverGroup/configure/wizard/BasicSettings.tsx new file mode 100644 index 00000000000..10597662011 --- /dev/null +++ b/packages/cloudrun/src/serverGroup/configure/wizard/BasicSettings.tsx @@ -0,0 +1,109 @@ +import type { FormikProps } from 'formik'; +import React from 'react'; +import type { IAccount } from '@spinnaker/core'; +import { AccountSelectInput, HelpField } from '@spinnaker/core'; +import type { ICloudrunServerGroupCommandData } from '../serverGroupCommandBuilder.service'; + +export interface IServerGroupBasicSettingsProps { + accounts: IAccount[]; + onAccountSelect: (account: string) => void; + selectedAccount: string; + formik: IWizardServerGroupBasicSettingsProps['formik']; + onEnterStack: (stack: string) => void; + detailsChanged: (detail: string) => void; +} + +export function ServerGroupBasicSettings({ + accounts, + onAccountSelect, + selectedAccount, + formik, + onEnterStack, + detailsChanged, +}: IServerGroupBasicSettingsProps) { + const { values } = formik; + const { stack = '' } = values.command; + + return ( +
    +
    +
    Account
    +
    + onAccountSelect(evt.target.value)} + readOnly={false} + accounts={accounts} + provider="cloudrun" + /> +
    +
    + +
    +
    + Stack +
    +
    + onEnterStack(e.target.value)} + /> +
    +
    + +
    +
    + Detail +
    +
    + detailsChanged(e.target.value)} + /> +
    +
    +
    + ); +} + +export interface IWizardServerGroupBasicSettingsProps { + formik: FormikProps; +} + +export class WizardServerGroupBasicSettings extends React.Component { + private accountUpdated = (account: string): void => { + const { formik } = this.props; + formik.values.command.account = account; + formik.setFieldValue('account', account); + }; + + private stackChanged = (stack: string): void => { + const { setFieldValue, values } = this.props.formik; + values.command.stack = stack; + setFieldValue('stack', stack); + }; + + private freeFormDetailsChanged = (freeFormDetails: string) => { + const { setFieldValue, values } = this.props.formik; + values.command.freeFormDetails = freeFormDetails; + setFieldValue('freeFormDetails', freeFormDetails); + }; + + public render() { + const { formik } = this.props; + return ( + + ); + } +} diff --git a/packages/cloudrun/src/serverGroup/configure/wizard/ConfigFiles.tsx b/packages/cloudrun/src/serverGroup/configure/wizard/ConfigFiles.tsx new file mode 100644 index 00000000000..d0b959fdf51 --- /dev/null +++ b/packages/cloudrun/src/serverGroup/configure/wizard/ConfigFiles.tsx @@ -0,0 +1,98 @@ +import type { FormikProps } from 'formik'; +import React, { useState } from 'react'; + +import { HelpField, TextAreaInput } from '@spinnaker/core'; +import type { ICloudrunServerGroupCommandData } from '../serverGroupCommandBuilder.service'; + +export interface IServerGroupConfigFilesSettingsProps { + configFiles: string[]; + onEnterConfig: (file: string[]) => void; +} +type configFiles = IServerGroupConfigFilesSettingsProps['configFiles']; + +export function ServerGroupConfigFilesSettings({ configFiles, onEnterConfig }: IServerGroupConfigFilesSettingsProps) { + const [configValues, setConfigValues] = useState(configFiles); + + const deleteConfig = (i: number) => { + const newConfigValues = [...configValues]; + newConfigValues.splice(i, 1); + setConfigValues(newConfigValues); + onEnterConfig(newConfigValues); + }; + function mapTabToSpaces(event: any, i: number) { + if (event.which === 9) { + event.preventDefault(); + const cursorPosition = event.target.selectionStart; + const inputValue = event.target.value; + event.target.value = `${inputValue.substring(0, cursorPosition)} ${inputValue.substring(cursorPosition)}`; + event.target.selectionStart += 2; + } + const newConfigValues = [...configValues]; + newConfigValues[i] = event.target.value; + setConfigValues(newConfigValues); + onEnterConfig(newConfigValues); + } + + const addConfigFile = () => { + const newConfigValues = [...configValues]; + newConfigValues.push(''); + setConfigValues(newConfigValues); + onEnterConfig(newConfigValues); + }; + + return ( +
    +
    + {configValues.map((configFile, index) => ( + <> +
    + ConfigFile {' '} +
    +
    + mapTabToSpaces(e, index)} + /> +
    +
    + +
    + + ))} +
    + +
    + +
    +
    + ); +} + +export interface IWizardServerGroupConfigFilesSettingsProps { + formik: FormikProps; +} + +export class WizardServerGroupConfigFilesSettings extends React.Component { + private configUpdated = (configFiles: string[]): void => { + const { formik } = this.props; + formik.values.command.configFiles = configFiles; + formik.setFieldValue('configFiles', configFiles); + }; + + // yaml config files input from server group wizard + public render() { + const { formik } = this.props; + return ( + + ); + } +} diff --git a/packages/cloudrun/src/serverGroup/configure/wizard/serverGroupWizard.tsx b/packages/cloudrun/src/serverGroup/configure/wizard/serverGroupWizard.tsx new file mode 100644 index 00000000000..18caa92b867 --- /dev/null +++ b/packages/cloudrun/src/serverGroup/configure/wizard/serverGroupWizard.tsx @@ -0,0 +1,135 @@ +import React from 'react'; +import type { Application, IModalComponentProps, IStage } from '@spinnaker/core'; +import { noop, ReactInjector, ReactModal, TaskMonitor, WizardModal, WizardPage } from '@spinnaker/core'; +import { WizardServerGroupBasicSettings } from './BasicSettings'; +import { WizardServerGroupConfigFilesSettings } from './ConfigFiles'; +import type { ICloudrunServerGroupCommandData } from '../serverGroupCommandBuilder.service'; +import { CloudrunServerGroupCommandBuilder } from '../serverGroupCommandBuilder.service'; + +export interface ICloudrunServerGroupModalProps extends IModalComponentProps { + title: string; + application: Application; + command: ICloudrunServerGroupCommandData; + isNew?: boolean; +} + +export interface ICloudrunServerGroupModalState { + command: ICloudrunServerGroupCommandData; + loaded: boolean; + taskMonitor: TaskMonitor; +} + +export class ServerGroupWizard extends React.Component { + public static defaultProps: Partial = { + closeModal: noop, + dismissModal: noop, + }; + + private _isUnmounted = false; + + /* private serverGroupWriter: ServerGroupWriter; */ + public static show(props: ICloudrunServerGroupModalProps): Promise { + const modalProps = { dialogClassName: 'wizard-modal modal-lg' }; + return ReactModal.show(ServerGroupWizard, props, modalProps); + } + + constructor(props: ICloudrunServerGroupModalProps) { + super(props); + if (!props.command) { + CloudrunServerGroupCommandBuilder.buildNewServerGroupCommand(props.application).then((command) => { + Object.assign(this.state.command, command); + this.setState({ loaded: true }); + }); + } + + this.state = { + loaded: !!props.command, + command: props.command || ({} as ICloudrunServerGroupCommandData), + taskMonitor: new TaskMonitor({ + application: props.application, + title: `${this.props.isNew ? 'Creating' : 'Updating'} your Server Group`, + modalInstance: TaskMonitor.modalInstanceEmulation(() => this.props.dismissModal()), + onTaskComplete: this.onTaskComplete, + }), + }; + } + + private onTaskComplete = () => { + this.props.application.serverGroups.refresh(); + this.props.application.serverGroups.onNextRefresh(null, this.onApplicationRefresh); + }; + + protected onApplicationRefresh = (): void => { + if (this._isUnmounted) { + return; + } + + const { command } = this.props; + const { taskMonitor } = this.state; + const cloneStage = taskMonitor.task.execution.stages.find((stage: IStage) => stage.type === 'cloneServerGroup'); + if (cloneStage && cloneStage.context['deploy.server.groups']) { + const newServerGroupName = cloneStage.context['deploy.server.groups'][command.command.region]; + if (newServerGroupName) { + const newStateParams = { + serverGroup: newServerGroupName, + accountId: command.command.credentials, + region: command.command.region, + provider: 'cloudrun', + }; + let transitionTo = '^.^.^.clusters.serverGroup'; + if (ReactInjector.$state.includes('**.clusters.serverGroup')) { + // clone via details, all view + transitionTo = '^.serverGroup'; + } + if (ReactInjector.$state.includes('**.clusters.cluster.serverGroup')) { + // clone or create with details open + transitionTo = '^.^.serverGroup'; + } + if (ReactInjector.$state.includes('**.clusters')) { + // create new, no details open + transitionTo = '.serverGroup'; + } + ReactInjector.$state.go(transitionTo, newStateParams); + } + } + }; + + private submit = (c: ICloudrunServerGroupCommandData): void => { + const command: any = CloudrunServerGroupCommandBuilder.copyAndCleanCommand(c.command); + const submitMethod = () => ReactInjector.serverGroupWriter.cloneServerGroup(command, this.props.application); + this.state.taskMonitor.submit(submitMethod); + }; + public render() { + const { dismissModal, isNew } = this.props; + const { loaded, taskMonitor, command } = this.state; + + return ( + + heading={`${isNew ? 'Create New' : 'Update'} Server Group`} + initialValues={command} + loading={!loaded} + taskMonitor={taskMonitor} + dismissModal={dismissModal} + closeModal={this.submit} + submitButtonLabel={isNew ? 'Create' : 'Edit'} + render={({ formik, nextIdx, wizard }) => ( + <> + } + /> + + } + /> + + )} + /> + ); + } +} diff --git a/packages/cloudrun/src/serverGroup/details/details.controller.ts b/packages/cloudrun/src/serverGroup/details/details.controller.ts new file mode 100644 index 00000000000..c1224fde687 --- /dev/null +++ b/packages/cloudrun/src/serverGroup/details/details.controller.ts @@ -0,0 +1,234 @@ +import type { IController, IScope } from 'angular'; +import { module } from 'angular'; +import { cloneDeep, map, mapValues, reduce } from 'lodash'; +import type { + Application, + IConfirmationModalParams, + ILoadBalancer, + IServerGroup, + ServerGroupWriter, +} from '@spinnaker/core'; +import { + ConfirmationModalService, + SERVER_GROUP_WRITER, + ServerGroupReader, + ServerGroupWarningMessageService, +} from '@spinnaker/core'; + +import { CloudrunHealth } from '../../common/cloudrunHealth'; +import type { ICloudrunLoadBalancer } from '../../common/domain/ICloudrunLoadBalancer'; +import type { ICloudrunServerGroup } from '../../interfaces'; + +interface IServerGroupFromStateParams { + accountId: string; + region: string; + name: string; +} + +class CloudrunServerGroupDetailsController implements IController { + public state = { loading: true }; + public serverGroup: ICloudrunServerGroup; + + // allocation table to calculate the traffic to each loadbalancer which must sums up to 100 % + private static buildExpectedAllocationsTable(expectedAllocations: { [key: string]: number }): string { + const tableRows = map(expectedAllocations, (percent, revisionName) => { + return ` + + ${revisionName} + ${percent}% + `; + }).join(''); + + return ` + + + + + + + + + ${tableRows} + +
    Server GroupAllocation
    `; + } + + public static $inject = ['$state', '$scope', 'serverGroup', 'app', 'serverGroupWriter']; + constructor( + private $state: any, + private $scope: IScope, + serverGroup: IServerGroupFromStateParams, + public app: Application, + private serverGroupWriter: ServerGroupWriter, + ) { + this.extractServerGroup(serverGroup) + .then(() => { + if (!this.$scope.$$destroyed) { + this.app.getDataSource('serverGroups').onRefresh(this.$scope, () => this.extractServerGroup(serverGroup)); + } + }) + .catch(() => this.autoClose()); + } + + // on disabling a server group , the load balancer will be set to 0 percent and will be added to enabled ones + private expectedAllocationsAfterDisableOperation( + serverGroup: IServerGroup, + app: Application, + ): { [key: string]: number } { + const loadBalancer = app.getDataSource('loadBalancers').data.find((toCheck: ICloudrunLoadBalancer): boolean => { + const allocations = toCheck.split?.trafficTargets ?? {}; + const enabledServerGroups = Object.keys(allocations); + return enabledServerGroups.includes(serverGroup.name); + }); + + if (loadBalancer) { + let allocations = cloneDeep(loadBalancer.split.trafficTargets); + delete allocations[serverGroup.name]; + const denominator = reduce(allocations, (partialSum: number, allocation: number) => partialSum + allocation, 0); + allocations = mapValues(allocations, (allocation) => Math.round(allocation / denominator)); + return allocations; + } else { + return null; + } + } + + // destroy existing server group + public canDestroyServerGroup(): boolean { + if (this.serverGroup) { + if (this.serverGroup.disabled) { + return true; + } + + const expectedAllocations = this.expectedAllocationsAfterDisableOperation(this.serverGroup, this.app); + if (expectedAllocations) { + return Object.keys(expectedAllocations).length > 0; + } else { + return false; + } + } else { + return false; + } + } + + public destroyServerGroup(): void { + const stateParams = { + name: this.serverGroup.name, + accountId: this.serverGroup.account, + region: this.serverGroup.region, + }; + + const taskMonitor = { + application: this.app, + title: 'Destroying ' + this.serverGroup.name, + onTaskComplete: () => { + if (this.$state.includes('**.serverGroup', stateParams)) { + this.$state.go('^'); + } + }, + }; + + const submitMethod = (params: any) => this.serverGroupWriter.destroyServerGroup(this.serverGroup, this.app, params); + + const confirmationModalParams = { + header: 'Really destroy ' + this.serverGroup.name + '?', + buttonText: 'Destroy ' + this.serverGroup.name, + account: this.serverGroup.account, + taskMonitorConfig: taskMonitor, + submitMethod, + askForReason: true, + platformHealthOnlyShowOverride: this.app.attributes.platformHealthOnlyShowOverride, + platformHealthType: CloudrunHealth.PLATFORM, + body: this.getBodyTemplate(this.serverGroup, this.app), + interestingHealthProviderNames: [] as string[], + }; + + if (this.app.attributes.platformHealthOnlyShowOverride && this.app.attributes.platformHealthOnly) { + confirmationModalParams.interestingHealthProviderNames = [CloudrunHealth.PLATFORM]; + } + + ConfirmationModalService.confirm(confirmationModalParams); + } + + private getBodyTemplate(serverGroup: ICloudrunServerGroup, app: Application): string { + let template = ''; + const params: IConfirmationModalParams = {}; + ServerGroupWarningMessageService.addDestroyWarningMessage(app, serverGroup, params); + if (params.body) { + template += params.body; + } + + if (!serverGroup.disabled) { + const expectedAllocations = this.expectedAllocationsAfterDisableOperation(serverGroup, app); + + template += ` +
    +

    + A destroy operation will first disable this server group. +

    +

    + For CloudRun, a disable operation sets this server group's allocation + to 0% and sets the other enabled server groups' allocations to their relative proportions + before the disable operation. The approximate allocations that will result from this operation are shown below. +

    +

    + If you would like more fine-grained control over your server groups' allocations, + edit ${serverGroup.loadBalancers[0]} under the Load Balancers tab. +

    +
    +
    + ${CloudrunServerGroupDetailsController.buildExpectedAllocationsTable(expectedAllocations)} +
    +
    +
    + `; + } + + return template; + } + + private autoClose(): void { + if (this.$scope.$$destroyed) { + return; + } else { + this.$state.params.allowModalToStayOpen = true; + this.$state.go('^', null, { location: 'replace' }); + } + } + + private extractServerGroup({ name, accountId, region }: IServerGroupFromStateParams): PromiseLike { + return ServerGroupReader.getServerGroup(this.app.name, accountId, region, name).then( + (serverGroupDetails: IServerGroup) => { + let fromApp = this.app.getDataSource('serverGroups').data.find((toCheck: IServerGroup) => { + return toCheck.name === name && toCheck.account === accountId && toCheck.region === region; + }); + + if (!fromApp) { + this.app.getDataSource('loadBalancers').data.some((loadBalancer: ILoadBalancer) => { + if (loadBalancer.account === accountId) { + return loadBalancer.serverGroups.some((toCheck: IServerGroup) => { + let result = false; + if (toCheck.name === name) { + fromApp = toCheck; + result = true; + } + return result; + }); + } else { + return false; + } + }); + } + + this.serverGroup = { ...serverGroupDetails, ...fromApp }; + this.state.loading = false; + }, + ); + } +} + +export const CLOUDRUN_SERVER_GROUP_DETAILS_CTRL = 'spinnaker.cloudrun.serverGroup.details.controller'; + +module(CLOUDRUN_SERVER_GROUP_DETAILS_CTRL, [SERVER_GROUP_WRITER]).controller( + 'cloudrunV2ServerGroupDetailsCtrl', + CloudrunServerGroupDetailsController, +); diff --git a/packages/cloudrun/src/serverGroup/details/details.html b/packages/cloudrun/src/serverGroup/details/details.html new file mode 100644 index 00000000000..d5f0dd2fba7 --- /dev/null +++ b/packages/cloudrun/src/serverGroup/details/details.html @@ -0,0 +1,88 @@ +
    +
    + +

    + +

    +
    + +
    +
    + + + +
    +
    + +

    {{ctrl.serverGroup.name}}

    +
    +
    + +
    +
    +
    + +
    Disabled
    + + +
    +
    Created
    +
    {{ctrl.serverGroup.createdTime | timestamp}}
    +
    In
    +
    +
    Region
    +
    {{ctrl.serverGroup.region}}
    +
    +
    + + +
    +
    Min/Max
    +
    {{ctrl.serverGroup.capacity.min}}
    +
    Current
    +
    {{ctrl.serverGroup.instances.length}}
    +
    +
    +
    Min
    +
    {{ctrl.serverGroup.capacity.min}}
    +
    Max
    +
    {{ctrl.serverGroup.capacity.max}}
    +
    Current
    +
    {{ctrl.serverGroup.instances.length}}
    +
    +
    + +
    +
    Instances
    +
    + +
    +
    +
    +
    +
    diff --git a/packages/cloudrun/src/serverGroup/index.ts b/packages/cloudrun/src/serverGroup/index.ts new file mode 100644 index 00000000000..4bfea14496c --- /dev/null +++ b/packages/cloudrun/src/serverGroup/index.ts @@ -0,0 +1,3 @@ +export * from './configure/serverGroupCommandBuilder.service'; +export * from './serverGroupTransformer.service'; +export * from './details/details.controller'; diff --git a/packages/cloudrun/src/serverGroup/serverGroupTransformer.service.ts b/packages/cloudrun/src/serverGroup/serverGroupTransformer.service.ts new file mode 100644 index 00000000000..4258376a7ca --- /dev/null +++ b/packages/cloudrun/src/serverGroup/serverGroupTransformer.service.ts @@ -0,0 +1,65 @@ +import { module } from 'angular'; +import type { Application } from '@spinnaker/core'; +import type { ICloudrunServerGroup, ICloudrunServerGroupManager } from '../interfaces'; + +import type { ICloudrunServerGroupCommandData } from '../serverGroup/configure/serverGroupCommandBuilder.service'; + +export class CloudrunV2ServerGroupTransformer { + public normalizeServerGroup( + serverGroup: ICloudrunServerGroup, + application: Application, + ): PromiseLike { + return application + .getDataSource('serverGroupManagers') + .ready() + .then((sgManagers: ICloudrunServerGroupManager[]) => { + (serverGroup.serverGroupManagers || []).forEach((managerRef) => { + const sgManager = sgManagers.find( + (manager: ICloudrunServerGroupManager) => + managerRef.account == manager.account && + managerRef.location == manager.region && + `${manager.kind} ${managerRef.name}` == manager.name, + ); + if (sgManager) { + managerRef.name = sgManager.name; + } + }); + return serverGroup; + }); + } + + public convertServerGroupCommandToDeployConfiguration(base: ICloudrunServerGroupCommandData): any { + const deployConfig = { ...base } as any; + + deployConfig.cloudProvider = 'cloudrun'; + deployConfig.account = deployConfig.credentials; + + const deleteFields = [ + 'regions', + 'viewState', + 'backingData', + 'selectedProvider', + 'instanceProfile', + 'vpcId', + 'relationships', + 'manifest', + 'manifests', + 'moniker', + 'stack', + 'versioned', + 'availabilityZones', + 'source', + ]; + deleteFields.forEach((key: keyof typeof deployConfig) => { + delete deployConfig[key]; + }); + + return deployConfig; + } +} + +export const CLOUDRUN_SERVER_GROUP_TRANSFORMER = 'spinnaker.cloudrun.serverGroup.transformer.service'; +module(CLOUDRUN_SERVER_GROUP_TRANSFORMER, []).service( + 'cloudrunV2ServerGroupTransformer', + CloudrunV2ServerGroupTransformer, +); diff --git a/packages/cloudrun/tsconfig.json b/packages/cloudrun/tsconfig.json new file mode 100644 index 00000000000..4d7b8b545e8 --- /dev/null +++ b/packages/cloudrun/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../tsconfig.app.base.json", + "compilerOptions": { + "jsx": "react", + "outDir": "dist", + "rootDir": "./src" + }, + "include": ["src/**/*.ts", "src/**/*.tsx"], + "exclude": ["**/*.spec.*"] +} diff --git a/scripts/buildModules.js b/scripts/buildModules.js index bd458574b91..53bc2d20051 100755 --- a/scripts/buildModules.js +++ b/scripts/buildModules.js @@ -27,6 +27,7 @@ async function buildModules() { 'google', 'huaweicloud', 'kubernetes', + 'cloudrun', 'oracle', 'tencentcloud', ].map((module) => runYarnBuild(`${PACKAGES_ROOT}/${module}`)), diff --git a/scripts/build_order.sh b/scripts/build_order.sh index 89a69d5f0c2..a3f00d22052 100755 --- a/scripts/build_order.sh +++ b/scripts/build_order.sh @@ -17,6 +17,7 @@ ModuleDeps () { google) echo "core" ;; huaweicloud) echo "core" ;; kubernetes) echo "core" ;; + cloudrun) echo "core" ;; oracle) echo "core" ;; presentation) echo "presentation";; titus) echo "amazon docker core" ;; diff --git a/scripts/bumpPackage.js b/scripts/bumpPackage.js index 1144e0f15e4..4e53cd118e8 100755 --- a/scripts/bumpPackage.js +++ b/scripts/bumpPackage.js @@ -28,6 +28,7 @@ const packages = [ 'packages/google/', 'packages/huaweicloud/', 'packages/kubernetes/', + 'packages/cloudrun/', 'packages/oracle/', 'packages/tencentcloud/', 'packages/titus/', diff --git a/tsconfig.json b/tsconfig.json index b5639f1baad..7facf087461 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -22,6 +22,9 @@ "@spinnaker/core": ["core/src"], "core/*": ["core/src/*"], "core": ["core/src"], + "@spinnaker/cloudrun": ["cloudrun/src"], + "cloudrun/*": ["cloudrun/src/*"], + "cloudrun": ["cloudrun/src"], "@spinnaker/docker": ["docker/src"], "docker/*": ["docker/src/*"], "docker": ["docker/src"],