@spinnaker/google 2026.0.1 → 2026.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/dist/domain/loadBalancer.d.ts +4 -2
  2. package/dist/index.js +1 -1
  3. package/dist/index.js.map +1 -1
  4. package/dist/loadBalancer/configure/choice/gceLoadBalancerPipelineModal.d.ts +9 -0
  5. package/dist/loadBalancer/configure/http/templates.d.ts +3 -0
  6. package/package.json +3 -3
  7. package/src/domain/loadBalancer.ts +4 -2
  8. package/src/gce.module.ts +2 -0
  9. package/src/help/gce.help.ts +2 -0
  10. package/src/loadBalancer/configure/choice/gceLoadBalancerChoice.modal.spec.js +66 -0
  11. package/src/loadBalancer/configure/choice/gceLoadBalancerChoice.modal.ts +20 -5
  12. package/src/loadBalancer/configure/choice/gceLoadBalancerPipelineModal.ts +47 -0
  13. package/src/loadBalancer/configure/gceL4LoadBalancerPipeline.spec.js +143 -0
  14. package/src/loadBalancer/configure/http/createHttpLoadBalancer.controller.js +22 -0
  15. package/src/loadBalancer/configure/http/createHttpLoadBalancer.controller.spec.js +96 -0
  16. package/src/loadBalancer/configure/http/listeners/listener.component.html +79 -24
  17. package/src/loadBalancer/configure/http/listeners/listener.component.js +82 -0
  18. package/src/loadBalancer/configure/http/listeners/listener.component.spec.js +125 -0
  19. package/src/loadBalancer/configure/http/templates.ts +3 -0
  20. package/src/loadBalancer/configure/http/transformer.service.js +16 -1
  21. package/src/loadBalancer/configure/http/transformer.service.spec.js +182 -0
  22. package/src/loadBalancer/configure/internal/gceCreateInternalLoadBalancer.controller.ts +8 -0
  23. package/src/loadBalancer/configure/internalhttp/createInternalHttpLoadBalancer.controller.spec.js +89 -0
  24. package/src/loadBalancer/configure/internalhttp/createInternalHttpLoadBalancer.controller.ts +23 -0
  25. package/src/loadBalancer/configure/network/createLoadBalancer.controller.js +48 -32
  26. package/src/loadBalancer/configure/network/createLoadBalancer.controller.spec.js +36 -0
  27. package/src/loadBalancer/configure/ssl/gceCreateSslLoadBalancer.controller.ts +8 -0
  28. package/src/loadBalancer/configure/tcp/gceCreateTcpLoadBalancer.controller.ts +8 -0
  29. package/src/loadBalancer/details/loadBalancerType/loadBalancerType.component.js +3 -1
  30. package/src/loadBalancer/details/loadBalancerType/loadBalancerType.component.spec.js +85 -0
  31. package/src/loadBalancer/loadBalancer.setTransformer.ts +1 -0
  32. package/src/serverGroup/configure/serverGroupConfiguration.service.js +0 -1
  33. package/src/serverGroup/configure/wizard/autoScalingPolicy/autoScalingPolicySelector.component.js +1 -4
@@ -0,0 +1,9 @@
1
+ import type { IModalService } from 'angular-ui-bootstrap';
2
+ import type { Application } from '@spinnaker/core';
3
+ export interface IGCEPipelineLoadBalancerModalOptions {
4
+ application: Application;
5
+ loadBalancer: any;
6
+ isNew: boolean;
7
+ $uibModal: IModalService;
8
+ }
9
+ export declare const openGCEPipelineLoadBalancerModal: ({ application, loadBalancer, isNew, $uibModal, }: IGCEPipelineLoadBalancerModalOptions) => PromiseLike<any>;
@@ -6,6 +6,7 @@ export declare class HttpLoadBalancerTemplate {
6
6
  region: string;
7
7
  loadBalancerType: string;
8
8
  certificate: string;
9
+ certificateMap: string;
9
10
  defaultService: BackendServiceTemplate;
10
11
  hostRules: HostRuleTemplate[];
11
12
  listeners: ListenerTemplate[];
@@ -39,4 +40,6 @@ export declare class ListenerTemplate {
39
40
  name: string;
40
41
  port: number;
41
42
  certificate: string | null;
43
+ certificateMap: string | null;
44
+ certificateSource: 'certificate' | 'certificateMap';
42
45
  }
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "url": "https://github.com/spinnaker/spinnaker.git"
6
6
  },
7
7
  "license": "Apache-2.0",
8
- "version": "2026.0.1",
8
+ "version": "2026.1.0",
9
9
  "module": "dist/index.js",
10
10
  "typings": "dist/index.d.ts",
11
11
  "publishConfig": {
@@ -25,7 +25,7 @@
25
25
  "@uirouter/angularjs": "1.0.26",
26
26
  "angular": "1.6.10",
27
27
  "angular-ui-bootstrap": "2.5.0",
28
- "lodash": "4.17.21",
28
+ "lodash": "4.18.1",
29
29
  "ngimport": "0.6.1",
30
30
  "react": "16.14.0",
31
31
  "react-select": "1.2.1",
@@ -45,5 +45,5 @@
45
45
  "shx": "0.3.3",
46
46
  "typescript": "5.0.4"
47
47
  },
48
- "gitHead": "230864a7fc8b5dfb38edb1164c7719671f7b4fa3"
48
+ "gitHead": "b43289872672de89234911de45d1cba0dd18373f"
49
49
  }
@@ -14,7 +14,8 @@ export interface IGceLoadBalancer extends ILoadBalancer {
14
14
  }
15
15
 
16
16
  export interface IGceHttpLoadBalancer extends IGceLoadBalancer {
17
- certificate: string;
17
+ certificate?: string;
18
+ certificateMap?: string;
18
19
  defaultService: IGceBackendService;
19
20
  detail: string;
20
21
  hostRules: IGceHostRule;
@@ -42,7 +43,8 @@ export interface IGcePathRule {
42
43
  }
43
44
 
44
45
  export interface IGceListener {
45
- certificate: string;
46
+ certificate?: string | null;
47
+ certificateMap?: string | null;
46
48
  name: string;
47
49
  port: string;
48
50
  ipAddress: string;
package/src/gce.module.ts CHANGED
@@ -13,6 +13,7 @@ import { GOOGLE_INSTANCE_GCEINSTANCETYPE_SERVICE } from './instance/gceInstanceT
13
13
  import { GOOGLE_INSTANCE_GCEMULTIINSTANCETASK_TRANSFORMER } from './instance/gceMultiInstanceTask.transformer';
14
14
  import { IAP_INTERCEPTOR } from './interceptors/iap.interceptor';
15
15
  import { GCE_LOAD_BALANCER_CHOICE_MODAL } from './loadBalancer/configure/choice/gceLoadBalancerChoice.modal';
16
+ import { openGCEPipelineLoadBalancerModal } from './loadBalancer/configure/choice/gceLoadBalancerPipelineModal';
16
17
  import { GOOGLE_LOADBALANCER_CONFIGURE_HTTP_CREATEHTTPLOADBALANCER_CONTROLLER } from './loadBalancer/configure/http/createHttpLoadBalancer.controller';
17
18
  import { GCE_INTERNAL_LOAD_BALANCER_CTRL } from './loadBalancer/configure/internal/gceCreateInternalLoadBalancer.controller';
18
19
  import { GOOGLE_LOADBALANCER_CONFIGURE_INTERNAL_HTTP_CREATEHTTPLOADBALANCER_CONTROLLER } from './loadBalancer/configure/internalhttp/createInternalHttpLoadBalancer.controller';
@@ -133,6 +134,7 @@ module(GOOGLE_MODULE, [
133
134
  detailsController: 'gceLoadBalancerDetailsCtrl',
134
135
  createLoadBalancerTemplateUrl: require('./loadBalancer/configure/choice/gceLoadBalancerChoice.modal.html'),
135
136
  createLoadBalancerController: 'gceLoadBalancerChoiceCtrl',
137
+ pipelineCreateLoadBalancerModal: openGCEPipelineLoadBalancerModal,
136
138
  },
137
139
  securityGroup: {
138
140
  transformer: 'gceSecurityGroupTransformer',
@@ -3,6 +3,8 @@ import { HelpContentsRegistry } from '@spinnaker/core';
3
3
  const helpContents: { [key: string]: string } = {
4
4
  'gce.httpLoadBalancer.certificate':
5
5
  'The name of an SSL certificate. If specified, Spinnaker will create an HTTPS load balancer.',
6
+ 'gce.httpLoadBalancer.certificateMap':
7
+ 'A Certificate Manager certificate map name. Certificate maps let GCP manage and auto-rotate TLS certificates, replacing individual certificate selection.',
6
8
  'gce.httpLoadBalancer.defaultService':
7
9
  'A default service handles any requests that do not match a specified host rule or path matching rule.',
8
10
  'gce.httpLoadBalancer.externalIP':
@@ -0,0 +1,66 @@
1
+ import { GCE_LOAD_BALANCER_CHOICE_MODAL } from './gceLoadBalancerChoice.modal';
2
+
3
+ describe('Controller: gceLoadBalancerChoiceCtrl', () => {
4
+ beforeEach(() => {
5
+ window.module(GCE_LOAD_BALANCER_CHOICE_MODAL);
6
+ });
7
+
8
+ const buildController = (overrides = {}) => {
9
+ let result;
10
+ window.inject(function ($controller, $rootScope, $q) {
11
+ const $scope = $rootScope.$new();
12
+ const modalInstance = {
13
+ close: jasmine.createSpy('close'),
14
+ dismiss: jasmine.createSpy('dismiss'),
15
+ };
16
+ const wizardResult = $q.when('wizard');
17
+ const $uibModal = {
18
+ open: jasmine.createSpy('open').and.returnValue({ result: wizardResult }),
19
+ };
20
+ const ctrl = $controller('gceLoadBalancerChoiceCtrl', {
21
+ $scope,
22
+ $uibModal,
23
+ $uibModalInstance: modalInstance,
24
+ application: { name: 'app' },
25
+ loadBalancerTypeToWizardMap: {
26
+ NETWORK: { label: 'Network', createTemplateUrl: 'template', controller: 'ctrl' },
27
+ },
28
+ forPipelineConfig: true,
29
+ ...overrides,
30
+ });
31
+
32
+ result = { ctrl, $uibModal, modalInstance, wizardResult };
33
+ });
34
+
35
+ return result;
36
+ };
37
+
38
+ it('closes with wizard config in pipeline mode', function () {
39
+ const { ctrl, $uibModal, modalInstance } = buildController();
40
+
41
+ ctrl.choose('Network');
42
+
43
+ expect($uibModal.open).not.toHaveBeenCalled();
44
+ expect(modalInstance.close).toHaveBeenCalledWith(jasmine.objectContaining({ controller: 'ctrl' }));
45
+ expect(modalInstance.dismiss).not.toHaveBeenCalled();
46
+ });
47
+
48
+ it('opens wizard in non-pipeline mode', function () {
49
+ const { ctrl, $uibModal, modalInstance, wizardResult } = buildController({ forPipelineConfig: false });
50
+
51
+ ctrl.choose('Network');
52
+
53
+ expect($uibModal.open).toHaveBeenCalled();
54
+ expect(modalInstance.close).toHaveBeenCalledWith(wizardResult);
55
+ });
56
+
57
+ it('dismisses when choice has no wizard config', function () {
58
+ const { ctrl, $uibModal, modalInstance } = buildController();
59
+
60
+ ctrl.choose('Missing');
61
+
62
+ expect($uibModal.open).not.toHaveBeenCalled();
63
+ expect(modalInstance.close).not.toHaveBeenCalled();
64
+ expect(modalInstance.dismiss).toHaveBeenCalled();
65
+ });
66
+ });
@@ -12,12 +12,19 @@ class GceLoadBalancerChoiceCtrl implements IController {
12
12
  public choices: string[];
13
13
  public choice = 'Network';
14
14
 
15
- public static $inject = ['$uibModal', '$uibModalInstance', 'application', 'loadBalancerTypeToWizardMap'];
15
+ public static $inject = [
16
+ '$uibModal',
17
+ '$uibModalInstance',
18
+ 'application',
19
+ 'loadBalancerTypeToWizardMap',
20
+ 'forPipelineConfig',
21
+ ];
16
22
  constructor(
17
23
  public $uibModal: IModalService,
18
24
  public $uibModalInstance: IModalInstanceService,
19
25
  private application: Application,
20
26
  private loadBalancerTypeToWizardMap: IGceLoadBalancerToWizardMap,
27
+ private forPipelineConfig: boolean,
21
28
  ) {}
22
29
 
23
30
  public $onInit(): void {
@@ -26,8 +33,15 @@ class GceLoadBalancerChoiceCtrl implements IController {
26
33
 
27
34
  public choose(choice: string): void {
28
35
  const wizard = find(this.loadBalancerTypeToWizardMap, (wizardConfig) => wizardConfig.label === choice);
29
- this.$uibModalInstance.dismiss();
30
- this.$uibModal.open({
36
+ if (!wizard) {
37
+ this.$uibModalInstance.dismiss('No wizard config for choice');
38
+ return;
39
+ }
40
+ if (this.forPipelineConfig) {
41
+ this.$uibModalInstance.close(wizard);
42
+ return;
43
+ }
44
+ const wizardResult = this.$uibModal.open({
31
45
  templateUrl: wizard.createTemplateUrl,
32
46
  controller: `${wizard.controller} as ctrl`,
33
47
  size: 'lg',
@@ -35,9 +49,10 @@ class GceLoadBalancerChoiceCtrl implements IController {
35
49
  application: () => this.application,
36
50
  loadBalancer: (): null => null,
37
51
  isNew: () => true,
38
- forPipelineConfig: () => false,
52
+ forPipelineConfig: () => this.forPipelineConfig,
39
53
  },
40
- });
54
+ }).result;
55
+ this.$uibModalInstance.close(wizardResult);
41
56
  }
42
57
  }
43
58
 
@@ -0,0 +1,47 @@
1
+ import type { IModalService } from 'angular-ui-bootstrap';
2
+
3
+ import type { Application } from '@spinnaker/core';
4
+
5
+ import type { IGceLoadBalancerWizardConfig } from './loadBalancerTypeToWizardMap.constant';
6
+
7
+ export interface IGCEPipelineLoadBalancerModalOptions {
8
+ application: Application;
9
+ loadBalancer: any;
10
+ isNew: boolean;
11
+ $uibModal: IModalService;
12
+ }
13
+
14
+ export const openGCEPipelineLoadBalancerModal = ({
15
+ application,
16
+ loadBalancer,
17
+ isNew,
18
+ $uibModal,
19
+ }: IGCEPipelineLoadBalancerModalOptions): PromiseLike<any> => {
20
+ return $uibModal
21
+ .open({
22
+ templateUrl: require('./gceLoadBalancerChoice.modal.html'),
23
+ controller: 'gceLoadBalancerChoiceCtrl as ctrl',
24
+ size: 'lg',
25
+ resolve: {
26
+ application: () => application,
27
+ forPipelineConfig: () => true,
28
+ },
29
+ })
30
+ .result.then((wizardConfig: IGceLoadBalancerWizardConfig) => {
31
+ if (!wizardConfig) {
32
+ return null;
33
+ }
34
+ const templateUrl = isNew ? wizardConfig.createTemplateUrl : wizardConfig.editTemplateUrl;
35
+ return $uibModal.open({
36
+ templateUrl,
37
+ controller: `${wizardConfig.controller} as ctrl`,
38
+ size: 'lg',
39
+ resolve: {
40
+ application: () => application,
41
+ loadBalancer: () => loadBalancer,
42
+ isNew: () => isNew,
43
+ forPipelineConfig: () => true,
44
+ },
45
+ }).result;
46
+ });
47
+ };
@@ -0,0 +1,143 @@
1
+ import { ApplicationModelBuilder } from '@spinnaker/core';
2
+ import { GCE_INTERNAL_LOAD_BALANCER_CTRL } from './internal/gceCreateInternalLoadBalancer.controller';
3
+ import { GCE_SSL_LOAD_BALANCER_CTRL } from './ssl/gceCreateSslLoadBalancer.controller';
4
+ import { GCE_TCP_LOAD_BALANCER_CTRL } from './tcp/gceCreateTcpLoadBalancer.controller';
5
+
6
+ function buildApp() {
7
+ return ApplicationModelBuilder.createApplicationForTests('app', {
8
+ key: 'loadBalancers',
9
+ lazy: true,
10
+ defaultData: [],
11
+ });
12
+ }
13
+
14
+ describe('GCE L4 load balancer controllers (pipeline mode)', function () {
15
+ describe('gceSslLoadBalancerCtrl', function () {
16
+ beforeEach(() => {
17
+ window.module(GCE_SSL_LOAD_BALANCER_CTRL);
18
+ });
19
+
20
+ it(
21
+ 'closes with a pipeline command',
22
+ window.inject(function ($controller, $rootScope) {
23
+ const modalInstance = { close: jasmine.createSpy('close'), dismiss: jasmine.createSpy('dismiss') };
24
+ const loadBalancer = {
25
+ loadBalancerName: 'ssl-lb',
26
+ backendService: { healthCheck: { healthCheckType: 'TCP' } },
27
+ instances: [],
28
+ credentials: 'test',
29
+ region: 'global',
30
+ };
31
+
32
+ const ctrl = $controller('gceSslLoadBalancerCtrl', {
33
+ $scope: $rootScope.$new(),
34
+ application: buildApp(),
35
+ $uibModalInstance: modalInstance,
36
+ loadBalancer,
37
+ gceCommonLoadBalancerCommandBuilder: {},
38
+ isNew: true,
39
+ forPipelineConfig: true,
40
+ wizardSubFormValidation: {},
41
+ $state: {},
42
+ });
43
+
44
+ ctrl.submit();
45
+
46
+ expect(modalInstance.close).toHaveBeenCalledWith(
47
+ jasmine.objectContaining({
48
+ loadBalancerName: 'ssl-lb',
49
+ cloudProvider: 'gce',
50
+ healthCheck: {},
51
+ }),
52
+ );
53
+ }),
54
+ );
55
+ });
56
+
57
+ describe('gceTcpLoadBalancerCtrl', function () {
58
+ beforeEach(() => {
59
+ window.module(GCE_TCP_LOAD_BALANCER_CTRL);
60
+ });
61
+
62
+ it(
63
+ 'closes with a pipeline command',
64
+ window.inject(function ($controller, $rootScope) {
65
+ const modalInstance = { close: jasmine.createSpy('close'), dismiss: jasmine.createSpy('dismiss') };
66
+ const loadBalancer = {
67
+ loadBalancerName: 'tcp-lb',
68
+ backendService: { healthCheck: { healthCheckType: 'TCP' } },
69
+ instances: [],
70
+ credentials: 'test',
71
+ region: 'global',
72
+ };
73
+
74
+ const ctrl = $controller('gceTcpLoadBalancerCtrl', {
75
+ $scope: $rootScope.$new(),
76
+ application: buildApp(),
77
+ $uibModalInstance: modalInstance,
78
+ loadBalancer,
79
+ gceCommonLoadBalancerCommandBuilder: {},
80
+ isNew: true,
81
+ forPipelineConfig: true,
82
+ wizardSubFormValidation: {},
83
+ $state: {},
84
+ });
85
+
86
+ ctrl.submit();
87
+
88
+ expect(modalInstance.close).toHaveBeenCalledWith(
89
+ jasmine.objectContaining({
90
+ loadBalancerName: 'tcp-lb',
91
+ cloudProvider: 'gce',
92
+ healthCheck: {},
93
+ }),
94
+ );
95
+ }),
96
+ );
97
+ });
98
+
99
+ describe('gceInternalLoadBalancerCtrl', function () {
100
+ beforeEach(() => {
101
+ window.module(GCE_INTERNAL_LOAD_BALANCER_CTRL);
102
+ });
103
+
104
+ it(
105
+ 'closes with a pipeline command',
106
+ window.inject(function ($controller, $rootScope) {
107
+ const modalInstance = { close: jasmine.createSpy('close'), dismiss: jasmine.createSpy('dismiss') };
108
+ const loadBalancer = {
109
+ loadBalancerName: 'internal-lb',
110
+ backendService: { healthCheck: { healthCheckType: 'TCP' } },
111
+ instances: [],
112
+ ports: '80, 8080',
113
+ credentials: 'test',
114
+ region: 'us-west-2',
115
+ };
116
+
117
+ const ctrl = $controller('gceInternalLoadBalancerCtrl', {
118
+ $scope: $rootScope.$new(),
119
+ application: buildApp(),
120
+ $uibModalInstance: modalInstance,
121
+ loadBalancer,
122
+ gceCommonLoadBalancerCommandBuilder: {},
123
+ isNew: true,
124
+ forPipelineConfig: true,
125
+ wizardSubFormValidation: {},
126
+ gceXpnNamingService: {},
127
+ $state: {},
128
+ });
129
+
130
+ ctrl.submit();
131
+
132
+ expect(modalInstance.close).toHaveBeenCalledWith(
133
+ jasmine.objectContaining({
134
+ loadBalancerName: 'internal-lb',
135
+ cloudProvider: 'gce',
136
+ healthCheck: {},
137
+ ports: ['80', '8080'],
138
+ }),
139
+ );
140
+ }),
141
+ );
142
+ });
143
+ });
@@ -43,6 +43,7 @@ module(GOOGLE_LOADBALANCER_CONFIGURE_HTTP_CREATEHTTPLOADBALANCER_CONTROLLER, [
43
43
  'application',
44
44
  'loadBalancer',
45
45
  'isNew',
46
+ 'forPipelineConfig',
46
47
  'gceHttpLoadBalancerWriter',
47
48
  '$state',
48
49
  'wizardSubFormValidation',
@@ -55,6 +56,7 @@ module(GOOGLE_LOADBALANCER_CONFIGURE_HTTP_CREATEHTTPLOADBALANCER_CONTROLLER, [
55
56
  application,
56
57
  loadBalancer,
57
58
  isNew,
59
+ forPipelineConfig,
58
60
  gceHttpLoadBalancerWriter,
59
61
  $state,
60
62
  wizardSubFormValidation,
@@ -129,6 +131,26 @@ module(GOOGLE_LOADBALANCER_CONFIGURE_HTTP_CREATEHTTPLOADBALANCER_CONTROLLER, [
129
131
  const serializedCommands = gceHttpLoadBalancerTransformer.serialize(this.command, loadBalancer);
130
132
  const descriptor = this.isNew ? 'Create' : 'Update';
131
133
 
134
+ if (forPipelineConfig) {
135
+ const pipelineCommands = serializedCommands.map((command) => ({
136
+ ...command,
137
+ cloudProvider: 'gce',
138
+ loadBalancerName: command.name,
139
+ listeners: [
140
+ {
141
+ name: command.name,
142
+ port: command.portRange,
143
+ certificate: command.certificate || null,
144
+ certificateMap: command.certificateMap || null,
145
+ ipAddress: command.ipAddress,
146
+ subnet: command.subnet,
147
+ },
148
+ ],
149
+ }));
150
+ $uibModalInstance.close(pipelineCommands);
151
+ return;
152
+ }
153
+
132
154
  this.taskMonitor.submit(() =>
133
155
  gceHttpLoadBalancerWriter.upsertLoadBalancers(serializedCommands, application, descriptor),
134
156
  );
@@ -0,0 +1,96 @@
1
+ import { ApplicationModelBuilder } from '@spinnaker/core';
2
+
3
+ describe('Controller: gceCreateHttpLoadBalancerCtrl (pipeline mode)', function () {
4
+ beforeEach(function () {
5
+ this.serializedCommands = [
6
+ {
7
+ name: 'app-http',
8
+ portRange: '443',
9
+ certificate: null,
10
+ certificateMap: 'map',
11
+ ipAddress: null,
12
+ subnet: null,
13
+ urlMapName: 'app-http',
14
+ credentials: 'test',
15
+ region: 'global',
16
+ loadBalancerType: 'HTTP',
17
+ },
18
+ ];
19
+ this.transformer = {
20
+ serialize: jasmine.createSpy('serialize').and.returnValue(this.serializedCommands),
21
+ };
22
+ this.writer = {
23
+ upsertLoadBalancers: jasmine.createSpy('upsertLoadBalancers'),
24
+ };
25
+ });
26
+
27
+ beforeEach(function () {
28
+ window.module(require('./createHttpLoadBalancer.controller').name, ($provide) => {
29
+ $provide.factory('gceHttpLoadBalancerCommandBuilder', ($q) => ({
30
+ buildCommand: () =>
31
+ $q.when({
32
+ loadBalancer: {
33
+ listeners: [{ name: 'app-http', port: 443 }],
34
+ credentials: 'test',
35
+ region: 'global',
36
+ urlMapName: 'app-http',
37
+ },
38
+ }),
39
+ }));
40
+ const wizardSubFormValidation = {
41
+ config: () => wizardSubFormValidation,
42
+ register: () => wizardSubFormValidation,
43
+ };
44
+ $provide.value('wizardSubFormValidation', wizardSubFormValidation);
45
+ $provide.value('gceHttpLoadBalancerTransformer', this.transformer);
46
+ $provide.value('gceHttpLoadBalancerWriter', this.writer);
47
+ });
48
+ });
49
+
50
+ beforeEach(
51
+ window.inject(function ($controller, $rootScope) {
52
+ this.$scope = $rootScope.$new();
53
+ this.modalInstance = {
54
+ close: jasmine.createSpy('close'),
55
+ dismiss: jasmine.createSpy('dismiss'),
56
+ result: { then: () => {} },
57
+ };
58
+ const app = ApplicationModelBuilder.createApplicationForTests('app', {
59
+ key: 'loadBalancers',
60
+ lazy: true,
61
+ defaultData: [],
62
+ });
63
+ this.ctrl = $controller('gceCreateHttpLoadBalancerCtrl', {
64
+ $scope: this.$scope,
65
+ $uibModal: {},
66
+ $uibModalInstance: this.modalInstance,
67
+ application: app,
68
+ loadBalancer: null,
69
+ isNew: true,
70
+ forPipelineConfig: true,
71
+ $state: {},
72
+ });
73
+ $rootScope.$digest();
74
+ }),
75
+ );
76
+
77
+ it('returns pipeline commands without submitting', function () {
78
+ this.ctrl.submit();
79
+
80
+ expect(this.writer.upsertLoadBalancers).not.toHaveBeenCalled();
81
+ expect(this.modalInstance.close).toHaveBeenCalledWith([
82
+ jasmine.objectContaining({
83
+ name: 'app-http',
84
+ loadBalancerName: 'app-http',
85
+ cloudProvider: 'gce',
86
+ listeners: [
87
+ jasmine.objectContaining({
88
+ name: 'app-http',
89
+ port: '443',
90
+ certificateMap: 'map',
91
+ }),
92
+ ],
93
+ }),
94
+ ]);
95
+ });
96
+ });
@@ -112,6 +112,7 @@
112
112
  <ui-select
113
113
  ng-model="$ctrl.listener.port"
114
114
  ng-disabled="$ctrl.listener.created"
115
+ ng-change="$ctrl.onPortChanged($ctrl.listener)"
115
116
  required
116
117
  class="form-control input-sm"
117
118
  >
@@ -123,31 +124,80 @@
123
124
  </ui-select-choices>
124
125
  </ui-select>
125
126
  </div>
126
- <div ng-if="$ctrl.isHttps($ctrl.listener.port)">
127
- <div class="col-md-2 sm-label-right">
127
+ </div>
128
+ <!-- Certificate controls on a separate row to avoid exceeding Bootstrap's 12-column
129
+ grid when cert-source radios are visible (Port 2+3 + CertSrc 2+3 + Cert 2+3+1 = 16). -->
130
+ <div class="form-group" ng-if="$ctrl.isHttps($ctrl.listener.port)">
131
+ <div class="col-md-2 sm-label-right" ng-if="$ctrl.supportsCertificateMap()">Certificate Source</div>
132
+ <div class="col-md-3" ng-if="$ctrl.supportsCertificateMap()">
133
+ <label class="radio-inline">
134
+ <input
135
+ type="radio"
136
+ ng-model="$ctrl.listener.certificateSource"
137
+ value="certificate"
138
+ ng-change="$ctrl.onCertificateSourceChanged($ctrl.listener)"
139
+ />
128
140
  Certificate
129
- <help-field key="gce.httpLoadBalancer.certificate" class="help-field-absolute"></help-field>
130
- </div>
131
- <div class="col-md-3">
132
- <ui-select
133
- ng-model="$ctrl.listener.certificate"
134
- ng-disabled="$ctrl.listener.created && !$ctrl.listener.certificate"
135
- required
136
- class="form-control input-sm"
137
- >
138
- <ui-select-match allow-clear placeholder="Select...">{{ $select.selected }}</ui-select-match>
139
- <ui-select-choices repeat="certificate in $ctrl.getCertificates() | filter: $select.search">
140
- <span ng-bind-html="certificate | highlight: $select.search"></span>
141
- </ui-select-choices>
142
- </ui-select>
143
- </div>
144
- <div class="col-md-1" style="padding-left: 0; margin-top: 4px">
145
- <gce-cache-refresh
146
- cache-key="certificates"
147
- render-compact="true"
148
- on-refresh="$ctrl.command.onCertificateRefresh($ctrl.command)"
149
- ></gce-cache-refresh>
150
- </div>
141
+ </label>
142
+ <label class="radio-inline">
143
+ <input
144
+ type="radio"
145
+ ng-model="$ctrl.listener.certificateSource"
146
+ value="certificateMap"
147
+ ng-change="$ctrl.onCertificateSourceChanged($ctrl.listener)"
148
+ />
149
+ Certificate Map
150
+ </label>
151
+ </div>
152
+ <div class="col-md-2 sm-label-right">
153
+ <span ng-if="$ctrl.listener.certificateSource !== 'certificateMap'">Certificate</span>
154
+ <span ng-if="$ctrl.listener.certificateSource === 'certificateMap'">Certificate Map</span>
155
+ <help-field
156
+ ng-if="$ctrl.listener.certificateSource !== 'certificateMap'"
157
+ key="gce.httpLoadBalancer.certificate"
158
+ class="help-field-absolute"
159
+ ></help-field>
160
+ <help-field
161
+ ng-if="$ctrl.listener.certificateSource === 'certificateMap'"
162
+ key="gce.httpLoadBalancer.certificateMap"
163
+ class="help-field-absolute"
164
+ ></help-field>
165
+ </div>
166
+ <div class="col-md-3" ng-if="$ctrl.listener.certificateSource !== 'certificateMap'">
167
+ <ui-select
168
+ ng-model="$ctrl.listener.certificate"
169
+ ng-change="$ctrl.onCertificateSelected($ctrl.listener)"
170
+ ng-required="$ctrl.isHttps($ctrl.listener.port) && $ctrl.listener.certificateSource !== 'certificateMap'"
171
+ class="form-control input-sm"
172
+ >
173
+ <ui-select-match allow-clear placeholder="Select...">{{ $select.selected }}</ui-select-match>
174
+ <ui-select-choices repeat="certificate in $ctrl.getCertificates() | filter: $select.search">
175
+ <span ng-bind-html="certificate | highlight: $select.search"></span>
176
+ </ui-select-choices>
177
+ </ui-select>
178
+ </div>
179
+ <div class="col-md-3" ng-if="$ctrl.listener.certificateSource === 'certificateMap'">
180
+ <input
181
+ type="text"
182
+ class="form-control input-sm"
183
+ name="certificateMap"
184
+ ng-model="$ctrl.listener.certificateMap"
185
+ ng-change="$ctrl.onCertificateMapChanged($ctrl.listener)"
186
+ ng-required="$ctrl.isHttps($ctrl.listener.port) && $ctrl.listener.certificateSource === 'certificateMap'"
187
+ ng-pattern="$ctrl.certificateMapPattern"
188
+ placeholder="certificate-map-name"
189
+ />
190
+ </div>
191
+ <div
192
+ class="col-md-1"
193
+ style="padding-left: 0; margin-top: 4px"
194
+ ng-if="$ctrl.listener.certificateSource !== 'certificateMap'"
195
+ >
196
+ <gce-cache-refresh
197
+ cache-key="certificates"
198
+ render-compact="true"
199
+ on-refresh="$ctrl.command.onCertificateRefresh($ctrl.command)"
200
+ ></gce-cache-refresh>
151
201
  </div>
152
202
  </div>
153
203
 
@@ -184,6 +234,11 @@
184
234
  message="Detail can only contain lowercase letters, numbers, and dashes(-)."
185
235
  ></validation-error>
186
236
  </div>
237
+ <div class="col-md-7 col-md-offset-2" ng-if="listener.certificateMap.$error.pattern">
238
+ <validation-error
239
+ message="Certificate map can only contain lowercase letters, numbers, and dashes(-), and must start with a letter."
240
+ ></validation-error>
241
+ </div>
187
242
  </div>
188
243
  </div>
189
244
  </ng-form>