diff --git a/halconfig/settings.js b/halconfig/settings.js
index 039ea00f95c..3e3d6aaf303 100644
--- a/halconfig/settings.js
+++ b/halconfig/settings.js
@@ -63,6 +63,11 @@ var cloudfoundry = {
account: '{%cloudfoundry.default.account%}',
},
};
+var cloudrun = {
+ defaults: {
+ account: '{%cloudrun.default.account%}',
+ },
+};
var dcos = {
defaults: {
account: '{%dcos.default.account%}',
@@ -145,6 +150,7 @@ window.spinnakerSettings = {
aws: aws,
azure: azure,
cloudfoundry: cloudfoundry,
+ cloudrun: cloudrun,
dcos: dcos,
ecs: ecs,
gce: gce,
diff --git a/karma-shim.js b/karma-shim.js
index 6aa2110839b..c0172403488 100644
--- a/karma-shim.js
+++ b/karma-shim.js
@@ -34,6 +34,9 @@ testContext.keys().forEach(testContext);
testContext = require.context('./packages/cloudfoundry/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);
+
testContext = require.context('./packages/core/src', true, /\.spec\.(js|ts|tsx)$/);
testContext.keys().forEach(testContext);
diff --git a/karma.conf.js b/karma.conf.js
index c5252fd14bd..1e00dae0485 100644
--- a/karma.conf.js
+++ b/karma.conf.js
@@ -32,6 +32,8 @@ const webpackConfig = {
'@spinnaker/azure': path.resolve(`${MODULES_ROOT}/azure/src`),
cloudfoundry: path.resolve(`${MODULES_ROOT}/cloudfoundry/src`),
'@spinnaker/cloudfoundry': path.resolve(`${MODULES_ROOT}/cloudfoundry/src`),
+ cloudrun: path.resolve(`${MODULES_ROOT}/cloudrun/src`),
+ '@spinnaker/cloudrun': path.resolve(`${MODULES_ROOT}/cloudrun/src`),
core: path.resolve(`${MODULES_ROOT}/core/src`),
'@spinnaker/core': path.resolve(`${MODULES_ROOT}/core/src`),
dcos: path.resolve(`${MODULES_ROOT}/dcos/src`),
diff --git a/package.json b/package.json
index 652749cef8b..189c3f8b7bf 100644
--- a/package.json
+++ b/package.json
@@ -86,6 +86,7 @@
"packages/appengine",
"packages/azure",
"packages/cloudfoundry",
+ "packages/cloudrun",
"packages/core",
"packages/dcos",
"packages/docker",
diff --git a/packages/app/package.json b/packages/app/package.json
index 0ce56b9134b..63b502f7f63 100644
--- a/packages/app/package.json
+++ b/packages/app/package.json
@@ -17,6 +17,7 @@
"@spinnaker/amazon": "^0.13.5",
"@spinnaker/appengine": "^0.1.3",
"@spinnaker/cloudfoundry": "^0.1.3",
+ "@spinnaker/cloudrun": "^0.0.1",
"@spinnaker/core": "^0.23.0",
"@spinnaker/docker": "^0.0.137",
"@spinnaker/ecs": "^0.0.356",
diff --git a/packages/app/src/app.ts b/packages/app/src/app.ts
index 86c6d8fa28d..788fa85bb2e 100644
--- a/packages/app/src/app.ts
+++ b/packages/app/src/app.ts
@@ -15,6 +15,7 @@ import { ORACLE_MODULE } from '@spinnaker/oracle';
import { KAYENTA_MODULE } from '@spinnaker/kayenta';
import { TITUS_MODULE } from '@spinnaker/titus';
import { ECS_MODULE } from '@spinnaker/ecs';
+import { CLOUDRUN_MODULE } from '@spinnaker/cloudrun';
import '@spinnaker/cloudfoundry';
module('netflix.spinnaker', [
@@ -30,4 +31,5 @@ module('netflix.spinnaker', [
KUBERNETES_MODULE,
KAYENTA_MODULE,
TITUS_MODULE,
+ CLOUDRUN_MODULE,
]);
diff --git a/packages/app/src/settings.js b/packages/app/src/settings.js
index 6683223489c..555c7a55434 100644
--- a/packages/app/src/settings.js
+++ b/packages/app/src/settings.js
@@ -91,6 +91,7 @@ window.spinnakerSettings = {
'aws',
'azure',
'cloudfoundry',
+ 'cloudrun',
'dcos',
'ecs',
'gce',
@@ -211,6 +212,11 @@ window.spinnakerSettings = {
account: 'my-cloudfoundry-account',
},
},
+ cloudrun: {
+ defaults: {
+ account: 'my-cloudrun-account',
+ },
+ },
dcos: {
defaults: {
account: 'my-dcos-account',
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..ca1d181e31e
--- /dev/null
+++ b/packages/cloudrun/package.json
@@ -0,0 +1,53 @@
+{
+ "name": "@spinnaker/cloudrun",
+ "license": "Apache-2.0",
+ "version": "0.0.1",
+ "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.23.0",
+ "@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.3.10",
+ "enzyme": "3.11.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.3.0",
+ "@types/angular": "1.6.26",
+ "@types/angular-ui-bootstrap": "0.13.41",
+ "@types/dompurify": "^2.3.3",
+ "@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..4443a4ed27a
--- /dev/null
+++ b/packages/cloudrun/src/cloudrun.module.ts
@@ -0,0 +1,72 @@
+import { module } from 'angular';
+
+import { CloudProviderRegistry, DeploymentStrategyRegistry } from '@spinnaker/core';
+
+import { CLOUDRUN_COMPONENT_URL_DETAILS } from './common/componentUrlDetails.component';
+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_COMPONENT_URL_DETAILS,
+ 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',
+ 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',
+ skipUpstreamStageCheck: true,
+ },
+
+ loadBalancer: {
+ transformer: 'cloudrunLoadBalancerTransformer',
+ createLoadBalancerTemplateUrl: require('./loadBalancer/configure/wizard/wizard.html'),
+ createLoadBalancerController: 'cloudrunLoadBalancerWizardCtrl',
+ detailsTemplateUrl: require('./loadBalancer/details/details.html'),
+ detailsController: 'cloudrunLoadBalancerDetailsCtrl',
+ },
+ });
+});
+
+DeploymentStrategyRegistry.registerProvider('cloudrun', ['custom', 'redblack', 'rollingpush', 'rollingredblack']);
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..5dfc89243af
--- /dev/null
+++ b/packages/cloudrun/src/common/cloudrunHealth.ts
@@ -0,0 +1,3 @@
+export class CloudrunHealth {
+ public static PLATFORM = 'Cloud Run Service';
+}
diff --git a/packages/cloudrun/src/common/componentUrlDetails.component.ts b/packages/cloudrun/src/common/componentUrlDetails.component.ts
new file mode 100644
index 00000000000..3fdf0e43e58
--- /dev/null
+++ b/packages/cloudrun/src/common/componentUrlDetails.component.ts
@@ -0,0 +1,22 @@
+import type { IComponentOptions } from 'angular';
+import { module } from 'angular';
+
+const cloudrunComponentUrlDetailsComponent: IComponentOptions = {
+ bindings: { component: '<' },
+ template: `
+
HTTPS
+
+ {{$ctrl.component.url}}
+
+
+ `,
+};
+
+export const CLOUDRUN_COMPONENT_URL_DETAILS = 'spinnaker.cloudrun.componentUrlDetails.component';
+
+module(CLOUDRUN_COMPONENT_URL_DETAILS, []).component(
+ 'cloudrunComponentUrlDetails',
+ cloudrunComponentUrlDetailsComponent,
+);
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..b59d7f5555a
--- /dev/null
+++ b/packages/cloudrun/src/common/loadBalancerMessage.component.html
@@ -0,0 +1,11 @@
+
+
+
+
+ Spinnaker cannot create a load balancer for Cloud Run.
+ A Spinnaker load balancer maps to an Cloud Run Service, which will be created automatically alongside a
+ Revision. Once created, the Service can be edited as a Load Balancer.
+
+
+
+
diff --git a/packages/cloudrun/src/common/loadBalancerMessage.component.ts b/packages/cloudrun/src/common/loadBalancerMessage.component.ts
new file mode 100644
index 00000000000..c0d14e6a5f1
--- /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..2f4be46b1be
--- /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 Cloud Run Service 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..d8fa5a8f555
--- /dev/null
+++ b/packages/cloudrun/src/instance/details/details.controller.ts
@@ -0,0 +1,84 @@
+import type { IController, IQService } from 'angular';
+import { module } from 'angular';
+import { flattenDeep } from 'lodash';
+
+import type { Application, ILoadBalancer } from '@spinnaker/core';
+import { InstanceReader, 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 = "A Cloud Run instance is 'Up' if a load balancer is directing traffic to its server group.";
+ public outOfServiceToolTip = `
+ A Cloud Run 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;
+ });
+ }
+
+ 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..cd8117a3658
--- /dev/null
+++ b/packages/cloudrun/src/instance/details/details.html
@@ -0,0 +1,68 @@
+
+
+
+
+
+ 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..a1afe582c6a
--- /dev/null
+++ b/packages/cloudrun/src/interfaces/infrastructure.types.ts
@@ -0,0 +1,23 @@
+import type { IInstance, ILoadBalancer, IMoniker, IServerGroup } 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: `
+
+ `,
+ 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 @@
+
+
+
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..afae927d82b
--- /dev/null
+++ b/packages/cloudrun/src/loadBalancer/configure/wizard/basicSettings.component.ts
@@ -0,0 +1,86 @@
+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,
+ });
+ this.updateServerGroupOptions();
+ } else if (this.forPipelineConfig) {
+ this.loadBalancer.splitDescription.allocationDescriptions.push({
+ percent: 0,
+ 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..ce62882f2c2
--- /dev/null
+++ b/packages/cloudrun/src/loadBalancer/configure/wizard/stageAllocationConfigurationRow.component.ts
@@ -0,0 +1,79 @@
+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;
+
+ 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..66523c33345
--- /dev/null
+++ b/packages/cloudrun/src/loadBalancer/configure/wizard/wizard.less
@@ -0,0 +1,9 @@
+cloudrun-load-balancer-basic-settings {
+ a.btn.btn-link {
+ padding: 0;
+ }
+
+ .form-group {
+ margin-top: 0.4rem;
+ }
+}
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..0b6eb57b28c
--- /dev/null
+++ b/packages/cloudrun/src/loadBalancer/details/details.html
@@ -0,0 +1,86 @@
+
+
+
+
+
+
+
+
+ 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..ed1be0485cc
--- /dev/null
+++ b/packages/cloudrun/src/loadBalancer/loadBalancerTransformer.ts
@@ -0,0 +1,168 @@
+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 {
+ revisionName?: string;
+ target?: string;
+ cluster?: string;
+ percent: number;
+}
+
+export interface ICloudrunTrafficSplitDescription {
+ 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;
+ return acc.concat({ percent, revisionName, locatorType: 'fromExisting' });
+ },
+ [],
+ );
+ 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..91d2c6dc124
--- /dev/null
+++ b/packages/cloudrun/src/logo/cloudrun.icon.svg
@@ -0,0 +1,27 @@
+
+
+
+
+ Icon_24px_CloudRun_Color
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/cloudrun/src/logo/cloudrun.logo.less b/packages/cloudrun/src/logo/cloudrun.logo.less
new file mode 100644
index 00000000000..914b571a583
--- /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 00000000000..423cf5cb794
Binary files /dev/null and b/packages/cloudrun/src/logo/cloudrun.logo.png differ
diff --git a/packages/cloudrun/src/pipeline/pipeline.module.ts b/packages/cloudrun/src/pipeline/pipeline.module.ts
new file mode 100644
index 00000000000..203bdc9f84b
--- /dev/null
+++ b/packages/cloudrun/src/pipeline/pipeline.module.ts
@@ -0,0 +1,6 @@
+import { module } from 'angular';
+
+import { CLOUDRUN_EDIT_LOAD_BALANCER_STAGE } from './stages/editLoadBalancer/cloudrunEditLoadBalancerStage';
+
+export const CLOUDRUN_PIPELINE_MODULE = 'spinnaker.cloudrun.pipeline.module';
+module(CLOUDRUN_PIPELINE_MODULE, [CLOUDRUN_EDIT_LOAD_BALANCER_STAGE]);
diff --git a/packages/cloudrun/src/pipeline/stages/editLoadBalancer/cloudrunEditLoadBalancerStage.ts b/packages/cloudrun/src/pipeline/stages/editLoadBalancer/cloudrunEditLoadBalancerStage.ts
new file mode 100644
index 00000000000..cbddfdb92d8
--- /dev/null
+++ b/packages/cloudrun/src/pipeline/stages/editLoadBalancer/cloudrunEditLoadBalancerStage.ts
@@ -0,0 +1,74 @@
+import type { IController } from 'angular';
+import { module } from 'angular';
+import type { IModalService } from 'angular-ui-bootstrap';
+import { cloneDeep } from 'lodash';
+
+import type { ILoadBalancer } from '@spinnaker/core';
+import { CloudProviderRegistry, Registry } from '@spinnaker/core';
+
+import { CLOUDRUN_LOAD_BALANCER_CHOICE_MODAL_CTRL } from './loadBalancerChoice.modal.controller';
+
+class CloudrunEditLoadBalancerStageCtrl implements IController {
+ public static $inject = ['$scope', '$uibModal'];
+ constructor(public $scope: any, private $uibModal: IModalService) {
+ $scope.stage.loadBalancers = $scope.stage.loadBalancers || [];
+ $scope.stage.cloudProvider = 'cloudrun';
+ }
+
+ public addLoadBalancer(): void {
+ this.$uibModal
+ .open({
+ templateUrl: require('./loadBalancerChoice.modal.html'),
+ controller: `cloudrunLoadBalancerChoiceModelCtrl as ctrl`,
+ resolve: {
+ application: () => 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 (Cloudrun)',
+ description: 'Edits a load balancer',
+ key: 'upsertCloudrunLoadBalancers',
+ 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 @@
+
+
+
+
+
+
+
+
+ Account
+ Name
+ Region
+
+
+
+
+
+
+
+ {{ 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 @@
+
+
+
+
+
+
+
+ Account
+ Name
+ Region
+ Actions
+
+
+
+
+
+
+
+ {{ loadBalancer.name }}
+ {{ loadBalancer.region }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Add load balancer
+
+
+
+
+
+
+
+
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..7d1faf64e35
--- /dev/null
+++ b/packages/cloudrun/src/pipeline/stages/editLoadBalancer/loadBalancerChoice.modal.html
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
+
+
+ Spinnaker cannot create a load balancer for Cloud Run. A Spinnaker load balancer maps to a Cloud Run service
+ which along with a Revision are created from Create Server Group page using a
+
+
yaml file
+
+ If a service does not exist when a Revision is deployed, it will be created. It will then be editable as a load
+ balancer within Spinnaker.
+
+
+
+
+
+
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..dd3629d3a5d
--- /dev/null
+++ b/packages/cloudrun/src/serverGroup/configure/serverGroupCommandBuilder.service.ts
@@ -0,0 +1,254 @@
+import { module } from 'angular';
+import { cloneDeep } from 'lodash';
+import { $q } from 'ngimport';
+
+import type {
+ Application,
+ IAccountDetails,
+ IExpectedArtifact,
+ IMoniker,
+ IPipeline,
+ IServerGroupCommand,
+ IServerGroupCommandViewState,
+ IStage,
+} from '@spinnaker/core';
+import { AccountService } from '@spinnaker/core';
+
+import { CloudrunProviderSettings } from '../../cloudrun.settings';
+import type { CloudrunDeployDescription } from '../serverGroupTransformer.service';
+
+export enum ServerGroupSource {
+ TEXT = 'text',
+ ARTIFACT = 'artifact',
+}
+
+export interface ICloudrunServerGroupCommandData {
+ command: ICloudrunServerGroupCommand;
+ metadata: ICloudrunServerGroupCommandMetadata;
+ stack: string;
+ freeFormDetails: string;
+ configFiles: string[];
+}
+
+export interface ICloudrunServerGroupCommand extends Omit {
+ application?: string;
+ stack?: string;
+ detail?: 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, 'cloudrun', 'create');
+ }
+
+ // add servergroup from deploy stage of pipeline
+ public buildNewServerGroupCommandForPipeline(_stage: IStage, pipeline: IPipeline) {
+ return CloudrunServerGroupCommandBuilder.buildNewServerGroupCommandForPipeline(_stage, pipeline);
+ }
+
+ // edit servergroup from deploy stage of pipeline
+ // add servergroup from deploy stage of pipeline
+ public buildServerGroupCommandFromPipeline(
+ app: Application,
+ cluster: CloudrunDeployDescription,
+ _stage: IStage,
+ pipeline: IPipeline,
+ ) {
+ return CloudrunServerGroupCommandBuilder.buildServerGroupCommandFromPipeline(app, cluster, _stage, pipeline);
+ }
+}
+
+export class CloudrunServerGroupCommandBuilder {
+ public static $inject = ['$q'];
+ 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,
+ 'cloudrun',
+ 'createPipeline',
+ );
+ command.viewState = {
+ ...command.viewState,
+ pipeline,
+ requiresTemplateSelection: true,
+ stage,
+ };
+ return command;
+ }
+
+ private static getExpectedArtifacts(pipeline: IPipeline): IExpectedArtifact[] {
+ return pipeline.expectedArtifacts || [];
+ }
+
+ public static buildServerGroupCommandFromPipeline(
+ app: Application,
+ cluster: CloudrunDeployDescription,
+ _stage: IStage,
+ pipeline: IPipeline,
+ ): PromiseLike {
+ return CloudrunServerGroupCommandBuilder.buildNewServerGroupCommand(app, 'cloudrun', 'editPipeline').then(
+ (command: ICloudrunServerGroupCommandData) => {
+ command = {
+ ...command,
+ ...cluster,
+
+ backingData: {
+ ...command.metadata.backingData.backingData,
+
+ expectedArtifacts: CloudrunServerGroupCommandBuilder.getExpectedArtifacts(pipeline),
+ },
+ credentials: cluster.account || command.metadata.backingData.credentials,
+ viewState: {
+ ...command.metadata.backingData.viewState,
+ stage: _stage,
+ pipeline,
+ },
+ } 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];
+ }
+
+ public static getRegion(accounts: any[], credentials: string): string {
+ const account = accounts.find((_account) => _account.name === credentials);
+ return account ? account.region : null;
+ }
+
+ // new servergroup command
+ public static buildNewServerGroupCommand(
+ app: Application,
+ sourceAccount: string,
+ mode: string,
+ ): PromiseLike {
+ const dataToFetch = {
+ accounts: AccountService.getAllAccountDetailsForProvider('cloudrun'),
+ artifactAccounts: AccountService.getArtifactAccounts(),
+ };
+
+ return $q.all(dataToFetch).then((backingData: { accounts: IAccountDetails[] }) => {
+ const { accounts } = 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',
+ };
+ const credentials = account ? account : this.getCredentials(accounts);
+ const region = this.getRegion(backingData.accounts, credentials);
+ const cloudProvider = 'cloudrun';
+
+ return {
+ command: {
+ application: app.name,
+ configFiles: [''],
+ cloudProvider,
+ selectedProvider: cloudProvider,
+ provider: cloudProvider,
+ region,
+ 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..9a37499fd31
--- /dev/null
+++ b/packages/cloudrun/src/serverGroup/configure/wizard/BasicSettings.tsx
@@ -0,0 +1,160 @@
+import type { FormikProps } from 'formik';
+import React from 'react';
+
+import type { Application, IAccount, IServerGroup } from '@spinnaker/core';
+import { AccountSelectInput, HelpField, NameUtils, ReactInjector, ServerGroupNamePreview } 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;
+ app: Application;
+}
+
+export interface IServerGroupBasicSettingsState {
+ namePreview: string;
+ createsNewCluster: boolean;
+ latestServerGroup: IServerGroup;
+}
+
+export function ServerGroupBasicSettings({
+ accounts,
+ onAccountSelect,
+ selectedAccount,
+ formik,
+ onEnterStack,
+ detailsChanged,
+ app,
+}: IServerGroupBasicSettingsProps) {
+ const { values } = formik;
+ const { stack = '', freeFormDetails } = values;
+
+ const namePreview = NameUtils.getClusterName(app.name, stack, freeFormDetails);
+ const createsNewCluster = !app.clusters.find((c) => c.name === namePreview);
+ const inCluster = (app.serverGroups.data as IServerGroup[])
+ .filter((serverGroup) => {
+ return (
+ serverGroup.cluster === namePreview &&
+ serverGroup.account === values.command.credentials &&
+ serverGroup.region === values.command.region
+ );
+ })
+ .sort((a, b) => a.createdTime - b.createdTime);
+ const latestServerGroup = inCluster.length ? inCluster.pop() : null;
+
+ const navigateToLatestServerGroup = () => {
+ const { values } = formik;
+ const params = {
+ provider: values.command.selectedProvider,
+ accountId: latestServerGroup.account,
+ region: latestServerGroup.region,
+ serverGroup: latestServerGroup.name,
+ };
+
+ const { $state } = ReactInjector;
+ if ($state.is('home.applications.application.insight.clusters')) {
+ $state.go('.serverGroup', params);
+ } else {
+ $state.go('^.serverGroup', params);
+ }
+ };
+
+ return (
+
+
+
Account
+
+
onAccountSelect(evt.target.value)}
+ readOnly={false}
+ accounts={accounts}
+ provider="cloudrun"
+ />
+
+
+
+
+
+ Stack
+
+
+ onEnterStack(e.target.value)}
+ />
+
+
+
+
+
+ Detail
+
+
+ detailsChanged(e.target.value)}
+ />
+
+
+ {!values.command.viewState.hideClusterNamePreview && (
+
+ )}
+
+ );
+}
+
+export interface IWizardServerGroupBasicSettingsProps {
+ formik: FormikProps;
+ app: Application;
+}
+
+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, app } = 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..330f0dc407d
--- /dev/null
+++ b/packages/cloudrun/src/serverGroup/configure/wizard/ConfigFiles.tsx
@@ -0,0 +1,75 @@
+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);
+
+ 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);
+ }
+
+ return (
+
+
+ {configValues.map((configFile, index) => (
+ <>
+
+ Service Yaml
+ {' '}
+
+
+ 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..3877f49aac4
--- /dev/null
+++ b/packages/cloudrun/src/serverGroup/configure/wizard/serverGroupWizard.tsx
@@ -0,0 +1,150 @@
+import React from 'react';
+import type { Application, IModalComponentProps, IStage } from '@spinnaker/core';
+//import type { IModalInstanceService } from 'angular-ui-bootstrap';
+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, 'cloudrun', 'create').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: `${
+ props.command.command.viewState.submitButtonLabel === 'Create' ? '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 forPipelineConfig = command.viewState.mode === 'editPipeline' || command.viewState.mode === 'createPipeline';
+ if (forPipelineConfig) {
+ this.props.closeModal && this.props.closeModal(command);
+ } else {
+ //command.viewState.mode = 'create';
+ const submitMethod = () => ReactInjector.serverGroupWriter.cloneServerGroup(command, this.props.application);
+ this.state.taskMonitor.submit(submitMethod);
+ return null;
+ }
+ };
+ public render() {
+ const { dismissModal, application } = this.props;
+ const { loaded, taskMonitor, command } = this.state;
+ const labelButton = this.state.command.command.viewState.submitButtonLabel;
+
+ return (
+
+ heading={`${labelButton === 'Add' || labelButton === 'Create' ? 'Create New' : 'Update'} Server Group`}
+ initialValues={command}
+ loading={!loaded}
+ taskMonitor={taskMonitor}
+ dismissModal={dismissModal}
+ closeModal={this.submit}
+ submitButtonLabel={labelButton}
+ 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..6f1fd8ff7f5
--- /dev/null
+++ b/packages/cloudrun/src/serverGroup/details/details.controller.ts
@@ -0,0 +1,134 @@
+import type { IController, IScope } from 'angular';
+import { module } from 'angular';
+import type { Application, ILoadBalancer, IServerGroup, ServerGroupWriter } from '@spinnaker/core';
+import { ConfirmationModalService, SERVER_GROUP_WRITER, ServerGroupReader } from '@spinnaker/core';
+import { CloudrunHealth } from '../../common/cloudrunHealth';
+import type { ICloudrunServerGroup } from '../../interfaces';
+
+interface IServerGroupFromStateParams {
+ accountId: string;
+ region: string;
+ name: string;
+}
+
+class CloudrunServerGroupDetailsController implements IController {
+ public state = { loading: true };
+ public serverGroup: ICloudrunServerGroup;
+
+ 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());
+ }
+
+ // destroy existing server group
+ public canDestroyServerGroup(): boolean {
+ if (this.serverGroup) {
+ const isCurrentRevision = this.serverGroup.tags.isLatest;
+ if (isCurrentRevision) {
+ return false;
+ } else if (this.serverGroup.disabled) {
+ return true;
+ } 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,
+
+ interestingHealthProviderNames: [] as string[],
+ };
+
+ if (this.app.attributes.platformHealthOnlyShowOverride && this.app.attributes.platformHealthOnly) {
+ confirmationModalParams.interestingHealthProviderNames = [CloudrunHealth.PLATFORM];
+ }
+
+ ConfirmationModalService.confirm(confirmationModalParams);
+ }
+
+ 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..6bcd91474c2
--- /dev/null
+++ b/packages/cloudrun/src/serverGroup/details/details.html
@@ -0,0 +1,94 @@
+
+
+
+
+
+
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..577b54b0638
--- /dev/null
+++ b/packages/cloudrun/src/serverGroup/serverGroupTransformer.service.ts
@@ -0,0 +1,61 @@
+import { module } from 'angular';
+import type { IServerGroup } from '@spinnaker/core';
+
+import type { ICloudrunServerGroupCommand } from '../serverGroup/configure/serverGroupCommandBuilder.service';
+
+export class CloudrunV2ServerGroupTransformer {
+ public static $inject = ['$q'];
+ constructor(private $q: ng.IQService) {}
+
+ public normalizeServerGroup(serverGroup: IServerGroup): PromiseLike {
+ return this.$q.resolve(serverGroup);
+ }
+
+ public convertServerGroupCommandToDeployConfiguration(command: ICloudrunServerGroupCommand): any {
+ return new CloudrunDeployDescription(command);
+ }
+}
+
+export class CloudrunDeployDescription {
+ public cloudProvider = 'cloudrun';
+ public provider = 'cloudrun';
+ public credentials: string;
+ public account: string;
+ public application: string;
+ public stack?: string;
+ public freeFormDetails?: string;
+ public configFiles: string[];
+ public region: string;
+ public strategy?: string;
+ public type?: string;
+ public fromArtifact: boolean;
+ public configArtifacts: string[];
+ public strategyApplication?: string;
+ public strategyPipeline?: string;
+ public gitCredentialType: string;
+ public interestingHealthProviderNames: string[];
+ public sourceType: string;
+
+ constructor(command: ICloudrunServerGroupCommand) {
+ this.credentials = command.credentials;
+ this.account = command.credentials;
+ this.application = command.application;
+ this.stack = command.stack;
+ this.freeFormDetails = command.freeFormDetails;
+ this.region = command.region;
+ this.strategy = command.strategy;
+ this.type = command.type;
+ this.fromArtifact = command.fromArtifact;
+ this.gitCredentialType = command.gitCredentialType;
+ this.configFiles = command.configFiles;
+ this.sourceType = command.sourceType;
+ this.interestingHealthProviderNames = command.interestingHealthProviderNames || [];
+ this.configArtifacts = [];
+ }
+}
+
+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..40f627d0557 100755
--- a/scripts/buildModules.js
+++ b/scripts/buildModules.js
@@ -23,6 +23,7 @@ async function buildModules() {
'appengine',
'azure',
'cloudfoundry',
+ 'cloudrun',
'docker',
'google',
'huaweicloud',
diff --git a/scripts/build_order.sh b/scripts/build_order.sh
index 89a69d5f0c2..c59e362b748 100755
--- a/scripts/build_order.sh
+++ b/scripts/build_order.sh
@@ -11,6 +11,7 @@ ModuleDeps () {
appengine) echo "core" ;;
azure) echo "core" ;;
cloudfoundry) echo "core" ;;
+ cloudrun) echo "core" ;;
core) echo "presentation";;
docker) echo "core" ;;
ecs) echo "amazon docker core" ;;
diff --git a/scripts/bumpPackage.js b/scripts/bumpPackage.js
index 1144e0f15e4..95cdd8cfd9b 100755
--- a/scripts/bumpPackage.js
+++ b/scripts/bumpPackage.js
@@ -22,6 +22,7 @@ const packages = [
'packages/appengine/',
'packages/azure/',
'packages/cloudfoundry/',
+ 'packages/cloudrun/',
'packages/core/',
'packages/docker/',
'packages/ecs/',
diff --git a/tsconfig.json b/tsconfig.json
index b5639f1baad..978da4e0ae8 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -57,7 +57,10 @@
"azure": ["azure/src"],
"@spinnaker/ecs": ["ecs/src"],
"ecs/*": ["ecs/src/*"],
- "ecs": ["ecs/src"]
+ "ecs": ["ecs/src"],
+ "@spinnaker/cloudrun": ["cloudrun/src"],
+ "cloudrun/*": ["cloudrun/src/*"],
+ "cloudrun": ["cloudrun/src"],
},
"pretty": true,
"removeComments": false,