This is an automated email from the ASF dual-hosted git repository.
brahmareddybattula pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/ambari.git
The following commit(s) were added to refs/heads/trunk by this push:
new 3cdadc742b Capacity Scheduler View: tolerant node label response
parsing (#4150)
3cdadc742b is described below
commit 3cdadc742b68bb49a67b67fbceaddc09d69481b3
Author: SANRAJ RAJENDRA BANDRE <[email protected]>
AuthorDate: Sun Aug 23 23:23:42 2026 +0530
Capacity Scheduler View: tolerant node label response parsing (#4150)
getNodeLabels assumed the ResourceManager node-label response was always a
JSON string and chose the response shape from the stack version. That check
(stackVersion >= 2.5) is unreliable: Number('3.2.0') is NaN, so 3-part
versions silently fell through to the legacy branch.
Extract normalization into QueueAdapter#parseNodeLabels, which:
- parses only when the response is a string; accepts an already-parsed
object
- unwraps an optional nodeLabelsInfo wrapper
- supports nodeLabelInfo (array or single object) and legacy nodeLabels
- coerces exclusivity to a Boolean (true / "true" -> true)
- derives the shape from the payload, not the stack version
isNodeLabelsConfiguredByRM semantics are unchanged. Adds unit tests covering
wrapper, JSON string, array, single-object, legacy, boolean/string
exclusivity, malformed JSON, and empty responses.
---
.../src/main/resources/ui/app/adapters.js | 97 +++++++++++--------
.../ui/test/unit/adapters/adapters_test.js | 107 +++++++++++++++++++++
2 files changed, 162 insertions(+), 42 deletions(-)
diff --git
a/contrib/views/capacity-scheduler/src/main/resources/ui/app/adapters.js
b/contrib/views/capacity-scheduler/src/main/resources/ui/app/adapters.js
index 4c7da12937..edc51f6eb7 100644
--- a/contrib/views/capacity-scheduler/src/main/resources/ui/app/adapters.js
+++ b/contrib/views/capacity-scheduler/src/main/resources/ui/app/adapters.js
@@ -243,55 +243,68 @@ App.QueueAdapter = DS.Adapter.extend({
},'App: QueueAdapter#findAllTagged ' + tag);
},
- getNodeLabels:function (store) {
- var uri = [_getCapacitySchedulerViewUri(this),'nodeLabels'].join('/');
- var stackId = store.get('stackId'),
- stackVersion = stackId.substr(stackId.indexOf('-') + 1);
+ getNodeLabels: function (store) {
+ var uri = [_getCapacitySchedulerViewUri(this), 'nodeLabels'].join('/'),
+ adapter = this;
if (App.testMode)
uri = uri + ".json";
- return new Ember.RSVP.Promise(function(resolve, reject) {
- _ajax(uri,'GET').then(function(data) {
- var parsedData;
-
- try {
- parsedData = JSON.parse(data);
- } catch(e) {
- console.warn('Failed to parse node labels data:', e);
- parsedData = null;
- }
-
- if (parsedData !== null) {
- store.set('isNodeLabelsConfiguredByRM', true);
- } else {
- store.set('isNodeLabelsConfiguredByRM', false);
- }
-
- if (stackVersion >= 2.5) {
- if (parsedData && Em.isArray(parsedData.nodeLabelInfo)) {
- labels = parsedData.nodeLabelInfo;
- } else {
- labels = (parsedData &&
parsedData.nodeLabelInfo)?[parsedData.nodeLabelInfo]:[];
- }
- Ember.run(null, resolve, labels.map(function (label) {
- return {name:label.name,exclusivity:label.exclusivity};
- }));
- } else {
- if (parsedData && Em.isArray(parsedData.nodeLabels)) {
- labels = parsedData.nodeLabels;
- } else {
- labels = (parsedData &&
parsedData.nodeLabels)?[parsedData.nodeLabels]:[];
- }
- Ember.run(null, resolve, labels.map(function (label) {
- return {name:label};
- }));
- }
- }, function(jqXHR) {
+ return new Ember.RSVP.Promise(function (resolve, reject) {
+ _ajax(uri, 'GET').then(function (data) {
+ var result = adapter.parseNodeLabels(data);
+ store.set('isNodeLabelsConfiguredByRM', result.configured);
+ Ember.run(null, resolve, result.labels);
+ }, function (jqXHR) {
jqXHR.then = null;
Ember.run(null, reject, jqXHR);
});
- }.bind(this),'App: QueueAdapter#getNodeLabels');
+ }.bind(this), 'App: QueueAdapter#getNodeLabels');
+ },
+
+ /**
+ * Normalizes the ResourceManager node-label response into a flat list.
+ *
+ * Tolerates a JSON string or an already-parsed object, the optional
+ * `nodeLabelsInfo` wrapper, both the current `nodeLabelInfo` and the legacy
+ * `nodeLabels` shapes, and a single label object instead of an array.
+ * `exclusivity` is coerced to a real Boolean (Boolean or the strings
+ * "true"/"false"). The response format is derived from the payload shape,
+ * not from the stack version.
+ *
+ * @param {String|Object} data raw ResourceManager response
+ * @return {{configured: Boolean, labels: Array}}
+ */
+ parseNodeLabels: function (data) {
+ var parsedData;
+
+ try {
+ parsedData = (typeof data === 'string') ? JSON.parse(data) : data;
+ } catch (e) {
+ console.warn('Failed to parse node labels data:', e);
+ parsedData = null;
+ }
+
+ var configured = (parsedData !== null && parsedData !== undefined),
+ labels = [];
+
+ if (parsedData && parsedData.nodeLabelsInfo) {
+ parsedData = parsedData.nodeLabelsInfo;
+ }
+
+ if (parsedData && parsedData.nodeLabelInfo) {
+ labels = Em.isArray(parsedData.nodeLabelInfo) ? parsedData.nodeLabelInfo
: [parsedData.nodeLabelInfo];
+ labels = labels.map(function (label) {
+ return { name: label.name, exclusivity: label.exclusivity === true ||
label.exclusivity === 'true' };
+ });
+ } else if (parsedData && parsedData.nodeLabels) {
+ labels = Em.isArray(parsedData.nodeLabels) ? parsedData.nodeLabels :
[parsedData.nodeLabels];
+ labels = labels.map(function (label) {
+ return { name: (label && label.name !== undefined) ? label.name :
label };
+ });
+ }
+
+ return { configured: configured, labels: labels };
},
getPrivilege:function () {
diff --git
a/contrib/views/capacity-scheduler/src/main/resources/ui/test/unit/adapters/adapters_test.js
b/contrib/views/capacity-scheduler/src/main/resources/ui/test/unit/adapters/adapters_test.js
new file mode 100644
index 0000000000..aec980e1e5
--- /dev/null
+++
b/contrib/views/capacity-scheduler/src/main/resources/ui/test/unit/adapters/adapters_test.js
@@ -0,0 +1,107 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+var adapter;
+
+QUnit.module('unit/adapters - QueueAdapter#parseNodeLabels', {
+ setup: function () {
+ adapter = App.QueueAdapter.create();
+ },
+ teardown: function () {
+ adapter = null;
+ }
+});
+
+test('parsed object with nodeLabelsInfo wrapper and mixed exclusivity',
function () {
+ var result = adapter.parseNodeLabels({
+ nodeLabelsInfo: {
+ nodeLabelInfo: [
+ { name: 'label-a', exclusivity: true },
+ { name: 'label-b', exclusivity: 'false' }
+ ]
+ }
+ });
+ equal(result.configured, true, 'configured is true');
+ deepEqual(result.labels, [
+ { name: 'label-a', exclusivity: true },
+ { name: 'label-b', exclusivity: false }
+ ], 'wrapper unwrapped, exclusivity normalized');
+});
+
+test('JSON string response with single nodeLabelInfo object and string
"true"', function () {
+ var result = adapter.parseNodeLabels(JSON.stringify({
+ nodeLabelInfo: { name: 'label-a', exclusivity: 'true' }
+ }));
+ equal(result.configured, true);
+ deepEqual(result.labels, [{ name: 'label-a', exclusivity: true }],
+ 'string parsed, single object wrapped, "true" -> true');
+});
+
+test('nodeLabelInfo as array', function () {
+ var result = adapter.parseNodeLabels({
+ nodeLabelInfo: [{ name: 'label-a', exclusivity: false }]
+ });
+ deepEqual(result.labels, [{ name: 'label-a', exclusivity: false }]);
+});
+
+test('exclusivity string "false" and boolean false both normalize to false',
function () {
+ var result = adapter.parseNodeLabels({
+ nodeLabelInfo: [
+ { name: 'label-a', exclusivity: 'false' },
+ { name: 'label-b', exclusivity: false }
+ ]
+ });
+ deepEqual(result.labels, [
+ { name: 'label-a', exclusivity: false },
+ { name: 'label-b', exclusivity: false }
+ ]);
+});
+
+test('legacy nodeLabels with mixed string and object entries', function () {
+ var result = adapter.parseNodeLabels({
+ nodeLabels: ['label-a', { name: 'label-b' }]
+ });
+ equal(result.configured, true);
+ deepEqual(result.labels, [{ name: 'label-a' }, { name: 'label-b' }],
+ 'string and object legacy entries both normalize to {name}');
+});
+
+test('legacy nodeLabels as a single string', function () {
+ var result = adapter.parseNodeLabels({ nodeLabels: 'label-a' });
+ deepEqual(result.labels, [{ name: 'label-a' }]);
+});
+
+test('malformed JSON string logs a warning and returns empty labels', function
() {
+ var warned = false,
+ originalWarn = console.warn;
+ console.warn = function () { warned = true; };
+ try {
+ var result = adapter.parseNodeLabels('{ this is not json');
+ ok(warned, 'a parse warning was logged');
+ equal(result.configured, false, 'configured is false on parse failure');
+ deepEqual(result.labels, [], 'empty label list on parse failure');
+ } finally {
+ console.warn = originalWarn;
+ }
+});
+
+test('empty valid object returns empty labels and configured true', function
() {
+ var result = adapter.parseNodeLabels({});
+ equal(result.configured, true, 'a valid empty object still counts as
configured');
+ deepEqual(result.labels, []);
+});
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]