Skip to content

Commit

Permalink
feat(google): add support for partnerMetadata in GCE servergroup (spi…
Browse files Browse the repository at this point in the history
…nnaker#10150)

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

* feat(google): fix prettier issues

* feat(google): render JSON object as its string representation

* feat(google): update tests with textarea

* feat(google): fix prettier issue

* feat(google): move display logic to controller

* feat(google): fix tests

* feat(google): expect JSON string in test

* feat(google): use formatValueForDisplay in ng-model

* feat(google): define json-text directive to handle view

* feat(google): add space to JSON format

* feat(google): remove hiddenKeys test
  • Loading branch information
edgarulg authored and christosarvanitis committed Nov 11, 2024
1 parent 14977d1 commit 6ce262a
Show file tree
Hide file tree
Showing 9 changed files with 297 additions and 0 deletions.
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
2 changes: 2 additions & 0 deletions packages/kubernetes/src/help/kubernetes.help.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,8 @@ const helpContents: { [key: string]: string } = {
'These artifacts must be present in the context for this stage to successfully complete. Artifacts specified will be <a href="https://www.spinnaker.io/reference/artifacts/in-kubernetes-v2/#binding-artifacts-in-manifests" target="_blank">bound to the deployed manifest.</a>',
'kubernetes.manifest.skipExpressionEvaluation':
'<p>Skip SpEL expression evaluation of the manifest artifact in this stage. Can be paired with the "Evaluate SpEL expressions in overrides at bake time" option in the Bake Manifest stage when baking a third-party manifest artifact with expressions not meant for Spinnaker to evaluate as SpEL.</p>',
'kubernetes.manifest.skipSpecTemplateLabels': `
<p>Skip applying labels to a manifest's <b><i>.spec.template.metadata.labels.</i></b></p>`,
'kubernetes.manifest.undoRollout.revisionsBack': `
<p>How many revisions to rollback from the current active revision. This is not a hard-coded revision to rollout.</p>
<p>For example: If you specify "1", and this stage executes, the prior revision will be active upon success.</p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ interface IDeployManifestStageConfigFormProps {
interface IDeployManifestStageConfigFormState {
rawManifest: string;
overrideNamespace: boolean;
skipSpecTemplateLabels: boolean;
}

export class DeployManifestStageForm extends React.Component<
Expand All @@ -55,6 +56,7 @@ export class DeployManifestStageForm extends React.Component<
this.state = {
rawManifest: !isEmpty(manifests) && isTextManifest ? yamlDocumentsToString(manifests) : '',
overrideNamespace: get(stage, 'namespaceOverride', '') !== '',
skipSpecTemplateLabels: get(stage, 'skipSpecTemplateLabels', false),
};
}

Expand Down Expand Up @@ -141,6 +143,12 @@ export class DeployManifestStageForm extends React.Component<
/>
</StageConfigField>
)}
<StageConfigField label="Skip Spec Template Labels" helpKey="kubernetes.manifest.skipSpecTemplateLabels">
<CheckboxInput
checked={stage.skipSpecTemplateLabels === true}
onChange={(e: any) => this.props.formik.setFieldValue('skipSpecTemplateLabels', e.target.checked)}
/>
</StageConfigField>
<hr />
<h4>Manifest Configuration</h4>
<StageConfigField label="Manifest Source" helpKey="kubernetes.manifest.source">
Expand Down

0 comments on commit 6ce262a

Please sign in to comment.