From a530dca579333ae90878187b32071a4953945a7a Mon Sep 17 00:00:00 2001 From: jamesread Date: Mon, 6 Jul 2026 20:36:30 +0100 Subject: [PATCH 1/2] refactor: collapse action justification into a string template field Use a single justification string for both requirement and templating so empty values skip prompts while templates can prefill audit reasons at start. Co-authored-by: Cursor --- config.yaml | 2 +- .../gen/olivetin/api/v1/olivetin_pb.d.ts | 4 +- .../gen/olivetin/api/v1/olivetin_pb.js | 2 +- .../vue/utils/justificationTemplate.js | 15 +++ .../resources/vue/utils/needsArgumentForm.js | 4 +- .../resources/vue/utils/rerunArguments.js | 5 +- .../vue/utils/rerunArguments.test.mjs | 4 +- .../resources/vue/views/ActionDetailsView.vue | 8 +- proto/olivetin/api/v1/olivetin.proto | 2 +- service/gen/olivetin/api/v1/olivetin.pb.go | 8 +- service/internal/api/api.go | 14 ++- service/internal/api/api_justification.go | 34 ++++++- .../internal/api/api_justification_test.go | 96 ++++++++++++++++++- service/internal/api/api_log_arguments.go | 2 +- .../internal/api/api_log_arguments_test.go | 4 +- service/internal/config/config.go | 14 ++- service/internal/executor/justification.go | 2 +- .../internal/executor/justification_test.go | 10 +- var/macos/config.yaml | 2 +- 19 files changed, 194 insertions(+), 38 deletions(-) create mode 100644 frontend/resources/vue/utils/justificationTemplate.js diff --git a/config.yaml b/config.yaml index d8b503a..b194e5c 100644 --- a/config.yaml +++ b/config.yaml @@ -133,7 +133,7 @@ actions: # Docs: https://docs.olivetin.app/args/input_confirmation.html - title: Delete old backups icon: ashtonished - justification: true + justification: " " shell: rm -rf /opt/oliveTinOldBackups/ && sleep 5 arguments: - type: html diff --git a/frontend/resources/scripts/gen/olivetin/api/v1/olivetin_pb.d.ts b/frontend/resources/scripts/gen/olivetin/api/v1/olivetin_pb.d.ts index 03a2c00..925fe92 100644 --- a/frontend/resources/scripts/gen/olivetin/api/v1/olivetin_pb.d.ts +++ b/frontend/resources/scripts/gen/olivetin/api/v1/olivetin_pb.d.ts @@ -92,9 +92,9 @@ export declare type Action = Message<"olivetin.api.v1.Action"> & { execOnWebhooks: ActionWebhookExecHint[]; /** - * @generated from field: bool justification = 16; + * @generated from field: string justification = 16; */ - justification: boolean; + justification: string; /** * @generated from field: bool has_running_instance = 17; diff --git a/frontend/resources/scripts/gen/olivetin/api/v1/olivetin_pb.js b/frontend/resources/scripts/gen/olivetin/api/v1/olivetin_pb.js index 5e993ca..b9fb551 100644 --- a/frontend/resources/scripts/gen/olivetin/api/v1/olivetin_pb.js +++ b/frontend/resources/scripts/gen/olivetin/api/v1/olivetin_pb.js @@ -8,7 +8,7 @@ import { fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv2 * Describes the file olivetin/api/v1/olivetin.proto. */ export const file_olivetin_api_v1_olivetin = /*@__PURE__*/ - fileDesc("Ch5vbGl2ZXRpbi9hcGkvdjEvb2xpdmV0aW4ucHJvdG8SD29saXZldGluLmFwaS52MSLABAoGQWN0aW9uEhIKCmJpbmRpbmdfaWQYASABKAkSDQoFdGl0bGUYAiABKAkSDAoEaWNvbhgDIAEoCRIQCghjYW5fZXhlYxgEIAEoCBIyCglhcmd1bWVudHMYBSADKAsyHy5vbGl2ZXRpbi5hcGkudjEuQWN0aW9uQXJndW1lbnQSFgoOcG9wdXBfb25fc3RhcnQYBiABKAkSDQoFb3JkZXIYByABKAUSDwoHdGltZW91dBgIIAEoBRIjChtkYXRldGltZV9yYXRlX2xpbWl0X2V4cGlyZXMYCSABKAkSFwoPZXhlY19vbl9zdGFydHVwGAogASgIEhQKDGV4ZWNfb25fY3JvbhgLIAMoCRIjChtleGVjX29uX2ZpbGVfY3JlYXRlZF9pbl9kaXIYDCADKAkSIwobZXhlY19vbl9maWxlX2NoYW5nZWRfaW5fZGlyGA0gAygJEh0KFWV4ZWNfb25fY2FsZW5kYXJfZmlsZRgOIAEoCRJAChBleGVjX29uX3dlYmhvb2tzGA8gAygLMiYub2xpdmV0aW4uYXBpLnYxLkFjdGlvbldlYmhvb2tFeGVjSGludBIVCg1qdXN0aWZpY2F0aW9uGBAgASgIEhwKFGhhc19ydW5uaW5nX2luc3RhbmNlGBEgASgIEhsKE2hhc19xdWV1ZWRfaW5zdGFuY2UYEiABKAgSNgoGZ3JvdXBzGBMgAygLMiYub2xpdmV0aW4uYXBpLnYxLkFjdGlvbkdyb3VwTWVtYmVyc2hpcCJRChVBY3Rpb25Hcm91cE1lbWJlcnNoaXASDAoEbmFtZRgBIAEoCRIWCg5tYXhfY29uY3VycmVudBgCIAEoBRISCgpxdWV1ZV9zaXplGAMgASgFIsMCChVBY3Rpb25XZWJob29rRXhlY0hpbnQSEAoIdGVtcGxhdGUYASABKAkSEgoKbWF0Y2hfcGF0aBgCIAEoCRJPCg1tYXRjaF9oZWFkZXJzGAMgAygLMjgub2xpdmV0aW4uYXBpLnYxLkFjdGlvbldlYmhvb2tFeGVjSGludC5NYXRjaEhlYWRlcnNFbnRyeRJLCgttYXRjaF9xdWVyeRgEIAMoCzI2Lm9saXZldGluLmFwaS52MS5BY3Rpb25XZWJob29rRXhlY0hpbnQuTWF0Y2hRdWVyeUVudHJ5GjMKEU1hdGNoSGVhZGVyc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEaMQoPTWF0Y2hRdWVyeUVudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiuwIKDkFjdGlvbkFyZ3VtZW50EgwKBG5hbWUYASABKAkSDQoFdGl0bGUYAiABKAkSDAoEdHlwZRgDIAEoCRIVCg1kZWZhdWx0X3ZhbHVlGAQgASgJEjYKB2Nob2ljZXMYBSADKAsyJS5vbGl2ZXRpbi5hcGkudjEuQWN0aW9uQXJndW1lbnRDaG9pY2USEwoLZGVzY3JpcHRpb24YBiABKAkSRQoLc3VnZ2VzdGlvbnMYByADKAsyMC5vbGl2ZXRpbi5hcGkudjEuQWN0aW9uQXJndW1lbnQuU3VnZ2VzdGlvbnNFbnRyeRIfChdzdWdnZXN0aW9uc19icm93c2VyX2tleRgIIAEoCRoyChBTdWdnZXN0aW9uc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiNAoUQWN0aW9uQXJndW1lbnRDaG9pY2USDQoFdmFsdWUYASABKAkSDQoFdGl0bGUYAiABKAkisgEKBkVudGl0eRINCgV0aXRsZRgBIAEoCRISCgp1bmlxdWVfa2V5GAIgASgJEgwKBHR5cGUYAyABKAkSEwoLZGlyZWN0b3JpZXMYBCADKAkSMwoGZmllbGRzGAUgAygLMiMub2xpdmV0aW4uYXBpLnYxLkVudGl0eS5GaWVsZHNFbnRyeRotCgtGaWVsZHNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBIlQKFEdldERhc2hib2FyZFJlc3BvbnNlEg0KBXRpdGxlGAEgASgJEi0KCWRhc2hib2FyZBgEIAEoCzIaLm9saXZldGluLmFwaS52MS5EYXNoYm9hcmQiXwoPRWZmZWN0aXZlUG9saWN5EhgKEHNob3dfZGlhZ25vc3RpY3MYASABKAgSFQoNc2hvd19sb2dfbGlzdBgCIAEoCBIbChNzaG93X3ZlcnNpb25fbnVtYmVyGAMgASgIIk0KE0dldERhc2hib2FyZFJlcXVlc3QSDQoFdGl0bGUYASABKAkSEwoLZW50aXR5X3R5cGUYAiABKAkSEgoKZW50aXR5X2tleRgDIAEoCSJRCglEYXNoYm9hcmQSDQoFdGl0bGUYASABKAkSNQoIY29udGVudHMYAiADKAsyIy5vbGl2ZXRpbi5hcGkudjEuRGFzaGJvYXJkQ29tcG9uZW50ItsBChJEYXNoYm9hcmRDb21wb25lbnQSDQoFdGl0bGUYASABKAkSDAoEdHlwZRgCIAEoCRI1Cghjb250ZW50cxgDIAMoCzIjLm9saXZldGluLmFwaS52MS5EYXNoYm9hcmRDb21wb25lbnQSDAoEaWNvbhgEIAEoCRIRCgljc3NfY2xhc3MYBSABKAkSJwoGYWN0aW9uGAYgASgLMhcub2xpdmV0aW4uYXBpLnYxLkFjdGlvbhITCgtlbnRpdHlfdHlwZRgHIAEoCRISCgplbnRpdHlfa2V5GAggASgJIpQBChJTdGFydEFjdGlvblJlcXVlc3QSEgoKYmluZGluZ19pZBgBIAEoCRI3Cglhcmd1bWVudHMYAiADKAsyJC5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25Bcmd1bWVudBIaChJ1bmlxdWVfdHJhY2tpbmdfaWQYAyABKAkSFQoNanVzdGlmaWNhdGlvbhgEIAEoCSIyChNTdGFydEFjdGlvbkFyZ3VtZW50EgwKBG5hbWUYASABKAkSDQoFdmFsdWUYAiABKAkiNAoTU3RhcnRBY3Rpb25SZXNwb25zZRIdChVleGVjdXRpb25fdHJhY2tpbmdfaWQYAiABKAkifgoZU3RhcnRBY3Rpb25BbmRXYWl0UmVxdWVzdBIRCglhY3Rpb25faWQYASABKAkSNwoJYXJndW1lbnRzGAIgAygLMiQub2xpdmV0aW4uYXBpLnYxLlN0YXJ0QWN0aW9uQXJndW1lbnQSFQoNanVzdGlmaWNhdGlvbhgDIAEoCSJKChpTdGFydEFjdGlvbkFuZFdhaXRSZXNwb25zZRIsCglsb2dfZW50cnkYASABKAsyGS5vbGl2ZXRpbi5hcGkudjEuTG9nRW50cnkiLAoXU3RhcnRBY3Rpb25CeUdldFJlcXVlc3QSEQoJYWN0aW9uX2lkGAEgASgJIjkKGFN0YXJ0QWN0aW9uQnlHZXRSZXNwb25zZRIdChVleGVjdXRpb25fdHJhY2tpbmdfaWQYAiABKAkiMwoeU3RhcnRBY3Rpb25CeUdldEFuZFdhaXRSZXF1ZXN0EhEKCWFjdGlvbl9pZBgBIAEoCSJPCh9TdGFydEFjdGlvbkJ5R2V0QW5kV2FpdFJlc3BvbnNlEiwKCWxvZ19lbnRyeRgBIAEoCzIZLm9saXZldGluLmFwaS52MS5Mb2dFbnRyeSJeCg5HZXRMb2dzUmVxdWVzdBIUCgxzdGFydF9vZmZzZXQYASABKAMSEwoLZGF0ZV9maWx0ZXIYAiABKAkSEQoJcGFnZV9zaXplGAMgASgDEg4KBmZpbHRlchgEIAEoCSKUBAoITG9nRW50cnkSGAoQZGF0ZXRpbWVfc3RhcnRlZBgBIAEoCRIUCgxhY3Rpb25fdGl0bGUYAiABKAkSDgoGb3V0cHV0GAMgASgJEhEKCXRpbWVkX291dBgFIAEoCBIRCglleGl0X2NvZGUYBiABKAUSDAoEdXNlchgHIAEoCRISCgp1c2VyX2NsYXNzGAggASgJEhMKC2FjdGlvbl9pY29uGAkgASgJEgwKBHRhZ3MYCiADKAkSHQoVZXhlY3V0aW9uX3RyYWNraW5nX2lkGAsgASgJEhkKEWRhdGV0aW1lX2ZpbmlzaGVkGAwgASgJEhkKEWV4ZWN1dGlvbl9zdGFydGVkGA4gASgIEhoKEmV4ZWN1dGlvbl9maW5pc2hlZBgPIAEoCBIPCgdibG9ja2VkGBAgASgIEhYKDmRhdGV0aW1lX2luZGV4GBEgASgDEhAKCGNhbl9raWxsGBIgASgIEiMKG2RhdGV0aW1lX3JhdGVfbGltaXRfZXhwaXJlcxgTIAEoCRISCgpiaW5kaW5nX2lkGBQgASgJEg4KBnF1ZXVlZBgVIAEoCBIYChBxdWV1ZWRfZm9yX2dyb3VwGBYgASgJEhUKDWp1c3RpZmljYXRpb24YFyABKAkSNwoJYXJndW1lbnRzGBggAygLMiQub2xpdmV0aW4uYXBpLnYxLlN0YXJ0QWN0aW9uQXJndW1lbnQikQEKD0dldExvZ3NSZXNwb25zZRInCgRsb2dzGAEgAygLMhkub2xpdmV0aW4uYXBpLnYxLkxvZ0VudHJ5EhcKD2NvdW50X3JlbWFpbmluZxgCIAEoAxIRCglwYWdlX3NpemUYAyABKAMSEwoLdG90YWxfY291bnQYBCABKAMSFAoMc3RhcnRfb2Zmc2V0GAUgASgDIj8KFEdldEFjdGlvbkxvZ3NSZXF1ZXN0EhEKCWFjdGlvbl9pZBgBIAEoCRIUCgxzdGFydF9vZmZzZXQYAiABKAMilwEKFUdldEFjdGlvbkxvZ3NSZXNwb25zZRInCgRsb2dzGAEgAygLMhkub2xpdmV0aW4uYXBpLnYxLkxvZ0VudHJ5EhcKD2NvdW50X3JlbWFpbmluZxgCIAEoAxIRCglwYWdlX3NpemUYAyABKAMSEwoLdG90YWxfY291bnQYBCABKAMSFAoMc3RhcnRfb2Zmc2V0GAUgASgDIhoKGEdldEV4ZWN1dGlvblF1ZXVlUmVxdWVzdCLGAQoURXhlY3V0aW9uUXVldWVBY3Rpb24SEgoKYmluZGluZ19pZBgBIAEoCRIUCgxhY3Rpb25fdGl0bGUYAiABKAkSEwoLYWN0aW9uX2ljb24YAyABKAkSFgoObWF4X2NvbmN1cnJlbnQYBCABKAUSFAoMYWN0aXZlX2NvdW50GAUgASgFEhUKDWVudGl0eV9wcmVmaXgYBiABKAkSKgoHZW50cmllcxgHIAMoCzIZLm9saXZldGluLmFwaS52MS5Mb2dFbnRyeSLBAQoTRXhlY3V0aW9uUXVldWVHcm91cBIMCgRuYW1lGAEgASgJEgwKBGljb24YAiABKAkSFgoObWF4X2NvbmN1cnJlbnQYAyABKAUSFAoMYWN0aXZlX2NvdW50GAQgASgFEjYKB2FjdGlvbnMYBSADKAsyJS5vbGl2ZXRpbi5hcGkudjEuRXhlY3V0aW9uUXVldWVBY3Rpb24SFAoMcXVldWVkX2NvdW50GAYgASgFEhIKCnF1ZXVlX3NpemUYByABKAUiZwoZR2V0RXhlY3V0aW9uUXVldWVSZXNwb25zZRI0CgZncm91cHMYASADKAsyJC5vbGl2ZXRpbi5hcGkudjEuRXhlY3V0aW9uUXVldWVHcm91cBIUCgx0b3RhbF9hY3RpdmUYAiABKAUiZQobVmFsaWRhdGVBcmd1bWVudFR5cGVSZXF1ZXN0Eg0KBXZhbHVlGAEgASgJEgwKBHR5cGUYAiABKAkSEgoKYmluZGluZ19pZBgDIAEoCRIVCg1hcmd1bWVudF9uYW1lGAQgASgJIkIKHFZhbGlkYXRlQXJndW1lbnRUeXBlUmVzcG9uc2USDQoFdmFsaWQYASABKAgSEwoLZGVzY3JpcHRpb24YAiABKAkiNgoVV2F0Y2hFeGVjdXRpb25SZXF1ZXN0Eh0KFWV4ZWN1dGlvbl90cmFja2luZ19pZBgBIAEoCSImChRXYXRjaEV4ZWN1dGlvblVwZGF0ZRIOCgZ1cGRhdGUYASABKAkiSgoWRXhlY3V0aW9uU3RhdHVzUmVxdWVzdBIdChVleGVjdXRpb25fdHJhY2tpbmdfaWQYASABKAkSEQoJYWN0aW9uX2lkGAIgASgJImEKGURhc2hib2FyZE5hdmlnYXRpb25UYXJnZXQSDQoFdGl0bGUYASABKAkSEwoLZW50aXR5X3R5cGUYAiABKAkSEgoKZW50aXR5X2tleRgDIAEoCRIMCgRwYXRoGAQgASgJIo8BChdFeGVjdXRpb25TdGF0dXNSZXNwb25zZRIsCglsb2dfZW50cnkYASABKAsyGS5vbGl2ZXRpbi5hcGkudjEuTG9nRW50cnkSRgoSYmFja190b19kYXNoYm9hcmRzGAIgAygLMioub2xpdmV0aW4uYXBpLnYxLkRhc2hib2FyZE5hdmlnYXRpb25UYXJnZXQiDwoNV2hvQW1JUmVxdWVzdCJsCg5XaG9BbUlSZXNwb25zZRIaChJhdXRoZW50aWNhdGVkX3VzZXIYASABKAkSEQoJdXNlcmdyb3VwGAIgASgJEhAKCHByb3ZpZGVyGAMgASgJEgwKBGFjbHMYBCADKAkSCwoDc2lkGAUgASgJIhIKEFNvc1JlcG9ydFJlcXVlc3QiIgoRU29zUmVwb3J0UmVzcG9uc2USDQoFYWxlcnQYASABKAkiEQoPRHVtcFZhcnNSZXF1ZXN0IpUBChBEdW1wVmFyc1Jlc3BvbnNlEg0KBWFsZXJ0GAEgASgJEkEKCGNvbnRlbnRzGAIgAygLMi8ub2xpdmV0aW4uYXBpLnYxLkR1bXBWYXJzUmVzcG9uc2UuQ29udGVudHNFbnRyeRovCg1Db250ZW50c0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiOwoMRGVidWdCaW5kaW5nEhQKDGFjdGlvbl90aXRsZRgBIAEoCRIVCg1lbnRpdHlfcHJlZml4GAIgASgJIh4KHER1bXBQdWJsaWNJZEFjdGlvbk1hcFJlcXVlc3QizgEKHUR1bXBQdWJsaWNJZEFjdGlvbk1hcFJlc3BvbnNlEg0KBWFsZXJ0GAEgASgJEk4KCGNvbnRlbnRzGAIgAygLMjwub2xpdmV0aW4uYXBpLnYxLkR1bXBQdWJsaWNJZEFjdGlvbk1hcFJlc3BvbnNlLkNvbnRlbnRzRW50cnkaTgoNQ29udGVudHNFbnRyeRILCgNrZXkYASABKAkSLAoFdmFsdWUYAiABKAsyHS5vbGl2ZXRpbi5hcGkudjEuRGVidWdCaW5kaW5nOgI4ASISChBHZXRSZWFkeXpSZXF1ZXN0IiMKEUdldFJlYWR5elJlc3BvbnNlEg4KBnN0YXR1cxgBIAEoCSIUChJFdmVudFN0cmVhbVJlcXVlc3QimQMKE0V2ZW50U3RyZWFtUmVzcG9uc2USPQoOZW50aXR5X2NoYW5nZWQYAiABKAsyIy5vbGl2ZXRpbi5hcGkudjEuRXZlbnRFbnRpdHlDaGFuZ2VkSAASPQoOY29uZmlnX2NoYW5nZWQYAyABKAsyIy5vbGl2ZXRpbi5hcGkudjEuRXZlbnRDb25maWdDaGFuZ2VkSAASRQoSZXhlY3V0aW9uX2ZpbmlzaGVkGAQgASgLMicub2xpdmV0aW4uYXBpLnYxLkV2ZW50RXhlY3V0aW9uRmluaXNoZWRIABJDChFleGVjdXRpb25fc3RhcnRlZBgFIAEoCzImLm9saXZldGluLmFwaS52MS5FdmVudEV4ZWN1dGlvblN0YXJ0ZWRIABI5CgxvdXRwdXRfY2h1bmsYBiABKAsyIS5vbGl2ZXRpbi5hcGkudjEuRXZlbnRPdXRwdXRDaHVua0gAEjQKCWhlYXJ0YmVhdBgHIAEoCzIfLm9saXZldGluLmFwaS52MS5FdmVudEhlYXJ0YmVhdEgAQgcKBWV2ZW50IkEKEEV2ZW50T3V0cHV0Q2h1bmsSHQoVZXhlY3V0aW9uX3RyYWNraW5nX2lkGAEgASgJEg4KBm91dHB1dBgCIAEoCSIUChJFdmVudEVudGl0eUNoYW5nZWQiFAoSRXZlbnRDb25maWdDaGFuZ2VkIhAKDkV2ZW50SGVhcnRiZWF0IkYKFkV2ZW50RXhlY3V0aW9uRmluaXNoZWQSLAoJbG9nX2VudHJ5GAEgASgLMhkub2xpdmV0aW4uYXBpLnYxLkxvZ0VudHJ5IkUKFUV2ZW50RXhlY3V0aW9uU3RhcnRlZBIsCglsb2dfZW50cnkYASABKAsyGS5vbGl2ZXRpbi5hcGkudjEuTG9nRW50cnkiMgoRS2lsbEFjdGlvblJlcXVlc3QSHQoVZXhlY3V0aW9uX3RyYWNraW5nX2lkGAEgASgJIm0KEktpbGxBY3Rpb25SZXNwb25zZRIdChVleGVjdXRpb25fdHJhY2tpbmdfaWQYASABKAkSDgoGa2lsbGVkGAIgASgIEhkKEWFscmVhZHlfY29tcGxldGVkGAMgASgIEg0KBWZvdW5kGAQgASgIIjsKFUxvY2FsVXNlckxvZ2luUmVxdWVzdBIQCgh1c2VybmFtZRgBIAEoCRIQCghwYXNzd29yZBgCIAEoCSIpChZMb2NhbFVzZXJMb2dpblJlc3BvbnNlEg8KB3N1Y2Nlc3MYASABKAgiJwoTUGFzc3dvcmRIYXNoUmVxdWVzdBIQCghwYXNzd29yZBgBIAEoCSIkChRQYXNzd29yZEhhc2hSZXNwb25zZRIMCgRoYXNoGAEgASgJIg8KDUxvZ291dFJlcXVlc3QiEAoOTG9nb3V0UmVzcG9uc2UiFwoVR2V0RGlhZ25vc3RpY3NSZXF1ZXN0IkUKFkdldERpYWdub3N0aWNzUmVzcG9uc2USEwoLU3NoRm91bmRLZXkYASABKAkSFgoOU3NoRm91bmRDb25maWcYAiABKAkiDQoLSW5pdFJlcXVlc3Qi6wUKDEluaXRSZXNwb25zZRISCgpzaG93Rm9vdGVyGAEgASgIEhYKDnNob3dOYXZpZ2F0aW9uGAIgASgIEhcKD3Nob3dOZXdWZXJzaW9ucxgDIAEoCBIYChBhdmFpbGFibGVWZXJzaW9uGAQgASgJEhYKDmN1cnJlbnRWZXJzaW9uGAUgASgJEhEKCXBhZ2VUaXRsZRgGIAEoCRIeChZzZWN0aW9uTmF2aWdhdGlvblN0eWxlGAcgASgJEhoKEmRlZmF1bHRJY29uRm9yQmFjaxgIIAEoCRIWCg5lbmFibGVDdXN0b21KcxgJIAEoCBIUCgxhdXRoTG9naW5VcmwYCiABKAkSFgoOYXV0aExvY2FsTG9naW4YCyABKAgSEQoJc3R5bGVNb2RzGAwgAygJEjgKD29BdXRoMlByb3ZpZGVycxgNIAMoCzIfLm9saXZldGluLmFwaS52MS5PQXV0aDJQcm92aWRlchI4Cg9hZGRpdGlvbmFsTGlua3MYDiADKAsyHy5vbGl2ZXRpbi5hcGkudjEuQWRkaXRpb25hbExpbmsSFgoOcm9vdERhc2hib2FyZHMYDyADKAkSGgoSYXV0aGVudGljYXRlZF91c2VyGBAgASgJEiMKG2F1dGhlbnRpY2F0ZWRfdXNlcl9wcm92aWRlchgRIAEoCRI6ChBlZmZlY3RpdmVfcG9saWN5GBIgASgLMiAub2xpdmV0aW4uYXBpLnYxLkVmZmVjdGl2ZVBvbGljeRIWCg5iYW5uZXJfbWVzc2FnZRgTIAEoCRISCgpiYW5uZXJfY3NzGBQgASgJEhgKEHNob3dfZGlhZ25vc3RpY3MYFSABKAgSFQoNc2hvd19sb2dfbGlzdBgWIAEoCBIWCg5sb2dpbl9yZXF1aXJlZBgXIAEoCBIYChBhdmFpbGFibGVfdGhlbWVzGBggAygJEiQKHHNob3dfbmF2aWdhdGVfb25fc3RhcnRfaWNvbnMYGSABKAgiLAoOQWRkaXRpb25hbExpbmsSDQoFdGl0bGUYASABKAkSCwoDdXJsGAIgASgJIjoKDk9BdXRoMlByb3ZpZGVyEg0KBXRpdGxlGAEgASgJEgwKBGljb24YAyABKAkSCwoDa2V5GAQgASgJIi0KF0dldEFjdGlvbkJpbmRpbmdSZXF1ZXN0EhIKCmJpbmRpbmdfaWQYASABKAkiiwEKGEdldEFjdGlvbkJpbmRpbmdSZXNwb25zZRInCgZhY3Rpb24YASABKAsyFy5vbGl2ZXRpbi5hcGkudjEuQWN0aW9uEkYKEmJhY2tfdG9fZGFzaGJvYXJkcxgCIAMoCzIqLm9saXZldGluLmFwaS52MS5EYXNoYm9hcmROYXZpZ2F0aW9uVGFyZ2V0IhQKEkdldEVudGl0aWVzUmVxdWVzdCJUChNHZXRFbnRpdGllc1Jlc3BvbnNlEj0KEmVudGl0eV9kZWZpbml0aW9ucxgBIAMoCzIhLm9saXZldGluLmFwaS52MS5FbnRpdHlEZWZpbml0aW9uImkKEEVudGl0eURlZmluaXRpb24SDQoFdGl0bGUYASABKAkSKgoJaW5zdGFuY2VzGAIgAygLMhcub2xpdmV0aW4uYXBpLnYxLkVudGl0eRIaChJ1c2VkX29uX2Rhc2hib2FyZHMYAyADKAkiNAoQR2V0RW50aXR5UmVxdWVzdBISCgp1bmlxdWVfa2V5GAEgASgJEgwKBHR5cGUYAiABKAkiNQoUUmVzdGFydEFjdGlvblJlcXVlc3QSHQoVZXhlY3V0aW9uX3RyYWNraW5nX2lkGAEgASgJMtYTChJPbGl2ZVRpbkFwaVNlcnZpY2USXQoMR2V0RGFzaGJvYXJkEiQub2xpdmV0aW4uYXBpLnYxLkdldERhc2hib2FyZFJlcXVlc3QaJS5vbGl2ZXRpbi5hcGkudjEuR2V0RGFzaGJvYXJkUmVzcG9uc2UiABJaCgtTdGFydEFjdGlvbhIjLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvblJlcXVlc3QaJC5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25SZXNwb25zZSIAEm8KElN0YXJ0QWN0aW9uQW5kV2FpdBIqLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkFuZFdhaXRSZXF1ZXN0Gisub2xpdmV0aW4uYXBpLnYxLlN0YXJ0QWN0aW9uQW5kV2FpdFJlc3BvbnNlIgASaQoQU3RhcnRBY3Rpb25CeUdldBIoLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkJ5R2V0UmVxdWVzdBopLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkJ5R2V0UmVzcG9uc2UiABJ+ChdTdGFydEFjdGlvbkJ5R2V0QW5kV2FpdBIvLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkJ5R2V0QW5kV2FpdFJlcXVlc3QaMC5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25CeUdldEFuZFdhaXRSZXNwb25zZSIAEl4KDVJlc3RhcnRBY3Rpb24SJS5vbGl2ZXRpbi5hcGkudjEuUmVzdGFydEFjdGlvblJlcXVlc3QaJC5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25SZXNwb25zZSIAElcKCktpbGxBY3Rpb24SIi5vbGl2ZXRpbi5hcGkudjEuS2lsbEFjdGlvblJlcXVlc3QaIy5vbGl2ZXRpbi5hcGkudjEuS2lsbEFjdGlvblJlc3BvbnNlIgASZgoPRXhlY3V0aW9uU3RhdHVzEicub2xpdmV0aW4uYXBpLnYxLkV4ZWN1dGlvblN0YXR1c1JlcXVlc3QaKC5vbGl2ZXRpbi5hcGkudjEuRXhlY3V0aW9uU3RhdHVzUmVzcG9uc2UiABJOCgdHZXRMb2dzEh8ub2xpdmV0aW4uYXBpLnYxLkdldExvZ3NSZXF1ZXN0GiAub2xpdmV0aW4uYXBpLnYxLkdldExvZ3NSZXNwb25zZSIAEmAKDUdldEFjdGlvbkxvZ3MSJS5vbGl2ZXRpbi5hcGkudjEuR2V0QWN0aW9uTG9nc1JlcXVlc3QaJi5vbGl2ZXRpbi5hcGkudjEuR2V0QWN0aW9uTG9nc1Jlc3BvbnNlIgASbAoRR2V0RXhlY3V0aW9uUXVldWUSKS5vbGl2ZXRpbi5hcGkudjEuR2V0RXhlY3V0aW9uUXVldWVSZXF1ZXN0Gioub2xpdmV0aW4uYXBpLnYxLkdldEV4ZWN1dGlvblF1ZXVlUmVzcG9uc2UiABJ1ChRWYWxpZGF0ZUFyZ3VtZW50VHlwZRIsLm9saXZldGluLmFwaS52MS5WYWxpZGF0ZUFyZ3VtZW50VHlwZVJlcXVlc3QaLS5vbGl2ZXRpbi5hcGkudjEuVmFsaWRhdGVBcmd1bWVudFR5cGVSZXNwb25zZSIAEksKBldob0FtSRIeLm9saXZldGluLmFwaS52MS5XaG9BbUlSZXF1ZXN0Gh8ub2xpdmV0aW4uYXBpLnYxLldob0FtSVJlc3BvbnNlIgASVAoJU29zUmVwb3J0EiEub2xpdmV0aW4uYXBpLnYxLlNvc1JlcG9ydFJlcXVlc3QaIi5vbGl2ZXRpbi5hcGkudjEuU29zUmVwb3J0UmVzcG9uc2UiABJRCghEdW1wVmFycxIgLm9saXZldGluLmFwaS52MS5EdW1wVmFyc1JlcXVlc3QaIS5vbGl2ZXRpbi5hcGkudjEuRHVtcFZhcnNSZXNwb25zZSIAEngKFUR1bXBQdWJsaWNJZEFjdGlvbk1hcBItLm9saXZldGluLmFwaS52MS5EdW1wUHVibGljSWRBY3Rpb25NYXBSZXF1ZXN0Gi4ub2xpdmV0aW4uYXBpLnYxLkR1bXBQdWJsaWNJZEFjdGlvbk1hcFJlc3BvbnNlIgASVAoJR2V0UmVhZHl6EiEub2xpdmV0aW4uYXBpLnYxLkdldFJlYWR5elJlcXVlc3QaIi5vbGl2ZXRpbi5hcGkudjEuR2V0UmVhZHl6UmVzcG9uc2UiABJjCg5Mb2NhbFVzZXJMb2dpbhImLm9saXZldGluLmFwaS52MS5Mb2NhbFVzZXJMb2dpblJlcXVlc3QaJy5vbGl2ZXRpbi5hcGkudjEuTG9jYWxVc2VyTG9naW5SZXNwb25zZSIAEl0KDFBhc3N3b3JkSGFzaBIkLm9saXZldGluLmFwaS52MS5QYXNzd29yZEhhc2hSZXF1ZXN0GiUub2xpdmV0aW4uYXBpLnYxLlBhc3N3b3JkSGFzaFJlc3BvbnNlIgASSwoGTG9nb3V0Eh4ub2xpdmV0aW4uYXBpLnYxLkxvZ291dFJlcXVlc3QaHy5vbGl2ZXRpbi5hcGkudjEuTG9nb3V0UmVzcG9uc2UiABJcCgtFdmVudFN0cmVhbRIjLm9saXZldGluLmFwaS52MS5FdmVudFN0cmVhbVJlcXVlc3QaJC5vbGl2ZXRpbi5hcGkudjEuRXZlbnRTdHJlYW1SZXNwb25zZSIAMAESYwoOR2V0RGlhZ25vc3RpY3MSJi5vbGl2ZXRpbi5hcGkudjEuR2V0RGlhZ25vc3RpY3NSZXF1ZXN0Gicub2xpdmV0aW4uYXBpLnYxLkdldERpYWdub3N0aWNzUmVzcG9uc2UiABJFCgRJbml0Ehwub2xpdmV0aW4uYXBpLnYxLkluaXRSZXF1ZXN0Gh0ub2xpdmV0aW4uYXBpLnYxLkluaXRSZXNwb25zZSIAEmkKEEdldEFjdGlvbkJpbmRpbmcSKC5vbGl2ZXRpbi5hcGkudjEuR2V0QWN0aW9uQmluZGluZ1JlcXVlc3QaKS5vbGl2ZXRpbi5hcGkudjEuR2V0QWN0aW9uQmluZGluZ1Jlc3BvbnNlIgASWgoLR2V0RW50aXRpZXMSIy5vbGl2ZXRpbi5hcGkudjEuR2V0RW50aXRpZXNSZXF1ZXN0GiQub2xpdmV0aW4uYXBpLnYxLkdldEVudGl0aWVzUmVzcG9uc2UiABJJCglHZXRFbnRpdHkSIS5vbGl2ZXRpbi5hcGkudjEuR2V0RW50aXR5UmVxdWVzdBoXLm9saXZldGluLmFwaS52MS5FbnRpdHkiAEI4WjZnaXRodWIuY29tL09saXZlVGluL09saXZlVGluL2dlbi9vbGl2ZXRpbi9hcGkvdjE7YXBpdjFiBnByb3RvMw"); + fileDesc("Ch5vbGl2ZXRpbi9hcGkvdjEvb2xpdmV0aW4ucHJvdG8SD29saXZldGluLmFwaS52MSLABAoGQWN0aW9uEhIKCmJpbmRpbmdfaWQYASABKAkSDQoFdGl0bGUYAiABKAkSDAoEaWNvbhgDIAEoCRIQCghjYW5fZXhlYxgEIAEoCBIyCglhcmd1bWVudHMYBSADKAsyHy5vbGl2ZXRpbi5hcGkudjEuQWN0aW9uQXJndW1lbnQSFgoOcG9wdXBfb25fc3RhcnQYBiABKAkSDQoFb3JkZXIYByABKAUSDwoHdGltZW91dBgIIAEoBRIjChtkYXRldGltZV9yYXRlX2xpbWl0X2V4cGlyZXMYCSABKAkSFwoPZXhlY19vbl9zdGFydHVwGAogASgIEhQKDGV4ZWNfb25fY3JvbhgLIAMoCRIjChtleGVjX29uX2ZpbGVfY3JlYXRlZF9pbl9kaXIYDCADKAkSIwobZXhlY19vbl9maWxlX2NoYW5nZWRfaW5fZGlyGA0gAygJEh0KFWV4ZWNfb25fY2FsZW5kYXJfZmlsZRgOIAEoCRJAChBleGVjX29uX3dlYmhvb2tzGA8gAygLMiYub2xpdmV0aW4uYXBpLnYxLkFjdGlvbldlYmhvb2tFeGVjSGludBIVCg1qdXN0aWZpY2F0aW9uGBAgASgJEhwKFGhhc19ydW5uaW5nX2luc3RhbmNlGBEgASgIEhsKE2hhc19xdWV1ZWRfaW5zdGFuY2UYEiABKAgSNgoGZ3JvdXBzGBMgAygLMiYub2xpdmV0aW4uYXBpLnYxLkFjdGlvbkdyb3VwTWVtYmVyc2hpcCJRChVBY3Rpb25Hcm91cE1lbWJlcnNoaXASDAoEbmFtZRgBIAEoCRIWCg5tYXhfY29uY3VycmVudBgCIAEoBRISCgpxdWV1ZV9zaXplGAMgASgFIsMCChVBY3Rpb25XZWJob29rRXhlY0hpbnQSEAoIdGVtcGxhdGUYASABKAkSEgoKbWF0Y2hfcGF0aBgCIAEoCRJPCg1tYXRjaF9oZWFkZXJzGAMgAygLMjgub2xpdmV0aW4uYXBpLnYxLkFjdGlvbldlYmhvb2tFeGVjSGludC5NYXRjaEhlYWRlcnNFbnRyeRJLCgttYXRjaF9xdWVyeRgEIAMoCzI2Lm9saXZldGluLmFwaS52MS5BY3Rpb25XZWJob29rRXhlY0hpbnQuTWF0Y2hRdWVyeUVudHJ5GjMKEU1hdGNoSGVhZGVyc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEaMQoPTWF0Y2hRdWVyeUVudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiuwIKDkFjdGlvbkFyZ3VtZW50EgwKBG5hbWUYASABKAkSDQoFdGl0bGUYAiABKAkSDAoEdHlwZRgDIAEoCRIVCg1kZWZhdWx0X3ZhbHVlGAQgASgJEjYKB2Nob2ljZXMYBSADKAsyJS5vbGl2ZXRpbi5hcGkudjEuQWN0aW9uQXJndW1lbnRDaG9pY2USEwoLZGVzY3JpcHRpb24YBiABKAkSRQoLc3VnZ2VzdGlvbnMYByADKAsyMC5vbGl2ZXRpbi5hcGkudjEuQWN0aW9uQXJndW1lbnQuU3VnZ2VzdGlvbnNFbnRyeRIfChdzdWdnZXN0aW9uc19icm93c2VyX2tleRgIIAEoCRoyChBTdWdnZXN0aW9uc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiNAoUQWN0aW9uQXJndW1lbnRDaG9pY2USDQoFdmFsdWUYASABKAkSDQoFdGl0bGUYAiABKAkisgEKBkVudGl0eRINCgV0aXRsZRgBIAEoCRISCgp1bmlxdWVfa2V5GAIgASgJEgwKBHR5cGUYAyABKAkSEwoLZGlyZWN0b3JpZXMYBCADKAkSMwoGZmllbGRzGAUgAygLMiMub2xpdmV0aW4uYXBpLnYxLkVudGl0eS5GaWVsZHNFbnRyeRotCgtGaWVsZHNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBIlQKFEdldERhc2hib2FyZFJlc3BvbnNlEg0KBXRpdGxlGAEgASgJEi0KCWRhc2hib2FyZBgEIAEoCzIaLm9saXZldGluLmFwaS52MS5EYXNoYm9hcmQiXwoPRWZmZWN0aXZlUG9saWN5EhgKEHNob3dfZGlhZ25vc3RpY3MYASABKAgSFQoNc2hvd19sb2dfbGlzdBgCIAEoCBIbChNzaG93X3ZlcnNpb25fbnVtYmVyGAMgASgIIk0KE0dldERhc2hib2FyZFJlcXVlc3QSDQoFdGl0bGUYASABKAkSEwoLZW50aXR5X3R5cGUYAiABKAkSEgoKZW50aXR5X2tleRgDIAEoCSJRCglEYXNoYm9hcmQSDQoFdGl0bGUYASABKAkSNQoIY29udGVudHMYAiADKAsyIy5vbGl2ZXRpbi5hcGkudjEuRGFzaGJvYXJkQ29tcG9uZW50ItsBChJEYXNoYm9hcmRDb21wb25lbnQSDQoFdGl0bGUYASABKAkSDAoEdHlwZRgCIAEoCRI1Cghjb250ZW50cxgDIAMoCzIjLm9saXZldGluLmFwaS52MS5EYXNoYm9hcmRDb21wb25lbnQSDAoEaWNvbhgEIAEoCRIRCgljc3NfY2xhc3MYBSABKAkSJwoGYWN0aW9uGAYgASgLMhcub2xpdmV0aW4uYXBpLnYxLkFjdGlvbhITCgtlbnRpdHlfdHlwZRgHIAEoCRISCgplbnRpdHlfa2V5GAggASgJIpQBChJTdGFydEFjdGlvblJlcXVlc3QSEgoKYmluZGluZ19pZBgBIAEoCRI3Cglhcmd1bWVudHMYAiADKAsyJC5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25Bcmd1bWVudBIaChJ1bmlxdWVfdHJhY2tpbmdfaWQYAyABKAkSFQoNanVzdGlmaWNhdGlvbhgEIAEoCSIyChNTdGFydEFjdGlvbkFyZ3VtZW50EgwKBG5hbWUYASABKAkSDQoFdmFsdWUYAiABKAkiNAoTU3RhcnRBY3Rpb25SZXNwb25zZRIdChVleGVjdXRpb25fdHJhY2tpbmdfaWQYAiABKAkifgoZU3RhcnRBY3Rpb25BbmRXYWl0UmVxdWVzdBIRCglhY3Rpb25faWQYASABKAkSNwoJYXJndW1lbnRzGAIgAygLMiQub2xpdmV0aW4uYXBpLnYxLlN0YXJ0QWN0aW9uQXJndW1lbnQSFQoNanVzdGlmaWNhdGlvbhgDIAEoCSJKChpTdGFydEFjdGlvbkFuZFdhaXRSZXNwb25zZRIsCglsb2dfZW50cnkYASABKAsyGS5vbGl2ZXRpbi5hcGkudjEuTG9nRW50cnkiLAoXU3RhcnRBY3Rpb25CeUdldFJlcXVlc3QSEQoJYWN0aW9uX2lkGAEgASgJIjkKGFN0YXJ0QWN0aW9uQnlHZXRSZXNwb25zZRIdChVleGVjdXRpb25fdHJhY2tpbmdfaWQYAiABKAkiMwoeU3RhcnRBY3Rpb25CeUdldEFuZFdhaXRSZXF1ZXN0EhEKCWFjdGlvbl9pZBgBIAEoCSJPCh9TdGFydEFjdGlvbkJ5R2V0QW5kV2FpdFJlc3BvbnNlEiwKCWxvZ19lbnRyeRgBIAEoCzIZLm9saXZldGluLmFwaS52MS5Mb2dFbnRyeSJeCg5HZXRMb2dzUmVxdWVzdBIUCgxzdGFydF9vZmZzZXQYASABKAMSEwoLZGF0ZV9maWx0ZXIYAiABKAkSEQoJcGFnZV9zaXplGAMgASgDEg4KBmZpbHRlchgEIAEoCSKUBAoITG9nRW50cnkSGAoQZGF0ZXRpbWVfc3RhcnRlZBgBIAEoCRIUCgxhY3Rpb25fdGl0bGUYAiABKAkSDgoGb3V0cHV0GAMgASgJEhEKCXRpbWVkX291dBgFIAEoCBIRCglleGl0X2NvZGUYBiABKAUSDAoEdXNlchgHIAEoCRISCgp1c2VyX2NsYXNzGAggASgJEhMKC2FjdGlvbl9pY29uGAkgASgJEgwKBHRhZ3MYCiADKAkSHQoVZXhlY3V0aW9uX3RyYWNraW5nX2lkGAsgASgJEhkKEWRhdGV0aW1lX2ZpbmlzaGVkGAwgASgJEhkKEWV4ZWN1dGlvbl9zdGFydGVkGA4gASgIEhoKEmV4ZWN1dGlvbl9maW5pc2hlZBgPIAEoCBIPCgdibG9ja2VkGBAgASgIEhYKDmRhdGV0aW1lX2luZGV4GBEgASgDEhAKCGNhbl9raWxsGBIgASgIEiMKG2RhdGV0aW1lX3JhdGVfbGltaXRfZXhwaXJlcxgTIAEoCRISCgpiaW5kaW5nX2lkGBQgASgJEg4KBnF1ZXVlZBgVIAEoCBIYChBxdWV1ZWRfZm9yX2dyb3VwGBYgASgJEhUKDWp1c3RpZmljYXRpb24YFyABKAkSNwoJYXJndW1lbnRzGBggAygLMiQub2xpdmV0aW4uYXBpLnYxLlN0YXJ0QWN0aW9uQXJndW1lbnQikQEKD0dldExvZ3NSZXNwb25zZRInCgRsb2dzGAEgAygLMhkub2xpdmV0aW4uYXBpLnYxLkxvZ0VudHJ5EhcKD2NvdW50X3JlbWFpbmluZxgCIAEoAxIRCglwYWdlX3NpemUYAyABKAMSEwoLdG90YWxfY291bnQYBCABKAMSFAoMc3RhcnRfb2Zmc2V0GAUgASgDIj8KFEdldEFjdGlvbkxvZ3NSZXF1ZXN0EhEKCWFjdGlvbl9pZBgBIAEoCRIUCgxzdGFydF9vZmZzZXQYAiABKAMilwEKFUdldEFjdGlvbkxvZ3NSZXNwb25zZRInCgRsb2dzGAEgAygLMhkub2xpdmV0aW4uYXBpLnYxLkxvZ0VudHJ5EhcKD2NvdW50X3JlbWFpbmluZxgCIAEoAxIRCglwYWdlX3NpemUYAyABKAMSEwoLdG90YWxfY291bnQYBCABKAMSFAoMc3RhcnRfb2Zmc2V0GAUgASgDIhoKGEdldEV4ZWN1dGlvblF1ZXVlUmVxdWVzdCLGAQoURXhlY3V0aW9uUXVldWVBY3Rpb24SEgoKYmluZGluZ19pZBgBIAEoCRIUCgxhY3Rpb25fdGl0bGUYAiABKAkSEwoLYWN0aW9uX2ljb24YAyABKAkSFgoObWF4X2NvbmN1cnJlbnQYBCABKAUSFAoMYWN0aXZlX2NvdW50GAUgASgFEhUKDWVudGl0eV9wcmVmaXgYBiABKAkSKgoHZW50cmllcxgHIAMoCzIZLm9saXZldGluLmFwaS52MS5Mb2dFbnRyeSLBAQoTRXhlY3V0aW9uUXVldWVHcm91cBIMCgRuYW1lGAEgASgJEgwKBGljb24YAiABKAkSFgoObWF4X2NvbmN1cnJlbnQYAyABKAUSFAoMYWN0aXZlX2NvdW50GAQgASgFEjYKB2FjdGlvbnMYBSADKAsyJS5vbGl2ZXRpbi5hcGkudjEuRXhlY3V0aW9uUXVldWVBY3Rpb24SFAoMcXVldWVkX2NvdW50GAYgASgFEhIKCnF1ZXVlX3NpemUYByABKAUiZwoZR2V0RXhlY3V0aW9uUXVldWVSZXNwb25zZRI0CgZncm91cHMYASADKAsyJC5vbGl2ZXRpbi5hcGkudjEuRXhlY3V0aW9uUXVldWVHcm91cBIUCgx0b3RhbF9hY3RpdmUYAiABKAUiZQobVmFsaWRhdGVBcmd1bWVudFR5cGVSZXF1ZXN0Eg0KBXZhbHVlGAEgASgJEgwKBHR5cGUYAiABKAkSEgoKYmluZGluZ19pZBgDIAEoCRIVCg1hcmd1bWVudF9uYW1lGAQgASgJIkIKHFZhbGlkYXRlQXJndW1lbnRUeXBlUmVzcG9uc2USDQoFdmFsaWQYASABKAgSEwoLZGVzY3JpcHRpb24YAiABKAkiNgoVV2F0Y2hFeGVjdXRpb25SZXF1ZXN0Eh0KFWV4ZWN1dGlvbl90cmFja2luZ19pZBgBIAEoCSImChRXYXRjaEV4ZWN1dGlvblVwZGF0ZRIOCgZ1cGRhdGUYASABKAkiSgoWRXhlY3V0aW9uU3RhdHVzUmVxdWVzdBIdChVleGVjdXRpb25fdHJhY2tpbmdfaWQYASABKAkSEQoJYWN0aW9uX2lkGAIgASgJImEKGURhc2hib2FyZE5hdmlnYXRpb25UYXJnZXQSDQoFdGl0bGUYASABKAkSEwoLZW50aXR5X3R5cGUYAiABKAkSEgoKZW50aXR5X2tleRgDIAEoCRIMCgRwYXRoGAQgASgJIo8BChdFeGVjdXRpb25TdGF0dXNSZXNwb25zZRIsCglsb2dfZW50cnkYASABKAsyGS5vbGl2ZXRpbi5hcGkudjEuTG9nRW50cnkSRgoSYmFja190b19kYXNoYm9hcmRzGAIgAygLMioub2xpdmV0aW4uYXBpLnYxLkRhc2hib2FyZE5hdmlnYXRpb25UYXJnZXQiDwoNV2hvQW1JUmVxdWVzdCJsCg5XaG9BbUlSZXNwb25zZRIaChJhdXRoZW50aWNhdGVkX3VzZXIYASABKAkSEQoJdXNlcmdyb3VwGAIgASgJEhAKCHByb3ZpZGVyGAMgASgJEgwKBGFjbHMYBCADKAkSCwoDc2lkGAUgASgJIhIKEFNvc1JlcG9ydFJlcXVlc3QiIgoRU29zUmVwb3J0UmVzcG9uc2USDQoFYWxlcnQYASABKAkiEQoPRHVtcFZhcnNSZXF1ZXN0IpUBChBEdW1wVmFyc1Jlc3BvbnNlEg0KBWFsZXJ0GAEgASgJEkEKCGNvbnRlbnRzGAIgAygLMi8ub2xpdmV0aW4uYXBpLnYxLkR1bXBWYXJzUmVzcG9uc2UuQ29udGVudHNFbnRyeRovCg1Db250ZW50c0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiOwoMRGVidWdCaW5kaW5nEhQKDGFjdGlvbl90aXRsZRgBIAEoCRIVCg1lbnRpdHlfcHJlZml4GAIgASgJIh4KHER1bXBQdWJsaWNJZEFjdGlvbk1hcFJlcXVlc3QizgEKHUR1bXBQdWJsaWNJZEFjdGlvbk1hcFJlc3BvbnNlEg0KBWFsZXJ0GAEgASgJEk4KCGNvbnRlbnRzGAIgAygLMjwub2xpdmV0aW4uYXBpLnYxLkR1bXBQdWJsaWNJZEFjdGlvbk1hcFJlc3BvbnNlLkNvbnRlbnRzRW50cnkaTgoNQ29udGVudHNFbnRyeRILCgNrZXkYASABKAkSLAoFdmFsdWUYAiABKAsyHS5vbGl2ZXRpbi5hcGkudjEuRGVidWdCaW5kaW5nOgI4ASISChBHZXRSZWFkeXpSZXF1ZXN0IiMKEUdldFJlYWR5elJlc3BvbnNlEg4KBnN0YXR1cxgBIAEoCSIUChJFdmVudFN0cmVhbVJlcXVlc3QimQMKE0V2ZW50U3RyZWFtUmVzcG9uc2USPQoOZW50aXR5X2NoYW5nZWQYAiABKAsyIy5vbGl2ZXRpbi5hcGkudjEuRXZlbnRFbnRpdHlDaGFuZ2VkSAASPQoOY29uZmlnX2NoYW5nZWQYAyABKAsyIy5vbGl2ZXRpbi5hcGkudjEuRXZlbnRDb25maWdDaGFuZ2VkSAASRQoSZXhlY3V0aW9uX2ZpbmlzaGVkGAQgASgLMicub2xpdmV0aW4uYXBpLnYxLkV2ZW50RXhlY3V0aW9uRmluaXNoZWRIABJDChFleGVjdXRpb25fc3RhcnRlZBgFIAEoCzImLm9saXZldGluLmFwaS52MS5FdmVudEV4ZWN1dGlvblN0YXJ0ZWRIABI5CgxvdXRwdXRfY2h1bmsYBiABKAsyIS5vbGl2ZXRpbi5hcGkudjEuRXZlbnRPdXRwdXRDaHVua0gAEjQKCWhlYXJ0YmVhdBgHIAEoCzIfLm9saXZldGluLmFwaS52MS5FdmVudEhlYXJ0YmVhdEgAQgcKBWV2ZW50IkEKEEV2ZW50T3V0cHV0Q2h1bmsSHQoVZXhlY3V0aW9uX3RyYWNraW5nX2lkGAEgASgJEg4KBm91dHB1dBgCIAEoCSIUChJFdmVudEVudGl0eUNoYW5nZWQiFAoSRXZlbnRDb25maWdDaGFuZ2VkIhAKDkV2ZW50SGVhcnRiZWF0IkYKFkV2ZW50RXhlY3V0aW9uRmluaXNoZWQSLAoJbG9nX2VudHJ5GAEgASgLMhkub2xpdmV0aW4uYXBpLnYxLkxvZ0VudHJ5IkUKFUV2ZW50RXhlY3V0aW9uU3RhcnRlZBIsCglsb2dfZW50cnkYASABKAsyGS5vbGl2ZXRpbi5hcGkudjEuTG9nRW50cnkiMgoRS2lsbEFjdGlvblJlcXVlc3QSHQoVZXhlY3V0aW9uX3RyYWNraW5nX2lkGAEgASgJIm0KEktpbGxBY3Rpb25SZXNwb25zZRIdChVleGVjdXRpb25fdHJhY2tpbmdfaWQYASABKAkSDgoGa2lsbGVkGAIgASgIEhkKEWFscmVhZHlfY29tcGxldGVkGAMgASgIEg0KBWZvdW5kGAQgASgIIjsKFUxvY2FsVXNlckxvZ2luUmVxdWVzdBIQCgh1c2VybmFtZRgBIAEoCRIQCghwYXNzd29yZBgCIAEoCSIpChZMb2NhbFVzZXJMb2dpblJlc3BvbnNlEg8KB3N1Y2Nlc3MYASABKAgiJwoTUGFzc3dvcmRIYXNoUmVxdWVzdBIQCghwYXNzd29yZBgBIAEoCSIkChRQYXNzd29yZEhhc2hSZXNwb25zZRIMCgRoYXNoGAEgASgJIg8KDUxvZ291dFJlcXVlc3QiEAoOTG9nb3V0UmVzcG9uc2UiFwoVR2V0RGlhZ25vc3RpY3NSZXF1ZXN0IkUKFkdldERpYWdub3N0aWNzUmVzcG9uc2USEwoLU3NoRm91bmRLZXkYASABKAkSFgoOU3NoRm91bmRDb25maWcYAiABKAkiDQoLSW5pdFJlcXVlc3Qi6wUKDEluaXRSZXNwb25zZRISCgpzaG93Rm9vdGVyGAEgASgIEhYKDnNob3dOYXZpZ2F0aW9uGAIgASgIEhcKD3Nob3dOZXdWZXJzaW9ucxgDIAEoCBIYChBhdmFpbGFibGVWZXJzaW9uGAQgASgJEhYKDmN1cnJlbnRWZXJzaW9uGAUgASgJEhEKCXBhZ2VUaXRsZRgGIAEoCRIeChZzZWN0aW9uTmF2aWdhdGlvblN0eWxlGAcgASgJEhoKEmRlZmF1bHRJY29uRm9yQmFjaxgIIAEoCRIWCg5lbmFibGVDdXN0b21KcxgJIAEoCBIUCgxhdXRoTG9naW5VcmwYCiABKAkSFgoOYXV0aExvY2FsTG9naW4YCyABKAgSEQoJc3R5bGVNb2RzGAwgAygJEjgKD29BdXRoMlByb3ZpZGVycxgNIAMoCzIfLm9saXZldGluLmFwaS52MS5PQXV0aDJQcm92aWRlchI4Cg9hZGRpdGlvbmFsTGlua3MYDiADKAsyHy5vbGl2ZXRpbi5hcGkudjEuQWRkaXRpb25hbExpbmsSFgoOcm9vdERhc2hib2FyZHMYDyADKAkSGgoSYXV0aGVudGljYXRlZF91c2VyGBAgASgJEiMKG2F1dGhlbnRpY2F0ZWRfdXNlcl9wcm92aWRlchgRIAEoCRI6ChBlZmZlY3RpdmVfcG9saWN5GBIgASgLMiAub2xpdmV0aW4uYXBpLnYxLkVmZmVjdGl2ZVBvbGljeRIWCg5iYW5uZXJfbWVzc2FnZRgTIAEoCRISCgpiYW5uZXJfY3NzGBQgASgJEhgKEHNob3dfZGlhZ25vc3RpY3MYFSABKAgSFQoNc2hvd19sb2dfbGlzdBgWIAEoCBIWCg5sb2dpbl9yZXF1aXJlZBgXIAEoCBIYChBhdmFpbGFibGVfdGhlbWVzGBggAygJEiQKHHNob3dfbmF2aWdhdGVfb25fc3RhcnRfaWNvbnMYGSABKAgiLAoOQWRkaXRpb25hbExpbmsSDQoFdGl0bGUYASABKAkSCwoDdXJsGAIgASgJIjoKDk9BdXRoMlByb3ZpZGVyEg0KBXRpdGxlGAEgASgJEgwKBGljb24YAyABKAkSCwoDa2V5GAQgASgJIi0KF0dldEFjdGlvbkJpbmRpbmdSZXF1ZXN0EhIKCmJpbmRpbmdfaWQYASABKAkiiwEKGEdldEFjdGlvbkJpbmRpbmdSZXNwb25zZRInCgZhY3Rpb24YASABKAsyFy5vbGl2ZXRpbi5hcGkudjEuQWN0aW9uEkYKEmJhY2tfdG9fZGFzaGJvYXJkcxgCIAMoCzIqLm9saXZldGluLmFwaS52MS5EYXNoYm9hcmROYXZpZ2F0aW9uVGFyZ2V0IhQKEkdldEVudGl0aWVzUmVxdWVzdCJUChNHZXRFbnRpdGllc1Jlc3BvbnNlEj0KEmVudGl0eV9kZWZpbml0aW9ucxgBIAMoCzIhLm9saXZldGluLmFwaS52MS5FbnRpdHlEZWZpbml0aW9uImkKEEVudGl0eURlZmluaXRpb24SDQoFdGl0bGUYASABKAkSKgoJaW5zdGFuY2VzGAIgAygLMhcub2xpdmV0aW4uYXBpLnYxLkVudGl0eRIaChJ1c2VkX29uX2Rhc2hib2FyZHMYAyADKAkiNAoQR2V0RW50aXR5UmVxdWVzdBISCgp1bmlxdWVfa2V5GAEgASgJEgwKBHR5cGUYAiABKAkiNQoUUmVzdGFydEFjdGlvblJlcXVlc3QSHQoVZXhlY3V0aW9uX3RyYWNraW5nX2lkGAEgASgJMtYTChJPbGl2ZVRpbkFwaVNlcnZpY2USXQoMR2V0RGFzaGJvYXJkEiQub2xpdmV0aW4uYXBpLnYxLkdldERhc2hib2FyZFJlcXVlc3QaJS5vbGl2ZXRpbi5hcGkudjEuR2V0RGFzaGJvYXJkUmVzcG9uc2UiABJaCgtTdGFydEFjdGlvbhIjLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvblJlcXVlc3QaJC5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25SZXNwb25zZSIAEm8KElN0YXJ0QWN0aW9uQW5kV2FpdBIqLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkFuZFdhaXRSZXF1ZXN0Gisub2xpdmV0aW4uYXBpLnYxLlN0YXJ0QWN0aW9uQW5kV2FpdFJlc3BvbnNlIgASaQoQU3RhcnRBY3Rpb25CeUdldBIoLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkJ5R2V0UmVxdWVzdBopLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkJ5R2V0UmVzcG9uc2UiABJ+ChdTdGFydEFjdGlvbkJ5R2V0QW5kV2FpdBIvLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkJ5R2V0QW5kV2FpdFJlcXVlc3QaMC5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25CeUdldEFuZFdhaXRSZXNwb25zZSIAEl4KDVJlc3RhcnRBY3Rpb24SJS5vbGl2ZXRpbi5hcGkudjEuUmVzdGFydEFjdGlvblJlcXVlc3QaJC5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25SZXNwb25zZSIAElcKCktpbGxBY3Rpb24SIi5vbGl2ZXRpbi5hcGkudjEuS2lsbEFjdGlvblJlcXVlc3QaIy5vbGl2ZXRpbi5hcGkudjEuS2lsbEFjdGlvblJlc3BvbnNlIgASZgoPRXhlY3V0aW9uU3RhdHVzEicub2xpdmV0aW4uYXBpLnYxLkV4ZWN1dGlvblN0YXR1c1JlcXVlc3QaKC5vbGl2ZXRpbi5hcGkudjEuRXhlY3V0aW9uU3RhdHVzUmVzcG9uc2UiABJOCgdHZXRMb2dzEh8ub2xpdmV0aW4uYXBpLnYxLkdldExvZ3NSZXF1ZXN0GiAub2xpdmV0aW4uYXBpLnYxLkdldExvZ3NSZXNwb25zZSIAEmAKDUdldEFjdGlvbkxvZ3MSJS5vbGl2ZXRpbi5hcGkudjEuR2V0QWN0aW9uTG9nc1JlcXVlc3QaJi5vbGl2ZXRpbi5hcGkudjEuR2V0QWN0aW9uTG9nc1Jlc3BvbnNlIgASbAoRR2V0RXhlY3V0aW9uUXVldWUSKS5vbGl2ZXRpbi5hcGkudjEuR2V0RXhlY3V0aW9uUXVldWVSZXF1ZXN0Gioub2xpdmV0aW4uYXBpLnYxLkdldEV4ZWN1dGlvblF1ZXVlUmVzcG9uc2UiABJ1ChRWYWxpZGF0ZUFyZ3VtZW50VHlwZRIsLm9saXZldGluLmFwaS52MS5WYWxpZGF0ZUFyZ3VtZW50VHlwZVJlcXVlc3QaLS5vbGl2ZXRpbi5hcGkudjEuVmFsaWRhdGVBcmd1bWVudFR5cGVSZXNwb25zZSIAEksKBldob0FtSRIeLm9saXZldGluLmFwaS52MS5XaG9BbUlSZXF1ZXN0Gh8ub2xpdmV0aW4uYXBpLnYxLldob0FtSVJlc3BvbnNlIgASVAoJU29zUmVwb3J0EiEub2xpdmV0aW4uYXBpLnYxLlNvc1JlcG9ydFJlcXVlc3QaIi5vbGl2ZXRpbi5hcGkudjEuU29zUmVwb3J0UmVzcG9uc2UiABJRCghEdW1wVmFycxIgLm9saXZldGluLmFwaS52MS5EdW1wVmFyc1JlcXVlc3QaIS5vbGl2ZXRpbi5hcGkudjEuRHVtcFZhcnNSZXNwb25zZSIAEngKFUR1bXBQdWJsaWNJZEFjdGlvbk1hcBItLm9saXZldGluLmFwaS52MS5EdW1wUHVibGljSWRBY3Rpb25NYXBSZXF1ZXN0Gi4ub2xpdmV0aW4uYXBpLnYxLkR1bXBQdWJsaWNJZEFjdGlvbk1hcFJlc3BvbnNlIgASVAoJR2V0UmVhZHl6EiEub2xpdmV0aW4uYXBpLnYxLkdldFJlYWR5elJlcXVlc3QaIi5vbGl2ZXRpbi5hcGkudjEuR2V0UmVhZHl6UmVzcG9uc2UiABJjCg5Mb2NhbFVzZXJMb2dpbhImLm9saXZldGluLmFwaS52MS5Mb2NhbFVzZXJMb2dpblJlcXVlc3QaJy5vbGl2ZXRpbi5hcGkudjEuTG9jYWxVc2VyTG9naW5SZXNwb25zZSIAEl0KDFBhc3N3b3JkSGFzaBIkLm9saXZldGluLmFwaS52MS5QYXNzd29yZEhhc2hSZXF1ZXN0GiUub2xpdmV0aW4uYXBpLnYxLlBhc3N3b3JkSGFzaFJlc3BvbnNlIgASSwoGTG9nb3V0Eh4ub2xpdmV0aW4uYXBpLnYxLkxvZ291dFJlcXVlc3QaHy5vbGl2ZXRpbi5hcGkudjEuTG9nb3V0UmVzcG9uc2UiABJcCgtFdmVudFN0cmVhbRIjLm9saXZldGluLmFwaS52MS5FdmVudFN0cmVhbVJlcXVlc3QaJC5vbGl2ZXRpbi5hcGkudjEuRXZlbnRTdHJlYW1SZXNwb25zZSIAMAESYwoOR2V0RGlhZ25vc3RpY3MSJi5vbGl2ZXRpbi5hcGkudjEuR2V0RGlhZ25vc3RpY3NSZXF1ZXN0Gicub2xpdmV0aW4uYXBpLnYxLkdldERpYWdub3N0aWNzUmVzcG9uc2UiABJFCgRJbml0Ehwub2xpdmV0aW4uYXBpLnYxLkluaXRSZXF1ZXN0Gh0ub2xpdmV0aW4uYXBpLnYxLkluaXRSZXNwb25zZSIAEmkKEEdldEFjdGlvbkJpbmRpbmcSKC5vbGl2ZXRpbi5hcGkudjEuR2V0QWN0aW9uQmluZGluZ1JlcXVlc3QaKS5vbGl2ZXRpbi5hcGkudjEuR2V0QWN0aW9uQmluZGluZ1Jlc3BvbnNlIgASWgoLR2V0RW50aXRpZXMSIy5vbGl2ZXRpbi5hcGkudjEuR2V0RW50aXRpZXNSZXF1ZXN0GiQub2xpdmV0aW4uYXBpLnYxLkdldEVudGl0aWVzUmVzcG9uc2UiABJJCglHZXRFbnRpdHkSIS5vbGl2ZXRpbi5hcGkudjEuR2V0RW50aXR5UmVxdWVzdBoXLm9saXZldGluLmFwaS52MS5FbnRpdHkiAEI4WjZnaXRodWIuY29tL09saXZlVGluL09saXZlVGluL2dlbi9vbGl2ZXRpbi9hcGkvdjE7YXBpdjFiBnByb3RvMw"); /** * Describes the message olivetin.api.v1.Action. diff --git a/frontend/resources/vue/utils/justificationTemplate.js b/frontend/resources/vue/utils/justificationTemplate.js new file mode 100644 index 0000000..39cec76 --- /dev/null +++ b/frontend/resources/vue/utils/justificationTemplate.js @@ -0,0 +1,15 @@ +export function applyArgumentTemplate(template, args) { + if (!template) { + return '' + } + + return template.replace(/\{\{\s*([a-zA-Z0-9_]+)\s*\}\}/g, (_, name) => args[name] ?? '') +} + +export function actionRequiresJustification(justification) { + return (justification ?? '').length > 0 +} + +export function actionJustificationTemplate(justification) { + return justification ?? '' +} diff --git a/frontend/resources/vue/utils/needsArgumentForm.js b/frontend/resources/vue/utils/needsArgumentForm.js index 387d5ec..11b61bd 100644 --- a/frontend/resources/vue/utils/needsArgumentForm.js +++ b/frontend/resources/vue/utils/needsArgumentForm.js @@ -1,3 +1,5 @@ +import { actionRequiresJustification } from './justificationTemplate.js' + export function needsArgumentForm (action) { - return (action?.arguments?.length > 0) || action?.justification + return (action?.arguments?.length > 0) || actionRequiresJustification(action?.justification) } diff --git a/frontend/resources/vue/utils/rerunArguments.js b/frontend/resources/vue/utils/rerunArguments.js index 180711b..d214960 100644 --- a/frontend/resources/vue/utils/rerunArguments.js +++ b/frontend/resources/vue/utils/rerunArguments.js @@ -1,4 +1,5 @@ import { needsArgumentForm } from './needsArgumentForm.js' +import { actionRequiresJustification } from './justificationTemplate.js' const nonStorableArgumentTypes = new Set([ 'password', @@ -17,7 +18,7 @@ export function logEntryArgumentsToStartActionArgs (logEntry) { } export function rerunNeedsArgumentForm (action, logEntry) { - if (action?.justification && !logEntry?.justification) { + if (actionRequiresJustification(action?.justification) && !logEntry?.justification) { return true } @@ -60,7 +61,7 @@ export function buildRerunStartActionArgs (bindingId, logEntry, action) { arguments: logEntryArgumentsToStartActionArgs(logEntry) } - if (action?.justification && logEntry?.justification) { + if (actionRequiresJustification(action?.justification) && logEntry?.justification) { startActionArgs.justification = logEntry.justification } diff --git a/frontend/resources/vue/utils/rerunArguments.test.mjs b/frontend/resources/vue/utils/rerunArguments.test.mjs index 1caf5e7..4f34b9f 100644 --- a/frontend/resources/vue/utils/rerunArguments.test.mjs +++ b/frontend/resources/vue/utils/rerunArguments.test.mjs @@ -75,7 +75,7 @@ test('rerunNeedsArgumentForm can start directly when stored args are complete', }) test('rerunNeedsArgumentForm opens the form when justification is missing', () => { - const action = { justification: true, arguments: [] } + const action = { justification: ' ', arguments: [] } assert.equal(rerunNeedsArgumentForm(action, {}), true) assert.equal( @@ -90,7 +90,7 @@ test('buildRerunStartActionArgs includes stored justification', () => { arguments: [{ name: 'host', value: 'db-1' }], justification: 'maintenance window' }, { - justification: true, + justification: ' ', arguments: [{ name: 'host', type: 'ascii_identifier' }] }), { diff --git a/frontend/resources/vue/views/ActionDetailsView.vue b/frontend/resources/vue/views/ActionDetailsView.vue index 4e8738e..15519b4 100644 --- a/frontend/resources/vue/views/ActionDetailsView.vue +++ b/frontend/resources/vue/views/ActionDetailsView.vue @@ -96,7 +96,9 @@ {{ formatExecutionDuration(log) }} - {{ log.executionTrackingId }} + + {{ log.executionTrackingId }} + @@ -134,6 +136,7 @@ import Section from 'picocrank/vue/components/Section.vue' import ActionIconGlyph from '../components/ActionIconGlyph.vue' import ActionStatusDisplay from '../components/ActionStatusDisplay.vue' import ActionGroupLimitsLabel from '../components/ActionGroupLimitsLabel.vue' +import LogActionTitle from '../components/LogActionTitle.vue' import { HugeiconsIcon } from '@hugeicons/vue' import { DashboardSquare01Icon, WorkoutRunIcon } from '@hugeicons/core-free-icons' import { requestReconnectNow } from '../../../js/websocket.js' @@ -162,7 +165,8 @@ const filteredLogs = computed(() => { const searchLower = searchText.value.toLowerCase() return logs.value.filter(log => log.executionTrackingId.toLowerCase().includes(searchLower) || - log.actionTitle.toLowerCase().includes(searchLower) + log.actionTitle.toLowerCase().includes(searchLower) || + (log.justification || '').toLowerCase().includes(searchLower) ) }) diff --git a/proto/olivetin/api/v1/olivetin.proto b/proto/olivetin/api/v1/olivetin.proto index 068ef69..013619d 100644 --- a/proto/olivetin/api/v1/olivetin.proto +++ b/proto/olivetin/api/v1/olivetin.proto @@ -20,7 +20,7 @@ message Action { repeated string exec_on_file_changed_in_dir = 13; string exec_on_calendar_file = 14; repeated ActionWebhookExecHint exec_on_webhooks = 15; - bool justification = 16; + string justification = 16; bool has_running_instance = 17; bool has_queued_instance = 18; repeated ActionGroupMembership groups = 19; diff --git a/service/gen/olivetin/api/v1/olivetin.pb.go b/service/gen/olivetin/api/v1/olivetin.pb.go index 89c4165..f65e732 100644 --- a/service/gen/olivetin/api/v1/olivetin.pb.go +++ b/service/gen/olivetin/api/v1/olivetin.pb.go @@ -38,7 +38,7 @@ type Action struct { ExecOnFileChangedInDir []string `protobuf:"bytes,13,rep,name=exec_on_file_changed_in_dir,json=execOnFileChangedInDir,proto3" json:"exec_on_file_changed_in_dir,omitempty"` ExecOnCalendarFile string `protobuf:"bytes,14,opt,name=exec_on_calendar_file,json=execOnCalendarFile,proto3" json:"exec_on_calendar_file,omitempty"` ExecOnWebhooks []*ActionWebhookExecHint `protobuf:"bytes,15,rep,name=exec_on_webhooks,json=execOnWebhooks,proto3" json:"exec_on_webhooks,omitempty"` - Justification bool `protobuf:"varint,16,opt,name=justification,proto3" json:"justification,omitempty"` + Justification string `protobuf:"bytes,16,opt,name=justification,proto3" json:"justification,omitempty"` HasRunningInstance bool `protobuf:"varint,17,opt,name=has_running_instance,json=hasRunningInstance,proto3" json:"has_running_instance,omitempty"` HasQueuedInstance bool `protobuf:"varint,18,opt,name=has_queued_instance,json=hasQueuedInstance,proto3" json:"has_queued_instance,omitempty"` Groups []*ActionGroupMembership `protobuf:"bytes,19,rep,name=groups,proto3" json:"groups,omitempty"` @@ -181,11 +181,11 @@ func (x *Action) GetExecOnWebhooks() []*ActionWebhookExecHint { return nil } -func (x *Action) GetJustification() bool { +func (x *Action) GetJustification() string { if x != nil { return x.Justification } - return false + return "" } func (x *Action) GetHasRunningInstance() bool { @@ -4595,7 +4595,7 @@ const file_olivetin_api_v1_olivetin_proto_rawDesc = "" + "\x1bexec_on_file_changed_in_dir\x18\r \x03(\tR\x16execOnFileChangedInDir\x121\n" + "\x15exec_on_calendar_file\x18\x0e \x01(\tR\x12execOnCalendarFile\x12P\n" + "\x10exec_on_webhooks\x18\x0f \x03(\v2&.olivetin.api.v1.ActionWebhookExecHintR\x0eexecOnWebhooks\x12$\n" + - "\rjustification\x18\x10 \x01(\bR\rjustification\x120\n" + + "\rjustification\x18\x10 \x01(\tR\rjustification\x120\n" + "\x14has_running_instance\x18\x11 \x01(\bR\x12hasRunningInstance\x12.\n" + "\x13has_queued_instance\x18\x12 \x01(\bR\x11hasQueuedInstance\x12>\n" + "\x06groups\x18\x13 \x03(\v2&.olivetin.api.v1.ActionGroupMembershipR\x06groups\"q\n" + diff --git a/service/internal/api/api.go b/service/internal/api/api.go index 2439a5d..c43a4a6 100644 --- a/service/internal/api/api.go +++ b/service/internal/api/api.go @@ -156,15 +156,17 @@ func (api *oliveTinAPI) StartAction(ctx ctx.Context, req *connect.Request[apiv1. } authenticatedUser := auth.UserFromApiCall(ctx, req, api.cfg) - if err := validateJustificationRequired(pair.Action, req.Msg.Justification, authenticatedUser); err != nil { + args := startActionArgumentsFromProto(req.Msg.Arguments) + justification := resolveStartJustification(pair.Action, pair, req.Msg.Justification, args) + if err := validateJustificationRequired(pair.Action, justification, authenticatedUser); err != nil { return nil, connectInvalidJustification(err) } execReq := executor.ExecutionRequest{ Binding: pair, TrackingID: req.Msg.UniqueTrackingId, - Arguments: startActionArgumentsFromProto(req.Msg.Arguments), - Justification: req.Msg.Justification, + Arguments: args, + Justification: justification, AuthenticatedUser: authenticatedUser, Cfg: api.cfg, } @@ -287,11 +289,13 @@ func (api *oliveTinAPI) StartActionAndWait(ctx ctx.Context, req *connect.Request } user := auth.UserFromApiCall(ctx, req, api.cfg) - if err := validateJustificationRequired(binding.Action, req.Msg.Justification, user); err != nil { + args := startActionArgumentsFromProto(req.Msg.Arguments) + justification := resolveStartJustification(binding.Action, binding, req.Msg.Justification, args) + if err := validateJustificationRequired(binding.Action, justification, user); err != nil { return nil, connectInvalidJustification(err) } - internalLogEntry, ok := api.startActionAndWaitRun(binding, startActionArgumentsFromProto(req.Msg.Arguments), req.Msg.Justification, user) + internalLogEntry, ok := api.startActionAndWaitRun(binding, args, justification, user) if !ok { return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("execution not found")) } diff --git a/service/internal/api/api_justification.go b/service/internal/api/api_justification.go index 6917167..02135f0 100644 --- a/service/internal/api/api_justification.go +++ b/service/internal/api/api_justification.go @@ -9,7 +9,9 @@ import ( apiv1 "github.com/OliveTin/OliveTin/gen/olivetin/api/v1" authpublic "github.com/OliveTin/OliveTin/internal/auth/authpublic" "github.com/OliveTin/OliveTin/internal/config" + "github.com/OliveTin/OliveTin/internal/entities" "github.com/OliveTin/OliveTin/internal/executor" + "github.com/OliveTin/OliveTin/internal/tpl" ) func validateJustificationRequired(action *config.Action, justification string, user *authpublic.AuthenticatedUser) error { @@ -21,7 +23,7 @@ func validateJustificationRequired(action *config.Action, justification string, } func actionRequiresJustificationConfig(action *config.Action) bool { - return action != nil && action.Justification + return action != nil && action.RequiresJustification() } func justificationProvided(justification string, user *authpublic.AuthenticatedUser) bool { @@ -40,6 +42,36 @@ func startActionArgumentsFromProto(args []*apiv1.StartActionArgument) map[string return result } +func resolveStartJustification(action *config.Action, binding *executor.ActionBinding, clientJustification string, args map[string]string) string { + if strings.TrimSpace(clientJustification) != "" { + return clientJustification + } + + return resolveJustificationFromTemplate(action, binding, clientJustification, args) +} + +func resolveJustificationFromTemplate(action *config.Action, binding *executor.ActionBinding, fallback string, args map[string]string) string { + templateText := action.JustificationTemplateText() + if templateText == "" { + return fallback + } + + resolved, err := tpl.ParseTemplateWithActionContext(templateText, bindingEntity(binding), args) + if err != nil { + return fallback + } + + return resolved +} + +func bindingEntity(binding *executor.ActionBinding) *entities.Entity { + if binding == nil { + return nil + } + + return binding.Entity +} + func restartRequiresJustificationError() error { return connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("justification is required for this action; use StartAction with a justification instead")) } diff --git a/service/internal/api/api_justification_test.go b/service/internal/api/api_justification_test.go index 7465e72..fc76f78 100644 --- a/service/internal/api/api_justification_test.go +++ b/service/internal/api/api_justification_test.go @@ -21,7 +21,7 @@ func TestStartActionRequiresJustificationForGuest(t *testing.T) { action := &config.Action{ Title: "Send email", ID: "send_email", - Justification: true, + Justification: " ", Shell: "echo done", } cfg.Actions = append(cfg.Actions, action) @@ -56,12 +56,12 @@ func TestStartActionRequiresJustificationForGuest(t *testing.T) { assert.Equal(t, "New user registration foo@example.com", entry.Justification) } -func TestBuildActionExposesJustificationFlag(t *testing.T) { +func TestBuildActionExposesJustificationTemplate(t *testing.T) { cfg := config.DefaultConfig() action := &config.Action{ Title: "Audited action", ID: "audited", - Justification: true, + Justification: "{{ target }}", Shell: "echo hi", } cfg.Actions = append(cfg.Actions, action) @@ -77,12 +77,98 @@ func TestBuildActionExposesJustificationFlag(t *testing.T) { }) require.NotNil(t, pb) - assert.True(t, pb.Justification) + assert.Equal(t, "{{ target }}", pb.Justification) +} + +func TestBuildActionExposesBlankRequiredJustification(t *testing.T) { + cfg := config.DefaultConfig() + action := &config.Action{ + Title: "Audited action", + ID: "audited", + Justification: " ", + Shell: "echo hi", + } + cfg.Actions = append(cfg.Actions, action) + + ex := executor.DefaultExecutor(cfg) + ex.RebuildActionMap() + binding := ex.FindBindingWithNoEntity(action) + require.NotNil(t, binding) + + pb := buildAction(binding, &DashboardRenderRequest{ + cfg: cfg, + ex: ex, + }) + + require.NotNil(t, pb) + assert.Equal(t, " ", pb.Justification) +} + +func TestResolveStartJustificationUsesTemplateWhenClientValueEmpty(t *testing.T) { + action := &config.Action{ + Justification: "{{ ansible_host }}", + } + binding := &executor.ActionBinding{} + + got := resolveStartJustification(action, binding, "", map[string]string{ + "ansible_host": "192.168.66.8", + }) + assert.Equal(t, "192.168.66.8", got) +} + +func TestResolveStartJustificationPrefersClientValue(t *testing.T) { + action := &config.Action{ + Justification: "{{ ansible_host }}", + } + binding := &executor.ActionBinding{} + + got := resolveStartJustification(action, binding, "manual reason", map[string]string{ + "ansible_host": "192.168.66.8", + }) + assert.Equal(t, "manual reason", got) +} + +func TestStartActionResolvesJustificationTemplateForGuest(t *testing.T) { + cfg := config.DefaultConfig() + action := &config.Action{ + Title: "Run playbook", + ID: "run_playbook", + Justification: "{{ ansible_host }}", + Shell: "echo done", + Arguments: []config.ActionArgument{ + {Name: "ansible_host", Title: "Host"}, + }, + } + cfg.Actions = append(cfg.Actions, action) + + ex := executor.DefaultExecutor(cfg) + ex.RebuildActionMap() + binding := ex.FindBindingWithNoEntity(action) + require.NotNil(t, binding) + + ts, client := getNewTestServerAndClientWithExecutor(cfg, ex) + defer ts.Close() + + resp, err := client.StartAction(context.Background(), connect.NewRequest(&apiv1.StartActionRequest{ + BindingId: binding.ID, + UniqueTrackingId: uuid.NewString(), + Arguments: []*apiv1.StartActionArgument{ + {Name: "ansible_host", Value: "stuffbox"}, + }, + })) + require.NoError(t, err) + require.NotEmpty(t, resp.Msg.ExecutionTrackingId) + + time.Sleep(200 * time.Millisecond) + + entry, ok := ex.GetLog(resp.Msg.ExecutionTrackingId) + require.True(t, ok) + assert.Equal(t, "stuffbox", entry.Justification) } func TestValidateJustificationRequiredAllowsSystemUser(t *testing.T) { cfg := config.DefaultConfig() - action := &config.Action{Title: "Cron job", Justification: true} + action := &config.Action{Title: "Cron job", Justification: " "} err := validateJustificationRequired(action, "", auth.UserFromSystem(cfg, "cron")) require.NoError(t, err) diff --git a/service/internal/api/api_log_arguments.go b/service/internal/api/api_log_arguments.go index b085d03..b33c874 100644 --- a/service/internal/api/api_log_arguments.go +++ b/service/internal/api/api_log_arguments.go @@ -47,7 +47,7 @@ func restartArgumentsIncompleteError() error { } func validateRestartLogEntry(entry *executor.InternalLogEntry) error { - if entry.Binding.Action.Justification && strings.TrimSpace(entry.Justification) == "" { + if entry.Binding.Action.RequiresJustification() && strings.TrimSpace(entry.Justification) == "" { return restartRequiresJustificationError() } diff --git a/service/internal/api/api_log_arguments_test.go b/service/internal/api/api_log_arguments_test.go index 27f34cb..a10afb2 100644 --- a/service/internal/api/api_log_arguments_test.go +++ b/service/internal/api/api_log_arguments_test.go @@ -286,7 +286,7 @@ func TestRestartActionRequiresJustificationWhenMissingFromStoredLog(t *testing.T Title: "Dangerous action", Shell: "echo ok", MaxConcurrent: 1, - Justification: true, + Justification: " ", }, } @@ -319,7 +319,7 @@ func TestRestartActionReusesStoredJustificationViaStartActionPath(t *testing.T) Title: "Dangerous action", Shell: "echo ok", MaxConcurrent: 1, - Justification: true, + Justification: " ", }, } diff --git a/service/internal/config/config.go b/service/internal/config/config.go index 9a05ec7..c994d9d 100644 --- a/service/internal/config/config.go +++ b/service/internal/config/config.go @@ -35,7 +35,19 @@ type Action struct { SaveLogs SaveLogsConfig `koanf:"saveLogs"` EnabledExpression string `koanf:"enabledExpression"` Groups []string `koanf:"groups"` - Justification bool `koanf:"justification"` + Justification string `koanf:"justification"` +} + +func (action *Action) RequiresJustification() bool { + return action != nil && action.Justification != "" +} + +func (action *Action) JustificationTemplateText() string { + if action == nil || !action.RequiresJustification() { + return "" + } + + return action.Justification } // ActionGroup defines shared limits and metadata for a set of actions. diff --git a/service/internal/executor/justification.go b/service/internal/executor/justification.go index 55542c9..1c81f2a 100644 --- a/service/internal/executor/justification.go +++ b/service/internal/executor/justification.go @@ -45,7 +45,7 @@ func ResolveJustification(req *ExecutionRequest) string { } func actionRequiresJustification(req *ExecutionRequest) bool { - return req != nil && req.Binding != nil && req.Binding.Action != nil && req.Binding.Action.Justification + return req != nil && req.Binding != nil && req.Binding.Action != nil && req.Binding.Action.RequiresJustification() } func defaultJustificationForRequest(req *ExecutionRequest) string { diff --git a/service/internal/executor/justification_test.go b/service/internal/executor/justification_test.go index c479760..80fa381 100644 --- a/service/internal/executor/justification_test.go +++ b/service/internal/executor/justification_test.go @@ -11,7 +11,7 @@ import ( func TestResolveJustificationUsesProvidedValue(t *testing.T) { cfg := config.DefaultConfig() - action := &config.Action{Title: "Send email", Justification: true, Shell: "echo hi"} + action := &config.Action{Title: "Send email", Justification: " ", Shell: "echo hi"} cfg.Actions = append(cfg.Actions, action) ex := DefaultExecutor(cfg) ex.RebuildActionMap() @@ -29,7 +29,7 @@ func TestResolveJustificationUsesProvidedValue(t *testing.T) { func TestResolveJustificationCronDefault(t *testing.T) { cfg := config.DefaultConfig() - action := &config.Action{Title: "Nightly backup", Justification: true, Shell: "echo hi"} + action := &config.Action{Title: "Nightly backup", Justification: " ", Shell: "echo hi"} cfg.Actions = append(cfg.Actions, action) ex := DefaultExecutor(cfg) ex.RebuildActionMap() @@ -45,7 +45,7 @@ func TestResolveJustificationCronDefault(t *testing.T) { func TestResolveJustificationStartupDefault(t *testing.T) { cfg := config.DefaultConfig() - action := &config.Action{Title: "Init", Justification: true, Shell: "echo hi"} + action := &config.Action{Title: "Init", Justification: " ", Shell: "echo hi"} cfg.Actions = append(cfg.Actions, action) ex := DefaultExecutor(cfg) ex.RebuildActionMap() @@ -61,7 +61,7 @@ func TestResolveJustificationStartupDefault(t *testing.T) { func TestResolveJustificationWebhookDefault(t *testing.T) { cfg := config.DefaultConfig() - action := &config.Action{Title: "Deploy", Justification: true, Exec: []string{"echo", "deploy"}} + action := &config.Action{Title: "Deploy", Justification: " ", Exec: []string{"echo", "deploy"}} cfg.Actions = append(cfg.Actions, action) ex := DefaultExecutor(cfg) ex.RebuildActionMap() @@ -95,7 +95,7 @@ func TestJustificationNotPassedToShellArgs(t *testing.T) { cfg := config.DefaultConfig() action := &config.Action{ Title: "Echo", - Justification: true, + Justification: " ", Shell: "echo {{ message }}", Arguments: []config.ActionArgument{ {Name: "message", Type: "ascii_sentence"}, diff --git a/var/macos/config.yaml b/var/macos/config.yaml index ea89835..253b4a0 100644 --- a/var/macos/config.yaml +++ b/var/macos/config.yaml @@ -147,7 +147,7 @@ actions: # Using a path under the user's home is more natural on macOS. - title: Delete old backups icon: ashtonished - justification: true + justification: " " shell: rm -rf "$HOME/Backups/old/" arguments: - name: confirm From 58dc804e784f7a18f2784a620b74ae9da2dc99fa Mon Sep 17 00:00:00 2001 From: jamesread Date: Mon, 6 Jul 2026 23:35:04 +0100 Subject: [PATCH 2/2] fix: coderabbit suggestions on the justification --- Makefile | 1 + config.yaml | 1 + .../gen/olivetin/api/v1/olivetin_pb.d.ts | 2 +- .../gen/olivetin/api/v1/olivetin_pb.js | 2 +- frontend/resources/vue/views/ArgumentForm.vue | 14 ++++- integration-tests/Makefile | 8 ++- proto/olivetin/api/v1/olivetin.proto | 3 +- service/gen/olivetin/api/v1/olivetin.pb.go | 8 +-- service/internal/api/api_justification.go | 6 ++ .../internal/api/api_justification_test.go | 8 +-- service/internal/config/config.go | 5 +- service/internal/config/config_reloader.go | 13 +++++ .../config/justification_compat_test.go | 58 +++++++++++++++++++ service/internal/config/sanitize.go | 10 ++++ 14 files changed, 123 insertions(+), 16 deletions(-) create mode 100644 service/internal/config/justification_compat_test.go diff --git a/Makefile b/Makefile index 186b56e..21a08f3 100644 --- a/Makefile +++ b/Makefile @@ -57,6 +57,7 @@ devrun: compile devcontainer: compile podman-image podman-container webui-dist: + $(call delete-files,webui) $(MAKE) -wC frontend dist mv frontend/dist webui diff --git a/config.yaml b/config.yaml index b194e5c..ef3ef38 100644 --- a/config.yaml +++ b/config.yaml @@ -133,6 +133,7 @@ actions: # Docs: https://docs.olivetin.app/args/input_confirmation.html - title: Delete old backups icon: ashtonished + # A single space requires justification with no prefilled template (empty disables it). justification: " " shell: rm -rf /opt/oliveTinOldBackups/ && sleep 5 arguments: diff --git a/frontend/resources/scripts/gen/olivetin/api/v1/olivetin_pb.d.ts b/frontend/resources/scripts/gen/olivetin/api/v1/olivetin_pb.d.ts index 925fe92..5ddda4e 100644 --- a/frontend/resources/scripts/gen/olivetin/api/v1/olivetin_pb.d.ts +++ b/frontend/resources/scripts/gen/olivetin/api/v1/olivetin_pb.d.ts @@ -92,7 +92,7 @@ export declare type Action = Message<"olivetin.api.v1.Action"> & { execOnWebhooks: ActionWebhookExecHint[]; /** - * @generated from field: string justification = 16; + * @generated from field: string justification = 20; */ justification: string; diff --git a/frontend/resources/scripts/gen/olivetin/api/v1/olivetin_pb.js b/frontend/resources/scripts/gen/olivetin/api/v1/olivetin_pb.js index b9fb551..9ff3d6c 100644 --- a/frontend/resources/scripts/gen/olivetin/api/v1/olivetin_pb.js +++ b/frontend/resources/scripts/gen/olivetin/api/v1/olivetin_pb.js @@ -8,7 +8,7 @@ import { fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv2 * Describes the file olivetin/api/v1/olivetin.proto. */ export const file_olivetin_api_v1_olivetin = /*@__PURE__*/ - fileDesc("Ch5vbGl2ZXRpbi9hcGkvdjEvb2xpdmV0aW4ucHJvdG8SD29saXZldGluLmFwaS52MSLABAoGQWN0aW9uEhIKCmJpbmRpbmdfaWQYASABKAkSDQoFdGl0bGUYAiABKAkSDAoEaWNvbhgDIAEoCRIQCghjYW5fZXhlYxgEIAEoCBIyCglhcmd1bWVudHMYBSADKAsyHy5vbGl2ZXRpbi5hcGkudjEuQWN0aW9uQXJndW1lbnQSFgoOcG9wdXBfb25fc3RhcnQYBiABKAkSDQoFb3JkZXIYByABKAUSDwoHdGltZW91dBgIIAEoBRIjChtkYXRldGltZV9yYXRlX2xpbWl0X2V4cGlyZXMYCSABKAkSFwoPZXhlY19vbl9zdGFydHVwGAogASgIEhQKDGV4ZWNfb25fY3JvbhgLIAMoCRIjChtleGVjX29uX2ZpbGVfY3JlYXRlZF9pbl9kaXIYDCADKAkSIwobZXhlY19vbl9maWxlX2NoYW5nZWRfaW5fZGlyGA0gAygJEh0KFWV4ZWNfb25fY2FsZW5kYXJfZmlsZRgOIAEoCRJAChBleGVjX29uX3dlYmhvb2tzGA8gAygLMiYub2xpdmV0aW4uYXBpLnYxLkFjdGlvbldlYmhvb2tFeGVjSGludBIVCg1qdXN0aWZpY2F0aW9uGBAgASgJEhwKFGhhc19ydW5uaW5nX2luc3RhbmNlGBEgASgIEhsKE2hhc19xdWV1ZWRfaW5zdGFuY2UYEiABKAgSNgoGZ3JvdXBzGBMgAygLMiYub2xpdmV0aW4uYXBpLnYxLkFjdGlvbkdyb3VwTWVtYmVyc2hpcCJRChVBY3Rpb25Hcm91cE1lbWJlcnNoaXASDAoEbmFtZRgBIAEoCRIWCg5tYXhfY29uY3VycmVudBgCIAEoBRISCgpxdWV1ZV9zaXplGAMgASgFIsMCChVBY3Rpb25XZWJob29rRXhlY0hpbnQSEAoIdGVtcGxhdGUYASABKAkSEgoKbWF0Y2hfcGF0aBgCIAEoCRJPCg1tYXRjaF9oZWFkZXJzGAMgAygLMjgub2xpdmV0aW4uYXBpLnYxLkFjdGlvbldlYmhvb2tFeGVjSGludC5NYXRjaEhlYWRlcnNFbnRyeRJLCgttYXRjaF9xdWVyeRgEIAMoCzI2Lm9saXZldGluLmFwaS52MS5BY3Rpb25XZWJob29rRXhlY0hpbnQuTWF0Y2hRdWVyeUVudHJ5GjMKEU1hdGNoSGVhZGVyc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEaMQoPTWF0Y2hRdWVyeUVudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiuwIKDkFjdGlvbkFyZ3VtZW50EgwKBG5hbWUYASABKAkSDQoFdGl0bGUYAiABKAkSDAoEdHlwZRgDIAEoCRIVCg1kZWZhdWx0X3ZhbHVlGAQgASgJEjYKB2Nob2ljZXMYBSADKAsyJS5vbGl2ZXRpbi5hcGkudjEuQWN0aW9uQXJndW1lbnRDaG9pY2USEwoLZGVzY3JpcHRpb24YBiABKAkSRQoLc3VnZ2VzdGlvbnMYByADKAsyMC5vbGl2ZXRpbi5hcGkudjEuQWN0aW9uQXJndW1lbnQuU3VnZ2VzdGlvbnNFbnRyeRIfChdzdWdnZXN0aW9uc19icm93c2VyX2tleRgIIAEoCRoyChBTdWdnZXN0aW9uc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiNAoUQWN0aW9uQXJndW1lbnRDaG9pY2USDQoFdmFsdWUYASABKAkSDQoFdGl0bGUYAiABKAkisgEKBkVudGl0eRINCgV0aXRsZRgBIAEoCRISCgp1bmlxdWVfa2V5GAIgASgJEgwKBHR5cGUYAyABKAkSEwoLZGlyZWN0b3JpZXMYBCADKAkSMwoGZmllbGRzGAUgAygLMiMub2xpdmV0aW4uYXBpLnYxLkVudGl0eS5GaWVsZHNFbnRyeRotCgtGaWVsZHNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBIlQKFEdldERhc2hib2FyZFJlc3BvbnNlEg0KBXRpdGxlGAEgASgJEi0KCWRhc2hib2FyZBgEIAEoCzIaLm9saXZldGluLmFwaS52MS5EYXNoYm9hcmQiXwoPRWZmZWN0aXZlUG9saWN5EhgKEHNob3dfZGlhZ25vc3RpY3MYASABKAgSFQoNc2hvd19sb2dfbGlzdBgCIAEoCBIbChNzaG93X3ZlcnNpb25fbnVtYmVyGAMgASgIIk0KE0dldERhc2hib2FyZFJlcXVlc3QSDQoFdGl0bGUYASABKAkSEwoLZW50aXR5X3R5cGUYAiABKAkSEgoKZW50aXR5X2tleRgDIAEoCSJRCglEYXNoYm9hcmQSDQoFdGl0bGUYASABKAkSNQoIY29udGVudHMYAiADKAsyIy5vbGl2ZXRpbi5hcGkudjEuRGFzaGJvYXJkQ29tcG9uZW50ItsBChJEYXNoYm9hcmRDb21wb25lbnQSDQoFdGl0bGUYASABKAkSDAoEdHlwZRgCIAEoCRI1Cghjb250ZW50cxgDIAMoCzIjLm9saXZldGluLmFwaS52MS5EYXNoYm9hcmRDb21wb25lbnQSDAoEaWNvbhgEIAEoCRIRCgljc3NfY2xhc3MYBSABKAkSJwoGYWN0aW9uGAYgASgLMhcub2xpdmV0aW4uYXBpLnYxLkFjdGlvbhITCgtlbnRpdHlfdHlwZRgHIAEoCRISCgplbnRpdHlfa2V5GAggASgJIpQBChJTdGFydEFjdGlvblJlcXVlc3QSEgoKYmluZGluZ19pZBgBIAEoCRI3Cglhcmd1bWVudHMYAiADKAsyJC5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25Bcmd1bWVudBIaChJ1bmlxdWVfdHJhY2tpbmdfaWQYAyABKAkSFQoNanVzdGlmaWNhdGlvbhgEIAEoCSIyChNTdGFydEFjdGlvbkFyZ3VtZW50EgwKBG5hbWUYASABKAkSDQoFdmFsdWUYAiABKAkiNAoTU3RhcnRBY3Rpb25SZXNwb25zZRIdChVleGVjdXRpb25fdHJhY2tpbmdfaWQYAiABKAkifgoZU3RhcnRBY3Rpb25BbmRXYWl0UmVxdWVzdBIRCglhY3Rpb25faWQYASABKAkSNwoJYXJndW1lbnRzGAIgAygLMiQub2xpdmV0aW4uYXBpLnYxLlN0YXJ0QWN0aW9uQXJndW1lbnQSFQoNanVzdGlmaWNhdGlvbhgDIAEoCSJKChpTdGFydEFjdGlvbkFuZFdhaXRSZXNwb25zZRIsCglsb2dfZW50cnkYASABKAsyGS5vbGl2ZXRpbi5hcGkudjEuTG9nRW50cnkiLAoXU3RhcnRBY3Rpb25CeUdldFJlcXVlc3QSEQoJYWN0aW9uX2lkGAEgASgJIjkKGFN0YXJ0QWN0aW9uQnlHZXRSZXNwb25zZRIdChVleGVjdXRpb25fdHJhY2tpbmdfaWQYAiABKAkiMwoeU3RhcnRBY3Rpb25CeUdldEFuZFdhaXRSZXF1ZXN0EhEKCWFjdGlvbl9pZBgBIAEoCSJPCh9TdGFydEFjdGlvbkJ5R2V0QW5kV2FpdFJlc3BvbnNlEiwKCWxvZ19lbnRyeRgBIAEoCzIZLm9saXZldGluLmFwaS52MS5Mb2dFbnRyeSJeCg5HZXRMb2dzUmVxdWVzdBIUCgxzdGFydF9vZmZzZXQYASABKAMSEwoLZGF0ZV9maWx0ZXIYAiABKAkSEQoJcGFnZV9zaXplGAMgASgDEg4KBmZpbHRlchgEIAEoCSKUBAoITG9nRW50cnkSGAoQZGF0ZXRpbWVfc3RhcnRlZBgBIAEoCRIUCgxhY3Rpb25fdGl0bGUYAiABKAkSDgoGb3V0cHV0GAMgASgJEhEKCXRpbWVkX291dBgFIAEoCBIRCglleGl0X2NvZGUYBiABKAUSDAoEdXNlchgHIAEoCRISCgp1c2VyX2NsYXNzGAggASgJEhMKC2FjdGlvbl9pY29uGAkgASgJEgwKBHRhZ3MYCiADKAkSHQoVZXhlY3V0aW9uX3RyYWNraW5nX2lkGAsgASgJEhkKEWRhdGV0aW1lX2ZpbmlzaGVkGAwgASgJEhkKEWV4ZWN1dGlvbl9zdGFydGVkGA4gASgIEhoKEmV4ZWN1dGlvbl9maW5pc2hlZBgPIAEoCBIPCgdibG9ja2VkGBAgASgIEhYKDmRhdGV0aW1lX2luZGV4GBEgASgDEhAKCGNhbl9raWxsGBIgASgIEiMKG2RhdGV0aW1lX3JhdGVfbGltaXRfZXhwaXJlcxgTIAEoCRISCgpiaW5kaW5nX2lkGBQgASgJEg4KBnF1ZXVlZBgVIAEoCBIYChBxdWV1ZWRfZm9yX2dyb3VwGBYgASgJEhUKDWp1c3RpZmljYXRpb24YFyABKAkSNwoJYXJndW1lbnRzGBggAygLMiQub2xpdmV0aW4uYXBpLnYxLlN0YXJ0QWN0aW9uQXJndW1lbnQikQEKD0dldExvZ3NSZXNwb25zZRInCgRsb2dzGAEgAygLMhkub2xpdmV0aW4uYXBpLnYxLkxvZ0VudHJ5EhcKD2NvdW50X3JlbWFpbmluZxgCIAEoAxIRCglwYWdlX3NpemUYAyABKAMSEwoLdG90YWxfY291bnQYBCABKAMSFAoMc3RhcnRfb2Zmc2V0GAUgASgDIj8KFEdldEFjdGlvbkxvZ3NSZXF1ZXN0EhEKCWFjdGlvbl9pZBgBIAEoCRIUCgxzdGFydF9vZmZzZXQYAiABKAMilwEKFUdldEFjdGlvbkxvZ3NSZXNwb25zZRInCgRsb2dzGAEgAygLMhkub2xpdmV0aW4uYXBpLnYxLkxvZ0VudHJ5EhcKD2NvdW50X3JlbWFpbmluZxgCIAEoAxIRCglwYWdlX3NpemUYAyABKAMSEwoLdG90YWxfY291bnQYBCABKAMSFAoMc3RhcnRfb2Zmc2V0GAUgASgDIhoKGEdldEV4ZWN1dGlvblF1ZXVlUmVxdWVzdCLGAQoURXhlY3V0aW9uUXVldWVBY3Rpb24SEgoKYmluZGluZ19pZBgBIAEoCRIUCgxhY3Rpb25fdGl0bGUYAiABKAkSEwoLYWN0aW9uX2ljb24YAyABKAkSFgoObWF4X2NvbmN1cnJlbnQYBCABKAUSFAoMYWN0aXZlX2NvdW50GAUgASgFEhUKDWVudGl0eV9wcmVmaXgYBiABKAkSKgoHZW50cmllcxgHIAMoCzIZLm9saXZldGluLmFwaS52MS5Mb2dFbnRyeSLBAQoTRXhlY3V0aW9uUXVldWVHcm91cBIMCgRuYW1lGAEgASgJEgwKBGljb24YAiABKAkSFgoObWF4X2NvbmN1cnJlbnQYAyABKAUSFAoMYWN0aXZlX2NvdW50GAQgASgFEjYKB2FjdGlvbnMYBSADKAsyJS5vbGl2ZXRpbi5hcGkudjEuRXhlY3V0aW9uUXVldWVBY3Rpb24SFAoMcXVldWVkX2NvdW50GAYgASgFEhIKCnF1ZXVlX3NpemUYByABKAUiZwoZR2V0RXhlY3V0aW9uUXVldWVSZXNwb25zZRI0CgZncm91cHMYASADKAsyJC5vbGl2ZXRpbi5hcGkudjEuRXhlY3V0aW9uUXVldWVHcm91cBIUCgx0b3RhbF9hY3RpdmUYAiABKAUiZQobVmFsaWRhdGVBcmd1bWVudFR5cGVSZXF1ZXN0Eg0KBXZhbHVlGAEgASgJEgwKBHR5cGUYAiABKAkSEgoKYmluZGluZ19pZBgDIAEoCRIVCg1hcmd1bWVudF9uYW1lGAQgASgJIkIKHFZhbGlkYXRlQXJndW1lbnRUeXBlUmVzcG9uc2USDQoFdmFsaWQYASABKAgSEwoLZGVzY3JpcHRpb24YAiABKAkiNgoVV2F0Y2hFeGVjdXRpb25SZXF1ZXN0Eh0KFWV4ZWN1dGlvbl90cmFja2luZ19pZBgBIAEoCSImChRXYXRjaEV4ZWN1dGlvblVwZGF0ZRIOCgZ1cGRhdGUYASABKAkiSgoWRXhlY3V0aW9uU3RhdHVzUmVxdWVzdBIdChVleGVjdXRpb25fdHJhY2tpbmdfaWQYASABKAkSEQoJYWN0aW9uX2lkGAIgASgJImEKGURhc2hib2FyZE5hdmlnYXRpb25UYXJnZXQSDQoFdGl0bGUYASABKAkSEwoLZW50aXR5X3R5cGUYAiABKAkSEgoKZW50aXR5X2tleRgDIAEoCRIMCgRwYXRoGAQgASgJIo8BChdFeGVjdXRpb25TdGF0dXNSZXNwb25zZRIsCglsb2dfZW50cnkYASABKAsyGS5vbGl2ZXRpbi5hcGkudjEuTG9nRW50cnkSRgoSYmFja190b19kYXNoYm9hcmRzGAIgAygLMioub2xpdmV0aW4uYXBpLnYxLkRhc2hib2FyZE5hdmlnYXRpb25UYXJnZXQiDwoNV2hvQW1JUmVxdWVzdCJsCg5XaG9BbUlSZXNwb25zZRIaChJhdXRoZW50aWNhdGVkX3VzZXIYASABKAkSEQoJdXNlcmdyb3VwGAIgASgJEhAKCHByb3ZpZGVyGAMgASgJEgwKBGFjbHMYBCADKAkSCwoDc2lkGAUgASgJIhIKEFNvc1JlcG9ydFJlcXVlc3QiIgoRU29zUmVwb3J0UmVzcG9uc2USDQoFYWxlcnQYASABKAkiEQoPRHVtcFZhcnNSZXF1ZXN0IpUBChBEdW1wVmFyc1Jlc3BvbnNlEg0KBWFsZXJ0GAEgASgJEkEKCGNvbnRlbnRzGAIgAygLMi8ub2xpdmV0aW4uYXBpLnYxLkR1bXBWYXJzUmVzcG9uc2UuQ29udGVudHNFbnRyeRovCg1Db250ZW50c0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiOwoMRGVidWdCaW5kaW5nEhQKDGFjdGlvbl90aXRsZRgBIAEoCRIVCg1lbnRpdHlfcHJlZml4GAIgASgJIh4KHER1bXBQdWJsaWNJZEFjdGlvbk1hcFJlcXVlc3QizgEKHUR1bXBQdWJsaWNJZEFjdGlvbk1hcFJlc3BvbnNlEg0KBWFsZXJ0GAEgASgJEk4KCGNvbnRlbnRzGAIgAygLMjwub2xpdmV0aW4uYXBpLnYxLkR1bXBQdWJsaWNJZEFjdGlvbk1hcFJlc3BvbnNlLkNvbnRlbnRzRW50cnkaTgoNQ29udGVudHNFbnRyeRILCgNrZXkYASABKAkSLAoFdmFsdWUYAiABKAsyHS5vbGl2ZXRpbi5hcGkudjEuRGVidWdCaW5kaW5nOgI4ASISChBHZXRSZWFkeXpSZXF1ZXN0IiMKEUdldFJlYWR5elJlc3BvbnNlEg4KBnN0YXR1cxgBIAEoCSIUChJFdmVudFN0cmVhbVJlcXVlc3QimQMKE0V2ZW50U3RyZWFtUmVzcG9uc2USPQoOZW50aXR5X2NoYW5nZWQYAiABKAsyIy5vbGl2ZXRpbi5hcGkudjEuRXZlbnRFbnRpdHlDaGFuZ2VkSAASPQoOY29uZmlnX2NoYW5nZWQYAyABKAsyIy5vbGl2ZXRpbi5hcGkudjEuRXZlbnRDb25maWdDaGFuZ2VkSAASRQoSZXhlY3V0aW9uX2ZpbmlzaGVkGAQgASgLMicub2xpdmV0aW4uYXBpLnYxLkV2ZW50RXhlY3V0aW9uRmluaXNoZWRIABJDChFleGVjdXRpb25fc3RhcnRlZBgFIAEoCzImLm9saXZldGluLmFwaS52MS5FdmVudEV4ZWN1dGlvblN0YXJ0ZWRIABI5CgxvdXRwdXRfY2h1bmsYBiABKAsyIS5vbGl2ZXRpbi5hcGkudjEuRXZlbnRPdXRwdXRDaHVua0gAEjQKCWhlYXJ0YmVhdBgHIAEoCzIfLm9saXZldGluLmFwaS52MS5FdmVudEhlYXJ0YmVhdEgAQgcKBWV2ZW50IkEKEEV2ZW50T3V0cHV0Q2h1bmsSHQoVZXhlY3V0aW9uX3RyYWNraW5nX2lkGAEgASgJEg4KBm91dHB1dBgCIAEoCSIUChJFdmVudEVudGl0eUNoYW5nZWQiFAoSRXZlbnRDb25maWdDaGFuZ2VkIhAKDkV2ZW50SGVhcnRiZWF0IkYKFkV2ZW50RXhlY3V0aW9uRmluaXNoZWQSLAoJbG9nX2VudHJ5GAEgASgLMhkub2xpdmV0aW4uYXBpLnYxLkxvZ0VudHJ5IkUKFUV2ZW50RXhlY3V0aW9uU3RhcnRlZBIsCglsb2dfZW50cnkYASABKAsyGS5vbGl2ZXRpbi5hcGkudjEuTG9nRW50cnkiMgoRS2lsbEFjdGlvblJlcXVlc3QSHQoVZXhlY3V0aW9uX3RyYWNraW5nX2lkGAEgASgJIm0KEktpbGxBY3Rpb25SZXNwb25zZRIdChVleGVjdXRpb25fdHJhY2tpbmdfaWQYASABKAkSDgoGa2lsbGVkGAIgASgIEhkKEWFscmVhZHlfY29tcGxldGVkGAMgASgIEg0KBWZvdW5kGAQgASgIIjsKFUxvY2FsVXNlckxvZ2luUmVxdWVzdBIQCgh1c2VybmFtZRgBIAEoCRIQCghwYXNzd29yZBgCIAEoCSIpChZMb2NhbFVzZXJMb2dpblJlc3BvbnNlEg8KB3N1Y2Nlc3MYASABKAgiJwoTUGFzc3dvcmRIYXNoUmVxdWVzdBIQCghwYXNzd29yZBgBIAEoCSIkChRQYXNzd29yZEhhc2hSZXNwb25zZRIMCgRoYXNoGAEgASgJIg8KDUxvZ291dFJlcXVlc3QiEAoOTG9nb3V0UmVzcG9uc2UiFwoVR2V0RGlhZ25vc3RpY3NSZXF1ZXN0IkUKFkdldERpYWdub3N0aWNzUmVzcG9uc2USEwoLU3NoRm91bmRLZXkYASABKAkSFgoOU3NoRm91bmRDb25maWcYAiABKAkiDQoLSW5pdFJlcXVlc3Qi6wUKDEluaXRSZXNwb25zZRISCgpzaG93Rm9vdGVyGAEgASgIEhYKDnNob3dOYXZpZ2F0aW9uGAIgASgIEhcKD3Nob3dOZXdWZXJzaW9ucxgDIAEoCBIYChBhdmFpbGFibGVWZXJzaW9uGAQgASgJEhYKDmN1cnJlbnRWZXJzaW9uGAUgASgJEhEKCXBhZ2VUaXRsZRgGIAEoCRIeChZzZWN0aW9uTmF2aWdhdGlvblN0eWxlGAcgASgJEhoKEmRlZmF1bHRJY29uRm9yQmFjaxgIIAEoCRIWCg5lbmFibGVDdXN0b21KcxgJIAEoCBIUCgxhdXRoTG9naW5VcmwYCiABKAkSFgoOYXV0aExvY2FsTG9naW4YCyABKAgSEQoJc3R5bGVNb2RzGAwgAygJEjgKD29BdXRoMlByb3ZpZGVycxgNIAMoCzIfLm9saXZldGluLmFwaS52MS5PQXV0aDJQcm92aWRlchI4Cg9hZGRpdGlvbmFsTGlua3MYDiADKAsyHy5vbGl2ZXRpbi5hcGkudjEuQWRkaXRpb25hbExpbmsSFgoOcm9vdERhc2hib2FyZHMYDyADKAkSGgoSYXV0aGVudGljYXRlZF91c2VyGBAgASgJEiMKG2F1dGhlbnRpY2F0ZWRfdXNlcl9wcm92aWRlchgRIAEoCRI6ChBlZmZlY3RpdmVfcG9saWN5GBIgASgLMiAub2xpdmV0aW4uYXBpLnYxLkVmZmVjdGl2ZVBvbGljeRIWCg5iYW5uZXJfbWVzc2FnZRgTIAEoCRISCgpiYW5uZXJfY3NzGBQgASgJEhgKEHNob3dfZGlhZ25vc3RpY3MYFSABKAgSFQoNc2hvd19sb2dfbGlzdBgWIAEoCBIWCg5sb2dpbl9yZXF1aXJlZBgXIAEoCBIYChBhdmFpbGFibGVfdGhlbWVzGBggAygJEiQKHHNob3dfbmF2aWdhdGVfb25fc3RhcnRfaWNvbnMYGSABKAgiLAoOQWRkaXRpb25hbExpbmsSDQoFdGl0bGUYASABKAkSCwoDdXJsGAIgASgJIjoKDk9BdXRoMlByb3ZpZGVyEg0KBXRpdGxlGAEgASgJEgwKBGljb24YAyABKAkSCwoDa2V5GAQgASgJIi0KF0dldEFjdGlvbkJpbmRpbmdSZXF1ZXN0EhIKCmJpbmRpbmdfaWQYASABKAkiiwEKGEdldEFjdGlvbkJpbmRpbmdSZXNwb25zZRInCgZhY3Rpb24YASABKAsyFy5vbGl2ZXRpbi5hcGkudjEuQWN0aW9uEkYKEmJhY2tfdG9fZGFzaGJvYXJkcxgCIAMoCzIqLm9saXZldGluLmFwaS52MS5EYXNoYm9hcmROYXZpZ2F0aW9uVGFyZ2V0IhQKEkdldEVudGl0aWVzUmVxdWVzdCJUChNHZXRFbnRpdGllc1Jlc3BvbnNlEj0KEmVudGl0eV9kZWZpbml0aW9ucxgBIAMoCzIhLm9saXZldGluLmFwaS52MS5FbnRpdHlEZWZpbml0aW9uImkKEEVudGl0eURlZmluaXRpb24SDQoFdGl0bGUYASABKAkSKgoJaW5zdGFuY2VzGAIgAygLMhcub2xpdmV0aW4uYXBpLnYxLkVudGl0eRIaChJ1c2VkX29uX2Rhc2hib2FyZHMYAyADKAkiNAoQR2V0RW50aXR5UmVxdWVzdBISCgp1bmlxdWVfa2V5GAEgASgJEgwKBHR5cGUYAiABKAkiNQoUUmVzdGFydEFjdGlvblJlcXVlc3QSHQoVZXhlY3V0aW9uX3RyYWNraW5nX2lkGAEgASgJMtYTChJPbGl2ZVRpbkFwaVNlcnZpY2USXQoMR2V0RGFzaGJvYXJkEiQub2xpdmV0aW4uYXBpLnYxLkdldERhc2hib2FyZFJlcXVlc3QaJS5vbGl2ZXRpbi5hcGkudjEuR2V0RGFzaGJvYXJkUmVzcG9uc2UiABJaCgtTdGFydEFjdGlvbhIjLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvblJlcXVlc3QaJC5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25SZXNwb25zZSIAEm8KElN0YXJ0QWN0aW9uQW5kV2FpdBIqLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkFuZFdhaXRSZXF1ZXN0Gisub2xpdmV0aW4uYXBpLnYxLlN0YXJ0QWN0aW9uQW5kV2FpdFJlc3BvbnNlIgASaQoQU3RhcnRBY3Rpb25CeUdldBIoLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkJ5R2V0UmVxdWVzdBopLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkJ5R2V0UmVzcG9uc2UiABJ+ChdTdGFydEFjdGlvbkJ5R2V0QW5kV2FpdBIvLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkJ5R2V0QW5kV2FpdFJlcXVlc3QaMC5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25CeUdldEFuZFdhaXRSZXNwb25zZSIAEl4KDVJlc3RhcnRBY3Rpb24SJS5vbGl2ZXRpbi5hcGkudjEuUmVzdGFydEFjdGlvblJlcXVlc3QaJC5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25SZXNwb25zZSIAElcKCktpbGxBY3Rpb24SIi5vbGl2ZXRpbi5hcGkudjEuS2lsbEFjdGlvblJlcXVlc3QaIy5vbGl2ZXRpbi5hcGkudjEuS2lsbEFjdGlvblJlc3BvbnNlIgASZgoPRXhlY3V0aW9uU3RhdHVzEicub2xpdmV0aW4uYXBpLnYxLkV4ZWN1dGlvblN0YXR1c1JlcXVlc3QaKC5vbGl2ZXRpbi5hcGkudjEuRXhlY3V0aW9uU3RhdHVzUmVzcG9uc2UiABJOCgdHZXRMb2dzEh8ub2xpdmV0aW4uYXBpLnYxLkdldExvZ3NSZXF1ZXN0GiAub2xpdmV0aW4uYXBpLnYxLkdldExvZ3NSZXNwb25zZSIAEmAKDUdldEFjdGlvbkxvZ3MSJS5vbGl2ZXRpbi5hcGkudjEuR2V0QWN0aW9uTG9nc1JlcXVlc3QaJi5vbGl2ZXRpbi5hcGkudjEuR2V0QWN0aW9uTG9nc1Jlc3BvbnNlIgASbAoRR2V0RXhlY3V0aW9uUXVldWUSKS5vbGl2ZXRpbi5hcGkudjEuR2V0RXhlY3V0aW9uUXVldWVSZXF1ZXN0Gioub2xpdmV0aW4uYXBpLnYxLkdldEV4ZWN1dGlvblF1ZXVlUmVzcG9uc2UiABJ1ChRWYWxpZGF0ZUFyZ3VtZW50VHlwZRIsLm9saXZldGluLmFwaS52MS5WYWxpZGF0ZUFyZ3VtZW50VHlwZVJlcXVlc3QaLS5vbGl2ZXRpbi5hcGkudjEuVmFsaWRhdGVBcmd1bWVudFR5cGVSZXNwb25zZSIAEksKBldob0FtSRIeLm9saXZldGluLmFwaS52MS5XaG9BbUlSZXF1ZXN0Gh8ub2xpdmV0aW4uYXBpLnYxLldob0FtSVJlc3BvbnNlIgASVAoJU29zUmVwb3J0EiEub2xpdmV0aW4uYXBpLnYxLlNvc1JlcG9ydFJlcXVlc3QaIi5vbGl2ZXRpbi5hcGkudjEuU29zUmVwb3J0UmVzcG9uc2UiABJRCghEdW1wVmFycxIgLm9saXZldGluLmFwaS52MS5EdW1wVmFyc1JlcXVlc3QaIS5vbGl2ZXRpbi5hcGkudjEuRHVtcFZhcnNSZXNwb25zZSIAEngKFUR1bXBQdWJsaWNJZEFjdGlvbk1hcBItLm9saXZldGluLmFwaS52MS5EdW1wUHVibGljSWRBY3Rpb25NYXBSZXF1ZXN0Gi4ub2xpdmV0aW4uYXBpLnYxLkR1bXBQdWJsaWNJZEFjdGlvbk1hcFJlc3BvbnNlIgASVAoJR2V0UmVhZHl6EiEub2xpdmV0aW4uYXBpLnYxLkdldFJlYWR5elJlcXVlc3QaIi5vbGl2ZXRpbi5hcGkudjEuR2V0UmVhZHl6UmVzcG9uc2UiABJjCg5Mb2NhbFVzZXJMb2dpbhImLm9saXZldGluLmFwaS52MS5Mb2NhbFVzZXJMb2dpblJlcXVlc3QaJy5vbGl2ZXRpbi5hcGkudjEuTG9jYWxVc2VyTG9naW5SZXNwb25zZSIAEl0KDFBhc3N3b3JkSGFzaBIkLm9saXZldGluLmFwaS52MS5QYXNzd29yZEhhc2hSZXF1ZXN0GiUub2xpdmV0aW4uYXBpLnYxLlBhc3N3b3JkSGFzaFJlc3BvbnNlIgASSwoGTG9nb3V0Eh4ub2xpdmV0aW4uYXBpLnYxLkxvZ291dFJlcXVlc3QaHy5vbGl2ZXRpbi5hcGkudjEuTG9nb3V0UmVzcG9uc2UiABJcCgtFdmVudFN0cmVhbRIjLm9saXZldGluLmFwaS52MS5FdmVudFN0cmVhbVJlcXVlc3QaJC5vbGl2ZXRpbi5hcGkudjEuRXZlbnRTdHJlYW1SZXNwb25zZSIAMAESYwoOR2V0RGlhZ25vc3RpY3MSJi5vbGl2ZXRpbi5hcGkudjEuR2V0RGlhZ25vc3RpY3NSZXF1ZXN0Gicub2xpdmV0aW4uYXBpLnYxLkdldERpYWdub3N0aWNzUmVzcG9uc2UiABJFCgRJbml0Ehwub2xpdmV0aW4uYXBpLnYxLkluaXRSZXF1ZXN0Gh0ub2xpdmV0aW4uYXBpLnYxLkluaXRSZXNwb25zZSIAEmkKEEdldEFjdGlvbkJpbmRpbmcSKC5vbGl2ZXRpbi5hcGkudjEuR2V0QWN0aW9uQmluZGluZ1JlcXVlc3QaKS5vbGl2ZXRpbi5hcGkudjEuR2V0QWN0aW9uQmluZGluZ1Jlc3BvbnNlIgASWgoLR2V0RW50aXRpZXMSIy5vbGl2ZXRpbi5hcGkudjEuR2V0RW50aXRpZXNSZXF1ZXN0GiQub2xpdmV0aW4uYXBpLnYxLkdldEVudGl0aWVzUmVzcG9uc2UiABJJCglHZXRFbnRpdHkSIS5vbGl2ZXRpbi5hcGkudjEuR2V0RW50aXR5UmVxdWVzdBoXLm9saXZldGluLmFwaS52MS5FbnRpdHkiAEI4WjZnaXRodWIuY29tL09saXZlVGluL09saXZlVGluL2dlbi9vbGl2ZXRpbi9hcGkvdjE7YXBpdjFiBnByb3RvMw"); + fileDesc("Ch5vbGl2ZXRpbi9hcGkvdjEvb2xpdmV0aW4ucHJvdG8SD29saXZldGluLmFwaS52MSLGBAoGQWN0aW9uEhIKCmJpbmRpbmdfaWQYASABKAkSDQoFdGl0bGUYAiABKAkSDAoEaWNvbhgDIAEoCRIQCghjYW5fZXhlYxgEIAEoCBIyCglhcmd1bWVudHMYBSADKAsyHy5vbGl2ZXRpbi5hcGkudjEuQWN0aW9uQXJndW1lbnQSFgoOcG9wdXBfb25fc3RhcnQYBiABKAkSDQoFb3JkZXIYByABKAUSDwoHdGltZW91dBgIIAEoBRIjChtkYXRldGltZV9yYXRlX2xpbWl0X2V4cGlyZXMYCSABKAkSFwoPZXhlY19vbl9zdGFydHVwGAogASgIEhQKDGV4ZWNfb25fY3JvbhgLIAMoCRIjChtleGVjX29uX2ZpbGVfY3JlYXRlZF9pbl9kaXIYDCADKAkSIwobZXhlY19vbl9maWxlX2NoYW5nZWRfaW5fZGlyGA0gAygJEh0KFWV4ZWNfb25fY2FsZW5kYXJfZmlsZRgOIAEoCRJAChBleGVjX29uX3dlYmhvb2tzGA8gAygLMiYub2xpdmV0aW4uYXBpLnYxLkFjdGlvbldlYmhvb2tFeGVjSGludBIVCg1qdXN0aWZpY2F0aW9uGBQgASgJEhwKFGhhc19ydW5uaW5nX2luc3RhbmNlGBEgASgIEhsKE2hhc19xdWV1ZWRfaW5zdGFuY2UYEiABKAgSNgoGZ3JvdXBzGBMgAygLMiYub2xpdmV0aW4uYXBpLnYxLkFjdGlvbkdyb3VwTWVtYmVyc2hpcEoECBAQESJRChVBY3Rpb25Hcm91cE1lbWJlcnNoaXASDAoEbmFtZRgBIAEoCRIWCg5tYXhfY29uY3VycmVudBgCIAEoBRISCgpxdWV1ZV9zaXplGAMgASgFIsMCChVBY3Rpb25XZWJob29rRXhlY0hpbnQSEAoIdGVtcGxhdGUYASABKAkSEgoKbWF0Y2hfcGF0aBgCIAEoCRJPCg1tYXRjaF9oZWFkZXJzGAMgAygLMjgub2xpdmV0aW4uYXBpLnYxLkFjdGlvbldlYmhvb2tFeGVjSGludC5NYXRjaEhlYWRlcnNFbnRyeRJLCgttYXRjaF9xdWVyeRgEIAMoCzI2Lm9saXZldGluLmFwaS52MS5BY3Rpb25XZWJob29rRXhlY0hpbnQuTWF0Y2hRdWVyeUVudHJ5GjMKEU1hdGNoSGVhZGVyc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEaMQoPTWF0Y2hRdWVyeUVudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiuwIKDkFjdGlvbkFyZ3VtZW50EgwKBG5hbWUYASABKAkSDQoFdGl0bGUYAiABKAkSDAoEdHlwZRgDIAEoCRIVCg1kZWZhdWx0X3ZhbHVlGAQgASgJEjYKB2Nob2ljZXMYBSADKAsyJS5vbGl2ZXRpbi5hcGkudjEuQWN0aW9uQXJndW1lbnRDaG9pY2USEwoLZGVzY3JpcHRpb24YBiABKAkSRQoLc3VnZ2VzdGlvbnMYByADKAsyMC5vbGl2ZXRpbi5hcGkudjEuQWN0aW9uQXJndW1lbnQuU3VnZ2VzdGlvbnNFbnRyeRIfChdzdWdnZXN0aW9uc19icm93c2VyX2tleRgIIAEoCRoyChBTdWdnZXN0aW9uc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiNAoUQWN0aW9uQXJndW1lbnRDaG9pY2USDQoFdmFsdWUYASABKAkSDQoFdGl0bGUYAiABKAkisgEKBkVudGl0eRINCgV0aXRsZRgBIAEoCRISCgp1bmlxdWVfa2V5GAIgASgJEgwKBHR5cGUYAyABKAkSEwoLZGlyZWN0b3JpZXMYBCADKAkSMwoGZmllbGRzGAUgAygLMiMub2xpdmV0aW4uYXBpLnYxLkVudGl0eS5GaWVsZHNFbnRyeRotCgtGaWVsZHNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBIlQKFEdldERhc2hib2FyZFJlc3BvbnNlEg0KBXRpdGxlGAEgASgJEi0KCWRhc2hib2FyZBgEIAEoCzIaLm9saXZldGluLmFwaS52MS5EYXNoYm9hcmQiXwoPRWZmZWN0aXZlUG9saWN5EhgKEHNob3dfZGlhZ25vc3RpY3MYASABKAgSFQoNc2hvd19sb2dfbGlzdBgCIAEoCBIbChNzaG93X3ZlcnNpb25fbnVtYmVyGAMgASgIIk0KE0dldERhc2hib2FyZFJlcXVlc3QSDQoFdGl0bGUYASABKAkSEwoLZW50aXR5X3R5cGUYAiABKAkSEgoKZW50aXR5X2tleRgDIAEoCSJRCglEYXNoYm9hcmQSDQoFdGl0bGUYASABKAkSNQoIY29udGVudHMYAiADKAsyIy5vbGl2ZXRpbi5hcGkudjEuRGFzaGJvYXJkQ29tcG9uZW50ItsBChJEYXNoYm9hcmRDb21wb25lbnQSDQoFdGl0bGUYASABKAkSDAoEdHlwZRgCIAEoCRI1Cghjb250ZW50cxgDIAMoCzIjLm9saXZldGluLmFwaS52MS5EYXNoYm9hcmRDb21wb25lbnQSDAoEaWNvbhgEIAEoCRIRCgljc3NfY2xhc3MYBSABKAkSJwoGYWN0aW9uGAYgASgLMhcub2xpdmV0aW4uYXBpLnYxLkFjdGlvbhITCgtlbnRpdHlfdHlwZRgHIAEoCRISCgplbnRpdHlfa2V5GAggASgJIpQBChJTdGFydEFjdGlvblJlcXVlc3QSEgoKYmluZGluZ19pZBgBIAEoCRI3Cglhcmd1bWVudHMYAiADKAsyJC5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25Bcmd1bWVudBIaChJ1bmlxdWVfdHJhY2tpbmdfaWQYAyABKAkSFQoNanVzdGlmaWNhdGlvbhgEIAEoCSIyChNTdGFydEFjdGlvbkFyZ3VtZW50EgwKBG5hbWUYASABKAkSDQoFdmFsdWUYAiABKAkiNAoTU3RhcnRBY3Rpb25SZXNwb25zZRIdChVleGVjdXRpb25fdHJhY2tpbmdfaWQYAiABKAkifgoZU3RhcnRBY3Rpb25BbmRXYWl0UmVxdWVzdBIRCglhY3Rpb25faWQYASABKAkSNwoJYXJndW1lbnRzGAIgAygLMiQub2xpdmV0aW4uYXBpLnYxLlN0YXJ0QWN0aW9uQXJndW1lbnQSFQoNanVzdGlmaWNhdGlvbhgDIAEoCSJKChpTdGFydEFjdGlvbkFuZFdhaXRSZXNwb25zZRIsCglsb2dfZW50cnkYASABKAsyGS5vbGl2ZXRpbi5hcGkudjEuTG9nRW50cnkiLAoXU3RhcnRBY3Rpb25CeUdldFJlcXVlc3QSEQoJYWN0aW9uX2lkGAEgASgJIjkKGFN0YXJ0QWN0aW9uQnlHZXRSZXNwb25zZRIdChVleGVjdXRpb25fdHJhY2tpbmdfaWQYAiABKAkiMwoeU3RhcnRBY3Rpb25CeUdldEFuZFdhaXRSZXF1ZXN0EhEKCWFjdGlvbl9pZBgBIAEoCSJPCh9TdGFydEFjdGlvbkJ5R2V0QW5kV2FpdFJlc3BvbnNlEiwKCWxvZ19lbnRyeRgBIAEoCzIZLm9saXZldGluLmFwaS52MS5Mb2dFbnRyeSJeCg5HZXRMb2dzUmVxdWVzdBIUCgxzdGFydF9vZmZzZXQYASABKAMSEwoLZGF0ZV9maWx0ZXIYAiABKAkSEQoJcGFnZV9zaXplGAMgASgDEg4KBmZpbHRlchgEIAEoCSKUBAoITG9nRW50cnkSGAoQZGF0ZXRpbWVfc3RhcnRlZBgBIAEoCRIUCgxhY3Rpb25fdGl0bGUYAiABKAkSDgoGb3V0cHV0GAMgASgJEhEKCXRpbWVkX291dBgFIAEoCBIRCglleGl0X2NvZGUYBiABKAUSDAoEdXNlchgHIAEoCRISCgp1c2VyX2NsYXNzGAggASgJEhMKC2FjdGlvbl9pY29uGAkgASgJEgwKBHRhZ3MYCiADKAkSHQoVZXhlY3V0aW9uX3RyYWNraW5nX2lkGAsgASgJEhkKEWRhdGV0aW1lX2ZpbmlzaGVkGAwgASgJEhkKEWV4ZWN1dGlvbl9zdGFydGVkGA4gASgIEhoKEmV4ZWN1dGlvbl9maW5pc2hlZBgPIAEoCBIPCgdibG9ja2VkGBAgASgIEhYKDmRhdGV0aW1lX2luZGV4GBEgASgDEhAKCGNhbl9raWxsGBIgASgIEiMKG2RhdGV0aW1lX3JhdGVfbGltaXRfZXhwaXJlcxgTIAEoCRISCgpiaW5kaW5nX2lkGBQgASgJEg4KBnF1ZXVlZBgVIAEoCBIYChBxdWV1ZWRfZm9yX2dyb3VwGBYgASgJEhUKDWp1c3RpZmljYXRpb24YFyABKAkSNwoJYXJndW1lbnRzGBggAygLMiQub2xpdmV0aW4uYXBpLnYxLlN0YXJ0QWN0aW9uQXJndW1lbnQikQEKD0dldExvZ3NSZXNwb25zZRInCgRsb2dzGAEgAygLMhkub2xpdmV0aW4uYXBpLnYxLkxvZ0VudHJ5EhcKD2NvdW50X3JlbWFpbmluZxgCIAEoAxIRCglwYWdlX3NpemUYAyABKAMSEwoLdG90YWxfY291bnQYBCABKAMSFAoMc3RhcnRfb2Zmc2V0GAUgASgDIj8KFEdldEFjdGlvbkxvZ3NSZXF1ZXN0EhEKCWFjdGlvbl9pZBgBIAEoCRIUCgxzdGFydF9vZmZzZXQYAiABKAMilwEKFUdldEFjdGlvbkxvZ3NSZXNwb25zZRInCgRsb2dzGAEgAygLMhkub2xpdmV0aW4uYXBpLnYxLkxvZ0VudHJ5EhcKD2NvdW50X3JlbWFpbmluZxgCIAEoAxIRCglwYWdlX3NpemUYAyABKAMSEwoLdG90YWxfY291bnQYBCABKAMSFAoMc3RhcnRfb2Zmc2V0GAUgASgDIhoKGEdldEV4ZWN1dGlvblF1ZXVlUmVxdWVzdCLGAQoURXhlY3V0aW9uUXVldWVBY3Rpb24SEgoKYmluZGluZ19pZBgBIAEoCRIUCgxhY3Rpb25fdGl0bGUYAiABKAkSEwoLYWN0aW9uX2ljb24YAyABKAkSFgoObWF4X2NvbmN1cnJlbnQYBCABKAUSFAoMYWN0aXZlX2NvdW50GAUgASgFEhUKDWVudGl0eV9wcmVmaXgYBiABKAkSKgoHZW50cmllcxgHIAMoCzIZLm9saXZldGluLmFwaS52MS5Mb2dFbnRyeSLBAQoTRXhlY3V0aW9uUXVldWVHcm91cBIMCgRuYW1lGAEgASgJEgwKBGljb24YAiABKAkSFgoObWF4X2NvbmN1cnJlbnQYAyABKAUSFAoMYWN0aXZlX2NvdW50GAQgASgFEjYKB2FjdGlvbnMYBSADKAsyJS5vbGl2ZXRpbi5hcGkudjEuRXhlY3V0aW9uUXVldWVBY3Rpb24SFAoMcXVldWVkX2NvdW50GAYgASgFEhIKCnF1ZXVlX3NpemUYByABKAUiZwoZR2V0RXhlY3V0aW9uUXVldWVSZXNwb25zZRI0CgZncm91cHMYASADKAsyJC5vbGl2ZXRpbi5hcGkudjEuRXhlY3V0aW9uUXVldWVHcm91cBIUCgx0b3RhbF9hY3RpdmUYAiABKAUiZQobVmFsaWRhdGVBcmd1bWVudFR5cGVSZXF1ZXN0Eg0KBXZhbHVlGAEgASgJEgwKBHR5cGUYAiABKAkSEgoKYmluZGluZ19pZBgDIAEoCRIVCg1hcmd1bWVudF9uYW1lGAQgASgJIkIKHFZhbGlkYXRlQXJndW1lbnRUeXBlUmVzcG9uc2USDQoFdmFsaWQYASABKAgSEwoLZGVzY3JpcHRpb24YAiABKAkiNgoVV2F0Y2hFeGVjdXRpb25SZXF1ZXN0Eh0KFWV4ZWN1dGlvbl90cmFja2luZ19pZBgBIAEoCSImChRXYXRjaEV4ZWN1dGlvblVwZGF0ZRIOCgZ1cGRhdGUYASABKAkiSgoWRXhlY3V0aW9uU3RhdHVzUmVxdWVzdBIdChVleGVjdXRpb25fdHJhY2tpbmdfaWQYASABKAkSEQoJYWN0aW9uX2lkGAIgASgJImEKGURhc2hib2FyZE5hdmlnYXRpb25UYXJnZXQSDQoFdGl0bGUYASABKAkSEwoLZW50aXR5X3R5cGUYAiABKAkSEgoKZW50aXR5X2tleRgDIAEoCRIMCgRwYXRoGAQgASgJIo8BChdFeGVjdXRpb25TdGF0dXNSZXNwb25zZRIsCglsb2dfZW50cnkYASABKAsyGS5vbGl2ZXRpbi5hcGkudjEuTG9nRW50cnkSRgoSYmFja190b19kYXNoYm9hcmRzGAIgAygLMioub2xpdmV0aW4uYXBpLnYxLkRhc2hib2FyZE5hdmlnYXRpb25UYXJnZXQiDwoNV2hvQW1JUmVxdWVzdCJsCg5XaG9BbUlSZXNwb25zZRIaChJhdXRoZW50aWNhdGVkX3VzZXIYASABKAkSEQoJdXNlcmdyb3VwGAIgASgJEhAKCHByb3ZpZGVyGAMgASgJEgwKBGFjbHMYBCADKAkSCwoDc2lkGAUgASgJIhIKEFNvc1JlcG9ydFJlcXVlc3QiIgoRU29zUmVwb3J0UmVzcG9uc2USDQoFYWxlcnQYASABKAkiEQoPRHVtcFZhcnNSZXF1ZXN0IpUBChBEdW1wVmFyc1Jlc3BvbnNlEg0KBWFsZXJ0GAEgASgJEkEKCGNvbnRlbnRzGAIgAygLMi8ub2xpdmV0aW4uYXBpLnYxLkR1bXBWYXJzUmVzcG9uc2UuQ29udGVudHNFbnRyeRovCg1Db250ZW50c0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiOwoMRGVidWdCaW5kaW5nEhQKDGFjdGlvbl90aXRsZRgBIAEoCRIVCg1lbnRpdHlfcHJlZml4GAIgASgJIh4KHER1bXBQdWJsaWNJZEFjdGlvbk1hcFJlcXVlc3QizgEKHUR1bXBQdWJsaWNJZEFjdGlvbk1hcFJlc3BvbnNlEg0KBWFsZXJ0GAEgASgJEk4KCGNvbnRlbnRzGAIgAygLMjwub2xpdmV0aW4uYXBpLnYxLkR1bXBQdWJsaWNJZEFjdGlvbk1hcFJlc3BvbnNlLkNvbnRlbnRzRW50cnkaTgoNQ29udGVudHNFbnRyeRILCgNrZXkYASABKAkSLAoFdmFsdWUYAiABKAsyHS5vbGl2ZXRpbi5hcGkudjEuRGVidWdCaW5kaW5nOgI4ASISChBHZXRSZWFkeXpSZXF1ZXN0IiMKEUdldFJlYWR5elJlc3BvbnNlEg4KBnN0YXR1cxgBIAEoCSIUChJFdmVudFN0cmVhbVJlcXVlc3QimQMKE0V2ZW50U3RyZWFtUmVzcG9uc2USPQoOZW50aXR5X2NoYW5nZWQYAiABKAsyIy5vbGl2ZXRpbi5hcGkudjEuRXZlbnRFbnRpdHlDaGFuZ2VkSAASPQoOY29uZmlnX2NoYW5nZWQYAyABKAsyIy5vbGl2ZXRpbi5hcGkudjEuRXZlbnRDb25maWdDaGFuZ2VkSAASRQoSZXhlY3V0aW9uX2ZpbmlzaGVkGAQgASgLMicub2xpdmV0aW4uYXBpLnYxLkV2ZW50RXhlY3V0aW9uRmluaXNoZWRIABJDChFleGVjdXRpb25fc3RhcnRlZBgFIAEoCzImLm9saXZldGluLmFwaS52MS5FdmVudEV4ZWN1dGlvblN0YXJ0ZWRIABI5CgxvdXRwdXRfY2h1bmsYBiABKAsyIS5vbGl2ZXRpbi5hcGkudjEuRXZlbnRPdXRwdXRDaHVua0gAEjQKCWhlYXJ0YmVhdBgHIAEoCzIfLm9saXZldGluLmFwaS52MS5FdmVudEhlYXJ0YmVhdEgAQgcKBWV2ZW50IkEKEEV2ZW50T3V0cHV0Q2h1bmsSHQoVZXhlY3V0aW9uX3RyYWNraW5nX2lkGAEgASgJEg4KBm91dHB1dBgCIAEoCSIUChJFdmVudEVudGl0eUNoYW5nZWQiFAoSRXZlbnRDb25maWdDaGFuZ2VkIhAKDkV2ZW50SGVhcnRiZWF0IkYKFkV2ZW50RXhlY3V0aW9uRmluaXNoZWQSLAoJbG9nX2VudHJ5GAEgASgLMhkub2xpdmV0aW4uYXBpLnYxLkxvZ0VudHJ5IkUKFUV2ZW50RXhlY3V0aW9uU3RhcnRlZBIsCglsb2dfZW50cnkYASABKAsyGS5vbGl2ZXRpbi5hcGkudjEuTG9nRW50cnkiMgoRS2lsbEFjdGlvblJlcXVlc3QSHQoVZXhlY3V0aW9uX3RyYWNraW5nX2lkGAEgASgJIm0KEktpbGxBY3Rpb25SZXNwb25zZRIdChVleGVjdXRpb25fdHJhY2tpbmdfaWQYASABKAkSDgoGa2lsbGVkGAIgASgIEhkKEWFscmVhZHlfY29tcGxldGVkGAMgASgIEg0KBWZvdW5kGAQgASgIIjsKFUxvY2FsVXNlckxvZ2luUmVxdWVzdBIQCgh1c2VybmFtZRgBIAEoCRIQCghwYXNzd29yZBgCIAEoCSIpChZMb2NhbFVzZXJMb2dpblJlc3BvbnNlEg8KB3N1Y2Nlc3MYASABKAgiJwoTUGFzc3dvcmRIYXNoUmVxdWVzdBIQCghwYXNzd29yZBgBIAEoCSIkChRQYXNzd29yZEhhc2hSZXNwb25zZRIMCgRoYXNoGAEgASgJIg8KDUxvZ291dFJlcXVlc3QiEAoOTG9nb3V0UmVzcG9uc2UiFwoVR2V0RGlhZ25vc3RpY3NSZXF1ZXN0IkUKFkdldERpYWdub3N0aWNzUmVzcG9uc2USEwoLU3NoRm91bmRLZXkYASABKAkSFgoOU3NoRm91bmRDb25maWcYAiABKAkiDQoLSW5pdFJlcXVlc3Qi6wUKDEluaXRSZXNwb25zZRISCgpzaG93Rm9vdGVyGAEgASgIEhYKDnNob3dOYXZpZ2F0aW9uGAIgASgIEhcKD3Nob3dOZXdWZXJzaW9ucxgDIAEoCBIYChBhdmFpbGFibGVWZXJzaW9uGAQgASgJEhYKDmN1cnJlbnRWZXJzaW9uGAUgASgJEhEKCXBhZ2VUaXRsZRgGIAEoCRIeChZzZWN0aW9uTmF2aWdhdGlvblN0eWxlGAcgASgJEhoKEmRlZmF1bHRJY29uRm9yQmFjaxgIIAEoCRIWCg5lbmFibGVDdXN0b21KcxgJIAEoCBIUCgxhdXRoTG9naW5VcmwYCiABKAkSFgoOYXV0aExvY2FsTG9naW4YCyABKAgSEQoJc3R5bGVNb2RzGAwgAygJEjgKD29BdXRoMlByb3ZpZGVycxgNIAMoCzIfLm9saXZldGluLmFwaS52MS5PQXV0aDJQcm92aWRlchI4Cg9hZGRpdGlvbmFsTGlua3MYDiADKAsyHy5vbGl2ZXRpbi5hcGkudjEuQWRkaXRpb25hbExpbmsSFgoOcm9vdERhc2hib2FyZHMYDyADKAkSGgoSYXV0aGVudGljYXRlZF91c2VyGBAgASgJEiMKG2F1dGhlbnRpY2F0ZWRfdXNlcl9wcm92aWRlchgRIAEoCRI6ChBlZmZlY3RpdmVfcG9saWN5GBIgASgLMiAub2xpdmV0aW4uYXBpLnYxLkVmZmVjdGl2ZVBvbGljeRIWCg5iYW5uZXJfbWVzc2FnZRgTIAEoCRISCgpiYW5uZXJfY3NzGBQgASgJEhgKEHNob3dfZGlhZ25vc3RpY3MYFSABKAgSFQoNc2hvd19sb2dfbGlzdBgWIAEoCBIWCg5sb2dpbl9yZXF1aXJlZBgXIAEoCBIYChBhdmFpbGFibGVfdGhlbWVzGBggAygJEiQKHHNob3dfbmF2aWdhdGVfb25fc3RhcnRfaWNvbnMYGSABKAgiLAoOQWRkaXRpb25hbExpbmsSDQoFdGl0bGUYASABKAkSCwoDdXJsGAIgASgJIjoKDk9BdXRoMlByb3ZpZGVyEg0KBXRpdGxlGAEgASgJEgwKBGljb24YAyABKAkSCwoDa2V5GAQgASgJIi0KF0dldEFjdGlvbkJpbmRpbmdSZXF1ZXN0EhIKCmJpbmRpbmdfaWQYASABKAkiiwEKGEdldEFjdGlvbkJpbmRpbmdSZXNwb25zZRInCgZhY3Rpb24YASABKAsyFy5vbGl2ZXRpbi5hcGkudjEuQWN0aW9uEkYKEmJhY2tfdG9fZGFzaGJvYXJkcxgCIAMoCzIqLm9saXZldGluLmFwaS52MS5EYXNoYm9hcmROYXZpZ2F0aW9uVGFyZ2V0IhQKEkdldEVudGl0aWVzUmVxdWVzdCJUChNHZXRFbnRpdGllc1Jlc3BvbnNlEj0KEmVudGl0eV9kZWZpbml0aW9ucxgBIAMoCzIhLm9saXZldGluLmFwaS52MS5FbnRpdHlEZWZpbml0aW9uImkKEEVudGl0eURlZmluaXRpb24SDQoFdGl0bGUYASABKAkSKgoJaW5zdGFuY2VzGAIgAygLMhcub2xpdmV0aW4uYXBpLnYxLkVudGl0eRIaChJ1c2VkX29uX2Rhc2hib2FyZHMYAyADKAkiNAoQR2V0RW50aXR5UmVxdWVzdBISCgp1bmlxdWVfa2V5GAEgASgJEgwKBHR5cGUYAiABKAkiNQoUUmVzdGFydEFjdGlvblJlcXVlc3QSHQoVZXhlY3V0aW9uX3RyYWNraW5nX2lkGAEgASgJMtYTChJPbGl2ZVRpbkFwaVNlcnZpY2USXQoMR2V0RGFzaGJvYXJkEiQub2xpdmV0aW4uYXBpLnYxLkdldERhc2hib2FyZFJlcXVlc3QaJS5vbGl2ZXRpbi5hcGkudjEuR2V0RGFzaGJvYXJkUmVzcG9uc2UiABJaCgtTdGFydEFjdGlvbhIjLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvblJlcXVlc3QaJC5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25SZXNwb25zZSIAEm8KElN0YXJ0QWN0aW9uQW5kV2FpdBIqLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkFuZFdhaXRSZXF1ZXN0Gisub2xpdmV0aW4uYXBpLnYxLlN0YXJ0QWN0aW9uQW5kV2FpdFJlc3BvbnNlIgASaQoQU3RhcnRBY3Rpb25CeUdldBIoLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkJ5R2V0UmVxdWVzdBopLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkJ5R2V0UmVzcG9uc2UiABJ+ChdTdGFydEFjdGlvbkJ5R2V0QW5kV2FpdBIvLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkJ5R2V0QW5kV2FpdFJlcXVlc3QaMC5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25CeUdldEFuZFdhaXRSZXNwb25zZSIAEl4KDVJlc3RhcnRBY3Rpb24SJS5vbGl2ZXRpbi5hcGkudjEuUmVzdGFydEFjdGlvblJlcXVlc3QaJC5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25SZXNwb25zZSIAElcKCktpbGxBY3Rpb24SIi5vbGl2ZXRpbi5hcGkudjEuS2lsbEFjdGlvblJlcXVlc3QaIy5vbGl2ZXRpbi5hcGkudjEuS2lsbEFjdGlvblJlc3BvbnNlIgASZgoPRXhlY3V0aW9uU3RhdHVzEicub2xpdmV0aW4uYXBpLnYxLkV4ZWN1dGlvblN0YXR1c1JlcXVlc3QaKC5vbGl2ZXRpbi5hcGkudjEuRXhlY3V0aW9uU3RhdHVzUmVzcG9uc2UiABJOCgdHZXRMb2dzEh8ub2xpdmV0aW4uYXBpLnYxLkdldExvZ3NSZXF1ZXN0GiAub2xpdmV0aW4uYXBpLnYxLkdldExvZ3NSZXNwb25zZSIAEmAKDUdldEFjdGlvbkxvZ3MSJS5vbGl2ZXRpbi5hcGkudjEuR2V0QWN0aW9uTG9nc1JlcXVlc3QaJi5vbGl2ZXRpbi5hcGkudjEuR2V0QWN0aW9uTG9nc1Jlc3BvbnNlIgASbAoRR2V0RXhlY3V0aW9uUXVldWUSKS5vbGl2ZXRpbi5hcGkudjEuR2V0RXhlY3V0aW9uUXVldWVSZXF1ZXN0Gioub2xpdmV0aW4uYXBpLnYxLkdldEV4ZWN1dGlvblF1ZXVlUmVzcG9uc2UiABJ1ChRWYWxpZGF0ZUFyZ3VtZW50VHlwZRIsLm9saXZldGluLmFwaS52MS5WYWxpZGF0ZUFyZ3VtZW50VHlwZVJlcXVlc3QaLS5vbGl2ZXRpbi5hcGkudjEuVmFsaWRhdGVBcmd1bWVudFR5cGVSZXNwb25zZSIAEksKBldob0FtSRIeLm9saXZldGluLmFwaS52MS5XaG9BbUlSZXF1ZXN0Gh8ub2xpdmV0aW4uYXBpLnYxLldob0FtSVJlc3BvbnNlIgASVAoJU29zUmVwb3J0EiEub2xpdmV0aW4uYXBpLnYxLlNvc1JlcG9ydFJlcXVlc3QaIi5vbGl2ZXRpbi5hcGkudjEuU29zUmVwb3J0UmVzcG9uc2UiABJRCghEdW1wVmFycxIgLm9saXZldGluLmFwaS52MS5EdW1wVmFyc1JlcXVlc3QaIS5vbGl2ZXRpbi5hcGkudjEuRHVtcFZhcnNSZXNwb25zZSIAEngKFUR1bXBQdWJsaWNJZEFjdGlvbk1hcBItLm9saXZldGluLmFwaS52MS5EdW1wUHVibGljSWRBY3Rpb25NYXBSZXF1ZXN0Gi4ub2xpdmV0aW4uYXBpLnYxLkR1bXBQdWJsaWNJZEFjdGlvbk1hcFJlc3BvbnNlIgASVAoJR2V0UmVhZHl6EiEub2xpdmV0aW4uYXBpLnYxLkdldFJlYWR5elJlcXVlc3QaIi5vbGl2ZXRpbi5hcGkudjEuR2V0UmVhZHl6UmVzcG9uc2UiABJjCg5Mb2NhbFVzZXJMb2dpbhImLm9saXZldGluLmFwaS52MS5Mb2NhbFVzZXJMb2dpblJlcXVlc3QaJy5vbGl2ZXRpbi5hcGkudjEuTG9jYWxVc2VyTG9naW5SZXNwb25zZSIAEl0KDFBhc3N3b3JkSGFzaBIkLm9saXZldGluLmFwaS52MS5QYXNzd29yZEhhc2hSZXF1ZXN0GiUub2xpdmV0aW4uYXBpLnYxLlBhc3N3b3JkSGFzaFJlc3BvbnNlIgASSwoGTG9nb3V0Eh4ub2xpdmV0aW4uYXBpLnYxLkxvZ291dFJlcXVlc3QaHy5vbGl2ZXRpbi5hcGkudjEuTG9nb3V0UmVzcG9uc2UiABJcCgtFdmVudFN0cmVhbRIjLm9saXZldGluLmFwaS52MS5FdmVudFN0cmVhbVJlcXVlc3QaJC5vbGl2ZXRpbi5hcGkudjEuRXZlbnRTdHJlYW1SZXNwb25zZSIAMAESYwoOR2V0RGlhZ25vc3RpY3MSJi5vbGl2ZXRpbi5hcGkudjEuR2V0RGlhZ25vc3RpY3NSZXF1ZXN0Gicub2xpdmV0aW4uYXBpLnYxLkdldERpYWdub3N0aWNzUmVzcG9uc2UiABJFCgRJbml0Ehwub2xpdmV0aW4uYXBpLnYxLkluaXRSZXF1ZXN0Gh0ub2xpdmV0aW4uYXBpLnYxLkluaXRSZXNwb25zZSIAEmkKEEdldEFjdGlvbkJpbmRpbmcSKC5vbGl2ZXRpbi5hcGkudjEuR2V0QWN0aW9uQmluZGluZ1JlcXVlc3QaKS5vbGl2ZXRpbi5hcGkudjEuR2V0QWN0aW9uQmluZGluZ1Jlc3BvbnNlIgASWgoLR2V0RW50aXRpZXMSIy5vbGl2ZXRpbi5hcGkudjEuR2V0RW50aXRpZXNSZXF1ZXN0GiQub2xpdmV0aW4uYXBpLnYxLkdldEVudGl0aWVzUmVzcG9uc2UiABJJCglHZXRFbnRpdHkSIS5vbGl2ZXRpbi5hcGkudjEuR2V0RW50aXR5UmVxdWVzdBoXLm9saXZldGluLmFwaS52MS5FbnRpdHkiAEI4WjZnaXRodWIuY29tL09saXZlVGluL09saXZlVGluL2dlbi9vbGl2ZXRpbi9hcGkvdjE7YXBpdjFiBnByb3RvMw"); /** * Describes the message olivetin.api.v1.Action. diff --git a/frontend/resources/vue/views/ArgumentForm.vue b/frontend/resources/vue/views/ArgumentForm.vue index e00f23e..ba243bd 100644 --- a/frontend/resources/vue/views/ArgumentForm.vue +++ b/frontend/resources/vue/views/ArgumentForm.vue @@ -70,6 +70,11 @@ import { useRouter } from 'vue-router' import { requestReconnectNow } from '../../../js/websocket.js' import ChoiceCombobox from '../components/ChoiceCombobox.vue' import ChoiceChecklist from '../components/ChoiceChecklist.vue' +import { + actionJustificationTemplate, + actionRequiresJustification, + applyArgumentTemplate +} from '../utils/justificationTemplate.js' const router = useRouter() @@ -114,8 +119,7 @@ async function setup() { icon.value = action.icon popupOnStart.value = action.popupOnStart || '' actionArguments.value = action.arguments || [] - justificationRequired.value = action.justification || false - justificationValue.value = '' + justificationRequired.value = actionRequiresJustification(action.justification) argValues.value = {} formErrors.value = {} confirmationChecked.value = false @@ -151,6 +155,12 @@ async function setup() { } }) + const prefilledJustification = applyArgumentTemplate( + actionJustificationTemplate(action.justification), + argValues.value + ) + justificationValue.value = prefilledJustification.trim() === '' ? '' : prefilledJustification + // Run initial validation on all fields after DOM is updated await nextTick() for (const arg of actionArguments.value) { diff --git a/integration-tests/Makefile b/integration-tests/Makefile index 48dfed1..56e91fb 100644 --- a/integration-tests/Makefile +++ b/integration-tests/Makefile @@ -1,8 +1,12 @@ -default: test-install test-run +default: test-install prep test-run test-install: npm install --no-fund +prep: + $(MAKE) -wC .. webui-dist + $(MAKE) -wC ../service compile-currentenv + test-run: # GitHub Actions fails badly on the default timeout of 2000ms npx mocha tests --recursive -t 10000 @@ -24,4 +28,4 @@ getsnapshot: rm -rf /opt/OliveTin-snapshot/* gh run download -D /opt/OliveTin-snapshot/ -.PHONY: default find-flakey-tests find-flakey-tests-inf +.PHONY: default find-flakey-tests find-flakey-tests-inf prep diff --git a/proto/olivetin/api/v1/olivetin.proto b/proto/olivetin/api/v1/olivetin.proto index 013619d..c0c0f30 100644 --- a/proto/olivetin/api/v1/olivetin.proto +++ b/proto/olivetin/api/v1/olivetin.proto @@ -20,7 +20,8 @@ message Action { repeated string exec_on_file_changed_in_dir = 13; string exec_on_calendar_file = 14; repeated ActionWebhookExecHint exec_on_webhooks = 15; - string justification = 16; + reserved 16; + string justification = 20; bool has_running_instance = 17; bool has_queued_instance = 18; repeated ActionGroupMembership groups = 19; diff --git a/service/gen/olivetin/api/v1/olivetin.pb.go b/service/gen/olivetin/api/v1/olivetin.pb.go index f65e732..8886d90 100644 --- a/service/gen/olivetin/api/v1/olivetin.pb.go +++ b/service/gen/olivetin/api/v1/olivetin.pb.go @@ -38,7 +38,7 @@ type Action struct { ExecOnFileChangedInDir []string `protobuf:"bytes,13,rep,name=exec_on_file_changed_in_dir,json=execOnFileChangedInDir,proto3" json:"exec_on_file_changed_in_dir,omitempty"` ExecOnCalendarFile string `protobuf:"bytes,14,opt,name=exec_on_calendar_file,json=execOnCalendarFile,proto3" json:"exec_on_calendar_file,omitempty"` ExecOnWebhooks []*ActionWebhookExecHint `protobuf:"bytes,15,rep,name=exec_on_webhooks,json=execOnWebhooks,proto3" json:"exec_on_webhooks,omitempty"` - Justification string `protobuf:"bytes,16,opt,name=justification,proto3" json:"justification,omitempty"` + Justification string `protobuf:"bytes,20,opt,name=justification,proto3" json:"justification,omitempty"` HasRunningInstance bool `protobuf:"varint,17,opt,name=has_running_instance,json=hasRunningInstance,proto3" json:"has_running_instance,omitempty"` HasQueuedInstance bool `protobuf:"varint,18,opt,name=has_queued_instance,json=hasQueuedInstance,proto3" json:"has_queued_instance,omitempty"` Groups []*ActionGroupMembership `protobuf:"bytes,19,rep,name=groups,proto3" json:"groups,omitempty"` @@ -4575,7 +4575,7 @@ var File_olivetin_api_v1_olivetin_proto protoreflect.FileDescriptor const file_olivetin_api_v1_olivetin_proto_rawDesc = "" + "\n" + - "\x1eolivetin/api/v1/olivetin.proto\x12\x0folivetin.api.v1\"\xd1\x06\n" + + "\x1eolivetin/api/v1/olivetin.proto\x12\x0folivetin.api.v1\"\xd7\x06\n" + "\x06Action\x12\x1d\n" + "\n" + "binding_id\x18\x01 \x01(\tR\tbindingId\x12\x14\n" + @@ -4595,10 +4595,10 @@ const file_olivetin_api_v1_olivetin_proto_rawDesc = "" + "\x1bexec_on_file_changed_in_dir\x18\r \x03(\tR\x16execOnFileChangedInDir\x121\n" + "\x15exec_on_calendar_file\x18\x0e \x01(\tR\x12execOnCalendarFile\x12P\n" + "\x10exec_on_webhooks\x18\x0f \x03(\v2&.olivetin.api.v1.ActionWebhookExecHintR\x0eexecOnWebhooks\x12$\n" + - "\rjustification\x18\x10 \x01(\tR\rjustification\x120\n" + + "\rjustification\x18\x14 \x01(\tR\rjustification\x120\n" + "\x14has_running_instance\x18\x11 \x01(\bR\x12hasRunningInstance\x12.\n" + "\x13has_queued_instance\x18\x12 \x01(\bR\x11hasQueuedInstance\x12>\n" + - "\x06groups\x18\x13 \x03(\v2&.olivetin.api.v1.ActionGroupMembershipR\x06groups\"q\n" + + "\x06groups\x18\x13 \x03(\v2&.olivetin.api.v1.ActionGroupMembershipR\x06groupsJ\x04\b\x10\x10\x11\"q\n" + "\x15ActionGroupMembership\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12%\n" + "\x0emax_concurrent\x18\x02 \x01(\x05R\rmaxConcurrent\x12\x1d\n" + diff --git a/service/internal/api/api_justification.go b/service/internal/api/api_justification.go index 02135f0..56f1927 100644 --- a/service/internal/api/api_justification.go +++ b/service/internal/api/api_justification.go @@ -12,6 +12,7 @@ import ( "github.com/OliveTin/OliveTin/internal/entities" "github.com/OliveTin/OliveTin/internal/executor" "github.com/OliveTin/OliveTin/internal/tpl" + log "github.com/sirupsen/logrus" ) func validateJustificationRequired(action *config.Action, justification string, user *authpublic.AuthenticatedUser) error { @@ -58,6 +59,11 @@ func resolveJustificationFromTemplate(action *config.Action, binding *executor.A resolved, err := tpl.ParseTemplateWithActionContext(templateText, bindingEntity(binding), args) if err != nil { + log.WithFields(log.Fields{ + "template": templateText, + "entity": bindingEntity(binding), + "error": err, + }).Warn("Failed to resolve justification template") return fallback } diff --git a/service/internal/api/api_justification_test.go b/service/internal/api/api_justification_test.go index fc76f78..4758fd4 100644 --- a/service/internal/api/api_justification_test.go +++ b/service/internal/api/api_justification_test.go @@ -21,7 +21,7 @@ func TestStartActionRequiresJustificationForGuest(t *testing.T) { action := &config.Action{ Title: "Send email", ID: "send_email", - Justification: " ", + Justification: config.JustificationRequiredNoTemplate, Shell: "echo done", } cfg.Actions = append(cfg.Actions, action) @@ -85,7 +85,7 @@ func TestBuildActionExposesBlankRequiredJustification(t *testing.T) { action := &config.Action{ Title: "Audited action", ID: "audited", - Justification: " ", + Justification: config.JustificationRequiredNoTemplate, Shell: "echo hi", } cfg.Actions = append(cfg.Actions, action) @@ -101,7 +101,7 @@ func TestBuildActionExposesBlankRequiredJustification(t *testing.T) { }) require.NotNil(t, pb) - assert.Equal(t, " ", pb.Justification) + assert.Equal(t, config.JustificationRequiredNoTemplate, pb.Justification) } func TestResolveStartJustificationUsesTemplateWhenClientValueEmpty(t *testing.T) { @@ -168,7 +168,7 @@ func TestStartActionResolvesJustificationTemplateForGuest(t *testing.T) { func TestValidateJustificationRequiredAllowsSystemUser(t *testing.T) { cfg := config.DefaultConfig() - action := &config.Action{Title: "Cron job", Justification: " "} + action := &config.Action{Title: "Cron job", Justification: config.JustificationRequiredNoTemplate} err := validateJustificationRequired(action, "", auth.UserFromSystem(cfg, "cron")) require.NoError(t, err) diff --git a/service/internal/config/config.go b/service/internal/config/config.go index c994d9d..a3b255a 100644 --- a/service/internal/config/config.go +++ b/service/internal/config/config.go @@ -7,6 +7,9 @@ import ( // ReservedArgumentNamePrefix is reserved for OliveTin-injected system arguments. const ReservedArgumentNamePrefix = "ot_" +// JustificationRequiredNoTemplate requires a manual justification with no prefilled template. +const JustificationRequiredNoTemplate = " " + // Action represents the core functionality of OliveTin - commands that show up // as buttons in the UI. type Action struct { @@ -43,7 +46,7 @@ func (action *Action) RequiresJustification() bool { } func (action *Action) JustificationTemplateText() string { - if action == nil || !action.RequiresJustification() { + if !action.RequiresJustification() { return "" } diff --git a/service/internal/config/config_reloader.go b/service/internal/config/config_reloader.go index b931e80..bc8631d 100644 --- a/service/internal/config/config_reloader.go +++ b/service/internal/config/config_reloader.go @@ -55,6 +55,7 @@ func unmarshalRoot(k *koanf.Koanf, cfg *Config) bool { DecoderConfig: &mapstructure.DecoderConfig{ DecodeHook: mapstructure.ComposeDecodeHookFunc( envDecodeHookFunc, + justificationDecodeHookFunc, mapstructure.StringToTimeDurationHookFunc(), mapstructure.TextUnmarshallerHookFunc(), ), @@ -259,6 +260,18 @@ func mergeFunc(src map[string]interface{}, dest map[string]interface{}) error { var envRegex = regexp.MustCompile(`\${{ *?(\S+) *?}}`) +func justificationDecodeHookFunc(from reflect.Type, to reflect.Type, data any) (any, error) { + if to.Kind() != reflect.String || from.Kind() != reflect.Bool { + return data, nil + } + + if data.(bool) { + return JustificationRequiredNoTemplate, nil + } + + return "", nil +} + func envDecodeHookFunc(from reflect.Type, to reflect.Type, data any) (any, error) { log.Debugf("envDecodeHookFunc called: from=%v, to=%v, data=%v", from, to, data) if from.Kind() != reflect.String { diff --git a/service/internal/config/justification_compat_test.go b/service/internal/config/justification_compat_test.go new file mode 100644 index 0000000..28908dc --- /dev/null +++ b/service/internal/config/justification_compat_test.go @@ -0,0 +1,58 @@ +package config + +import ( + "testing" + + "github.com/knadh/koanf/parsers/yaml" + "github.com/knadh/koanf/providers/rawbytes" + "github.com/knadh/koanf/v2" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestJustificationDecodeHookMigratesLegacyBooleanFalse(t *testing.T) { + cfg := loadJustificationCompatConfig(t, ` +actions: + - title: Legacy disabled + shell: echo hi + justification: false +`) + + require.Len(t, cfg.Actions, 1) + assert.Empty(t, cfg.Actions[0].Justification) +} + +func TestJustificationDecodeHookMigratesLegacyBooleanTrue(t *testing.T) { + cfg := loadJustificationCompatConfig(t, ` +actions: + - title: Legacy required + shell: echo hi + justification: true +`) + + require.Len(t, cfg.Actions, 1) + assert.Equal(t, JustificationRequiredNoTemplate, cfg.Actions[0].Justification) +} + +func TestSanitizeJustificationMigratesWeaklyTypedLegacyStrings(t *testing.T) { + action := &Action{Justification: "false"} + action.sanitizeJustification() + assert.Empty(t, action.Justification) + + action.Justification = "true" + action.sanitizeJustification() + assert.Equal(t, JustificationRequiredNoTemplate, action.Justification) +} + +func loadJustificationCompatConfig(t *testing.T, yamlBody string) *Config { + t.Helper() + + k := koanf.New(".") + require.NoError(t, k.Load(rawbytes.Provider([]byte(yamlBody)), yaml.Parser())) + + cfg := DefaultConfig() + require.True(t, unmarshalRoot(k, cfg)) + cfg.Sanitize() + + return cfg +} diff --git a/service/internal/config/sanitize.go b/service/internal/config/sanitize.go index 2ee2313..a24061e 100644 --- a/service/internal/config/sanitize.go +++ b/service/internal/config/sanitize.go @@ -227,6 +227,7 @@ func (action *Action) sanitize(cfg *Config) { action.ID = getActionID(action) action.Icon = lookupHTMLIcon(action.Icon, cfg.DefaultIconForActions) migrateActionOnClick(action) + action.sanitizeJustification() action.OnClick = sanitizeOnClick(action.OnClick, cfg) action.PopupOnStart = action.OnClick @@ -449,6 +450,15 @@ func migrateActionOnClick(action *Action) { } } +func (action *Action) sanitizeJustification() { + switch action.Justification { + case "false": + action.Justification = "" + case "true": + action.Justification = JustificationRequiredNoTemplate + } +} + func shouldMigrateDefaultOnClickFromPopup(onClick, popupOnStart string) bool { if popupOnStart == "" { return false