Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(google): add support for partnerMetadata in GCE servergroup #10150

Merged
merged 12 commits into from
Oct 21, 2024
2 changes: 2 additions & 0 deletions packages/core/src/forms/forms.module.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { CORE_FORMS_CHECKLIST_CHECKLIST_DIRECTIVE } from './checklist/checklist.
import { CORE_FORMS_CHECKMAP_CHECKMAP_DIRECTIVE } from './checkmap/checkmap.directive';
import { CORE_FORMS_IGNOREEMPTYDELETE_DIRECTIVE } from './ignoreEmptyDelete.directive';
import { CORE_FORMS_MAPEDITOR_MAPEDITOR_COMPONENT } from './mapEditor/mapEditor.component';
import { CORE_FORMS_MAPOBJECTEDITOR_MAPOBJECTEDITOR_COMPONENT } from './mapObjectEditor/mapObjectEditor.component';
import { NUMBER_LIST_COMPONENT } from './numberList/numberList.component';
import { CORE_FORMS_VALIDATEONSUBMIT_VALIDATEONSUBMIT_DIRECTIVE } from './validateOnSubmit/validateOnSubmit.directive';

Expand All @@ -18,6 +19,7 @@ module(CORE_FORMS_FORMS_MODULE, [
CORE_FORMS_CHECKMAP_CHECKMAP_DIRECTIVE,
CORE_FORMS_IGNOREEMPTYDELETE_DIRECTIVE,
CORE_FORMS_MAPEDITOR_MAPEDITOR_COMPONENT,
CORE_FORMS_MAPOBJECTEDITOR_MAPOBJECTEDITOR_COMPONENT,
CORE_FORMS_VALIDATEONSUBMIT_VALIDATEONSUBMIT_DIRECTIVE,
NUMBER_LIST_COMPONENT,
]);
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<form name="mapObjectEditor">
<div class="sm-label-left" ng-if="$ctrl.label">
<b>{{ $ctrl.label }}</b>
</div>
<input class="form-control input-sm" ng-model="$ctrl.model" ng-if="$ctrl.isParameterized" />
<table class="table table-condensed packed tags {{ $ctrl.tableClass }}" ng-if="!$ctrl.isParameterized">
<thead>
<tr ng-if="!$ctrl.labelsLeft">
<th ng-bind="$ctrl.keyLabel"></th>
<th ng-bind="$ctrl.valueLabel"></th>
<th></th>
</tr>
</thead>
<tbody>
<tr ng-repeat="pair in $ctrl.backingModel" ng-if="!$ctrl.hiddenKeys.includes(pair.key)">
<td class="table-label" ng-if="$ctrl.labelsLeft">
<b>{{ $ctrl.keyLabel }}</b>
</td>
<td>
<input
class="form-control input input-sm"
type="text"
name="{{ $index }}"
ng-model="pair.key"
validate-unique="pair.checkUnique"
/>
<div class="error-message" ng-if="mapObjectEditor[$index].$error.validateUnique">Duplicate key</div>
</td>
<td class="table-label" ng-if="$ctrl.labelsLeft">
<b>{{ $ctrl.valueLabel }}</b>
</td>
<td>
<textarea json-text class="form-control input input-sm" ng-model="pair.value" rows="4"></textarea>
</td>
<td>
<div class="form-control-static">
<a href ng-click="$ctrl.removeField($index)">
<span class="glyphicon glyphicon-trash"></span>
<span class="sr-only">Remove field</span>
</a>
</div>
</td>
</tr>
</tbody>
<tfoot>
<tr>
<td colspan="{{ $ctrl.columnCount }}">
<button class="btn btn-block btn-sm add-new" ng-click="$ctrl.addField()">
<span class="glyphicon glyphicon-plus-sign"></span>
{{ $ctrl.addButtonLabel }}
</button>
</td>
</tr>
</tfoot>
</table>
</form>
113 changes: 113 additions & 0 deletions packages/core/src/forms/mapObjectEditor/mapObjectEditor.component.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
'use strict';

import * as angular from 'angular';
import { isString } from 'lodash';

import { CORE_VALIDATION_VALIDATEUNIQUE_DIRECTIVE } from '../../validation/validateUnique.directive';

import './mapObjectEditor.component.less';

export const CORE_FORMS_MAPOBJECTEDITOR_MAPOBJECTEDITOR_COMPONENT = 'spinnaker.core.forms.mapObjectEditor.component';
export const name = CORE_FORMS_MAPOBJECTEDITOR_MAPOBJECTEDITOR_COMPONENT; // for backwards compatibility
angular
.module(CORE_FORMS_MAPOBJECTEDITOR_MAPOBJECTEDITOR_COMPONENT, [CORE_VALIDATION_VALIDATEUNIQUE_DIRECTIVE])
.directive('jsonText', function () {
return {
restrict: 'A',
require: 'ngModel',
link: function (scope, element, attr, ngModel) {
function into(input) {
return JSON.parse(input);
}
function out(data) {
return JSON.stringify(data, null, 2);
}
ngModel.$parsers.push(into);
ngModel.$formatters.push(out);
},
};
})
.component('mapObjectEditor', {
bindings: {
model: '=',
keyLabel: '@',
valueLabel: '@',
addButtonLabel: '@',
allowEmpty: '=?',
onChange: '&',
labelsLeft: '<?',
label: '@',
hiddenKeys: '<',
},
controller: [
'$scope',
function ($scope) {
this.backingModel = [];

const modelKeys = () => Object.keys(this.model);

this.addField = () => {
this.backingModel.push({ key: '', value: {}, checkUnique: modelKeys() });
// do not fire the onChange event, since no values have been committed to the object
};

this.removeField = (index) => {
this.backingModel.splice(index, 1);
this.synchronize();
this.onChange();
};

// Clears existing values from model, then replaces them
this.synchronize = () => {
if (this.isParameterized) {
return;
}
const modelStart = JSON.stringify(this.model);
const allKeys = this.backingModel.map((pair) => pair.key);
modelKeys().forEach((key) => delete this.model[key]);
this.backingModel.forEach((pair) => {
if (pair.key && (this.allowEmpty || pair.value)) {
try {
// Parse value if it is a valid JSON object
this.model[pair.key] = JSON.parse(pair.value);
} catch (e) {
// If value is not a valid JSON object, just store the raw value
this.model[pair.key] = pair.value;
}
}
// include other keys to verify no duplicates
pair.checkUnique = allKeys.filter((key) => pair.key !== key);
});
if (modelStart !== JSON.stringify(this.model)) {
this.onChange();
}
};

// In Angular 1.7 Directive bindings were removed in the constructor, default values now must be instantiated within $onInit
// See https://docs.angularjs.org/guide/migration#-compile- and https://docs.angularjs.org/guide/migration#migrate1.5to1.6-ng-services-$compile
this.$onInit = () => {
// Set default values for optional fields
this.onChange = this.onChange || angular.noop;
this.keyLabel = this.keyLabel || 'Key';
this.valueLabel = this.valueLabel || 'Value';
this.addButtonLabel = this.addButtonLabel || 'Add Field';
this.allowEmpty = this.allowEmpty || false;
this.labelsLeft = this.labelsLeft || false;
this.tableClass = this.label ? '' : 'no-border-top';
this.columnCount = this.labelsLeft ? 5 : 3;
this.model = this.model || {};
this.isParameterized = isString(this.model);
this.hiddenKeys = this.hiddenKeys || [];

if (this.model && !this.isParameterized) {
modelKeys().forEach((key) => {
this.backingModel.push({ key: key, value: this.model[key] });
});
}
};

$scope.$watch(() => JSON.stringify(this.backingModel), this.synchronize);
},
],
templateUrl: require('./mapObjectEditor.component.html'),
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
map-object-editor {
.table.no-border-top {
border-top: 2px solid var(--color-white);

.table-label {
padding: 0.8rem 0 0 1rem;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
'use strict';

describe('Component: mapObjectEditor', function () {
var scope;

beforeEach(window.module(require('./mapObjectEditor.component').name));

beforeEach(
window.inject(function ($rootScope, $compile) {
scope = $rootScope.$new();
this.compile = $compile;
}),
);

it('initializes with provided values', function () {
scope.model = { foo: { bar: 'baz' }, bah: 11 };
let dom = this.compile('<map-object-editor model="model"></map-object-editor>')(scope);
scope.$digest();

expect(dom.find('input').length).toBe(2);
expect(dom.find('textarea').length).toBe(2);

expect(dom.find('input').get(0).value).toBe('foo');
expect(dom.find('textarea').get(0).value).toBe(JSON.stringify({ bar: 'baz' }, null, 2));
expect(dom.find('input').get(1).value).toBe('bah');
expect(dom.find('textarea').get(1).value).toBe('11');
});

describe('adding new entries', function () {
it('creates a new row in the table, but does not synchronize to model', function () {
scope.model = {};
let dom = this.compile('<map-object-editor model="model"></map-object-editor>')(scope);
scope.$digest();
dom.find('button').click();
expect(dom.find('tbody tr').length).toBe(1);
expect(dom.find('input').length).toBe(1);
expect(dom.find('textarea').length).toBe(1);
});

it('does not flag multiple new rows without keys as having duplicate keys', function () {
scope.model = {};
let dom = this.compile('<map-object-editor model="model"></map-object-editor>')(scope);
scope.$digest();
dom.find('button').click();
dom.find('button').click();

expect(dom.find('tbody tr').length).toBe(2);
expect(dom.find('input').length).toBe(2);
expect(dom.find('textarea').length).toBe(2);

expect(dom.find('.error-message').length).toBe(0);
});
});

describe('removing entries', function () {
it('removes the entry when the trash can is clicked', function () {
scope.model = { foo: { bar: 'baz' } };
let dom = this.compile('<map-object-editor model="model"></map-object-editor>')(scope);
scope.$digest();

expect(dom.find('input').length).toBe(1);
expect(dom.find('textarea').length).toBe(1);

dom.find('a').click();

expect(dom.find('tbody tr').length).toBe(0);
expect(dom.find('input').length).toBe(0);
expect(dom.find('textarea').length).toBe(0);
expect(scope.model.foo).toBeUndefined();
});
});

describe('duplicate key handling', function () {
it('provides a warning when a duplicate key is entered', function () {
scope.model = { a: { bar: 'baz' }, b: '2' };
let dom = this.compile('<map-object-editor model="model"></map-object-editor>')(scope);
scope.$digest();

$(dom.find('input')[1]).val('a').trigger('input');
scope.$digest();

expect(dom.find('.error-message').length).toBe(1);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,12 @@ angular
}
}

function populatePartnerMetadata(instanceTemplatePartnerMetadata, command) {
if (instanceTemplatePartnerMetadata) {
Object.assign(command.partnerMetadata, instanceTemplatePartnerMetadata);
}
}

function populateLabels(instanceTemplateLabels, command) {
if (instanceTemplateLabels) {
Object.assign(command.labels, instanceTemplateLabels);
Expand Down Expand Up @@ -374,6 +380,7 @@ angular
tags: [],
labels: {},
resourceManagerTags: {},
partnerMetadata: {},
enableSecureBoot: false,
enableVtpm: false,
enableIntegrityMonitoring: false,
Expand Down Expand Up @@ -453,6 +460,7 @@ angular
tags: [],
labels: {},
resourceManagerTags: {},
partnerMetadata: {},
availabilityZones: [],
enableSecureBoot: serverGroup.enableSecureBoot,
enableVtpm: serverGroup.enableVtpm,
Expand Down Expand Up @@ -589,6 +597,9 @@ angular
const resourceManagerTags = extendedCommand.resourceManagerTags;
populateResourceManagerTags(resourceManagerTags, extendedCommand);

const partnerMetadata = extendedCommand.partnerMetadata;
populatePartnerMetadata(partnerMetadata, extendedCommand);

return extendedCommand;
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,17 @@
</div>
<map-editor model="vm.command.resourceManagerTags" add-button-label="Add New Tag" allow-empty="false"></map-editor>
</div>
<div class="form-group">
<div class="sm-label-left">
<b>Partner Metadata</b>
<help-field key="gce.serverGroup.partnerMetadata"></help-field>
</div>
<map-object-editor
model="vm.command.partnerMetadata"
add-button-label="Add New Metadata"
allow-empty="false"
></map-object-editor>
</div>
<div class="form-group">
<div class="sm-label-left">
Shielded VMs
Expand Down
Loading