diff options
author | Innokentii Mokin <innokentii@ydb.tech> | 2024-04-08 14:32:30 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-04-08 14:32:30 +0300 |
commit | 487b19d77a56355f6b38353abff37f7e09399494 (patch) | |
tree | 7943c374e7ab759da45caf3137548750e8be94a0 | |
parent | 1f1b84d1d160a2b1cfe4298b271c0078ec1602b1 (diff) | |
download | ydb-487b19d77a56355f6b38353abff37f7e09399494.tar.gz |
[Configs] Add basic audit for yaml configs (#3531)
-rw-r--r-- | ydb/core/cms/console/console__replace_yaml_config.cpp | 25 | ||||
-rw-r--r-- | ydb/core/cms/ui/console_log.js | 187 | ||||
-rw-r--r-- | ydb/core/cms/ui/ext/myers-diff/LICENSE | 201 | ||||
-rw-r--r-- | ydb/core/cms/ui/ext/myers-diff/myers_diff.js | 393 | ||||
-rw-r--r-- | ydb/core/cms/ui/index.html | 1 | ||||
-rw-r--r-- | ydb/core/cms/ya.make | 1 | ||||
-rw-r--r-- | ydb/core/protos/console.proto | 15 | ||||
-rw-r--r-- | ydb/core/protos/console_config.proto | 3 |
8 files changed, 780 insertions, 46 deletions
diff --git a/ydb/core/cms/console/console__replace_yaml_config.cpp b/ydb/core/cms/console/console__replace_yaml_config.cpp index 4df3681a0b9..1d684069249 100644 --- a/ydb/core/cms/console/console__replace_yaml_config.cpp +++ b/ydb/core/cms/console/console__replace_yaml_config.cpp @@ -2,6 +2,7 @@ #include "console_configs_provider.h" #include <ydb/core/tablet_flat/tablet_flat_executed.h> +#include <ydb/library/aclib/aclib.h> #include <ydb/library/yql/public/issue/protos/issue_severity.pb.h> namespace NKikimr::NConsole { @@ -16,6 +17,7 @@ class TConfigsManager::TTxReplaceYamlConfig : public TTransactionBase<TConfigsMa : TBase(self) , Config(ev->Get()->Record.GetRequest().config()) , Sender(ev->Sender) + , UserSID(NACLib::TUserToken(ev->Get()->Record.GetUserToken()).GetUserSID()) , Force(force) , AllowUnknownFields(ev->Get()->Record.GetRequest().allow_unknown_fields()) , DryRun(ev->Get()->Record.GetRequest().dry_run()) @@ -35,6 +37,26 @@ public: { } + void DoAudit(TTransactionContext &txc, const TActorContext &ctx) + { + auto logData = NKikimrConsole::TLogRecordData{}; + + // for backward compatibility in ui + logData.MutableAction()->AddActions()->MutableModifyConfigItem()->MutableConfigItem(); + logData.AddAffectedKinds(NKikimrConsole::TConfigItem::YamlConfigChangeItem); + + auto& yamlConfigChange = *logData.MutableYamlConfigChange(); + yamlConfigChange.SetOldYamlConfig(Self->YamlConfig); + yamlConfigChange.SetNewYamlConfig(UpdatedConfig); + for (auto& [id, config] : Self->VolatileYamlConfigs) { + auto& oldVolatileConfig = *yamlConfigChange.AddOldVolatileYamlConfigs(); + oldVolatileConfig.SetId(id); + oldVolatileConfig.SetConfig(config); + } + + Self->Logger.DbLogData(UserSID, logData, txc, ctx); + } + bool Execute(TTransactionContext &txc, const TActorContext &ctx) override { NIceDb::TNiceDb db(txc.DB); @@ -81,6 +103,8 @@ public: } if (!DryRun) { + DoAudit(txc, ctx); + db.Table<Schema::YamlConfig>().Key(Version + 1) .Update<Schema::YamlConfig::Config>(UpdatedConfig) // set config dropped by default to support rollback to previous versions @@ -151,6 +175,7 @@ public: private: const TString Config; const TActorId Sender; + const TString UserSID; const bool Force = false; const bool AllowUnknownFields = false; const bool DryRun = false; diff --git a/ydb/core/cms/ui/console_log.js b/ydb/core/cms/ui/console_log.js index 0ba77654168..8e1689dfbc6 100644 --- a/ydb/core/cms/ui/console_log.js +++ b/ydb/core/cms/ui/console_log.js @@ -123,6 +123,10 @@ function RenderRemoveConfigItems(content) { return "<b>RemoveItems:</b>\n\tCookies:\n\t\t" + cf.join("\n\t\t"); } +function RenderYamlConfigChange(content) { + return "<b>click to view</b>" +} + const ActionRenderers = { "AddConfigItem": RenderAddConfigItem, "RemoveConfigItem": RenderRemoveConfigItem, @@ -162,9 +166,15 @@ function onConsoleLogLoaded(data) { var cell2 = document.createElement('td'); var cell3 = document.createElement('td'); var cell4 = document.createElement('td'); + var isYamlChange = false; if (rec['Data'].hasOwnProperty('AffectedKinds')) { - cell3.innerHTML = "<pre>" + rec['Data']['AffectedKinds'].map(x => cmsEnums.get('ItemKinds', x)).join("\n") + "</pre>"; + var affectedKinds = rec['Data']['AffectedKinds']; + if (affectedKinds.length === 1 && affectedKinds[0] === 32768) { + isYamlChange = true; + } else { + cell3.innerHTML = "<pre>" + affectedKinds.map(x => cmsEnums.get('ItemKinds', x)).join("\n") + "</pre>"; + } } cell0.textContent = rec['Id']; @@ -173,15 +183,20 @@ function onConsoleLogLoaded(data) { cell1.textContent = timestamp.toLocaleString('en-GB', { timeZone: 'UTC' }); cell2.textContent = rec['User']; var contents = []; - for (var action of rec['Data']['Action']['Actions']) { - var key = (Object.keys(action)[0]); - var content = action[key]; - contents.push(ActionRenderers[key](content)); + if (!isYamlChange) { + for (var action of rec['Data']['Action']['Actions']) { + var key = (Object.keys(action)[0]); + var content = action[key]; + contents.push(ActionRenderers[key](content)); + } + cell4.innerHTML = "<pre>" + contents.join("<br/>") + "</pre>"; + cell4.title = JSON.stringify(rec['Data']['Action'], null, 2); + cell4.setAttribute('data', JSON.stringify(rec['Data'], null, 2)); + } else { + cell4.innerHTML = RenderYamlConfigChange(rec['Data']['YamlConfigChange']); + cell4.title = "yaml-config-change"; + cell4.setAttribute('data', JSON.stringify(rec['Data'], null, 2)); } - cell4.innerHTML = "<pre>" + contents.join("<br/>") + "</pre>"; - cell4.title = JSON.stringify(rec['Data']['Action'], null, 2); - cell4.setAttribute('data', JSON.stringify(rec['Data'], null, 2)); - line.appendChild(cell0); line.appendChild(cell1); line.appendChild(cell2); @@ -322,53 +337,133 @@ function copyDataToClipboard(ev) { temp.remove(); } +function GnuNormalFormat(item) { + var i, + nf = [], + op, + str = []; + + // del add description + // 0 >0 added count to rhs + // >0 0 deleted count from lhs + // >0 >0 changed count lines + if (item.lhs.del === 0 && item.rhs.add > 0) { + op = 'a'; + } else if (item.lhs.del > 0 && item.rhs.add === 0) { + op = 'd'; + } else { + op = 'c'; + } + + function encodeSide(side, key) { + // encode side as a start,stop if a range + str.push(side.at + 1); + if (side[key] > 1) { + str.push(','); + str.push(side[key] + side.at); + } + } + encodeSide(item.lhs, 'del'); + str.push(op); + encodeSide(item.rhs, 'add'); + + nf.push(str.join('')); + for (i = item.lhs.at; i < item.lhs.at + item.lhs.del; ++i) { + nf.push('< ' + item.lhs.ctx.getLine(i)); + } + if (item.rhs.add && item.lhs.del) { + nf.push('---'); + } + for (i = item.rhs.at; i < item.rhs.at + item.rhs.add; ++i) { + nf.push('> ' + item.rhs.ctx.getLine(i)); + } + return nf.join('\n'); +} + +function renderCopyablePre(data) { + var div = $("<div></div>"); + var pre = $("<pre></pre>"); + pre.html(data); + div.append(pre); + var copy = $("<div class=\"icon-copy\" style=\"float:right; cursor:pointer;\"></div>"); + copy.click({elem: pre}, copyDataToClipboard); + div.append(copy); + return div; +} + function renderConsolePopup(data, user, time) { // set header $("#popup-header").text("Change by \"" + user + "\" at " + time); var div = $("<div></div>"); - // create action view - var cmdDiv = $("<div></div>") - var cmd = $("<pre></pre>"); - cmd.html(syntaxHighlight(JSON.stringify(data['Action'], null, 2))); - var copyCmd = $("<div class=\"icon-copy\" style=\"float:right; cursor:pointer;\"></div>"); - copyCmd.click({elem: cmd}, copyDataToClipboard); - cmdDiv.append(copyCmd); - cmdDiv.append(cmd); - div.append(renderSpoiler("Command", cmdDiv)); - if (!data.hasOwnProperty('AffectedKinds')) { data['AffectedKinds'] = []; } - if (data.hasOwnProperty("AffectedConfigs")) { - for (var i in data['AffectedConfigs']) { - var oldConfigDiv = $("<div></div>") - var oldConfig = $("<pre></pre>"); - var oldConfigData = data['AffectedConfigs'][i]['OldConfig']; - oldConfigData = filterUnaffected(oldConfigData, data['AffectedKinds']); - oldConfig.html(syntaxHighlight(JSON.stringify(oldConfigData, null, 2))); - var copyOldConfig = $("<div class=\"icon-copy\" style=\"float:right; cursor:pointer;\"></div>"); - copyOldConfig.click({elem: oldConfig}, copyDataToClipboard); - oldConfigDiv.append(copyOldConfig); - oldConfigDiv.append(oldConfig); - div.append(renderSpoiler( - "Old Config for Tenant:\"" + data['AffectedConfigs'][i]['Tenant'] + - "\" NodeType:\"" + data['AffectedConfigs'][i]['NodeType'] + "\"", oldConfigDiv)); - - var newConfigDiv = $("<div></div>") - var newConfig = $("<pre></pre>"); - var newConfigData = data['AffectedConfigs'][i]['NewConfig']; - newConfigData = filterUnaffected(newConfigData, data['AffectedKinds']); - newConfig.html(syntaxHighlight(JSON.stringify(newConfigData, null, 2))); - var copyNewConfig = $("<div class=\"icon-copy\" style=\"float:right; cursor:pointer;\"></div>"); - copyNewConfig.click({elem: newConfig}, copyDataToClipboard); - newConfigDiv.append(copyNewConfig); - newConfigDiv.append(newConfig); - div.append(renderSpoiler( - "New Config for Tenant:\"" + data['AffectedConfigs'][i]['Tenant'] + - "\" NodeType:\"" + data['AffectedConfigs'][i]['NodeType'] + "\"", newConfigDiv)); + var affectedKinds = data['AffectedKinds']; + if (affectedKinds.length === 1 && affectedKinds[0] === 32768) { + var cmd = renderCopyablePre(syntaxHighlight(JSON.stringify(data, null, 2))); + div.append(renderSpoiler("Raw", cmd)); + + var lhs = data['YamlConfigChange']['OldYamlConfig']; + var old = renderCopyablePre(lhs); + div.append(renderSpoiler("Old", old)); + + var rhs = data['YamlConfigChange']['NewYamlConfig']; + var new_ = renderCopyablePre(rhs); + div.append(renderSpoiler("New", new_)); + + const diff = Myers.diff(lhs, rhs); + + var i = 0; + var out = []; + for (i = 0; i < diff.length; ++i) { + out.push(GnuNormalFormat(diff[i])); + } + var result = out.join('\n') + + var diffs = renderCopyablePre(result); + div.append(renderSpoiler("Diff", diffs)); + } else { + // create action view + var cmdDiv = $("<div></div>") + var cmd = $("<pre></pre>"); + cmd.html(syntaxHighlight(JSON.stringify(data['Action'], null, 2))); + var copyCmd = $("<div class=\"icon-copy\" style=\"float:right; cursor:pointer;\"></div>"); + copyCmd.click({elem: cmd}, copyDataToClipboard); + cmdDiv.append(copyCmd); + cmdDiv.append(cmd); + div.append(renderSpoiler("Command", cmdDiv)); + + if (!isYamlChangedata.hasOwnProperty("AffectedConfigs") && !isYamlChange) { + for (var i in data['AffectedConfigs']) { + var oldConfigDiv = $("<div></div>") + var oldConfig = $("<pre></pre>"); + var oldConfigData = data['AffectedConfigs'][i]['OldConfig']; + oldConfigData = filterUnaffected(oldConfigData, affectedKinds); + oldConfig.html(syntaxHighlight(JSON.stringify(oldConfigData, null, 2))); + var copyOldConfig = $("<div class=\"icon-copy\" style=\"float:right; cursor:pointer;\"></div>"); + copyOldConfig.click({elem: oldConfig}, copyDataToClipboard); + oldConfigDiv.append(copyOldConfig); + oldConfigDiv.append(oldConfig); + div.append(renderSpoiler( + "Old Config for Tenant:\"" + data['AffectedConfigs'][i]['Tenant'] + + "\" NodeType:\"" + data['AffectedConfigs'][i]['NodeType'] + "\"", oldConfigDiv)); + + var newConfigDiv = $("<div></div>") + var newConfig = $("<pre></pre>"); + var newConfigData = data['AffectedConfigs'][i]['NewConfig']; + newConfigData = filterUnaffected(newConfigData, affectedKinds); + newConfig.html(syntaxHighlight(JSON.stringify(newConfigData, null, 2))); + var copyNewConfig = $("<div class=\"icon-copy\" style=\"float:right; cursor:pointer;\"></div>"); + copyNewConfig.click({elem: newConfig}, copyDataToClipboard); + newConfigDiv.append(copyNewConfig); + newConfigDiv.append(newConfig); + div.append(renderSpoiler( + "New Config for Tenant:\"" + data['AffectedConfigs'][i]['Tenant'] + + "\" NodeType:\"" + data['AffectedConfigs'][i]['NodeType'] + "\"", newConfigDiv)); + } } } diff --git a/ydb/core/cms/ui/ext/myers-diff/LICENSE b/ydb/core/cms/ui/ext/myers-diff/LICENSE new file mode 100644 index 00000000000..f8ecba2c43e --- /dev/null +++ b/ydb/core/cms/ui/ext/myers-diff/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2022 wickedest + + Licensed 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. diff --git a/ydb/core/cms/ui/ext/myers-diff/myers_diff.js b/ydb/core/cms/ui/ext/myers-diff/myers_diff.js new file mode 100644 index 00000000000..152ee436063 --- /dev/null +++ b/ydb/core/cms/ui/ext/myers-diff/myers_diff.js @@ -0,0 +1,393 @@ +'use strict'; + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +/** + * Encodes text into diff-codes to prepare for Myers diff. + */ +var Encoder = function () { + function Encoder() { + _classCallCheck(this, Encoder); + + this.code = 0; + this.diff_codes = {}; + } + + _createClass(Encoder, [{ + key: 'encode', + value: function encode(text, settings) { + return new EncodeContext(this, text, settings); + } + }, { + key: 'getCode', + value: function getCode(line) { + return this.diff_codes[line]; + } + }, { + key: 'hasKey', + value: function hasKey(line) { + return this.diff_codes.hasOwnProperty(line); + } + }, { + key: 'getCodes', + value: function getCodes() { + return this.diff_codes; + } + }, { + key: 'newCode', + value: function newCode(line) { + this.code = this.code + 1; + this.diff_codes[line] = this.code; + return this.code; + } + }]); + + return Encoder; +}(); + +/** + * Encoder context + */ + +var EncodeContext = function () { + function EncodeContext(encoder, text, settings) { + _classCallCheck(this, EncodeContext); + + var lines = void 0, + re = void 0; + if (text && text.length) { + if (encoder === undefined) { + throw new Error('illegal argument \'encoder\''); + } + if (text === undefined) { + throw new Error('illegal argument \'text\''); + } + if (settings === undefined) { + throw new Error('illegal argument \'settings\''); + } + if (settings.compare === 'chars') { + // split all chars + re = new RegExp(settings.splitCharsRegex, "g"); + } else if (settings.compare === 'words') { + // split all of the text on spaces + re = new RegExp(settings.splitWordsRegex, "g"); + } else { + // lines (default) + re = new RegExp(settings.splitLinesRegex, "g"); + } + lines = text.split(re); + } else { + // line is empty + lines = []; + } + this._init(encoder, lines, settings); + } + + _createClass(EncodeContext, [{ + key: 'getLine', + value: function getLine(n) { + if (!this._codes.hasOwnProperty(n)) { + return; + } + var key = void 0, + ckey = this._codes[n]; + var keyCodes = this.encoder.getCodes(); + for (key in keyCodes) { + if (keyCodes.hasOwnProperty(key)) { + if (keyCodes[key] === ckey) { + return key; + } + } + } + } + }, { + key: '_init', + value: function _init(encoder, lines, settings) { + this.encoder = encoder; + this._codes = {}; + this._modified = {}; + + // for each line, if it exists in 'diff_codes', then 'codes' will + // be assgined the existing value from 'diff_codes'. if the line does + // not existin 'diff_codes', then a new diff_code will be generated + // (EncodeContext.code) and stored in 'codes' for the line. + for (var i = 0; i < lines.length; ++i) { + var line = lines[i]; + if (settings.ignoreWhitespace) { + line = line.replace(/\s+/g, ''); + } + var aCode = encoder.getCode(line); + if (aCode !== undefined) { + this._codes[i] = aCode; + } else { + this._codes[i] = encoder.newCode(line); + } + } + } + }, { + key: 'codes', + get: function get() { + return this._codes; + } + }, { + key: 'length', + get: function get() { + return Object.keys(this._codes).length; + } + }, { + key: 'modified', + get: function get() { + return this._modified; + } + }]); + + return EncodeContext; +}(); + +var Myers = function () { + function Myers() { + _classCallCheck(this, Myers); + } + + _createClass(Myers, null, [{ + key: 'compare_lcs', + value: function compare_lcs(lhs_modified, lhs_codes, lhs_codes_length, rhs_modified, rhs_codes, rhs_codes_length, callback) { + var lhs_start = 0, + rhs_start = 0, + lhs_line = 0, + rhs_line = 0, + item = void 0; + + while (lhs_line < lhs_codes_length || rhs_line < rhs_codes_length) { + if (lhs_line < lhs_codes_length && !lhs_modified[lhs_line] && rhs_line < rhs_codes_length && !rhs_modified[rhs_line]) { + // equal lines + lhs_line++; + rhs_line++; + } else { + // maybe deleted and/or inserted lines + lhs_start = lhs_line; + rhs_start = rhs_line; + while (lhs_line < lhs_codes_length && (rhs_line >= rhs_codes_length || lhs_modified[lhs_line])) { + lhs_line++; + } + while (rhs_line < rhs_codes_length && (lhs_line >= lhs_codes_length || rhs_modified[rhs_line])) { + rhs_line++; + } + if (lhs_start < lhs_line || rhs_start < rhs_line) { + item = { + lhs: { + at: Math.min(lhs_start, lhs_codes_length ? lhs_codes_length - 1 : 0), + del: lhs_line - lhs_start + }, + rhs: { + at: Math.min(rhs_start, rhs_codes_length ? rhs_codes_length - 1 : 0), + add: rhs_line - rhs_start + } + }; + callback(item); + } + } + } + } + }, { + key: 'getShortestMiddleSnake', + value: function getShortestMiddleSnake(lhs_codes, lhs_codes_length, lhs_lower, lhs_upper, rhs_codes, rhs_codes_length, rhs_lower, rhs_upper, vector_u, vector_d) { + var max = lhs_codes_length + rhs_codes_length + 1; + if (max === undefined) { + throw new Error('unexpected state'); + } + var kdown = lhs_lower - rhs_lower, + kup = lhs_upper - rhs_upper, + delta = lhs_upper - lhs_lower - (rhs_upper - rhs_lower), + odd = (delta & 1) != 0, + offset_down = max - kdown, + offset_up = max - kup, + maxd = (lhs_upper - lhs_lower + rhs_upper - rhs_lower) / 2 + 1, + ret = { x: 0, y: 0 }, + d = void 0, + k = void 0, + x = void 0, + y = void 0; + + vector_d[offset_down + kdown + 1] = lhs_lower; + vector_u[offset_up + kup - 1] = lhs_upper; + for (d = 0; d <= maxd; ++d) { + for (k = kdown - d; k <= kdown + d; k += 2) { + if (k === kdown - d) { + x = vector_d[offset_down + k + 1]; //down + } else { + x = vector_d[offset_down + k - 1] + 1; //right + if (k < kdown + d && vector_d[offset_down + k + 1] >= x) { + x = vector_d[offset_down + k + 1]; //down + } + } + y = x - k; + // find the end of the furthest reaching forward D-path in diagonal k. + while (x < lhs_upper && y < rhs_upper && lhs_codes[x] === rhs_codes[y]) { + x++;y++; + } + vector_d[offset_down + k] = x; + // overlap ? + if (odd && kup - d < k && k < kup + d) { + if (vector_u[offset_up + k] <= vector_d[offset_down + k]) { + ret.x = vector_d[offset_down + k]; + ret.y = vector_d[offset_down + k] - k; + return ret; + } + } + } + // Extend the reverse path. + for (k = kup - d; k <= kup + d; k += 2) { + // find the only or better starting point + if (k === kup + d) { + x = vector_u[offset_up + k - 1]; // up + } else { + x = vector_u[offset_up + k + 1] - 1; // left + if (k > kup - d && vector_u[offset_up + k - 1] < x) x = vector_u[offset_up + k - 1]; // up + } + y = x - k; + while (x > lhs_lower && y > rhs_lower && lhs_codes[x - 1] === rhs_codes[y - 1]) { + // diagonal + x--; + y--; + } + vector_u[offset_up + k] = x; + // overlap ? + if (!odd && kdown - d <= k && k <= kdown + d) { + if (vector_u[offset_up + k] <= vector_d[offset_down + k]) { + ret.x = vector_d[offset_down + k]; + ret.y = vector_d[offset_down + k] - k; + return ret; + } + } + } + } + // should never get to this state + throw new Error('unexpected state'); + } + }, { + key: 'getLongestCommonSubsequence', + value: function getLongestCommonSubsequence(lhs_modified, lhs_codes, lhs_codes_length, lhs_lower, lhs_upper, rhs_modified, rhs_codes, rhs_codes_length, rhs_lower, rhs_upper, vector_u, vector_d) { + // trim off the matching items at the beginning + while (lhs_lower < lhs_upper && rhs_lower < rhs_upper && lhs_codes[lhs_lower] === rhs_codes[rhs_lower]) { + ++lhs_lower; + ++rhs_lower; + } + // trim off the matching items at the end + while (lhs_lower < lhs_upper && rhs_lower < rhs_upper && lhs_codes[lhs_upper - 1] === rhs_codes[rhs_upper - 1]) { + --lhs_upper; + --rhs_upper; + } + if (lhs_lower === lhs_upper) { + while (rhs_lower < rhs_upper) { + rhs_modified[rhs_lower++] = true; + } + } else if (rhs_lower === rhs_upper) { + while (lhs_lower < lhs_upper) { + lhs_modified[lhs_lower++] = true; + } + } else { + var sms = Myers.getShortestMiddleSnake(lhs_codes, lhs_codes_length, lhs_lower, lhs_upper, rhs_codes, rhs_codes_length, rhs_lower, rhs_upper, vector_u, vector_d); + Myers.getLongestCommonSubsequence(lhs_modified, lhs_codes, lhs_codes_length, lhs_lower, sms.x, rhs_modified, rhs_codes, rhs_codes_length, rhs_lower, sms.y, vector_u, vector_d); + Myers.getLongestCommonSubsequence(lhs_modified, lhs_codes, lhs_codes_length, sms.x, lhs_upper, rhs_modified, rhs_codes, rhs_codes_length, sms.y, rhs_upper, vector_u, vector_d); + } + } + }, { + key: 'optimize', + value: function optimize(ctx) { + var start = 0, + end = 0; + while (start < ctx._codes.length) { + while (start < ctx._codes.length && (ctx._modified[start] === undefined || ctx._modified[start] === false)) { + start++; + } + end = start; + while (end < ctx._codes.length && ctx._modified[end] === true) { + end++; + } + if (end < ctx._codes.length && ctx._codes[start] === ctx._codes[end]) { + ctx._modified[start] = false; + ctx._modified[end] = true; + } else { + start = end; + } + } + } + }, { + key: 'LCS', + value: function LCS(lhsModified, lhsCodes, lhsLength, rhsModified, rhsCodes, rhsLength) { + var vector_u = [], + vector_d = []; + return Myers.getLongestCommonSubsequence(lhsModified, lhsCodes, lhsLength, 0, lhsLength, rhsModified, rhsCodes, rhsLength, 0, rhsLength, vector_u, vector_d); + } + }, { + key: 'CompareLCS', + value: function CompareLCS(lhsModified, lhsCodes, lhsLength, rhsModified, rhsCodes, rhsLength, callback) { + return Myers.compare_lcs(lhsModified, lhsCodes, lhsLength, rhsModified, rhsCodes, rhsLength, callback); + } + + /** + * Compare {@code lhs} to {@code rhs}. Changes are compared from left + * to right such that items are deleted from left, or added to right, + * or just otherwise changed between them. + * + * @param {String} lhs The left-hand source text. + * @param {String} rhs The right-hand source text. + * @param {Object} options Optional settings. + */ + + }, { + key: 'diff', + value: function diff(lhs, rhs, options) { + var settings = Myers._getDefaultSettings(), + encoder = new Encoder(); + + if (lhs === undefined) { + throw new Error('illegal argument \'lhs\''); + } + if (rhs === undefined) { + throw new Error('illegal argument \'rhs\''); + } + + Object.assign(settings, options); + + var lhsCtx = encoder.encode(lhs, settings), + rhsCtx = encoder.encode(rhs, settings); + + Myers.LCS(lhsCtx.modified, lhsCtx.codes, lhsCtx.length, rhsCtx.modified, rhsCtx.codes, rhsCtx.length); + + Myers.optimize(lhsCtx); + Myers.optimize(rhsCtx); + + // compare lhs/rhs codes and build a list of comparisons + var items = void 0; + Myers.CompareLCS(lhsCtx.modified, lhsCtx.codes, lhsCtx.length, rhsCtx.modified, rhsCtx.codes, rhsCtx.length, function (item) { + // add context information + item.lhs.ctx = lhsCtx; + item.rhs.ctx = rhsCtx; + if (items === undefined) { + items = []; + } + items.push(item); + }); + if (items === undefined) { + return []; + } + return items; + } + }, { + key: '_getDefaultSettings', + value: function _getDefaultSettings() { + return { + compare: 'lines', // lines|words|chars + ignoreWhitespace: false, + splitLinesRegex: '\n', + splitWordsRegex: '[ ]{1}', + splitCharsRegex: '' + }; + } + }]); + + return Myers; +}(); diff --git a/ydb/core/cms/ui/index.html b/ydb/core/cms/ui/index.html index 9a9737a0be3..91f80e70232 100644 --- a/ydb/core/cms/ui/index.html +++ b/ydb/core/cms/ui/index.html @@ -10,6 +10,7 @@ <link rel="stylesheet" href="cms/common.css"></link> <link rel="stylesheet" href="cms/cms.css"></link> <link rel="stylesheet" href="cms/sentinel.css"></link> + <script language="javascript" type="text/javascript" src="cms/ext/myers-diff/myers_diff.js"></script> <script language="javascript" type="text/javascript" src="cms/common.js"></script> <script language="javascript" type="text/javascript" src="cms/nanotable.js"></script> <script language="javascript" type="text/javascript" src="cms/enums.js"></script> diff --git a/ydb/core/cms/ya.make b/ydb/core/cms/ya.make index 183b5c32050..555073e7e90 100644 --- a/ydb/core/cms/ya.make +++ b/ydb/core/cms/ya.make @@ -82,6 +82,7 @@ RESOURCE( ui/datashard_rs.js cms/ui/datashard_rs.js ui/datashard_slow_ops.js cms/ui/datashard_slow_ops.js ui/enums.js cms/ui/enums.js + ui/ext/myers-diff/myers_diff.js cms/ui/ext/myers-diff/myers_diff.js ui/ext/bootstrap.min.css cms/ui/ext/bootstrap.min.css ui/ext/fuzzycomplete.min.css cms/ui/ext/fuzzycomplete.min.css ui/ext/fuzzycomplete.min.js cms/ui/ext/fuzzycomplete.min.js diff --git a/ydb/core/protos/console.proto b/ydb/core/protos/console.proto index 41e1cf3c522..e6d83427afc 100644 --- a/ydb/core/protos/console.proto +++ b/ydb/core/protos/console.proto @@ -26,10 +26,25 @@ message TSetConfigResponse { optional TStatus Status = 1; } +message TVolatileConfig { + optional uint64 Id = 1; + optional string Config = 2; +} + +message TYamlConfigChange { + optional string OldYamlConfig = 1; + optional string NewYamlConfig = 2; + repeated TVolatileConfig OldVolatileYamlConfigs = 3; + repeated TVolatileConfig NewVolatileYamlConfigs = 4; +} + message TLogRecordData { + // Old configs optional TConfigureRequest Action = 1; repeated TConfigureResponse.TAffectedConfig AffectedConfigs = 2; repeated uint32 AffectedKinds = 3; + // New configs + optional TYamlConfigChange YamlConfigChange = 4; } message TLogRecord { diff --git a/ydb/core/protos/console_config.proto b/ydb/core/protos/console_config.proto index d9a00f5cdb5..fefc4b14d52 100644 --- a/ydb/core/protos/console_config.proto +++ b/ydb/core/protos/console_config.proto @@ -141,6 +141,9 @@ message TConfigItem { NamedConfigsItem = 100; ClusterYamlConfigItem = 101; + + // synthetic kind for audit purposes only + YamlConfigChangeItem = 32768; } enum EMergeStrategy { |