From 42003da38452d8326838000a8c9b08bfdfca4515 Mon Sep 17 00:00:00 2001 From: jamesread Date: Fri, 26 Jun 2026 23:25:24 +0100 Subject: [PATCH] fix: rerun args --- .../gen/olivetin/api/v1/olivetin_pb.d.ts | 7 +- .../gen/olivetin/api/v1/olivetin_pb.js | 4 +- .../resources/vue/utils/rerunArguments.js | 59 ++++ .../vue/utils/rerunArguments.test.mjs | 103 ++++++ proto/olivetin/api/v1/olivetin.proto | 1 + service/gen/olivetin/api/v1/olivetin.pb.go | 178 +++++----- service/internal/api/api.go | 7 +- service/internal/api/api_log_arguments.go | 38 +++ .../internal/api/api_log_arguments_test.go | 307 ++++++++++++++++++ service/internal/executor/executor.go | 2 + service/internal/executor/log_arguments.go | 95 ++++++ .../internal/executor/log_arguments_test.go | 121 +++++++ 12 files changed, 833 insertions(+), 89 deletions(-) create mode 100644 frontend/resources/vue/utils/rerunArguments.js create mode 100644 frontend/resources/vue/utils/rerunArguments.test.mjs create mode 100644 service/internal/api/api_log_arguments.go create mode 100644 service/internal/api/api_log_arguments_test.go create mode 100644 service/internal/executor/log_arguments.go create mode 100644 service/internal/executor/log_arguments_test.go 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 78e8d7a..03a2c00 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 @@ -1,4 +1,4 @@ -// @generated by protoc-gen-es v2.12.0 +// @generated by protoc-gen-es v2.12.1 // @generated from file olivetin/api/v1/olivetin.proto (package olivetin.api.v1, syntax proto3) /* eslint-disable */ @@ -751,6 +751,11 @@ export declare type LogEntry = Message<"olivetin.api.v1.LogEntry"> & { * @generated from field: string justification = 23; */ justification: string; + + /** + * @generated from field: repeated olivetin.api.v1.StartActionArgument arguments = 24; + */ + arguments: StartActionArgument[]; }; /** 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 0ca3819..5e993ca 100644 --- a/frontend/resources/scripts/gen/olivetin/api/v1/olivetin_pb.js +++ b/frontend/resources/scripts/gen/olivetin/api/v1/olivetin_pb.js @@ -1,4 +1,4 @@ -// @generated by protoc-gen-es v2.12.0 +// @generated by protoc-gen-es v2.12.1 // @generated from file olivetin/api/v1/olivetin.proto (package olivetin.api.v1, syntax proto3) /* eslint-disable */ @@ -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("Ch5vbGl2ZXRpbi9hcGkvdjEvb2xpdmV0aW4ucHJvdG8SD29saXZldGluLmFwaS52MSLABAoGQWN0aW9uEhIKCmJpbmRpbmdfaWQYASABKAkSDQoFdGl0bGUYAiABKAkSDAoEaWNvbhgDIAEoCRIQCghjYW5fZXhlYxgEIAEoCBIyCglhcmd1bWVudHMYBSADKAsyHy5vbGl2ZXRpbi5hcGkudjEuQWN0aW9uQXJndW1lbnQSFgoOcG9wdXBfb25fc3RhcnQYBiABKAkSDQoFb3JkZXIYByABKAUSDwoHdGltZW91dBgIIAEoBRIjChtkYXRldGltZV9yYXRlX2xpbWl0X2V4cGlyZXMYCSABKAkSFwoPZXhlY19vbl9zdGFydHVwGAogASgIEhQKDGV4ZWNfb25fY3JvbhgLIAMoCRIjChtleGVjX29uX2ZpbGVfY3JlYXRlZF9pbl9kaXIYDCADKAkSIwobZXhlY19vbl9maWxlX2NoYW5nZWRfaW5fZGlyGA0gAygJEh0KFWV4ZWNfb25fY2FsZW5kYXJfZmlsZRgOIAEoCRJAChBleGVjX29uX3dlYmhvb2tzGA8gAygLMiYub2xpdmV0aW4uYXBpLnYxLkFjdGlvbldlYmhvb2tFeGVjSGludBIVCg1qdXN0aWZpY2F0aW9uGBAgASgIEhwKFGhhc19ydW5uaW5nX2luc3RhbmNlGBEgASgIEhsKE2hhc19xdWV1ZWRfaW5zdGFuY2UYEiABKAgSNgoGZ3JvdXBzGBMgAygLMiYub2xpdmV0aW4uYXBpLnYxLkFjdGlvbkdyb3VwTWVtYmVyc2hpcCJRChVBY3Rpb25Hcm91cE1lbWJlcnNoaXASDAoEbmFtZRgBIAEoCRIWCg5tYXhfY29uY3VycmVudBgCIAEoBRISCgpxdWV1ZV9zaXplGAMgASgFIsMCChVBY3Rpb25XZWJob29rRXhlY0hpbnQSEAoIdGVtcGxhdGUYASABKAkSEgoKbWF0Y2hfcGF0aBgCIAEoCRJPCg1tYXRjaF9oZWFkZXJzGAMgAygLMjgub2xpdmV0aW4uYXBpLnYxLkFjdGlvbldlYmhvb2tFeGVjSGludC5NYXRjaEhlYWRlcnNFbnRyeRJLCgttYXRjaF9xdWVyeRgEIAMoCzI2Lm9saXZldGluLmFwaS52MS5BY3Rpb25XZWJob29rRXhlY0hpbnQuTWF0Y2hRdWVyeUVudHJ5GjMKEU1hdGNoSGVhZGVyc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEaMQoPTWF0Y2hRdWVyeUVudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiuwIKDkFjdGlvbkFyZ3VtZW50EgwKBG5hbWUYASABKAkSDQoFdGl0bGUYAiABKAkSDAoEdHlwZRgDIAEoCRIVCg1kZWZhdWx0X3ZhbHVlGAQgASgJEjYKB2Nob2ljZXMYBSADKAsyJS5vbGl2ZXRpbi5hcGkudjEuQWN0aW9uQXJndW1lbnRDaG9pY2USEwoLZGVzY3JpcHRpb24YBiABKAkSRQoLc3VnZ2VzdGlvbnMYByADKAsyMC5vbGl2ZXRpbi5hcGkudjEuQWN0aW9uQXJndW1lbnQuU3VnZ2VzdGlvbnNFbnRyeRIfChdzdWdnZXN0aW9uc19icm93c2VyX2tleRgIIAEoCRoyChBTdWdnZXN0aW9uc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiNAoUQWN0aW9uQXJndW1lbnRDaG9pY2USDQoFdmFsdWUYASABKAkSDQoFdGl0bGUYAiABKAkisgEKBkVudGl0eRINCgV0aXRsZRgBIAEoCRISCgp1bmlxdWVfa2V5GAIgASgJEgwKBHR5cGUYAyABKAkSEwoLZGlyZWN0b3JpZXMYBCADKAkSMwoGZmllbGRzGAUgAygLMiMub2xpdmV0aW4uYXBpLnYxLkVudGl0eS5GaWVsZHNFbnRyeRotCgtGaWVsZHNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBIlQKFEdldERhc2hib2FyZFJlc3BvbnNlEg0KBXRpdGxlGAEgASgJEi0KCWRhc2hib2FyZBgEIAEoCzIaLm9saXZldGluLmFwaS52MS5EYXNoYm9hcmQiXwoPRWZmZWN0aXZlUG9saWN5EhgKEHNob3dfZGlhZ25vc3RpY3MYASABKAgSFQoNc2hvd19sb2dfbGlzdBgCIAEoCBIbChNzaG93X3ZlcnNpb25fbnVtYmVyGAMgASgIIk0KE0dldERhc2hib2FyZFJlcXVlc3QSDQoFdGl0bGUYASABKAkSEwoLZW50aXR5X3R5cGUYAiABKAkSEgoKZW50aXR5X2tleRgDIAEoCSJRCglEYXNoYm9hcmQSDQoFdGl0bGUYASABKAkSNQoIY29udGVudHMYAiADKAsyIy5vbGl2ZXRpbi5hcGkudjEuRGFzaGJvYXJkQ29tcG9uZW50ItsBChJEYXNoYm9hcmRDb21wb25lbnQSDQoFdGl0bGUYASABKAkSDAoEdHlwZRgCIAEoCRI1Cghjb250ZW50cxgDIAMoCzIjLm9saXZldGluLmFwaS52MS5EYXNoYm9hcmRDb21wb25lbnQSDAoEaWNvbhgEIAEoCRIRCgljc3NfY2xhc3MYBSABKAkSJwoGYWN0aW9uGAYgASgLMhcub2xpdmV0aW4uYXBpLnYxLkFjdGlvbhITCgtlbnRpdHlfdHlwZRgHIAEoCRISCgplbnRpdHlfa2V5GAggASgJIpQBChJTdGFydEFjdGlvblJlcXVlc3QSEgoKYmluZGluZ19pZBgBIAEoCRI3Cglhcmd1bWVudHMYAiADKAsyJC5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25Bcmd1bWVudBIaChJ1bmlxdWVfdHJhY2tpbmdfaWQYAyABKAkSFQoNanVzdGlmaWNhdGlvbhgEIAEoCSIyChNTdGFydEFjdGlvbkFyZ3VtZW50EgwKBG5hbWUYASABKAkSDQoFdmFsdWUYAiABKAkiNAoTU3RhcnRBY3Rpb25SZXNwb25zZRIdChVleGVjdXRpb25fdHJhY2tpbmdfaWQYAiABKAkifgoZU3RhcnRBY3Rpb25BbmRXYWl0UmVxdWVzdBIRCglhY3Rpb25faWQYASABKAkSNwoJYXJndW1lbnRzGAIgAygLMiQub2xpdmV0aW4uYXBpLnYxLlN0YXJ0QWN0aW9uQXJndW1lbnQSFQoNanVzdGlmaWNhdGlvbhgDIAEoCSJKChpTdGFydEFjdGlvbkFuZFdhaXRSZXNwb25zZRIsCglsb2dfZW50cnkYASABKAsyGS5vbGl2ZXRpbi5hcGkudjEuTG9nRW50cnkiLAoXU3RhcnRBY3Rpb25CeUdldFJlcXVlc3QSEQoJYWN0aW9uX2lkGAEgASgJIjkKGFN0YXJ0QWN0aW9uQnlHZXRSZXNwb25zZRIdChVleGVjdXRpb25fdHJhY2tpbmdfaWQYAiABKAkiMwoeU3RhcnRBY3Rpb25CeUdldEFuZFdhaXRSZXF1ZXN0EhEKCWFjdGlvbl9pZBgBIAEoCSJPCh9TdGFydEFjdGlvbkJ5R2V0QW5kV2FpdFJlc3BvbnNlEiwKCWxvZ19lbnRyeRgBIAEoCzIZLm9saXZldGluLmFwaS52MS5Mb2dFbnRyeSJeCg5HZXRMb2dzUmVxdWVzdBIUCgxzdGFydF9vZmZzZXQYASABKAMSEwoLZGF0ZV9maWx0ZXIYAiABKAkSEQoJcGFnZV9zaXplGAMgASgDEg4KBmZpbHRlchgEIAEoCSLbAwoITG9nRW50cnkSGAoQZGF0ZXRpbWVfc3RhcnRlZBgBIAEoCRIUCgxhY3Rpb25fdGl0bGUYAiABKAkSDgoGb3V0cHV0GAMgASgJEhEKCXRpbWVkX291dBgFIAEoCBIRCglleGl0X2NvZGUYBiABKAUSDAoEdXNlchgHIAEoCRISCgp1c2VyX2NsYXNzGAggASgJEhMKC2FjdGlvbl9pY29uGAkgASgJEgwKBHRhZ3MYCiADKAkSHQoVZXhlY3V0aW9uX3RyYWNraW5nX2lkGAsgASgJEhkKEWRhdGV0aW1lX2ZpbmlzaGVkGAwgASgJEhkKEWV4ZWN1dGlvbl9zdGFydGVkGA4gASgIEhoKEmV4ZWN1dGlvbl9maW5pc2hlZBgPIAEoCBIPCgdibG9ja2VkGBAgASgIEhYKDmRhdGV0aW1lX2luZGV4GBEgASgDEhAKCGNhbl9raWxsGBIgASgIEiMKG2RhdGV0aW1lX3JhdGVfbGltaXRfZXhwaXJlcxgTIAEoCRISCgpiaW5kaW5nX2lkGBQgASgJEg4KBnF1ZXVlZBgVIAEoCBIYChBxdWV1ZWRfZm9yX2dyb3VwGBYgASgJEhUKDWp1c3RpZmljYXRpb24YFyABKAkikQEKD0dldExvZ3NSZXNwb25zZRInCgRsb2dzGAEgAygLMhkub2xpdmV0aW4uYXBpLnYxLkxvZ0VudHJ5EhcKD2NvdW50X3JlbWFpbmluZxgCIAEoAxIRCglwYWdlX3NpemUYAyABKAMSEwoLdG90YWxfY291bnQYBCABKAMSFAoMc3RhcnRfb2Zmc2V0GAUgASgDIj8KFEdldEFjdGlvbkxvZ3NSZXF1ZXN0EhEKCWFjdGlvbl9pZBgBIAEoCRIUCgxzdGFydF9vZmZzZXQYAiABKAMilwEKFUdldEFjdGlvbkxvZ3NSZXNwb25zZRInCgRsb2dzGAEgAygLMhkub2xpdmV0aW4uYXBpLnYxLkxvZ0VudHJ5EhcKD2NvdW50X3JlbWFpbmluZxgCIAEoAxIRCglwYWdlX3NpemUYAyABKAMSEwoLdG90YWxfY291bnQYBCABKAMSFAoMc3RhcnRfb2Zmc2V0GAUgASgDIhoKGEdldEV4ZWN1dGlvblF1ZXVlUmVxdWVzdCLGAQoURXhlY3V0aW9uUXVldWVBY3Rpb24SEgoKYmluZGluZ19pZBgBIAEoCRIUCgxhY3Rpb25fdGl0bGUYAiABKAkSEwoLYWN0aW9uX2ljb24YAyABKAkSFgoObWF4X2NvbmN1cnJlbnQYBCABKAUSFAoMYWN0aXZlX2NvdW50GAUgASgFEhUKDWVudGl0eV9wcmVmaXgYBiABKAkSKgoHZW50cmllcxgHIAMoCzIZLm9saXZldGluLmFwaS52MS5Mb2dFbnRyeSLBAQoTRXhlY3V0aW9uUXVldWVHcm91cBIMCgRuYW1lGAEgASgJEgwKBGljb24YAiABKAkSFgoObWF4X2NvbmN1cnJlbnQYAyABKAUSFAoMYWN0aXZlX2NvdW50GAQgASgFEjYKB2FjdGlvbnMYBSADKAsyJS5vbGl2ZXRpbi5hcGkudjEuRXhlY3V0aW9uUXVldWVBY3Rpb24SFAoMcXVldWVkX2NvdW50GAYgASgFEhIKCnF1ZXVlX3NpemUYByABKAUiZwoZR2V0RXhlY3V0aW9uUXVldWVSZXNwb25zZRI0CgZncm91cHMYASADKAsyJC5vbGl2ZXRpbi5hcGkudjEuRXhlY3V0aW9uUXVldWVHcm91cBIUCgx0b3RhbF9hY3RpdmUYAiABKAUiZQobVmFsaWRhdGVBcmd1bWVudFR5cGVSZXF1ZXN0Eg0KBXZhbHVlGAEgASgJEgwKBHR5cGUYAiABKAkSEgoKYmluZGluZ19pZBgDIAEoCRIVCg1hcmd1bWVudF9uYW1lGAQgASgJIkIKHFZhbGlkYXRlQXJndW1lbnRUeXBlUmVzcG9uc2USDQoFdmFsaWQYASABKAgSEwoLZGVzY3JpcHRpb24YAiABKAkiNgoVV2F0Y2hFeGVjdXRpb25SZXF1ZXN0Eh0KFWV4ZWN1dGlvbl90cmFja2luZ19pZBgBIAEoCSImChRXYXRjaEV4ZWN1dGlvblVwZGF0ZRIOCgZ1cGRhdGUYASABKAkiSgoWRXhlY3V0aW9uU3RhdHVzUmVxdWVzdBIdChVleGVjdXRpb25fdHJhY2tpbmdfaWQYASABKAkSEQoJYWN0aW9uX2lkGAIgASgJImEKGURhc2hib2FyZE5hdmlnYXRpb25UYXJnZXQSDQoFdGl0bGUYASABKAkSEwoLZW50aXR5X3R5cGUYAiABKAkSEgoKZW50aXR5X2tleRgDIAEoCRIMCgRwYXRoGAQgASgJIo8BChdFeGVjdXRpb25TdGF0dXNSZXNwb25zZRIsCglsb2dfZW50cnkYASABKAsyGS5vbGl2ZXRpbi5hcGkudjEuTG9nRW50cnkSRgoSYmFja190b19kYXNoYm9hcmRzGAIgAygLMioub2xpdmV0aW4uYXBpLnYxLkRhc2hib2FyZE5hdmlnYXRpb25UYXJnZXQiDwoNV2hvQW1JUmVxdWVzdCJsCg5XaG9BbUlSZXNwb25zZRIaChJhdXRoZW50aWNhdGVkX3VzZXIYASABKAkSEQoJdXNlcmdyb3VwGAIgASgJEhAKCHByb3ZpZGVyGAMgASgJEgwKBGFjbHMYBCADKAkSCwoDc2lkGAUgASgJIhIKEFNvc1JlcG9ydFJlcXVlc3QiIgoRU29zUmVwb3J0UmVzcG9uc2USDQoFYWxlcnQYASABKAkiEQoPRHVtcFZhcnNSZXF1ZXN0IpUBChBEdW1wVmFyc1Jlc3BvbnNlEg0KBWFsZXJ0GAEgASgJEkEKCGNvbnRlbnRzGAIgAygLMi8ub2xpdmV0aW4uYXBpLnYxLkR1bXBWYXJzUmVzcG9uc2UuQ29udGVudHNFbnRyeRovCg1Db250ZW50c0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiOwoMRGVidWdCaW5kaW5nEhQKDGFjdGlvbl90aXRsZRgBIAEoCRIVCg1lbnRpdHlfcHJlZml4GAIgASgJIh4KHER1bXBQdWJsaWNJZEFjdGlvbk1hcFJlcXVlc3QizgEKHUR1bXBQdWJsaWNJZEFjdGlvbk1hcFJlc3BvbnNlEg0KBWFsZXJ0GAEgASgJEk4KCGNvbnRlbnRzGAIgAygLMjwub2xpdmV0aW4uYXBpLnYxLkR1bXBQdWJsaWNJZEFjdGlvbk1hcFJlc3BvbnNlLkNvbnRlbnRzRW50cnkaTgoNQ29udGVudHNFbnRyeRILCgNrZXkYASABKAkSLAoFdmFsdWUYAiABKAsyHS5vbGl2ZXRpbi5hcGkudjEuRGVidWdCaW5kaW5nOgI4ASISChBHZXRSZWFkeXpSZXF1ZXN0IiMKEUdldFJlYWR5elJlc3BvbnNlEg4KBnN0YXR1cxgBIAEoCSIUChJFdmVudFN0cmVhbVJlcXVlc3QimQMKE0V2ZW50U3RyZWFtUmVzcG9uc2USPQoOZW50aXR5X2NoYW5nZWQYAiABKAsyIy5vbGl2ZXRpbi5hcGkudjEuRXZlbnRFbnRpdHlDaGFuZ2VkSAASPQoOY29uZmlnX2NoYW5nZWQYAyABKAsyIy5vbGl2ZXRpbi5hcGkudjEuRXZlbnRDb25maWdDaGFuZ2VkSAASRQoSZXhlY3V0aW9uX2ZpbmlzaGVkGAQgASgLMicub2xpdmV0aW4uYXBpLnYxLkV2ZW50RXhlY3V0aW9uRmluaXNoZWRIABJDChFleGVjdXRpb25fc3RhcnRlZBgFIAEoCzImLm9saXZldGluLmFwaS52MS5FdmVudEV4ZWN1dGlvblN0YXJ0ZWRIABI5CgxvdXRwdXRfY2h1bmsYBiABKAsyIS5vbGl2ZXRpbi5hcGkudjEuRXZlbnRPdXRwdXRDaHVua0gAEjQKCWhlYXJ0YmVhdBgHIAEoCzIfLm9saXZldGluLmFwaS52MS5FdmVudEhlYXJ0YmVhdEgAQgcKBWV2ZW50IkEKEEV2ZW50T3V0cHV0Q2h1bmsSHQoVZXhlY3V0aW9uX3RyYWNraW5nX2lkGAEgASgJEg4KBm91dHB1dBgCIAEoCSIUChJFdmVudEVudGl0eUNoYW5nZWQiFAoSRXZlbnRDb25maWdDaGFuZ2VkIhAKDkV2ZW50SGVhcnRiZWF0IkYKFkV2ZW50RXhlY3V0aW9uRmluaXNoZWQSLAoJbG9nX2VudHJ5GAEgASgLMhkub2xpdmV0aW4uYXBpLnYxLkxvZ0VudHJ5IkUKFUV2ZW50RXhlY3V0aW9uU3RhcnRlZBIsCglsb2dfZW50cnkYASABKAsyGS5vbGl2ZXRpbi5hcGkudjEuTG9nRW50cnkiMgoRS2lsbEFjdGlvblJlcXVlc3QSHQoVZXhlY3V0aW9uX3RyYWNraW5nX2lkGAEgASgJIm0KEktpbGxBY3Rpb25SZXNwb25zZRIdChVleGVjdXRpb25fdHJhY2tpbmdfaWQYASABKAkSDgoGa2lsbGVkGAIgASgIEhkKEWFscmVhZHlfY29tcGxldGVkGAMgASgIEg0KBWZvdW5kGAQgASgIIjsKFUxvY2FsVXNlckxvZ2luUmVxdWVzdBIQCgh1c2VybmFtZRgBIAEoCRIQCghwYXNzd29yZBgCIAEoCSIpChZMb2NhbFVzZXJMb2dpblJlc3BvbnNlEg8KB3N1Y2Nlc3MYASABKAgiJwoTUGFzc3dvcmRIYXNoUmVxdWVzdBIQCghwYXNzd29yZBgBIAEoCSIkChRQYXNzd29yZEhhc2hSZXNwb25zZRIMCgRoYXNoGAEgASgJIg8KDUxvZ291dFJlcXVlc3QiEAoOTG9nb3V0UmVzcG9uc2UiFwoVR2V0RGlhZ25vc3RpY3NSZXF1ZXN0IkUKFkdldERpYWdub3N0aWNzUmVzcG9uc2USEwoLU3NoRm91bmRLZXkYASABKAkSFgoOU3NoRm91bmRDb25maWcYAiABKAkiDQoLSW5pdFJlcXVlc3Qi6wUKDEluaXRSZXNwb25zZRISCgpzaG93Rm9vdGVyGAEgASgIEhYKDnNob3dOYXZpZ2F0aW9uGAIgASgIEhcKD3Nob3dOZXdWZXJzaW9ucxgDIAEoCBIYChBhdmFpbGFibGVWZXJzaW9uGAQgASgJEhYKDmN1cnJlbnRWZXJzaW9uGAUgASgJEhEKCXBhZ2VUaXRsZRgGIAEoCRIeChZzZWN0aW9uTmF2aWdhdGlvblN0eWxlGAcgASgJEhoKEmRlZmF1bHRJY29uRm9yQmFjaxgIIAEoCRIWCg5lbmFibGVDdXN0b21KcxgJIAEoCBIUCgxhdXRoTG9naW5VcmwYCiABKAkSFgoOYXV0aExvY2FsTG9naW4YCyABKAgSEQoJc3R5bGVNb2RzGAwgAygJEjgKD29BdXRoMlByb3ZpZGVycxgNIAMoCzIfLm9saXZldGluLmFwaS52MS5PQXV0aDJQcm92aWRlchI4Cg9hZGRpdGlvbmFsTGlua3MYDiADKAsyHy5vbGl2ZXRpbi5hcGkudjEuQWRkaXRpb25hbExpbmsSFgoOcm9vdERhc2hib2FyZHMYDyADKAkSGgoSYXV0aGVudGljYXRlZF91c2VyGBAgASgJEiMKG2F1dGhlbnRpY2F0ZWRfdXNlcl9wcm92aWRlchgRIAEoCRI6ChBlZmZlY3RpdmVfcG9saWN5GBIgASgLMiAub2xpdmV0aW4uYXBpLnYxLkVmZmVjdGl2ZVBvbGljeRIWCg5iYW5uZXJfbWVzc2FnZRgTIAEoCRISCgpiYW5uZXJfY3NzGBQgASgJEhgKEHNob3dfZGlhZ25vc3RpY3MYFSABKAgSFQoNc2hvd19sb2dfbGlzdBgWIAEoCBIWCg5sb2dpbl9yZXF1aXJlZBgXIAEoCBIYChBhdmFpbGFibGVfdGhlbWVzGBggAygJEiQKHHNob3dfbmF2aWdhdGVfb25fc3RhcnRfaWNvbnMYGSABKAgiLAoOQWRkaXRpb25hbExpbmsSDQoFdGl0bGUYASABKAkSCwoDdXJsGAIgASgJIjoKDk9BdXRoMlByb3ZpZGVyEg0KBXRpdGxlGAEgASgJEgwKBGljb24YAyABKAkSCwoDa2V5GAQgASgJIi0KF0dldEFjdGlvbkJpbmRpbmdSZXF1ZXN0EhIKCmJpbmRpbmdfaWQYASABKAkiiwEKGEdldEFjdGlvbkJpbmRpbmdSZXNwb25zZRInCgZhY3Rpb24YASABKAsyFy5vbGl2ZXRpbi5hcGkudjEuQWN0aW9uEkYKEmJhY2tfdG9fZGFzaGJvYXJkcxgCIAMoCzIqLm9saXZldGluLmFwaS52MS5EYXNoYm9hcmROYXZpZ2F0aW9uVGFyZ2V0IhQKEkdldEVudGl0aWVzUmVxdWVzdCJUChNHZXRFbnRpdGllc1Jlc3BvbnNlEj0KEmVudGl0eV9kZWZpbml0aW9ucxgBIAMoCzIhLm9saXZldGluLmFwaS52MS5FbnRpdHlEZWZpbml0aW9uImkKEEVudGl0eURlZmluaXRpb24SDQoFdGl0bGUYASABKAkSKgoJaW5zdGFuY2VzGAIgAygLMhcub2xpdmV0aW4uYXBpLnYxLkVudGl0eRIaChJ1c2VkX29uX2Rhc2hib2FyZHMYAyADKAkiNAoQR2V0RW50aXR5UmVxdWVzdBISCgp1bmlxdWVfa2V5GAEgASgJEgwKBHR5cGUYAiABKAkiNQoUUmVzdGFydEFjdGlvblJlcXVlc3QSHQoVZXhlY3V0aW9uX3RyYWNraW5nX2lkGAEgASgJMtYTChJPbGl2ZVRpbkFwaVNlcnZpY2USXQoMR2V0RGFzaGJvYXJkEiQub2xpdmV0aW4uYXBpLnYxLkdldERhc2hib2FyZFJlcXVlc3QaJS5vbGl2ZXRpbi5hcGkudjEuR2V0RGFzaGJvYXJkUmVzcG9uc2UiABJaCgtTdGFydEFjdGlvbhIjLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvblJlcXVlc3QaJC5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25SZXNwb25zZSIAEm8KElN0YXJ0QWN0aW9uQW5kV2FpdBIqLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkFuZFdhaXRSZXF1ZXN0Gisub2xpdmV0aW4uYXBpLnYxLlN0YXJ0QWN0aW9uQW5kV2FpdFJlc3BvbnNlIgASaQoQU3RhcnRBY3Rpb25CeUdldBIoLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkJ5R2V0UmVxdWVzdBopLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkJ5R2V0UmVzcG9uc2UiABJ+ChdTdGFydEFjdGlvbkJ5R2V0QW5kV2FpdBIvLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkJ5R2V0QW5kV2FpdFJlcXVlc3QaMC5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25CeUdldEFuZFdhaXRSZXNwb25zZSIAEl4KDVJlc3RhcnRBY3Rpb24SJS5vbGl2ZXRpbi5hcGkudjEuUmVzdGFydEFjdGlvblJlcXVlc3QaJC5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25SZXNwb25zZSIAElcKCktpbGxBY3Rpb24SIi5vbGl2ZXRpbi5hcGkudjEuS2lsbEFjdGlvblJlcXVlc3QaIy5vbGl2ZXRpbi5hcGkudjEuS2lsbEFjdGlvblJlc3BvbnNlIgASZgoPRXhlY3V0aW9uU3RhdHVzEicub2xpdmV0aW4uYXBpLnYxLkV4ZWN1dGlvblN0YXR1c1JlcXVlc3QaKC5vbGl2ZXRpbi5hcGkudjEuRXhlY3V0aW9uU3RhdHVzUmVzcG9uc2UiABJOCgdHZXRMb2dzEh8ub2xpdmV0aW4uYXBpLnYxLkdldExvZ3NSZXF1ZXN0GiAub2xpdmV0aW4uYXBpLnYxLkdldExvZ3NSZXNwb25zZSIAEmAKDUdldEFjdGlvbkxvZ3MSJS5vbGl2ZXRpbi5hcGkudjEuR2V0QWN0aW9uTG9nc1JlcXVlc3QaJi5vbGl2ZXRpbi5hcGkudjEuR2V0QWN0aW9uTG9nc1Jlc3BvbnNlIgASbAoRR2V0RXhlY3V0aW9uUXVldWUSKS5vbGl2ZXRpbi5hcGkudjEuR2V0RXhlY3V0aW9uUXVldWVSZXF1ZXN0Gioub2xpdmV0aW4uYXBpLnYxLkdldEV4ZWN1dGlvblF1ZXVlUmVzcG9uc2UiABJ1ChRWYWxpZGF0ZUFyZ3VtZW50VHlwZRIsLm9saXZldGluLmFwaS52MS5WYWxpZGF0ZUFyZ3VtZW50VHlwZVJlcXVlc3QaLS5vbGl2ZXRpbi5hcGkudjEuVmFsaWRhdGVBcmd1bWVudFR5cGVSZXNwb25zZSIAEksKBldob0FtSRIeLm9saXZldGluLmFwaS52MS5XaG9BbUlSZXF1ZXN0Gh8ub2xpdmV0aW4uYXBpLnYxLldob0FtSVJlc3BvbnNlIgASVAoJU29zUmVwb3J0EiEub2xpdmV0aW4uYXBpLnYxLlNvc1JlcG9ydFJlcXVlc3QaIi5vbGl2ZXRpbi5hcGkudjEuU29zUmVwb3J0UmVzcG9uc2UiABJRCghEdW1wVmFycxIgLm9saXZldGluLmFwaS52MS5EdW1wVmFyc1JlcXVlc3QaIS5vbGl2ZXRpbi5hcGkudjEuRHVtcFZhcnNSZXNwb25zZSIAEngKFUR1bXBQdWJsaWNJZEFjdGlvbk1hcBItLm9saXZldGluLmFwaS52MS5EdW1wUHVibGljSWRBY3Rpb25NYXBSZXF1ZXN0Gi4ub2xpdmV0aW4uYXBpLnYxLkR1bXBQdWJsaWNJZEFjdGlvbk1hcFJlc3BvbnNlIgASVAoJR2V0UmVhZHl6EiEub2xpdmV0aW4uYXBpLnYxLkdldFJlYWR5elJlcXVlc3QaIi5vbGl2ZXRpbi5hcGkudjEuR2V0UmVhZHl6UmVzcG9uc2UiABJjCg5Mb2NhbFVzZXJMb2dpbhImLm9saXZldGluLmFwaS52MS5Mb2NhbFVzZXJMb2dpblJlcXVlc3QaJy5vbGl2ZXRpbi5hcGkudjEuTG9jYWxVc2VyTG9naW5SZXNwb25zZSIAEl0KDFBhc3N3b3JkSGFzaBIkLm9saXZldGluLmFwaS52MS5QYXNzd29yZEhhc2hSZXF1ZXN0GiUub2xpdmV0aW4uYXBpLnYxLlBhc3N3b3JkSGFzaFJlc3BvbnNlIgASSwoGTG9nb3V0Eh4ub2xpdmV0aW4uYXBpLnYxLkxvZ291dFJlcXVlc3QaHy5vbGl2ZXRpbi5hcGkudjEuTG9nb3V0UmVzcG9uc2UiABJcCgtFdmVudFN0cmVhbRIjLm9saXZldGluLmFwaS52MS5FdmVudFN0cmVhbVJlcXVlc3QaJC5vbGl2ZXRpbi5hcGkudjEuRXZlbnRTdHJlYW1SZXNwb25zZSIAMAESYwoOR2V0RGlhZ25vc3RpY3MSJi5vbGl2ZXRpbi5hcGkudjEuR2V0RGlhZ25vc3RpY3NSZXF1ZXN0Gicub2xpdmV0aW4uYXBpLnYxLkdldERpYWdub3N0aWNzUmVzcG9uc2UiABJFCgRJbml0Ehwub2xpdmV0aW4uYXBpLnYxLkluaXRSZXF1ZXN0Gh0ub2xpdmV0aW4uYXBpLnYxLkluaXRSZXNwb25zZSIAEmkKEEdldEFjdGlvbkJpbmRpbmcSKC5vbGl2ZXRpbi5hcGkudjEuR2V0QWN0aW9uQmluZGluZ1JlcXVlc3QaKS5vbGl2ZXRpbi5hcGkudjEuR2V0QWN0aW9uQmluZGluZ1Jlc3BvbnNlIgASWgoLR2V0RW50aXRpZXMSIy5vbGl2ZXRpbi5hcGkudjEuR2V0RW50aXRpZXNSZXF1ZXN0GiQub2xpdmV0aW4uYXBpLnYxLkdldEVudGl0aWVzUmVzcG9uc2UiABJJCglHZXRFbnRpdHkSIS5vbGl2ZXRpbi5hcGkudjEuR2V0RW50aXR5UmVxdWVzdBoXLm9saXZldGluLmFwaS52MS5FbnRpdHkiAEI4WjZnaXRodWIuY29tL09saXZlVGluL09saXZlVGluL2dlbi9vbGl2ZXRpbi9hcGkvdjE7YXBpdjFiBnByb3RvMw"); + fileDesc("Ch5vbGl2ZXRpbi9hcGkvdjEvb2xpdmV0aW4ucHJvdG8SD29saXZldGluLmFwaS52MSLABAoGQWN0aW9uEhIKCmJpbmRpbmdfaWQYASABKAkSDQoFdGl0bGUYAiABKAkSDAoEaWNvbhgDIAEoCRIQCghjYW5fZXhlYxgEIAEoCBIyCglhcmd1bWVudHMYBSADKAsyHy5vbGl2ZXRpbi5hcGkudjEuQWN0aW9uQXJndW1lbnQSFgoOcG9wdXBfb25fc3RhcnQYBiABKAkSDQoFb3JkZXIYByABKAUSDwoHdGltZW91dBgIIAEoBRIjChtkYXRldGltZV9yYXRlX2xpbWl0X2V4cGlyZXMYCSABKAkSFwoPZXhlY19vbl9zdGFydHVwGAogASgIEhQKDGV4ZWNfb25fY3JvbhgLIAMoCRIjChtleGVjX29uX2ZpbGVfY3JlYXRlZF9pbl9kaXIYDCADKAkSIwobZXhlY19vbl9maWxlX2NoYW5nZWRfaW5fZGlyGA0gAygJEh0KFWV4ZWNfb25fY2FsZW5kYXJfZmlsZRgOIAEoCRJAChBleGVjX29uX3dlYmhvb2tzGA8gAygLMiYub2xpdmV0aW4uYXBpLnYxLkFjdGlvbldlYmhvb2tFeGVjSGludBIVCg1qdXN0aWZpY2F0aW9uGBAgASgIEhwKFGhhc19ydW5uaW5nX2luc3RhbmNlGBEgASgIEhsKE2hhc19xdWV1ZWRfaW5zdGFuY2UYEiABKAgSNgoGZ3JvdXBzGBMgAygLMiYub2xpdmV0aW4uYXBpLnYxLkFjdGlvbkdyb3VwTWVtYmVyc2hpcCJRChVBY3Rpb25Hcm91cE1lbWJlcnNoaXASDAoEbmFtZRgBIAEoCRIWCg5tYXhfY29uY3VycmVudBgCIAEoBRISCgpxdWV1ZV9zaXplGAMgASgFIsMCChVBY3Rpb25XZWJob29rRXhlY0hpbnQSEAoIdGVtcGxhdGUYASABKAkSEgoKbWF0Y2hfcGF0aBgCIAEoCRJPCg1tYXRjaF9oZWFkZXJzGAMgAygLMjgub2xpdmV0aW4uYXBpLnYxLkFjdGlvbldlYmhvb2tFeGVjSGludC5NYXRjaEhlYWRlcnNFbnRyeRJLCgttYXRjaF9xdWVyeRgEIAMoCzI2Lm9saXZldGluLmFwaS52MS5BY3Rpb25XZWJob29rRXhlY0hpbnQuTWF0Y2hRdWVyeUVudHJ5GjMKEU1hdGNoSGVhZGVyc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEaMQoPTWF0Y2hRdWVyeUVudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiuwIKDkFjdGlvbkFyZ3VtZW50EgwKBG5hbWUYASABKAkSDQoFdGl0bGUYAiABKAkSDAoEdHlwZRgDIAEoCRIVCg1kZWZhdWx0X3ZhbHVlGAQgASgJEjYKB2Nob2ljZXMYBSADKAsyJS5vbGl2ZXRpbi5hcGkudjEuQWN0aW9uQXJndW1lbnRDaG9pY2USEwoLZGVzY3JpcHRpb24YBiABKAkSRQoLc3VnZ2VzdGlvbnMYByADKAsyMC5vbGl2ZXRpbi5hcGkudjEuQWN0aW9uQXJndW1lbnQuU3VnZ2VzdGlvbnNFbnRyeRIfChdzdWdnZXN0aW9uc19icm93c2VyX2tleRgIIAEoCRoyChBTdWdnZXN0aW9uc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiNAoUQWN0aW9uQXJndW1lbnRDaG9pY2USDQoFdmFsdWUYASABKAkSDQoFdGl0bGUYAiABKAkisgEKBkVudGl0eRINCgV0aXRsZRgBIAEoCRISCgp1bmlxdWVfa2V5GAIgASgJEgwKBHR5cGUYAyABKAkSEwoLZGlyZWN0b3JpZXMYBCADKAkSMwoGZmllbGRzGAUgAygLMiMub2xpdmV0aW4uYXBpLnYxLkVudGl0eS5GaWVsZHNFbnRyeRotCgtGaWVsZHNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBIlQKFEdldERhc2hib2FyZFJlc3BvbnNlEg0KBXRpdGxlGAEgASgJEi0KCWRhc2hib2FyZBgEIAEoCzIaLm9saXZldGluLmFwaS52MS5EYXNoYm9hcmQiXwoPRWZmZWN0aXZlUG9saWN5EhgKEHNob3dfZGlhZ25vc3RpY3MYASABKAgSFQoNc2hvd19sb2dfbGlzdBgCIAEoCBIbChNzaG93X3ZlcnNpb25fbnVtYmVyGAMgASgIIk0KE0dldERhc2hib2FyZFJlcXVlc3QSDQoFdGl0bGUYASABKAkSEwoLZW50aXR5X3R5cGUYAiABKAkSEgoKZW50aXR5X2tleRgDIAEoCSJRCglEYXNoYm9hcmQSDQoFdGl0bGUYASABKAkSNQoIY29udGVudHMYAiADKAsyIy5vbGl2ZXRpbi5hcGkudjEuRGFzaGJvYXJkQ29tcG9uZW50ItsBChJEYXNoYm9hcmRDb21wb25lbnQSDQoFdGl0bGUYASABKAkSDAoEdHlwZRgCIAEoCRI1Cghjb250ZW50cxgDIAMoCzIjLm9saXZldGluLmFwaS52MS5EYXNoYm9hcmRDb21wb25lbnQSDAoEaWNvbhgEIAEoCRIRCgljc3NfY2xhc3MYBSABKAkSJwoGYWN0aW9uGAYgASgLMhcub2xpdmV0aW4uYXBpLnYxLkFjdGlvbhITCgtlbnRpdHlfdHlwZRgHIAEoCRISCgplbnRpdHlfa2V5GAggASgJIpQBChJTdGFydEFjdGlvblJlcXVlc3QSEgoKYmluZGluZ19pZBgBIAEoCRI3Cglhcmd1bWVudHMYAiADKAsyJC5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25Bcmd1bWVudBIaChJ1bmlxdWVfdHJhY2tpbmdfaWQYAyABKAkSFQoNanVzdGlmaWNhdGlvbhgEIAEoCSIyChNTdGFydEFjdGlvbkFyZ3VtZW50EgwKBG5hbWUYASABKAkSDQoFdmFsdWUYAiABKAkiNAoTU3RhcnRBY3Rpb25SZXNwb25zZRIdChVleGVjdXRpb25fdHJhY2tpbmdfaWQYAiABKAkifgoZU3RhcnRBY3Rpb25BbmRXYWl0UmVxdWVzdBIRCglhY3Rpb25faWQYASABKAkSNwoJYXJndW1lbnRzGAIgAygLMiQub2xpdmV0aW4uYXBpLnYxLlN0YXJ0QWN0aW9uQXJndW1lbnQSFQoNanVzdGlmaWNhdGlvbhgDIAEoCSJKChpTdGFydEFjdGlvbkFuZFdhaXRSZXNwb25zZRIsCglsb2dfZW50cnkYASABKAsyGS5vbGl2ZXRpbi5hcGkudjEuTG9nRW50cnkiLAoXU3RhcnRBY3Rpb25CeUdldFJlcXVlc3QSEQoJYWN0aW9uX2lkGAEgASgJIjkKGFN0YXJ0QWN0aW9uQnlHZXRSZXNwb25zZRIdChVleGVjdXRpb25fdHJhY2tpbmdfaWQYAiABKAkiMwoeU3RhcnRBY3Rpb25CeUdldEFuZFdhaXRSZXF1ZXN0EhEKCWFjdGlvbl9pZBgBIAEoCSJPCh9TdGFydEFjdGlvbkJ5R2V0QW5kV2FpdFJlc3BvbnNlEiwKCWxvZ19lbnRyeRgBIAEoCzIZLm9saXZldGluLmFwaS52MS5Mb2dFbnRyeSJeCg5HZXRMb2dzUmVxdWVzdBIUCgxzdGFydF9vZmZzZXQYASABKAMSEwoLZGF0ZV9maWx0ZXIYAiABKAkSEQoJcGFnZV9zaXplGAMgASgDEg4KBmZpbHRlchgEIAEoCSKUBAoITG9nRW50cnkSGAoQZGF0ZXRpbWVfc3RhcnRlZBgBIAEoCRIUCgxhY3Rpb25fdGl0bGUYAiABKAkSDgoGb3V0cHV0GAMgASgJEhEKCXRpbWVkX291dBgFIAEoCBIRCglleGl0X2NvZGUYBiABKAUSDAoEdXNlchgHIAEoCRISCgp1c2VyX2NsYXNzGAggASgJEhMKC2FjdGlvbl9pY29uGAkgASgJEgwKBHRhZ3MYCiADKAkSHQoVZXhlY3V0aW9uX3RyYWNraW5nX2lkGAsgASgJEhkKEWRhdGV0aW1lX2ZpbmlzaGVkGAwgASgJEhkKEWV4ZWN1dGlvbl9zdGFydGVkGA4gASgIEhoKEmV4ZWN1dGlvbl9maW5pc2hlZBgPIAEoCBIPCgdibG9ja2VkGBAgASgIEhYKDmRhdGV0aW1lX2luZGV4GBEgASgDEhAKCGNhbl9raWxsGBIgASgIEiMKG2RhdGV0aW1lX3JhdGVfbGltaXRfZXhwaXJlcxgTIAEoCRISCgpiaW5kaW5nX2lkGBQgASgJEg4KBnF1ZXVlZBgVIAEoCBIYChBxdWV1ZWRfZm9yX2dyb3VwGBYgASgJEhUKDWp1c3RpZmljYXRpb24YFyABKAkSNwoJYXJndW1lbnRzGBggAygLMiQub2xpdmV0aW4uYXBpLnYxLlN0YXJ0QWN0aW9uQXJndW1lbnQikQEKD0dldExvZ3NSZXNwb25zZRInCgRsb2dzGAEgAygLMhkub2xpdmV0aW4uYXBpLnYxLkxvZ0VudHJ5EhcKD2NvdW50X3JlbWFpbmluZxgCIAEoAxIRCglwYWdlX3NpemUYAyABKAMSEwoLdG90YWxfY291bnQYBCABKAMSFAoMc3RhcnRfb2Zmc2V0GAUgASgDIj8KFEdldEFjdGlvbkxvZ3NSZXF1ZXN0EhEKCWFjdGlvbl9pZBgBIAEoCRIUCgxzdGFydF9vZmZzZXQYAiABKAMilwEKFUdldEFjdGlvbkxvZ3NSZXNwb25zZRInCgRsb2dzGAEgAygLMhkub2xpdmV0aW4uYXBpLnYxLkxvZ0VudHJ5EhcKD2NvdW50X3JlbWFpbmluZxgCIAEoAxIRCglwYWdlX3NpemUYAyABKAMSEwoLdG90YWxfY291bnQYBCABKAMSFAoMc3RhcnRfb2Zmc2V0GAUgASgDIhoKGEdldEV4ZWN1dGlvblF1ZXVlUmVxdWVzdCLGAQoURXhlY3V0aW9uUXVldWVBY3Rpb24SEgoKYmluZGluZ19pZBgBIAEoCRIUCgxhY3Rpb25fdGl0bGUYAiABKAkSEwoLYWN0aW9uX2ljb24YAyABKAkSFgoObWF4X2NvbmN1cnJlbnQYBCABKAUSFAoMYWN0aXZlX2NvdW50GAUgASgFEhUKDWVudGl0eV9wcmVmaXgYBiABKAkSKgoHZW50cmllcxgHIAMoCzIZLm9saXZldGluLmFwaS52MS5Mb2dFbnRyeSLBAQoTRXhlY3V0aW9uUXVldWVHcm91cBIMCgRuYW1lGAEgASgJEgwKBGljb24YAiABKAkSFgoObWF4X2NvbmN1cnJlbnQYAyABKAUSFAoMYWN0aXZlX2NvdW50GAQgASgFEjYKB2FjdGlvbnMYBSADKAsyJS5vbGl2ZXRpbi5hcGkudjEuRXhlY3V0aW9uUXVldWVBY3Rpb24SFAoMcXVldWVkX2NvdW50GAYgASgFEhIKCnF1ZXVlX3NpemUYByABKAUiZwoZR2V0RXhlY3V0aW9uUXVldWVSZXNwb25zZRI0CgZncm91cHMYASADKAsyJC5vbGl2ZXRpbi5hcGkudjEuRXhlY3V0aW9uUXVldWVHcm91cBIUCgx0b3RhbF9hY3RpdmUYAiABKAUiZQobVmFsaWRhdGVBcmd1bWVudFR5cGVSZXF1ZXN0Eg0KBXZhbHVlGAEgASgJEgwKBHR5cGUYAiABKAkSEgoKYmluZGluZ19pZBgDIAEoCRIVCg1hcmd1bWVudF9uYW1lGAQgASgJIkIKHFZhbGlkYXRlQXJndW1lbnRUeXBlUmVzcG9uc2USDQoFdmFsaWQYASABKAgSEwoLZGVzY3JpcHRpb24YAiABKAkiNgoVV2F0Y2hFeGVjdXRpb25SZXF1ZXN0Eh0KFWV4ZWN1dGlvbl90cmFja2luZ19pZBgBIAEoCSImChRXYXRjaEV4ZWN1dGlvblVwZGF0ZRIOCgZ1cGRhdGUYASABKAkiSgoWRXhlY3V0aW9uU3RhdHVzUmVxdWVzdBIdChVleGVjdXRpb25fdHJhY2tpbmdfaWQYASABKAkSEQoJYWN0aW9uX2lkGAIgASgJImEKGURhc2hib2FyZE5hdmlnYXRpb25UYXJnZXQSDQoFdGl0bGUYASABKAkSEwoLZW50aXR5X3R5cGUYAiABKAkSEgoKZW50aXR5X2tleRgDIAEoCRIMCgRwYXRoGAQgASgJIo8BChdFeGVjdXRpb25TdGF0dXNSZXNwb25zZRIsCglsb2dfZW50cnkYASABKAsyGS5vbGl2ZXRpbi5hcGkudjEuTG9nRW50cnkSRgoSYmFja190b19kYXNoYm9hcmRzGAIgAygLMioub2xpdmV0aW4uYXBpLnYxLkRhc2hib2FyZE5hdmlnYXRpb25UYXJnZXQiDwoNV2hvQW1JUmVxdWVzdCJsCg5XaG9BbUlSZXNwb25zZRIaChJhdXRoZW50aWNhdGVkX3VzZXIYASABKAkSEQoJdXNlcmdyb3VwGAIgASgJEhAKCHByb3ZpZGVyGAMgASgJEgwKBGFjbHMYBCADKAkSCwoDc2lkGAUgASgJIhIKEFNvc1JlcG9ydFJlcXVlc3QiIgoRU29zUmVwb3J0UmVzcG9uc2USDQoFYWxlcnQYASABKAkiEQoPRHVtcFZhcnNSZXF1ZXN0IpUBChBEdW1wVmFyc1Jlc3BvbnNlEg0KBWFsZXJ0GAEgASgJEkEKCGNvbnRlbnRzGAIgAygLMi8ub2xpdmV0aW4uYXBpLnYxLkR1bXBWYXJzUmVzcG9uc2UuQ29udGVudHNFbnRyeRovCg1Db250ZW50c0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiOwoMRGVidWdCaW5kaW5nEhQKDGFjdGlvbl90aXRsZRgBIAEoCRIVCg1lbnRpdHlfcHJlZml4GAIgASgJIh4KHER1bXBQdWJsaWNJZEFjdGlvbk1hcFJlcXVlc3QizgEKHUR1bXBQdWJsaWNJZEFjdGlvbk1hcFJlc3BvbnNlEg0KBWFsZXJ0GAEgASgJEk4KCGNvbnRlbnRzGAIgAygLMjwub2xpdmV0aW4uYXBpLnYxLkR1bXBQdWJsaWNJZEFjdGlvbk1hcFJlc3BvbnNlLkNvbnRlbnRzRW50cnkaTgoNQ29udGVudHNFbnRyeRILCgNrZXkYASABKAkSLAoFdmFsdWUYAiABKAsyHS5vbGl2ZXRpbi5hcGkudjEuRGVidWdCaW5kaW5nOgI4ASISChBHZXRSZWFkeXpSZXF1ZXN0IiMKEUdldFJlYWR5elJlc3BvbnNlEg4KBnN0YXR1cxgBIAEoCSIUChJFdmVudFN0cmVhbVJlcXVlc3QimQMKE0V2ZW50U3RyZWFtUmVzcG9uc2USPQoOZW50aXR5X2NoYW5nZWQYAiABKAsyIy5vbGl2ZXRpbi5hcGkudjEuRXZlbnRFbnRpdHlDaGFuZ2VkSAASPQoOY29uZmlnX2NoYW5nZWQYAyABKAsyIy5vbGl2ZXRpbi5hcGkudjEuRXZlbnRDb25maWdDaGFuZ2VkSAASRQoSZXhlY3V0aW9uX2ZpbmlzaGVkGAQgASgLMicub2xpdmV0aW4uYXBpLnYxLkV2ZW50RXhlY3V0aW9uRmluaXNoZWRIABJDChFleGVjdXRpb25fc3RhcnRlZBgFIAEoCzImLm9saXZldGluLmFwaS52MS5FdmVudEV4ZWN1dGlvblN0YXJ0ZWRIABI5CgxvdXRwdXRfY2h1bmsYBiABKAsyIS5vbGl2ZXRpbi5hcGkudjEuRXZlbnRPdXRwdXRDaHVua0gAEjQKCWhlYXJ0YmVhdBgHIAEoCzIfLm9saXZldGluLmFwaS52MS5FdmVudEhlYXJ0YmVhdEgAQgcKBWV2ZW50IkEKEEV2ZW50T3V0cHV0Q2h1bmsSHQoVZXhlY3V0aW9uX3RyYWNraW5nX2lkGAEgASgJEg4KBm91dHB1dBgCIAEoCSIUChJFdmVudEVudGl0eUNoYW5nZWQiFAoSRXZlbnRDb25maWdDaGFuZ2VkIhAKDkV2ZW50SGVhcnRiZWF0IkYKFkV2ZW50RXhlY3V0aW9uRmluaXNoZWQSLAoJbG9nX2VudHJ5GAEgASgLMhkub2xpdmV0aW4uYXBpLnYxLkxvZ0VudHJ5IkUKFUV2ZW50RXhlY3V0aW9uU3RhcnRlZBIsCglsb2dfZW50cnkYASABKAsyGS5vbGl2ZXRpbi5hcGkudjEuTG9nRW50cnkiMgoRS2lsbEFjdGlvblJlcXVlc3QSHQoVZXhlY3V0aW9uX3RyYWNraW5nX2lkGAEgASgJIm0KEktpbGxBY3Rpb25SZXNwb25zZRIdChVleGVjdXRpb25fdHJhY2tpbmdfaWQYASABKAkSDgoGa2lsbGVkGAIgASgIEhkKEWFscmVhZHlfY29tcGxldGVkGAMgASgIEg0KBWZvdW5kGAQgASgIIjsKFUxvY2FsVXNlckxvZ2luUmVxdWVzdBIQCgh1c2VybmFtZRgBIAEoCRIQCghwYXNzd29yZBgCIAEoCSIpChZMb2NhbFVzZXJMb2dpblJlc3BvbnNlEg8KB3N1Y2Nlc3MYASABKAgiJwoTUGFzc3dvcmRIYXNoUmVxdWVzdBIQCghwYXNzd29yZBgBIAEoCSIkChRQYXNzd29yZEhhc2hSZXNwb25zZRIMCgRoYXNoGAEgASgJIg8KDUxvZ291dFJlcXVlc3QiEAoOTG9nb3V0UmVzcG9uc2UiFwoVR2V0RGlhZ25vc3RpY3NSZXF1ZXN0IkUKFkdldERpYWdub3N0aWNzUmVzcG9uc2USEwoLU3NoRm91bmRLZXkYASABKAkSFgoOU3NoRm91bmRDb25maWcYAiABKAkiDQoLSW5pdFJlcXVlc3Qi6wUKDEluaXRSZXNwb25zZRISCgpzaG93Rm9vdGVyGAEgASgIEhYKDnNob3dOYXZpZ2F0aW9uGAIgASgIEhcKD3Nob3dOZXdWZXJzaW9ucxgDIAEoCBIYChBhdmFpbGFibGVWZXJzaW9uGAQgASgJEhYKDmN1cnJlbnRWZXJzaW9uGAUgASgJEhEKCXBhZ2VUaXRsZRgGIAEoCRIeChZzZWN0aW9uTmF2aWdhdGlvblN0eWxlGAcgASgJEhoKEmRlZmF1bHRJY29uRm9yQmFjaxgIIAEoCRIWCg5lbmFibGVDdXN0b21KcxgJIAEoCBIUCgxhdXRoTG9naW5VcmwYCiABKAkSFgoOYXV0aExvY2FsTG9naW4YCyABKAgSEQoJc3R5bGVNb2RzGAwgAygJEjgKD29BdXRoMlByb3ZpZGVycxgNIAMoCzIfLm9saXZldGluLmFwaS52MS5PQXV0aDJQcm92aWRlchI4Cg9hZGRpdGlvbmFsTGlua3MYDiADKAsyHy5vbGl2ZXRpbi5hcGkudjEuQWRkaXRpb25hbExpbmsSFgoOcm9vdERhc2hib2FyZHMYDyADKAkSGgoSYXV0aGVudGljYXRlZF91c2VyGBAgASgJEiMKG2F1dGhlbnRpY2F0ZWRfdXNlcl9wcm92aWRlchgRIAEoCRI6ChBlZmZlY3RpdmVfcG9saWN5GBIgASgLMiAub2xpdmV0aW4uYXBpLnYxLkVmZmVjdGl2ZVBvbGljeRIWCg5iYW5uZXJfbWVzc2FnZRgTIAEoCRISCgpiYW5uZXJfY3NzGBQgASgJEhgKEHNob3dfZGlhZ25vc3RpY3MYFSABKAgSFQoNc2hvd19sb2dfbGlzdBgWIAEoCBIWCg5sb2dpbl9yZXF1aXJlZBgXIAEoCBIYChBhdmFpbGFibGVfdGhlbWVzGBggAygJEiQKHHNob3dfbmF2aWdhdGVfb25fc3RhcnRfaWNvbnMYGSABKAgiLAoOQWRkaXRpb25hbExpbmsSDQoFdGl0bGUYASABKAkSCwoDdXJsGAIgASgJIjoKDk9BdXRoMlByb3ZpZGVyEg0KBXRpdGxlGAEgASgJEgwKBGljb24YAyABKAkSCwoDa2V5GAQgASgJIi0KF0dldEFjdGlvbkJpbmRpbmdSZXF1ZXN0EhIKCmJpbmRpbmdfaWQYASABKAkiiwEKGEdldEFjdGlvbkJpbmRpbmdSZXNwb25zZRInCgZhY3Rpb24YASABKAsyFy5vbGl2ZXRpbi5hcGkudjEuQWN0aW9uEkYKEmJhY2tfdG9fZGFzaGJvYXJkcxgCIAMoCzIqLm9saXZldGluLmFwaS52MS5EYXNoYm9hcmROYXZpZ2F0aW9uVGFyZ2V0IhQKEkdldEVudGl0aWVzUmVxdWVzdCJUChNHZXRFbnRpdGllc1Jlc3BvbnNlEj0KEmVudGl0eV9kZWZpbml0aW9ucxgBIAMoCzIhLm9saXZldGluLmFwaS52MS5FbnRpdHlEZWZpbml0aW9uImkKEEVudGl0eURlZmluaXRpb24SDQoFdGl0bGUYASABKAkSKgoJaW5zdGFuY2VzGAIgAygLMhcub2xpdmV0aW4uYXBpLnYxLkVudGl0eRIaChJ1c2VkX29uX2Rhc2hib2FyZHMYAyADKAkiNAoQR2V0RW50aXR5UmVxdWVzdBISCgp1bmlxdWVfa2V5GAEgASgJEgwKBHR5cGUYAiABKAkiNQoUUmVzdGFydEFjdGlvblJlcXVlc3QSHQoVZXhlY3V0aW9uX3RyYWNraW5nX2lkGAEgASgJMtYTChJPbGl2ZVRpbkFwaVNlcnZpY2USXQoMR2V0RGFzaGJvYXJkEiQub2xpdmV0aW4uYXBpLnYxLkdldERhc2hib2FyZFJlcXVlc3QaJS5vbGl2ZXRpbi5hcGkudjEuR2V0RGFzaGJvYXJkUmVzcG9uc2UiABJaCgtTdGFydEFjdGlvbhIjLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvblJlcXVlc3QaJC5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25SZXNwb25zZSIAEm8KElN0YXJ0QWN0aW9uQW5kV2FpdBIqLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkFuZFdhaXRSZXF1ZXN0Gisub2xpdmV0aW4uYXBpLnYxLlN0YXJ0QWN0aW9uQW5kV2FpdFJlc3BvbnNlIgASaQoQU3RhcnRBY3Rpb25CeUdldBIoLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkJ5R2V0UmVxdWVzdBopLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkJ5R2V0UmVzcG9uc2UiABJ+ChdTdGFydEFjdGlvbkJ5R2V0QW5kV2FpdBIvLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkJ5R2V0QW5kV2FpdFJlcXVlc3QaMC5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25CeUdldEFuZFdhaXRSZXNwb25zZSIAEl4KDVJlc3RhcnRBY3Rpb24SJS5vbGl2ZXRpbi5hcGkudjEuUmVzdGFydEFjdGlvblJlcXVlc3QaJC5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25SZXNwb25zZSIAElcKCktpbGxBY3Rpb24SIi5vbGl2ZXRpbi5hcGkudjEuS2lsbEFjdGlvblJlcXVlc3QaIy5vbGl2ZXRpbi5hcGkudjEuS2lsbEFjdGlvblJlc3BvbnNlIgASZgoPRXhlY3V0aW9uU3RhdHVzEicub2xpdmV0aW4uYXBpLnYxLkV4ZWN1dGlvblN0YXR1c1JlcXVlc3QaKC5vbGl2ZXRpbi5hcGkudjEuRXhlY3V0aW9uU3RhdHVzUmVzcG9uc2UiABJOCgdHZXRMb2dzEh8ub2xpdmV0aW4uYXBpLnYxLkdldExvZ3NSZXF1ZXN0GiAub2xpdmV0aW4uYXBpLnYxLkdldExvZ3NSZXNwb25zZSIAEmAKDUdldEFjdGlvbkxvZ3MSJS5vbGl2ZXRpbi5hcGkudjEuR2V0QWN0aW9uTG9nc1JlcXVlc3QaJi5vbGl2ZXRpbi5hcGkudjEuR2V0QWN0aW9uTG9nc1Jlc3BvbnNlIgASbAoRR2V0RXhlY3V0aW9uUXVldWUSKS5vbGl2ZXRpbi5hcGkudjEuR2V0RXhlY3V0aW9uUXVldWVSZXF1ZXN0Gioub2xpdmV0aW4uYXBpLnYxLkdldEV4ZWN1dGlvblF1ZXVlUmVzcG9uc2UiABJ1ChRWYWxpZGF0ZUFyZ3VtZW50VHlwZRIsLm9saXZldGluLmFwaS52MS5WYWxpZGF0ZUFyZ3VtZW50VHlwZVJlcXVlc3QaLS5vbGl2ZXRpbi5hcGkudjEuVmFsaWRhdGVBcmd1bWVudFR5cGVSZXNwb25zZSIAEksKBldob0FtSRIeLm9saXZldGluLmFwaS52MS5XaG9BbUlSZXF1ZXN0Gh8ub2xpdmV0aW4uYXBpLnYxLldob0FtSVJlc3BvbnNlIgASVAoJU29zUmVwb3J0EiEub2xpdmV0aW4uYXBpLnYxLlNvc1JlcG9ydFJlcXVlc3QaIi5vbGl2ZXRpbi5hcGkudjEuU29zUmVwb3J0UmVzcG9uc2UiABJRCghEdW1wVmFycxIgLm9saXZldGluLmFwaS52MS5EdW1wVmFyc1JlcXVlc3QaIS5vbGl2ZXRpbi5hcGkudjEuRHVtcFZhcnNSZXNwb25zZSIAEngKFUR1bXBQdWJsaWNJZEFjdGlvbk1hcBItLm9saXZldGluLmFwaS52MS5EdW1wUHVibGljSWRBY3Rpb25NYXBSZXF1ZXN0Gi4ub2xpdmV0aW4uYXBpLnYxLkR1bXBQdWJsaWNJZEFjdGlvbk1hcFJlc3BvbnNlIgASVAoJR2V0UmVhZHl6EiEub2xpdmV0aW4uYXBpLnYxLkdldFJlYWR5elJlcXVlc3QaIi5vbGl2ZXRpbi5hcGkudjEuR2V0UmVhZHl6UmVzcG9uc2UiABJjCg5Mb2NhbFVzZXJMb2dpbhImLm9saXZldGluLmFwaS52MS5Mb2NhbFVzZXJMb2dpblJlcXVlc3QaJy5vbGl2ZXRpbi5hcGkudjEuTG9jYWxVc2VyTG9naW5SZXNwb25zZSIAEl0KDFBhc3N3b3JkSGFzaBIkLm9saXZldGluLmFwaS52MS5QYXNzd29yZEhhc2hSZXF1ZXN0GiUub2xpdmV0aW4uYXBpLnYxLlBhc3N3b3JkSGFzaFJlc3BvbnNlIgASSwoGTG9nb3V0Eh4ub2xpdmV0aW4uYXBpLnYxLkxvZ291dFJlcXVlc3QaHy5vbGl2ZXRpbi5hcGkudjEuTG9nb3V0UmVzcG9uc2UiABJcCgtFdmVudFN0cmVhbRIjLm9saXZldGluLmFwaS52MS5FdmVudFN0cmVhbVJlcXVlc3QaJC5vbGl2ZXRpbi5hcGkudjEuRXZlbnRTdHJlYW1SZXNwb25zZSIAMAESYwoOR2V0RGlhZ25vc3RpY3MSJi5vbGl2ZXRpbi5hcGkudjEuR2V0RGlhZ25vc3RpY3NSZXF1ZXN0Gicub2xpdmV0aW4uYXBpLnYxLkdldERpYWdub3N0aWNzUmVzcG9uc2UiABJFCgRJbml0Ehwub2xpdmV0aW4uYXBpLnYxLkluaXRSZXF1ZXN0Gh0ub2xpdmV0aW4uYXBpLnYxLkluaXRSZXNwb25zZSIAEmkKEEdldEFjdGlvbkJpbmRpbmcSKC5vbGl2ZXRpbi5hcGkudjEuR2V0QWN0aW9uQmluZGluZ1JlcXVlc3QaKS5vbGl2ZXRpbi5hcGkudjEuR2V0QWN0aW9uQmluZGluZ1Jlc3BvbnNlIgASWgoLR2V0RW50aXRpZXMSIy5vbGl2ZXRpbi5hcGkudjEuR2V0RW50aXRpZXNSZXF1ZXN0GiQub2xpdmV0aW4uYXBpLnYxLkdldEVudGl0aWVzUmVzcG9uc2UiABJJCglHZXRFbnRpdHkSIS5vbGl2ZXRpbi5hcGkudjEuR2V0RW50aXR5UmVxdWVzdBoXLm9saXZldGluLmFwaS52MS5FbnRpdHkiAEI4WjZnaXRodWIuY29tL09saXZlVGluL09saXZlVGluL2dlbi9vbGl2ZXRpbi9hcGkvdjE7YXBpdjFiBnByb3RvMw"); /** * Describes the message olivetin.api.v1.Action. diff --git a/frontend/resources/vue/utils/rerunArguments.js b/frontend/resources/vue/utils/rerunArguments.js new file mode 100644 index 0000000..82499e9 --- /dev/null +++ b/frontend/resources/vue/utils/rerunArguments.js @@ -0,0 +1,59 @@ +import { needsArgumentForm } from './needsArgumentForm.js' + +export function logEntryArgumentsToStartActionArgs (logEntry) { + return (logEntry?.arguments ?? []).map((arg) => ({ + name: arg.name, + value: arg.value + })) +} + +export function rerunNeedsArgumentForm (action, logEntry) { + if (action?.justification && !logEntry?.justification) { + return true + } + + if (!needsArgumentForm(action)) { + return false + } + + return hasMissingRerunArguments(action, logEntry?.arguments ?? []) +} + +export function hasMissingRerunArguments (action, storedArgs) { + const stored = new Map(storedArgs.map((arg) => [arg.name, arg.value])) + + for (const arg of action?.arguments ?? []) { + if (arg.type === 'password') { + return true + } + + if (arg.required && !stored.has(arg.name)) { + return true + } + } + + return false +} + +export function buildArgumentFormQuery (logEntry) { + const query = {} + + for (const arg of logEntry?.arguments ?? []) { + query[arg.name] = arg.value + } + + return query +} + +export function buildRerunStartActionArgs (bindingId, logEntry, action) { + const startActionArgs = { + bindingId, + arguments: logEntryArgumentsToStartActionArgs(logEntry) + } + + if (action?.justification && logEntry?.justification) { + startActionArgs.justification = logEntry.justification + } + + return startActionArgs +} diff --git a/frontend/resources/vue/utils/rerunArguments.test.mjs b/frontend/resources/vue/utils/rerunArguments.test.mjs new file mode 100644 index 0000000..b5725f2 --- /dev/null +++ b/frontend/resources/vue/utils/rerunArguments.test.mjs @@ -0,0 +1,103 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import { + buildArgumentFormQuery, + buildRerunStartActionArgs, + hasMissingRerunArguments, + logEntryArgumentsToStartActionArgs, + rerunNeedsArgumentForm +} from '../utils/rerunArguments.js' + +test('logEntryArgumentsToStartActionArgs maps proto arguments for StartAction', () => { + assert.deepEqual( + logEntryArgumentsToStartActionArgs({ + arguments: [ + { name: 'host', value: 'example.com' }, + { name: 'port', value: '443' } + ] + }), + [ + { name: 'host', value: 'example.com' }, + { name: 'port', value: '443' } + ] + ) +}) + +test('hasMissingRerunArguments requires password fields to be re-entered', () => { + const action = { + arguments: [ + { name: 'user', type: 'ascii_identifier', required: true }, + { name: 'pass', type: 'password', required: true } + ] + } + + assert.equal( + hasMissingRerunArguments(action, [{ name: 'user', value: 'alice' }]), + true + ) +}) + +test('hasMissingRerunArguments detects missing required stored arguments', () => { + const action = { + arguments: [{ name: 'host', type: 'ascii_identifier', required: true }] + } + + assert.equal(hasMissingRerunArguments(action, []), true) + assert.equal( + hasMissingRerunArguments(action, [{ name: 'host', value: 'db-1' }]), + false + ) +}) + +test('rerunNeedsArgumentForm can start directly when stored args are complete', () => { + const action = { + arguments: [{ name: 'host', type: 'ascii_identifier', required: true }] + } + const logEntry = { + arguments: [{ name: 'host', value: 'db-1' }] + } + + assert.equal(rerunNeedsArgumentForm(action, logEntry), false) +}) + +test('rerunNeedsArgumentForm opens the form when justification is missing', () => { + const action = { justification: true, arguments: [] } + + assert.equal(rerunNeedsArgumentForm(action, {}), true) + assert.equal( + rerunNeedsArgumentForm(action, { justification: 'approved change' }), + false + ) +}) + +test('buildRerunStartActionArgs includes stored justification', () => { + assert.deepEqual( + buildRerunStartActionArgs('binding-1', { + arguments: [{ name: 'host', value: 'db-1' }], + justification: 'maintenance window' + }, { + justification: true, + arguments: [{ name: 'host', type: 'ascii_identifier' }] + }), + { + bindingId: 'binding-1', + arguments: [{ name: 'host', value: 'db-1' }], + justification: 'maintenance window' + } + ) +}) + +test('buildArgumentFormQuery prefills non-password stored arguments', () => { + assert.deepEqual( + buildArgumentFormQuery({ + arguments: [ + { name: 'host', value: 'db-1' }, + { name: 'port', value: '5432' } + ] + }), + { + host: 'db-1', + port: '5432' + } + ) +}) diff --git a/proto/olivetin/api/v1/olivetin.proto b/proto/olivetin/api/v1/olivetin.proto index c888c2e..068ef69 100644 --- a/proto/olivetin/api/v1/olivetin.proto +++ b/proto/olivetin/api/v1/olivetin.proto @@ -174,6 +174,7 @@ message LogEntry { bool queued = 21; string queued_for_group = 22; string justification = 23; + repeated StartActionArgument arguments = 24; } message GetLogsResponse { diff --git a/service/gen/olivetin/api/v1/olivetin.pb.go b/service/gen/olivetin/api/v1/olivetin.pb.go index 3505a1c..89c4165 100644 --- a/service/gen/olivetin/api/v1/olivetin.pb.go +++ b/service/gen/olivetin/api/v1/olivetin.pb.go @@ -1424,6 +1424,7 @@ type LogEntry struct { Queued bool `protobuf:"varint,21,opt,name=queued,proto3" json:"queued,omitempty"` QueuedForGroup string `protobuf:"bytes,22,opt,name=queued_for_group,json=queuedForGroup,proto3" json:"queued_for_group,omitempty"` Justification string `protobuf:"bytes,23,opt,name=justification,proto3" json:"justification,omitempty"` + Arguments []*StartActionArgument `protobuf:"bytes,24,rep,name=arguments,proto3" json:"arguments,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -1605,6 +1606,13 @@ func (x *LogEntry) GetJustification() string { return "" } +func (x *LogEntry) GetArguments() []*StartActionArgument { + if x != nil { + return x.Arguments + } + return nil +} + type GetLogsResponse struct { state protoimpl.MessageState `protogen:"open.v1"` Logs []*LogEntry `protobuf:"bytes,1,rep,name=logs,proto3" json:"logs,omitempty"` @@ -4691,7 +4699,7 @@ const file_olivetin_api_v1_olivetin_proto_rawDesc = "" + "\vdate_filter\x18\x02 \x01(\tR\n" + "dateFilter\x12\x1b\n" + "\tpage_size\x18\x03 \x01(\x03R\bpageSize\x12\x16\n" + - "\x06filter\x18\x04 \x01(\tR\x06filter\"\xf1\x05\n" + + "\x06filter\x18\x04 \x01(\tR\x06filter\"\xb5\x06\n" + "\bLogEntry\x12)\n" + "\x10datetime_started\x18\x01 \x01(\tR\x0fdatetimeStarted\x12!\n" + "\faction_title\x18\x02 \x01(\tR\vactionTitle\x12\x16\n" + @@ -4717,7 +4725,8 @@ const file_olivetin_api_v1_olivetin_proto_rawDesc = "" + "binding_id\x18\x14 \x01(\tR\tbindingId\x12\x16\n" + "\x06queued\x18\x15 \x01(\bR\x06queued\x12(\n" + "\x10queued_for_group\x18\x16 \x01(\tR\x0equeuedForGroup\x12$\n" + - "\rjustification\x18\x17 \x01(\tR\rjustification\"\xca\x01\n" + + "\rjustification\x18\x17 \x01(\tR\rjustification\x12B\n" + + "\targuments\x18\x18 \x03(\v2$.olivetin.api.v1.StartActionArgumentR\targuments\"\xca\x01\n" + "\x0fGetLogsResponse\x12-\n" + "\x04logs\x18\x01 \x03(\v2\x19.olivetin.api.v1.LogEntryR\x04logs\x12'\n" + "\x0fcount_remaining\x18\x02 \x01(\x03R\x0ecountRemaining\x12\x1b\n" + @@ -5055,88 +5064,89 @@ var file_olivetin_api_v1_olivetin_proto_depIdxs = []int32{ 12, // 13: olivetin.api.v1.StartActionAndWaitRequest.arguments:type_name -> olivetin.api.v1.StartActionArgument 21, // 14: olivetin.api.v1.StartActionAndWaitResponse.log_entry:type_name -> olivetin.api.v1.LogEntry 21, // 15: olivetin.api.v1.StartActionByGetAndWaitResponse.log_entry:type_name -> olivetin.api.v1.LogEntry - 21, // 16: olivetin.api.v1.GetLogsResponse.logs:type_name -> olivetin.api.v1.LogEntry - 21, // 17: olivetin.api.v1.GetActionLogsResponse.logs:type_name -> olivetin.api.v1.LogEntry - 21, // 18: olivetin.api.v1.ExecutionQueueAction.entries:type_name -> olivetin.api.v1.LogEntry - 26, // 19: olivetin.api.v1.ExecutionQueueGroup.actions:type_name -> olivetin.api.v1.ExecutionQueueAction - 27, // 20: olivetin.api.v1.GetExecutionQueueResponse.groups:type_name -> olivetin.api.v1.ExecutionQueueGroup - 21, // 21: olivetin.api.v1.ExecutionStatusResponse.log_entry:type_name -> olivetin.api.v1.LogEntry - 34, // 22: olivetin.api.v1.ExecutionStatusResponse.back_to_dashboards:type_name -> olivetin.api.v1.DashboardNavigationTarget - 80, // 23: olivetin.api.v1.DumpVarsResponse.contents:type_name -> olivetin.api.v1.DumpVarsResponse.ContentsEntry - 81, // 24: olivetin.api.v1.DumpPublicIdActionMapResponse.contents:type_name -> olivetin.api.v1.DumpPublicIdActionMapResponse.ContentsEntry - 50, // 25: olivetin.api.v1.EventStreamResponse.entity_changed:type_name -> olivetin.api.v1.EventEntityChanged - 51, // 26: olivetin.api.v1.EventStreamResponse.config_changed:type_name -> olivetin.api.v1.EventConfigChanged - 53, // 27: olivetin.api.v1.EventStreamResponse.execution_finished:type_name -> olivetin.api.v1.EventExecutionFinished - 54, // 28: olivetin.api.v1.EventStreamResponse.execution_started:type_name -> olivetin.api.v1.EventExecutionStarted - 49, // 29: olivetin.api.v1.EventStreamResponse.output_chunk:type_name -> olivetin.api.v1.EventOutputChunk - 52, // 30: olivetin.api.v1.EventStreamResponse.heartbeat:type_name -> olivetin.api.v1.EventHeartbeat - 21, // 31: olivetin.api.v1.EventExecutionFinished.log_entry:type_name -> olivetin.api.v1.LogEntry - 21, // 32: olivetin.api.v1.EventExecutionStarted.log_entry:type_name -> olivetin.api.v1.LogEntry - 68, // 33: olivetin.api.v1.InitResponse.oAuth2Providers:type_name -> olivetin.api.v1.OAuth2Provider - 67, // 34: olivetin.api.v1.InitResponse.additionalLinks:type_name -> olivetin.api.v1.AdditionalLink - 7, // 35: olivetin.api.v1.InitResponse.effective_policy:type_name -> olivetin.api.v1.EffectivePolicy - 0, // 36: olivetin.api.v1.GetActionBindingResponse.action:type_name -> olivetin.api.v1.Action - 34, // 37: olivetin.api.v1.GetActionBindingResponse.back_to_dashboards:type_name -> olivetin.api.v1.DashboardNavigationTarget - 73, // 38: olivetin.api.v1.GetEntitiesResponse.entity_definitions:type_name -> olivetin.api.v1.EntityDefinition - 5, // 39: olivetin.api.v1.EntityDefinition.instances:type_name -> olivetin.api.v1.Entity - 42, // 40: olivetin.api.v1.DumpPublicIdActionMapResponse.ContentsEntry.value:type_name -> olivetin.api.v1.DebugBinding - 8, // 41: olivetin.api.v1.OliveTinApiService.GetDashboard:input_type -> olivetin.api.v1.GetDashboardRequest - 11, // 42: olivetin.api.v1.OliveTinApiService.StartAction:input_type -> olivetin.api.v1.StartActionRequest - 14, // 43: olivetin.api.v1.OliveTinApiService.StartActionAndWait:input_type -> olivetin.api.v1.StartActionAndWaitRequest - 16, // 44: olivetin.api.v1.OliveTinApiService.StartActionByGet:input_type -> olivetin.api.v1.StartActionByGetRequest - 18, // 45: olivetin.api.v1.OliveTinApiService.StartActionByGetAndWait:input_type -> olivetin.api.v1.StartActionByGetAndWaitRequest - 75, // 46: olivetin.api.v1.OliveTinApiService.RestartAction:input_type -> olivetin.api.v1.RestartActionRequest - 55, // 47: olivetin.api.v1.OliveTinApiService.KillAction:input_type -> olivetin.api.v1.KillActionRequest - 33, // 48: olivetin.api.v1.OliveTinApiService.ExecutionStatus:input_type -> olivetin.api.v1.ExecutionStatusRequest - 20, // 49: olivetin.api.v1.OliveTinApiService.GetLogs:input_type -> olivetin.api.v1.GetLogsRequest - 23, // 50: olivetin.api.v1.OliveTinApiService.GetActionLogs:input_type -> olivetin.api.v1.GetActionLogsRequest - 25, // 51: olivetin.api.v1.OliveTinApiService.GetExecutionQueue:input_type -> olivetin.api.v1.GetExecutionQueueRequest - 29, // 52: olivetin.api.v1.OliveTinApiService.ValidateArgumentType:input_type -> olivetin.api.v1.ValidateArgumentTypeRequest - 36, // 53: olivetin.api.v1.OliveTinApiService.WhoAmI:input_type -> olivetin.api.v1.WhoAmIRequest - 38, // 54: olivetin.api.v1.OliveTinApiService.SosReport:input_type -> olivetin.api.v1.SosReportRequest - 40, // 55: olivetin.api.v1.OliveTinApiService.DumpVars:input_type -> olivetin.api.v1.DumpVarsRequest - 43, // 56: olivetin.api.v1.OliveTinApiService.DumpPublicIdActionMap:input_type -> olivetin.api.v1.DumpPublicIdActionMapRequest - 45, // 57: olivetin.api.v1.OliveTinApiService.GetReadyz:input_type -> olivetin.api.v1.GetReadyzRequest - 57, // 58: olivetin.api.v1.OliveTinApiService.LocalUserLogin:input_type -> olivetin.api.v1.LocalUserLoginRequest - 59, // 59: olivetin.api.v1.OliveTinApiService.PasswordHash:input_type -> olivetin.api.v1.PasswordHashRequest - 61, // 60: olivetin.api.v1.OliveTinApiService.Logout:input_type -> olivetin.api.v1.LogoutRequest - 47, // 61: olivetin.api.v1.OliveTinApiService.EventStream:input_type -> olivetin.api.v1.EventStreamRequest - 63, // 62: olivetin.api.v1.OliveTinApiService.GetDiagnostics:input_type -> olivetin.api.v1.GetDiagnosticsRequest - 65, // 63: olivetin.api.v1.OliveTinApiService.Init:input_type -> olivetin.api.v1.InitRequest - 69, // 64: olivetin.api.v1.OliveTinApiService.GetActionBinding:input_type -> olivetin.api.v1.GetActionBindingRequest - 71, // 65: olivetin.api.v1.OliveTinApiService.GetEntities:input_type -> olivetin.api.v1.GetEntitiesRequest - 74, // 66: olivetin.api.v1.OliveTinApiService.GetEntity:input_type -> olivetin.api.v1.GetEntityRequest - 6, // 67: olivetin.api.v1.OliveTinApiService.GetDashboard:output_type -> olivetin.api.v1.GetDashboardResponse - 13, // 68: olivetin.api.v1.OliveTinApiService.StartAction:output_type -> olivetin.api.v1.StartActionResponse - 15, // 69: olivetin.api.v1.OliveTinApiService.StartActionAndWait:output_type -> olivetin.api.v1.StartActionAndWaitResponse - 17, // 70: olivetin.api.v1.OliveTinApiService.StartActionByGet:output_type -> olivetin.api.v1.StartActionByGetResponse - 19, // 71: olivetin.api.v1.OliveTinApiService.StartActionByGetAndWait:output_type -> olivetin.api.v1.StartActionByGetAndWaitResponse - 13, // 72: olivetin.api.v1.OliveTinApiService.RestartAction:output_type -> olivetin.api.v1.StartActionResponse - 56, // 73: olivetin.api.v1.OliveTinApiService.KillAction:output_type -> olivetin.api.v1.KillActionResponse - 35, // 74: olivetin.api.v1.OliveTinApiService.ExecutionStatus:output_type -> olivetin.api.v1.ExecutionStatusResponse - 22, // 75: olivetin.api.v1.OliveTinApiService.GetLogs:output_type -> olivetin.api.v1.GetLogsResponse - 24, // 76: olivetin.api.v1.OliveTinApiService.GetActionLogs:output_type -> olivetin.api.v1.GetActionLogsResponse - 28, // 77: olivetin.api.v1.OliveTinApiService.GetExecutionQueue:output_type -> olivetin.api.v1.GetExecutionQueueResponse - 30, // 78: olivetin.api.v1.OliveTinApiService.ValidateArgumentType:output_type -> olivetin.api.v1.ValidateArgumentTypeResponse - 37, // 79: olivetin.api.v1.OliveTinApiService.WhoAmI:output_type -> olivetin.api.v1.WhoAmIResponse - 39, // 80: olivetin.api.v1.OliveTinApiService.SosReport:output_type -> olivetin.api.v1.SosReportResponse - 41, // 81: olivetin.api.v1.OliveTinApiService.DumpVars:output_type -> olivetin.api.v1.DumpVarsResponse - 44, // 82: olivetin.api.v1.OliveTinApiService.DumpPublicIdActionMap:output_type -> olivetin.api.v1.DumpPublicIdActionMapResponse - 46, // 83: olivetin.api.v1.OliveTinApiService.GetReadyz:output_type -> olivetin.api.v1.GetReadyzResponse - 58, // 84: olivetin.api.v1.OliveTinApiService.LocalUserLogin:output_type -> olivetin.api.v1.LocalUserLoginResponse - 60, // 85: olivetin.api.v1.OliveTinApiService.PasswordHash:output_type -> olivetin.api.v1.PasswordHashResponse - 62, // 86: olivetin.api.v1.OliveTinApiService.Logout:output_type -> olivetin.api.v1.LogoutResponse - 48, // 87: olivetin.api.v1.OliveTinApiService.EventStream:output_type -> olivetin.api.v1.EventStreamResponse - 64, // 88: olivetin.api.v1.OliveTinApiService.GetDiagnostics:output_type -> olivetin.api.v1.GetDiagnosticsResponse - 66, // 89: olivetin.api.v1.OliveTinApiService.Init:output_type -> olivetin.api.v1.InitResponse - 70, // 90: olivetin.api.v1.OliveTinApiService.GetActionBinding:output_type -> olivetin.api.v1.GetActionBindingResponse - 72, // 91: olivetin.api.v1.OliveTinApiService.GetEntities:output_type -> olivetin.api.v1.GetEntitiesResponse - 5, // 92: olivetin.api.v1.OliveTinApiService.GetEntity:output_type -> olivetin.api.v1.Entity - 67, // [67:93] is the sub-list for method output_type - 41, // [41:67] is the sub-list for method input_type - 41, // [41:41] is the sub-list for extension type_name - 41, // [41:41] is the sub-list for extension extendee - 0, // [0:41] is the sub-list for field type_name + 12, // 16: olivetin.api.v1.LogEntry.arguments:type_name -> olivetin.api.v1.StartActionArgument + 21, // 17: olivetin.api.v1.GetLogsResponse.logs:type_name -> olivetin.api.v1.LogEntry + 21, // 18: olivetin.api.v1.GetActionLogsResponse.logs:type_name -> olivetin.api.v1.LogEntry + 21, // 19: olivetin.api.v1.ExecutionQueueAction.entries:type_name -> olivetin.api.v1.LogEntry + 26, // 20: olivetin.api.v1.ExecutionQueueGroup.actions:type_name -> olivetin.api.v1.ExecutionQueueAction + 27, // 21: olivetin.api.v1.GetExecutionQueueResponse.groups:type_name -> olivetin.api.v1.ExecutionQueueGroup + 21, // 22: olivetin.api.v1.ExecutionStatusResponse.log_entry:type_name -> olivetin.api.v1.LogEntry + 34, // 23: olivetin.api.v1.ExecutionStatusResponse.back_to_dashboards:type_name -> olivetin.api.v1.DashboardNavigationTarget + 80, // 24: olivetin.api.v1.DumpVarsResponse.contents:type_name -> olivetin.api.v1.DumpVarsResponse.ContentsEntry + 81, // 25: olivetin.api.v1.DumpPublicIdActionMapResponse.contents:type_name -> olivetin.api.v1.DumpPublicIdActionMapResponse.ContentsEntry + 50, // 26: olivetin.api.v1.EventStreamResponse.entity_changed:type_name -> olivetin.api.v1.EventEntityChanged + 51, // 27: olivetin.api.v1.EventStreamResponse.config_changed:type_name -> olivetin.api.v1.EventConfigChanged + 53, // 28: olivetin.api.v1.EventStreamResponse.execution_finished:type_name -> olivetin.api.v1.EventExecutionFinished + 54, // 29: olivetin.api.v1.EventStreamResponse.execution_started:type_name -> olivetin.api.v1.EventExecutionStarted + 49, // 30: olivetin.api.v1.EventStreamResponse.output_chunk:type_name -> olivetin.api.v1.EventOutputChunk + 52, // 31: olivetin.api.v1.EventStreamResponse.heartbeat:type_name -> olivetin.api.v1.EventHeartbeat + 21, // 32: olivetin.api.v1.EventExecutionFinished.log_entry:type_name -> olivetin.api.v1.LogEntry + 21, // 33: olivetin.api.v1.EventExecutionStarted.log_entry:type_name -> olivetin.api.v1.LogEntry + 68, // 34: olivetin.api.v1.InitResponse.oAuth2Providers:type_name -> olivetin.api.v1.OAuth2Provider + 67, // 35: olivetin.api.v1.InitResponse.additionalLinks:type_name -> olivetin.api.v1.AdditionalLink + 7, // 36: olivetin.api.v1.InitResponse.effective_policy:type_name -> olivetin.api.v1.EffectivePolicy + 0, // 37: olivetin.api.v1.GetActionBindingResponse.action:type_name -> olivetin.api.v1.Action + 34, // 38: olivetin.api.v1.GetActionBindingResponse.back_to_dashboards:type_name -> olivetin.api.v1.DashboardNavigationTarget + 73, // 39: olivetin.api.v1.GetEntitiesResponse.entity_definitions:type_name -> olivetin.api.v1.EntityDefinition + 5, // 40: olivetin.api.v1.EntityDefinition.instances:type_name -> olivetin.api.v1.Entity + 42, // 41: olivetin.api.v1.DumpPublicIdActionMapResponse.ContentsEntry.value:type_name -> olivetin.api.v1.DebugBinding + 8, // 42: olivetin.api.v1.OliveTinApiService.GetDashboard:input_type -> olivetin.api.v1.GetDashboardRequest + 11, // 43: olivetin.api.v1.OliveTinApiService.StartAction:input_type -> olivetin.api.v1.StartActionRequest + 14, // 44: olivetin.api.v1.OliveTinApiService.StartActionAndWait:input_type -> olivetin.api.v1.StartActionAndWaitRequest + 16, // 45: olivetin.api.v1.OliveTinApiService.StartActionByGet:input_type -> olivetin.api.v1.StartActionByGetRequest + 18, // 46: olivetin.api.v1.OliveTinApiService.StartActionByGetAndWait:input_type -> olivetin.api.v1.StartActionByGetAndWaitRequest + 75, // 47: olivetin.api.v1.OliveTinApiService.RestartAction:input_type -> olivetin.api.v1.RestartActionRequest + 55, // 48: olivetin.api.v1.OliveTinApiService.KillAction:input_type -> olivetin.api.v1.KillActionRequest + 33, // 49: olivetin.api.v1.OliveTinApiService.ExecutionStatus:input_type -> olivetin.api.v1.ExecutionStatusRequest + 20, // 50: olivetin.api.v1.OliveTinApiService.GetLogs:input_type -> olivetin.api.v1.GetLogsRequest + 23, // 51: olivetin.api.v1.OliveTinApiService.GetActionLogs:input_type -> olivetin.api.v1.GetActionLogsRequest + 25, // 52: olivetin.api.v1.OliveTinApiService.GetExecutionQueue:input_type -> olivetin.api.v1.GetExecutionQueueRequest + 29, // 53: olivetin.api.v1.OliveTinApiService.ValidateArgumentType:input_type -> olivetin.api.v1.ValidateArgumentTypeRequest + 36, // 54: olivetin.api.v1.OliveTinApiService.WhoAmI:input_type -> olivetin.api.v1.WhoAmIRequest + 38, // 55: olivetin.api.v1.OliveTinApiService.SosReport:input_type -> olivetin.api.v1.SosReportRequest + 40, // 56: olivetin.api.v1.OliveTinApiService.DumpVars:input_type -> olivetin.api.v1.DumpVarsRequest + 43, // 57: olivetin.api.v1.OliveTinApiService.DumpPublicIdActionMap:input_type -> olivetin.api.v1.DumpPublicIdActionMapRequest + 45, // 58: olivetin.api.v1.OliveTinApiService.GetReadyz:input_type -> olivetin.api.v1.GetReadyzRequest + 57, // 59: olivetin.api.v1.OliveTinApiService.LocalUserLogin:input_type -> olivetin.api.v1.LocalUserLoginRequest + 59, // 60: olivetin.api.v1.OliveTinApiService.PasswordHash:input_type -> olivetin.api.v1.PasswordHashRequest + 61, // 61: olivetin.api.v1.OliveTinApiService.Logout:input_type -> olivetin.api.v1.LogoutRequest + 47, // 62: olivetin.api.v1.OliveTinApiService.EventStream:input_type -> olivetin.api.v1.EventStreamRequest + 63, // 63: olivetin.api.v1.OliveTinApiService.GetDiagnostics:input_type -> olivetin.api.v1.GetDiagnosticsRequest + 65, // 64: olivetin.api.v1.OliveTinApiService.Init:input_type -> olivetin.api.v1.InitRequest + 69, // 65: olivetin.api.v1.OliveTinApiService.GetActionBinding:input_type -> olivetin.api.v1.GetActionBindingRequest + 71, // 66: olivetin.api.v1.OliveTinApiService.GetEntities:input_type -> olivetin.api.v1.GetEntitiesRequest + 74, // 67: olivetin.api.v1.OliveTinApiService.GetEntity:input_type -> olivetin.api.v1.GetEntityRequest + 6, // 68: olivetin.api.v1.OliveTinApiService.GetDashboard:output_type -> olivetin.api.v1.GetDashboardResponse + 13, // 69: olivetin.api.v1.OliveTinApiService.StartAction:output_type -> olivetin.api.v1.StartActionResponse + 15, // 70: olivetin.api.v1.OliveTinApiService.StartActionAndWait:output_type -> olivetin.api.v1.StartActionAndWaitResponse + 17, // 71: olivetin.api.v1.OliveTinApiService.StartActionByGet:output_type -> olivetin.api.v1.StartActionByGetResponse + 19, // 72: olivetin.api.v1.OliveTinApiService.StartActionByGetAndWait:output_type -> olivetin.api.v1.StartActionByGetAndWaitResponse + 13, // 73: olivetin.api.v1.OliveTinApiService.RestartAction:output_type -> olivetin.api.v1.StartActionResponse + 56, // 74: olivetin.api.v1.OliveTinApiService.KillAction:output_type -> olivetin.api.v1.KillActionResponse + 35, // 75: olivetin.api.v1.OliveTinApiService.ExecutionStatus:output_type -> olivetin.api.v1.ExecutionStatusResponse + 22, // 76: olivetin.api.v1.OliveTinApiService.GetLogs:output_type -> olivetin.api.v1.GetLogsResponse + 24, // 77: olivetin.api.v1.OliveTinApiService.GetActionLogs:output_type -> olivetin.api.v1.GetActionLogsResponse + 28, // 78: olivetin.api.v1.OliveTinApiService.GetExecutionQueue:output_type -> olivetin.api.v1.GetExecutionQueueResponse + 30, // 79: olivetin.api.v1.OliveTinApiService.ValidateArgumentType:output_type -> olivetin.api.v1.ValidateArgumentTypeResponse + 37, // 80: olivetin.api.v1.OliveTinApiService.WhoAmI:output_type -> olivetin.api.v1.WhoAmIResponse + 39, // 81: olivetin.api.v1.OliveTinApiService.SosReport:output_type -> olivetin.api.v1.SosReportResponse + 41, // 82: olivetin.api.v1.OliveTinApiService.DumpVars:output_type -> olivetin.api.v1.DumpVarsResponse + 44, // 83: olivetin.api.v1.OliveTinApiService.DumpPublicIdActionMap:output_type -> olivetin.api.v1.DumpPublicIdActionMapResponse + 46, // 84: olivetin.api.v1.OliveTinApiService.GetReadyz:output_type -> olivetin.api.v1.GetReadyzResponse + 58, // 85: olivetin.api.v1.OliveTinApiService.LocalUserLogin:output_type -> olivetin.api.v1.LocalUserLoginResponse + 60, // 86: olivetin.api.v1.OliveTinApiService.PasswordHash:output_type -> olivetin.api.v1.PasswordHashResponse + 62, // 87: olivetin.api.v1.OliveTinApiService.Logout:output_type -> olivetin.api.v1.LogoutResponse + 48, // 88: olivetin.api.v1.OliveTinApiService.EventStream:output_type -> olivetin.api.v1.EventStreamResponse + 64, // 89: olivetin.api.v1.OliveTinApiService.GetDiagnostics:output_type -> olivetin.api.v1.GetDiagnosticsResponse + 66, // 90: olivetin.api.v1.OliveTinApiService.Init:output_type -> olivetin.api.v1.InitResponse + 70, // 91: olivetin.api.v1.OliveTinApiService.GetActionBinding:output_type -> olivetin.api.v1.GetActionBindingResponse + 72, // 92: olivetin.api.v1.OliveTinApiService.GetEntities:output_type -> olivetin.api.v1.GetEntitiesResponse + 5, // 93: olivetin.api.v1.OliveTinApiService.GetEntity:output_type -> olivetin.api.v1.Entity + 68, // [68:94] is the sub-list for method output_type + 42, // [42:68] is the sub-list for method input_type + 42, // [42:42] is the sub-list for extension type_name + 42, // [42:42] is the sub-list for extension extendee + 0, // [0:42] is the sub-list for field type_name } func init() { file_olivetin_api_v1_olivetin_proto_init() } diff --git a/service/internal/api/api.go b/service/internal/api/api.go index 5c3d240..736d639 100644 --- a/service/internal/api/api.go +++ b/service/internal/api/api.go @@ -7,6 +7,7 @@ import ( "os" "path" "sort" + "strings" "connectrpc.com/connect" "google.golang.org/protobuf/encoding/protojson" @@ -388,6 +389,7 @@ func (api *oliveTinAPI) internalLogEntryToPb(logEntry *executor.InternalLogEntry BindingId: logEntry.GetBindingId(), DatetimeRateLimitExpires: calculateRateLimitExpires(api, logEntry), Justification: logEntry.Justification, + Arguments: logEntryArgumentsToProto(logEntry.Arguments), } if !pble.ExecutionFinished && logEntry.Binding != nil && logEntry.Binding.Action != nil { @@ -1548,14 +1550,15 @@ func (api *oliveTinAPI) RestartAction(ctx ctx.Context, req *connect.Request[apiv return nil, err } - if execReqLogEntry.Binding.Action.Justification { + if execReqLogEntry.Binding.Action.Justification && strings.TrimSpace(execReqLogEntry.Justification) == "" { return nil, restartRequiresJustificationError() } authenticatedUser := auth.UserFromApiCall(ctx, req, api.cfg) execReq := executor.ExecutionRequest{ Binding: execReqLogEntry.Binding, - Arguments: make(map[string]string), + Arguments: copyStringMap(execReqLogEntry.Arguments), + Justification: execReqLogEntry.Justification, AuthenticatedUser: authenticatedUser, Cfg: api.cfg, } diff --git a/service/internal/api/api_log_arguments.go b/service/internal/api/api_log_arguments.go new file mode 100644 index 0000000..442cc89 --- /dev/null +++ b/service/internal/api/api_log_arguments.go @@ -0,0 +1,38 @@ +package api + +import ( + "sort" + + apiv1 "github.com/OliveTin/OliveTin/gen/olivetin/api/v1" +) + +func logEntryArgumentsToProto(args map[string]string) []*apiv1.StartActionArgument { + if len(args) == 0 { + return nil + } + + names := make([]string, 0, len(args)) + for name := range args { + names = append(names, name) + } + sort.Strings(names) + + out := make([]*apiv1.StartActionArgument, 0, len(names)) + for _, name := range names { + out = append(out, &apiv1.StartActionArgument{ + Name: name, + Value: args[name], + }) + } + + return out +} + +func copyStringMap(source map[string]string) map[string]string { + copied := make(map[string]string, len(source)) + for key, value := range source { + copied[key] = value + } + + return copied +} diff --git a/service/internal/api/api_log_arguments_test.go b/service/internal/api/api_log_arguments_test.go new file mode 100644 index 0000000..68ca4e5 --- /dev/null +++ b/service/internal/api/api_log_arguments_test.go @@ -0,0 +1,307 @@ +package api + +import ( + "context" + "testing" + "time" + + "connectrpc.com/connect" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + apiv1 "github.com/OliveTin/OliveTin/gen/olivetin/api/v1" + config "github.com/OliveTin/OliveTin/internal/config" + "github.com/OliveTin/OliveTin/internal/executor" +) + +func argumentAction(title, shell string, args []config.ActionArgument) *config.Action { + return &config.Action{ + Title: title, + Shell: shell, + MaxConcurrent: 1, + Arguments: args, + } +} + +func waitForLogArguments(t *testing.T, ex *executor.Executor, trackingID string) map[string]string { + t.Helper() + + deadline := time.Now().Add(2 * time.Second) + for time.Now().Before(deadline) { + entry, ok := ex.GetLog(trackingID) + if ok && entry.Arguments != nil { + return entry.Arguments + } + + time.Sleep(5 * time.Millisecond) + } + + t.Fatalf("timed out waiting for arguments on log %s", trackingID) + return nil +} + +func TestExecutionStatusIncludesStoredArguments(t *testing.T) { + cfg := config.DefaultConfig() + cfg.Actions = []*config.Action{ + argumentAction("Ping host", "echo {{ host }}", []config.ActionArgument{ + {Name: "host", Type: "ascii_identifier"}, + }), + } + + ex := executor.DefaultExecutor(cfg) + ex.RebuildActionMap() + binding := ex.FindBindingWithNoEntity(cfg.Actions[0]) + require.NotNil(t, binding) + + ts, client := getNewTestServerAndClientWithExecutor(cfg, ex) + defer ts.Close() + + startResp, err := client.StartAction(context.Background(), connect.NewRequest(&apiv1.StartActionRequest{ + BindingId: binding.ID, + Arguments: []*apiv1.StartActionArgument{ + {Name: "host", Value: "example.com"}, + }, + })) + require.NoError(t, err) + + statusResp, err := client.ExecutionStatus(context.Background(), connect.NewRequest(&apiv1.ExecutionStatusRequest{ + ExecutionTrackingId: startResp.Msg.ExecutionTrackingId, + })) + require.NoError(t, err) + require.NotNil(t, statusResp.Msg.LogEntry) + require.Len(t, statusResp.Msg.LogEntry.Arguments, 1) + assert.Equal(t, "host", statusResp.Msg.LogEntry.Arguments[0].Name) + assert.Equal(t, "example.com", statusResp.Msg.LogEntry.Arguments[0].Value) +} + +func TestExecutionStatusOmitsPasswordArguments(t *testing.T) { + cfg := config.DefaultConfig() + cfg.Actions = []*config.Action{ + { + Title: "Connect", + Exec: []string{"echo", "{{ user }}"}, + MaxConcurrent: 1, + Arguments: []config.ActionArgument{ + {Name: "user", Type: "ascii_identifier"}, + {Name: "pass", Type: "password"}, + }, + }, + } + + ex := executor.DefaultExecutor(cfg) + ex.RebuildActionMap() + binding := ex.FindBindingWithNoEntity(cfg.Actions[0]) + require.NotNil(t, binding) + + ts, client := getNewTestServerAndClientWithExecutor(cfg, ex) + defer ts.Close() + + startResp, err := client.StartAction(context.Background(), connect.NewRequest(&apiv1.StartActionRequest{ + BindingId: binding.ID, + Arguments: []*apiv1.StartActionArgument{ + {Name: "user", Value: "alice"}, + {Name: "pass", Value: "secret"}, + }, + })) + require.NoError(t, err) + + statusResp, err := client.ExecutionStatus(context.Background(), connect.NewRequest(&apiv1.ExecutionStatusRequest{ + ExecutionTrackingId: startResp.Msg.ExecutionTrackingId, + })) + require.NoError(t, err) + require.NotNil(t, statusResp.Msg.LogEntry) + + for _, arg := range statusResp.Msg.LogEntry.Arguments { + assert.NotEqual(t, "pass", arg.Name) + } + + require.Len(t, statusResp.Msg.LogEntry.Arguments, 1) + assert.Equal(t, "user", statusResp.Msg.LogEntry.Arguments[0].Name) + assert.Equal(t, "alice", statusResp.Msg.LogEntry.Arguments[0].Value) +} + +func TestRestartActionReusesStoredArguments(t *testing.T) { + cfg := config.DefaultConfig() + cfg.Actions = []*config.Action{ + argumentAction("Ping host", "echo {{ host }}", []config.ActionArgument{ + {Name: "host", Type: "ascii_identifier"}, + }), + } + + ex := executor.DefaultExecutor(cfg) + ex.RebuildActionMap() + binding := ex.FindBindingWithNoEntity(cfg.Actions[0]) + require.NotNil(t, binding) + + ts, client := getNewTestServerAndClientWithExecutor(cfg, ex) + defer ts.Close() + + startResp, err := client.StartAction(context.Background(), connect.NewRequest(&apiv1.StartActionRequest{ + BindingId: binding.ID, + Arguments: []*apiv1.StartActionArgument{ + {Name: "host", Value: "server-a"}, + }, + })) + require.NoError(t, err) + + originalArgs := waitForLogArguments(t, ex, startResp.Msg.ExecutionTrackingId) + assert.Equal(t, "server-a", originalArgs["host"]) + + restartResp, err := client.RestartAction(context.Background(), connect.NewRequest(&apiv1.RestartActionRequest{ + ExecutionTrackingId: startResp.Msg.ExecutionTrackingId, + })) + require.NoError(t, err) + require.NotEmpty(t, restartResp.Msg.ExecutionTrackingId) + assert.NotEqual(t, startResp.Msg.ExecutionTrackingId, restartResp.Msg.ExecutionTrackingId) + + restartedArgs := waitForLogArguments(t, ex, restartResp.Msg.ExecutionTrackingId) + assert.Equal(t, "server-a", restartedArgs["host"]) +} + +func TestLogEntryArgumentsToProto(t *testing.T) { + assert.Nil(t, logEntryArgumentsToProto(nil)) + assert.Nil(t, logEntryArgumentsToProto(map[string]string{})) + + out := logEntryArgumentsToProto(map[string]string{ + "host": "example.com", + "port": "443", + }) + require.Len(t, out, 2) + + values := map[string]string{} + for _, arg := range out { + values[arg.Name] = arg.Value + } + + assert.Equal(t, "example.com", values["host"]) + assert.Equal(t, "443", values["port"]) +} + +func TestCopyStringMap(t *testing.T) { + source := map[string]string{"host": "example.com"} + copied := copyStringMap(source) + + assert.Equal(t, source, copied) + source["host"] = "changed" + assert.Equal(t, "example.com", copied["host"]) + + empty := copyStringMap(nil) + assert.NotNil(t, empty) + assert.Empty(t, empty) +} + +func TestRestartActionRequiresJustificationWhenMissingFromStoredLog(t *testing.T) { + cfg := config.DefaultConfig() + cfg.Actions = []*config.Action{ + { + Title: "Dangerous action", + Shell: "echo ok", + MaxConcurrent: 1, + Justification: true, + }, + } + + ex := executor.DefaultExecutor(cfg) + ex.RebuildActionMap() + binding := ex.FindBindingWithNoEntity(cfg.Actions[0]) + require.NotNil(t, binding) + + trackingID := "manual-log-without-justification" + ex.SetLog(trackingID, &executor.InternalLogEntry{ + Binding: binding, + ExecutionFinished: true, + ExecutionTrackingID: trackingID, + }) + + ts, client := getNewTestServerAndClientWithExecutor(cfg, ex) + defer ts.Close() + + _, err := client.RestartAction(context.Background(), connect.NewRequest(&apiv1.RestartActionRequest{ + ExecutionTrackingId: trackingID, + })) + require.Error(t, err) + assert.Contains(t, err.Error(), "justification") +} + +func TestRestartActionReusesStoredJustificationViaStartActionPath(t *testing.T) { + cfg := config.DefaultConfig() + cfg.Actions = []*config.Action{ + { + Title: "Dangerous action", + Shell: "echo ok", + MaxConcurrent: 1, + Justification: true, + }, + } + + ex := executor.DefaultExecutor(cfg) + ex.RebuildActionMap() + binding := ex.FindBindingWithNoEntity(cfg.Actions[0]) + require.NotNil(t, binding) + + ts, client := getNewTestServerAndClientWithExecutor(cfg, ex) + defer ts.Close() + + startResp, err := client.StartAction(context.Background(), connect.NewRequest(&apiv1.StartActionRequest{ + BindingId: binding.ID, + Justification: "maintenance window", + })) + require.NoError(t, err) + + originalLog, ok := ex.GetLog(startResp.Msg.ExecutionTrackingId) + require.True(t, ok) + assert.Equal(t, "maintenance window", originalLog.Justification) + + restartResp, err := client.RestartAction(context.Background(), connect.NewRequest(&apiv1.RestartActionRequest{ + ExecutionTrackingId: startResp.Msg.ExecutionTrackingId, + })) + require.NoError(t, err) + + restartedLog, ok := ex.GetLog(restartResp.Msg.ExecutionTrackingId) + require.True(t, ok) + assert.Equal(t, "maintenance window", restartedLog.Justification) +} + +func TestGetLogsIncludesStoredArguments(t *testing.T) { + cfg := config.DefaultConfig() + cfg.Actions = []*config.Action{ + argumentAction("Ping host", "echo {{ host }}", []config.ActionArgument{ + {Name: "host", Type: "ascii_identifier"}, + }), + } + + ex := executor.DefaultExecutor(cfg) + ex.RebuildActionMap() + binding := ex.FindBindingWithNoEntity(cfg.Actions[0]) + require.NotNil(t, binding) + + ts, client := getNewTestServerAndClientWithExecutor(cfg, ex) + defer ts.Close() + + startResp, err := client.StartAction(context.Background(), connect.NewRequest(&apiv1.StartActionRequest{ + BindingId: binding.ID, + Arguments: []*apiv1.StartActionArgument{ + {Name: "host", Value: "db-1"}, + }, + })) + require.NoError(t, err) + require.NotEmpty(t, startResp.Msg.ExecutionTrackingId) + + logsResp, err := client.GetLogs(context.Background(), connect.NewRequest(&apiv1.GetLogsRequest{})) + require.NoError(t, err) + require.NotEmpty(t, logsResp.Msg.Logs) + + var matched bool + for _, entry := range logsResp.Msg.Logs { + if entry.ExecutionTrackingId != startResp.Msg.ExecutionTrackingId { + continue + } + + matched = true + require.Len(t, entry.Arguments, 1) + assert.Equal(t, "host", entry.Arguments[0].Name) + assert.Equal(t, "db-1", entry.Arguments[0].Value) + } + + assert.True(t, matched, "expected log entry with stored arguments") +} diff --git a/service/internal/executor/executor.go b/service/internal/executor/executor.go index 605c7ad..2ad1856 100644 --- a/service/internal/executor/executor.go +++ b/service/internal/executor/executor.go @@ -171,6 +171,7 @@ type InternalLogEntry struct { ActionTitle string ActionIcon string Justification string + Arguments map[string]string } // .Binding can be nil, so we need to handle that. @@ -863,6 +864,7 @@ func stepParseArgs(req *ExecutionRequest) bool { return fail(req, err) } mangleInvalidArgumentValues(req) + copyStorableArgumentsToLogEntry(req) if hasExec(req) { return handleExecBranch(req) diff --git a/service/internal/executor/log_arguments.go b/service/internal/executor/log_arguments.go new file mode 100644 index 0000000..0c71b30 --- /dev/null +++ b/service/internal/executor/log_arguments.go @@ -0,0 +1,95 @@ +package executor + +import ( + "strings" + + config "github.com/OliveTin/OliveTin/internal/config" +) + +func argumentTypeStorableInLog(argType string) bool { + switch argType { + case "password", "very_dangerous_raw_string": + return false + default: + return true + } +} + +func storableArgumentNames(action *config.Action) map[string]struct{} { + if action == nil { + return nil + } + + names := make(map[string]struct{}, len(action.Arguments)) + for i := range action.Arguments { + arg := &action.Arguments[i] + if !argumentTypeStorableInLog(arg.Type) { + continue + } + + names[arg.Name] = struct{}{} + } + + return names +} + +func storableArgumentNamesFromRequest(req *ExecutionRequest) map[string]struct{} { + if req == nil || req.Binding == nil || req.Binding.Action == nil { + return nil + } + + return storableArgumentNames(req.Binding.Action) +} + +func isStorableArgumentName(name string, allowedNames map[string]struct{}) bool { + if strings.HasPrefix(name, config.ReservedArgumentNamePrefix) { + return false + } + + _, ok := allowedNames[name] + return ok +} + +func collectStorableArguments(args map[string]string, allowedNames map[string]struct{}) map[string]string { + result := make(map[string]string) + for name, value := range args { + if isStorableArgumentName(name, allowedNames) { + result[name] = value + } + } + + return result +} + +func filterStorableArguments(args map[string]string, allowedNames map[string]struct{}) map[string]string { + if len(args) == 0 { + return nil + } + + result := collectStorableArguments(args, allowedNames) + if len(result) == 0 { + return nil + } + + return result +} + +func storableArgumentsFromRequest(req *ExecutionRequest) map[string]string { + allowedNames := storableArgumentNamesFromRequest(req) + if len(allowedNames) == 0 { + return nil + } + + return filterStorableArguments(req.Arguments, allowedNames) +} + +func copyStorableArgumentsToLogEntry(req *ExecutionRequest) { + args := storableArgumentsFromRequest(req) + if args == nil || req.logEntry == nil { + return + } + + req.mutateLogEntry(func(entry *InternalLogEntry) { + entry.Arguments = args + }) +} diff --git a/service/internal/executor/log_arguments_test.go b/service/internal/executor/log_arguments_test.go new file mode 100644 index 0000000..9a3ef32 --- /dev/null +++ b/service/internal/executor/log_arguments_test.go @@ -0,0 +1,121 @@ +package executor + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + auth "github.com/OliveTin/OliveTin/internal/auth" + config "github.com/OliveTin/OliveTin/internal/config" +) + +func TestArgumentTypeStorableInLog(t *testing.T) { + assert.True(t, argumentTypeStorableInLog("ascii")) + assert.True(t, argumentTypeStorableInLog("shell_safe_identifier")) + assert.False(t, argumentTypeStorableInLog("password")) + assert.False(t, argumentTypeStorableInLog("very_dangerous_raw_string")) +} + +func TestStorableArgumentsFromRequestExcludesSensitiveAndSystemArgs(t *testing.T) { + req := newExecRequest() + req.Binding.Action.Arguments = []config.ActionArgument{ + {Name: "host", Type: "ascii_identifier"}, + {Name: "secret", Type: "password"}, + {Name: "payload", Type: "very_dangerous_raw_string"}, + } + req.Arguments = map[string]string{ + "host": "example.com", + "secret": "hunter2", + "payload": "rm -rf /", + "ot_executionTrackingId": "track-123", + "ot_username": "alice", + "extra_undefined": "drop-me", + } + + args := storableArgumentsFromRequest(req) + + require.Len(t, args, 1) + assert.Equal(t, "example.com", args["host"]) + assert.NotContains(t, args, "secret") + assert.NotContains(t, args, "payload") + assert.NotContains(t, args, "ot_executionTrackingId") + assert.NotContains(t, args, "ot_username") + assert.NotContains(t, args, "extra_undefined") +} + +func TestStorableArgumentsFromRequestReturnsNilWhenEmpty(t *testing.T) { + req := newExecRequest() + req.Binding.Action.Arguments = []config.ActionArgument{ + {Name: "secret", Type: "password"}, + } + req.Arguments = map[string]string{ + "secret": "hunter2", + } + + assert.Nil(t, storableArgumentsFromRequest(req)) +} + +func TestStorableArgumentsFromRequestStoresMangledCheckboxValue(t *testing.T) { + req := newExecRequest() + req.Binding.Action.Arguments = []config.ActionArgument{ + { + Name: "mode", + Type: "checkbox", + Choices: []config.ActionArgumentChoice{ + {Title: "Enabled", Value: "1"}, + {Title: "Disabled", Value: "0"}, + }, + }, + } + req.Arguments = map[string]string{ + "mode": "Enabled", + } + + mangleInvalidArgumentValues(req) + args := storableArgumentsFromRequest(req) + + require.Len(t, args, 1) + assert.Equal(t, "1", args["mode"]) +} + +func TestCopyStorableArgumentsToLogEntry(t *testing.T) { + req := newExecRequest() + req.logEntry = &InternalLogEntry{} + req.Binding.Action.Arguments = []config.ActionArgument{ + {Name: "target", Type: "ascii_identifier"}, + } + req.Arguments = map[string]string{ + "target": "server-a", + } + + copyStorableArgumentsToLogEntry(req) + + require.NotNil(t, req.logEntry.Arguments) + assert.Equal(t, "server-a", req.logEntry.Arguments["target"]) +} + +func TestExecRequestStoresArgumentsOnLogEntry(t *testing.T) { + e, cfg := testingExecutor() + + e.RebuildActionMap() + binding := e.FindBindingWithNoEntity(cfg.Actions[0]) + require.NotNil(t, binding) + + req := ExecutionRequest{ + Binding: binding, + Cfg: cfg, + AuthenticatedUser: auth.UserGuest(cfg), + Arguments: map[string]string{ + "person": "yourself", + }, + } + + wg, trackingID := e.ExecRequest(&req) + wg.Wait() + + logEntry, ok := e.GetLog(trackingID) + require.True(t, ok) + require.NotNil(t, logEntry.Arguments) + assert.Equal(t, "yourself", logEntry.Arguments["person"]) +}