From abf4fc2cf0074c124458baf07c4243198ab9e449 Mon Sep 17 00:00:00 2001 From: jamesread Date: Fri, 26 Jun 2026 21:19:19 +0100 Subject: [PATCH 1/5] feat: Windows icons and MSI support --- .github/workflows/build-and-release.yml | 6 ++ .gitignore | 1 + .goreleaser.yml | 1 + .releaserc.yaml | 2 + Makefile | 7 ++ service/Makefile | 7 +- var/windows/OliveTin.exe.manifest | 25 +++++++ var/windows/OliveTin.ico | Bin 0 -> 109595 bytes var/windows/OliveTin.wxs | 46 ++++++++++++ var/windows/build-msi.sh | 93 ++++++++++++++++++++++++ var/windows/generate-resources.sh | 92 +++++++++++++++++++++++ var/windows/upload-msi-release.sh | 40 ++++++++++ var/windows/versioninfo.json | 41 +++++++++++ 13 files changed, 359 insertions(+), 2 deletions(-) create mode 100644 var/windows/OliveTin.exe.manifest create mode 100644 var/windows/OliveTin.ico create mode 100644 var/windows/OliveTin.wxs create mode 100755 var/windows/build-msi.sh create mode 100755 var/windows/generate-resources.sh create mode 100755 var/windows/upload-msi-release.sh create mode 100644 var/windows/versioninfo.json diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index b9ba5a0..5fc1d7c 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -13,6 +13,7 @@ on: - 'integration-tests/**' - 'proto/**' - 'service/**' + - 'var/windows/**' workflow_dispatch: push: tags: @@ -31,6 +32,7 @@ on: - 'integration-tests/**' - 'proto/**' - 'service/**' + - 'var/windows/**' jobs: build: @@ -112,6 +114,10 @@ jobs: integration-tests !integration-tests/node_modules + - name: Install msitools + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false + run: sudo apt-get update && sudo apt-get install -y msitools + - name: Install goreleaser if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false uses: goreleaser/goreleaser-action@v6 diff --git a/.gitignore b/.gitignore index 48e8d33..469fbd0 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ service/OliveTin service/OliveTin.armhf service/OliveTin.exe +service/resource_windows_*.syso service/reports releases/ dist/ diff --git a/.goreleaser.yml b/.goreleaser.yml index a6ba475..32c6a76 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -3,6 +3,7 @@ version: 2 before: hooks: - make service-prep + - make windows-resources VERSION={{ .Version }} builds: - env: diff --git a/.releaserc.yaml b/.releaserc.yaml index c3ddbdc..fc571cf 100644 --- a/.releaserc.yaml +++ b/.releaserc.yaml @@ -10,5 +10,7 @@ plugins: - - "@semantic-release/exec" - publishCmd: | goreleaser release --clean --timeout 60m + VERSION=${nextRelease.version} ./var/windows/build-msi.sh + ./var/windows/upload-msi-release.sh ${nextRelease.gitTag} tagFormat: '${version}' diff --git a/Makefile b/Makefile index 23062c2..7ce7955 100644 --- a/Makefile +++ b/Makefile @@ -8,6 +8,12 @@ service: service-prep: $(MAKE) -wC service prep +windows-resources: + VERSION="$(VERSION)" ./var/windows/generate-resources.sh + +windows-msi: + VERSION="$(VERSION)" ./var/windows/build-msi.sh + service-unittests: $(MAKE) -wC service unittests @@ -59,6 +65,7 @@ clean: $(call delete-files,OliveTin) $(call delete-files,OliveTin.armhf) $(call delete-files,OliveTin.exe) + rm -f service/resource_windows_*.syso $(call delete-files,reports) $(call delete-files,gen) diff --git a/service/Makefile b/service/Makefile index dee7cc9..5d3044a 100644 --- a/service/Makefile +++ b/service/Makefile @@ -19,11 +19,14 @@ compile-x64-lin: go build -o OliveTin go env -u GOOS -compile-x64-win: +compile-x64-win: windows-resources go env -w GOOS=windows GOARCH=amd64 go build -o OliveTin.exe go env -u GOOS GOARCH +windows-resources: + $(MAKE) -wC .. windows-resources + compile: compile-armhf compile-x64-lin compile-x64-win codestyle: go-tools @@ -33,7 +36,7 @@ codestyle: go-tools gocritic check ./... test: unittests - + tests: unittests unittests: diff --git a/var/windows/OliveTin.exe.manifest b/var/windows/OliveTin.exe.manifest new file mode 100644 index 0000000..90faaa7 --- /dev/null +++ b/var/windows/OliveTin.exe.manifest @@ -0,0 +1,25 @@ + + + + + + + + + + + true + PerMonitorV2 + + + diff --git a/var/windows/OliveTin.ico b/var/windows/OliveTin.ico new file mode 100644 index 0000000000000000000000000000000000000000..446dd82e1cfa38cd517d8b2027382c01da987f0a GIT binary patch literal 109595 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 literal 0 HcmV?d00001 diff --git a/var/windows/OliveTin.wxs b/var/windows/OliveTin.wxs new file mode 100644 index 0000000..807dd5b --- /dev/null +++ b/var/windows/OliveTin.wxs @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/var/windows/build-msi.sh b/var/windows/build-msi.sh new file mode 100755 index 0000000..42aeb07 --- /dev/null +++ b/var/windows/build-msi.sh @@ -0,0 +1,93 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" +DIST_DIR="${DIST_DIR:-${REPO_ROOT}/dist}" +ARCH="${ARCH:-amd64}" +ZIP_NAME="OliveTin-windows-${ARCH}.zip" +ZIP_PATH="${DIST_DIR}/${ZIP_NAME}" +MSI_NAME="OliveTin-windows-${ARCH}.msi" +MSI_PATH="${DIST_DIR}/${MSI_NAME}" + +if [[ ! -f "${ZIP_PATH}" ]]; then + echo "Windows archive not found: ${ZIP_PATH}" >&2 + exit 1 +fi + +if ! command -v wixl >/dev/null || ! command -v wixl-heat >/dev/null; then + echo "wixl and wixl-heat are required (install the wixl/msitools package)" >&2 + exit 1 +fi + +normalize_windows_version() { + local raw="${1#v}" + raw="${raw%%-*}" + if [[ ! "${raw}" =~ ^[0-9]+(\.[0-9]+){0,3}$ ]]; then + echo "0.0.0.0" + return + fi + local -a parts=() + IFS='.' read -r -a parts <<<"${raw}" + local major="${parts[0]:-0}" + local minor="${parts[1]:-0}" + local patch="${parts[2]:-0}" + local build="${parts[3]:-0}" + printf '%s.%s.%s.%s' "${major}" "${minor}" "${patch}" "${build}" +} + +VERSION="${VERSION:-}" +if [[ -z "${VERSION}" ]]; then + VERSION="$(git -C "${REPO_ROOT}" describe --tags --abbrev=0 2>/dev/null | sed 's/^v//' || true)" +fi +if [[ -z "${VERSION}" ]]; then + echo "Could not determine release version; set VERSION explicitly" >&2 + exit 1 +fi +WINDOWS_VERSION="$(normalize_windows_version "${VERSION}")" + +STAGING="$(mktemp -d)" +APP_STAGING="$(mktemp -d)" +HEAT_WXS="$(mktemp)" +trap 'rm -rf "${STAGING}" "${APP_STAGING}" "${HEAT_WXS}"' EXIT + +unzip -q "${ZIP_PATH}" -d "${STAGING}" +SOURCE_ROOT="${STAGING}/OliveTin-windows-${ARCH}" + +if [[ ! -f "${SOURCE_ROOT}/OliveTin.exe" ]]; then + echo "OliveTin.exe not found in ${SOURCE_ROOT}" >&2 + exit 1 +fi + +if [[ ! -f "${SOURCE_ROOT}/config.yaml" ]]; then + echo "config.yaml not found in ${SOURCE_ROOT}" >&2 + exit 1 +fi + +mkdir -p "${APP_STAGING}/webui" +cp "${SOURCE_ROOT}/OliveTin.exe" "${APP_STAGING}/" +cp -a "${SOURCE_ROOT}/webui/." "${APP_STAGING}/webui/" + +( + cd "${APP_STAGING}" + find . -type f | sed 's|^\./||' +) | wixl-heat \ + -p "" \ + --component-group CG.AppFiles \ + --var var.SourceDir \ + --directory-ref INSTALLDIR \ + --win64 \ + > "${HEAT_WXS}" + +wixl \ + -v \ + -a x64 \ + -D "Version=${WINDOWS_VERSION}" \ + -D "Win64=yes" \ + -D "SourceDir=${APP_STAGING}" \ + -D "ConfigSource=${SOURCE_ROOT}/config.yaml" \ + -o "${MSI_PATH}" \ + "${SCRIPT_DIR}/OliveTin.wxs" \ + "${HEAT_WXS}" + +echo "Built ${MSI_PATH}" diff --git a/var/windows/generate-resources.sh b/var/windows/generate-resources.sh new file mode 100755 index 0000000..50e562c --- /dev/null +++ b/var/windows/generate-resources.sh @@ -0,0 +1,92 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" +SERVICE_DIR="${REPO_ROOT}/service" +VERSIONINFO_JSON="${SCRIPT_DIR}/versioninfo.json" +ICON_PATH="${SCRIPT_DIR}/OliveTin.ico" +MANIFEST_PATH="${SCRIPT_DIR}/OliveTin.exe.manifest" +GOVERSIONINFO_VERSION="${GOVERSIONINFO_VERSION:-v1.5.0}" + +usage() { + cat </dev/null 2>&1; then + git -C "${REPO_ROOT}" describe --tags --abbrev=0 + return + fi + echo "0.0.0" +} + +if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then + usage + exit 0 +fi + +if [[ ! -f "${VERSIONINFO_JSON}" ]]; then + echo "versioninfo.json not found: ${VERSIONINFO_JSON}" >&2 + exit 1 +fi + +if [[ ! -f "${ICON_PATH}" ]]; then + echo "icon not found: ${ICON_PATH}" >&2 + exit 1 +fi + +WINDOWS_VERSION="$(normalize_windows_version "$(resolve_version "${1:-}")")" +echo "Generating Windows resources for version ${WINDOWS_VERSION}" + +go install "github.com/josephspurrier/goversioninfo/cmd/goversioninfo@${GOVERSIONINFO_VERSION}" + +WORK_DIR="$(mktemp -d)" +trap 'rm -rf "${WORK_DIR}"' EXIT + +( + cd "${WORK_DIR}" + goversioninfo \ + -64 \ + -platform-specific \ + -icon="${ICON_PATH}" \ + -manifest="${MANIFEST_PATH}" \ + -file-version="${WINDOWS_VERSION}" \ + -product-version="${WINDOWS_VERSION}" \ + "${VERSIONINFO_JSON}" +) + +rm -f "${SERVICE_DIR}"/resource_windows_*.syso +mv "${WORK_DIR}"/resource_windows_*.syso "${SERVICE_DIR}/" +echo "Wrote Windows resource files to ${SERVICE_DIR}" diff --git a/var/windows/upload-msi-release.sh b/var/windows/upload-msi-release.sh new file mode 100755 index 0000000..3773c28 --- /dev/null +++ b/var/windows/upload-msi-release.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" +DIST_DIR="${DIST_DIR:-${REPO_ROOT}/dist}" +ARCH="${ARCH:-amd64}" +MSI_NAME="OliveTin-windows-${ARCH}.msi" +MSI_PATH="${DIST_DIR}/${MSI_NAME}" +TAG="${1:-}" + +if [[ -z "${TAG}" ]]; then + echo "Usage: $(basename "$0") " >&2 + exit 1 +fi + +if [[ ! -f "${MSI_PATH}" ]]; then + echo "MSI not found: ${MSI_PATH}" >&2 + exit 1 +fi + +if ! command -v gh >/dev/null; then + echo "gh is required to upload the MSI to GitHub releases" >&2 + exit 1 +fi + +checksums_path="${DIST_DIR}/checksums.txt" +if [[ -f "${checksums_path}" ]] && ! grep -qF " ${MSI_NAME}" "${checksums_path}"; then + ( + cd "${DIST_DIR}" + sha256sum "${MSI_NAME}" + ) >> "${checksums_path}" +fi + +gh release upload "${TAG}" "${MSI_PATH}" --clobber +if [[ -f "${checksums_path}" ]]; then + gh release upload "${TAG}" "${checksums_path}" --clobber +fi + +echo "Uploaded ${MSI_NAME} to release ${TAG}" diff --git a/var/windows/versioninfo.json b/var/windows/versioninfo.json new file mode 100644 index 0000000..7822648 --- /dev/null +++ b/var/windows/versioninfo.json @@ -0,0 +1,41 @@ +{ + "FixedFileInfo": { + "FileVersion": { + "Major": 0, + "Minor": 0, + "Patch": 0, + "Build": 0 + }, + "ProductVersion": { + "Major": 0, + "Minor": 0, + "Patch": 0, + "Build": 0 + }, + "FileFlagsMask": "3f", + "FileFlags ": "00", + "FileOS": "040004", + "FileType": "01", + "FileSubType": "00" + }, + "StringFileInfo": { + "Comments": "https://github.com/OliveTin/OliveTin", + "CompanyName": "James Read", + "FileDescription": "OliveTin web interface for running shell commands", + "FileVersion": "0.0.0.0", + "InternalName": "OliveTin", + "LegalCopyright": "Copyright (C) James Read. Licensed under AGPL-3.0.", + "LegalTrademarks": "", + "OriginalFilename": "OliveTin.exe", + "PrivateBuild": "", + "ProductName": "OliveTin", + "ProductVersion": "0.0.0.0", + "SpecialBuild": "" + }, + "VarFileInfo": { + "Translation": { + "LangID": "0409", + "CharsetID": "04B0" + } + } +} From b749b797f86d0d3ad0727e00f0f19e6d186c34b5 Mon Sep 17 00:00:00 2001 From: jamesread Date: Fri, 26 Jun 2026 21:42:12 +0100 Subject: [PATCH 2/5] fix: Build MSI in goreleaser build --- .goreleaser.yml | 4 ++++ .releaserc.yaml | 4 +--- Makefile | 2 +- var/windows/OliveTin.wxs | 2 +- var/windows/build-msi.sh | 11 +++++------ var/windows/generate-resources.sh | 6 ++++-- var/windows/goreleaser-release-with-msi.sh | 17 +++++++++++++++++ var/windows/upload-msi-release.sh | 15 ++++++++++----- 8 files changed, 43 insertions(+), 18 deletions(-) create mode 100755 var/windows/goreleaser-release-with-msi.sh diff --git a/.goreleaser.yml b/.goreleaser.yml index 32c6a76..f299d24 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -43,6 +43,8 @@ builds: checksum: name_template: 'checksums.txt' + extra_files: + - glob: ./dist/OliveTin-windows-amd64.msi snapshot: version_template: "{{ .Branch }}-{{ .ShortCommit }}" changelog: @@ -176,6 +178,8 @@ nfpms: dst: /usr/share/man/man1/OliveTin.1.gz release: + extra_files: + - glob: ./dist/OliveTin-windows-amd64.msi footer: | ## Container images (from GitHub) diff --git a/.releaserc.yaml b/.releaserc.yaml index fc571cf..1aa94a5 100644 --- a/.releaserc.yaml +++ b/.releaserc.yaml @@ -9,8 +9,6 @@ plugins: - '@semantic-release/git' - - "@semantic-release/exec" - publishCmd: | - goreleaser release --clean --timeout 60m - VERSION=${nextRelease.version} ./var/windows/build-msi.sh - ./var/windows/upload-msi-release.sh ${nextRelease.gitTag} + VERSION=${nextRelease.version} GORELEASER_TIMEOUT=60m ./var/windows/goreleaser-release-with-msi.sh tagFormat: '${version}' diff --git a/Makefile b/Makefile index 7ce7955..186b56e 100644 --- a/Makefile +++ b/Makefile @@ -72,4 +72,4 @@ clean: config-tool: cd service && go run cmd/config-tool/main.go -.PHONY: proto service +.PHONY: proto service windows-resources windows-msi diff --git a/var/windows/OliveTin.wxs b/var/windows/OliveTin.wxs index 807dd5b..7884d8a 100644 --- a/var/windows/OliveTin.wxs +++ b/var/windows/OliveTin.wxs @@ -34,7 +34,7 @@ - + /dev/null || ! command -v wixl-heat >/dev/null; then exit 1 fi -normalize_windows_version() { +normalize_msi_version() { local raw="${1#v}" raw="${raw%%-*}" if [[ ! "${raw}" =~ ^[0-9]+(\.[0-9]+){0,3}$ ]]; then - echo "0.0.0.0" + echo "0.0.0" return fi local -a parts=() @@ -32,8 +32,7 @@ normalize_windows_version() { local major="${parts[0]:-0}" local minor="${parts[1]:-0}" local patch="${parts[2]:-0}" - local build="${parts[3]:-0}" - printf '%s.%s.%s.%s' "${major}" "${minor}" "${patch}" "${build}" + printf '%s.%s.%s' "${major}" "${minor}" "${patch}" } VERSION="${VERSION:-}" @@ -44,7 +43,7 @@ if [[ -z "${VERSION}" ]]; then echo "Could not determine release version; set VERSION explicitly" >&2 exit 1 fi -WINDOWS_VERSION="$(normalize_windows_version "${VERSION}")" +MSI_VERSION="$(normalize_msi_version "${VERSION}")" STAGING="$(mktemp -d)" APP_STAGING="$(mktemp -d)" @@ -82,7 +81,7 @@ cp -a "${SOURCE_ROOT}/webui/." "${APP_STAGING}/webui/" wixl \ -v \ -a x64 \ - -D "Version=${WINDOWS_VERSION}" \ + -D "Version=${MSI_VERSION}" \ -D "Win64=yes" \ -D "SourceDir=${APP_STAGING}" \ -D "ConfigSource=${SOURCE_ROOT}/config.yaml" \ diff --git a/var/windows/generate-resources.sh b/var/windows/generate-resources.sh index 50e562c..4728641 100755 --- a/var/windows/generate-resources.sh +++ b/var/windows/generate-resources.sh @@ -70,14 +70,16 @@ fi WINDOWS_VERSION="$(normalize_windows_version "$(resolve_version "${1:-}")")" echo "Generating Windows resources for version ${WINDOWS_VERSION}" +TOOL_BIN="$(mktemp -d)/bin" +export GOBIN="${TOOL_BIN}" go install "github.com/josephspurrier/goversioninfo/cmd/goversioninfo@${GOVERSIONINFO_VERSION}" WORK_DIR="$(mktemp -d)" -trap 'rm -rf "${WORK_DIR}"' EXIT +trap 'rm -rf "${WORK_DIR}" "${TOOL_BIN%/*}"' EXIT ( cd "${WORK_DIR}" - goversioninfo \ + "${TOOL_BIN}/goversioninfo" \ -64 \ -platform-specific \ -icon="${ICON_PATH}" \ diff --git a/var/windows/goreleaser-release-with-msi.sh b/var/windows/goreleaser-release-with-msi.sh new file mode 100755 index 0000000..d69a5e5 --- /dev/null +++ b/var/windows/goreleaser-release-with-msi.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +TIMEOUT="${GORELEASER_TIMEOUT:-60m}" + +if [[ -z "${VERSION:-}" ]]; then + echo "VERSION is required to build the Windows MSI" >&2 + exit 1 +fi + +goreleaser release --clean --timeout "${TIMEOUT}" --skip=checksum,publish "$@" + +"${SCRIPT_DIR}/build-msi.sh" + +goreleaser release --timeout "${TIMEOUT}" \ + --skip=validate,before,build,archive,nfpm,docker,sign,sbom,after,announce "$@" diff --git a/var/windows/upload-msi-release.sh b/var/windows/upload-msi-release.sh index 3773c28..6295c97 100755 --- a/var/windows/upload-msi-release.sh +++ b/var/windows/upload-msi-release.sh @@ -25,11 +25,16 @@ if ! command -v gh >/dev/null; then fi checksums_path="${DIST_DIR}/checksums.txt" -if [[ -f "${checksums_path}" ]] && ! grep -qF " ${MSI_NAME}" "${checksums_path}"; then - ( - cd "${DIST_DIR}" - sha256sum "${MSI_NAME}" - ) >> "${checksums_path}" +new_checksum="$(cd "${DIST_DIR}" && sha256sum "${MSI_NAME}")" +if [[ -f "${checksums_path}" ]] && grep -qF " ${MSI_NAME}" "${checksums_path}"; then + tmp="$(mktemp)" + grep -vF " ${MSI_NAME}" "${checksums_path}" > "${tmp}" + printf '%s\n' "${new_checksum}" >> "${tmp}" + mv "${tmp}" "${checksums_path}" +elif [[ -f "${checksums_path}" ]]; then + printf '%s\n' "${new_checksum}" >> "${checksums_path}" +else + printf '%s\n' "${new_checksum}" > "${checksums_path}" fi gh release upload "${TAG}" "${MSI_PATH}" --clobber From 42003da38452d8326838000a8c9b08bfdfca4515 Mon Sep 17 00:00:00 2001 From: jamesread Date: Fri, 26 Jun 2026 23:25:24 +0100 Subject: [PATCH 3/5] 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"]) +} From e63094e8e8478de202d7ad5e81b255997165c821 Mon Sep 17 00:00:00 2001 From: jamesread Date: Sun, 28 Jun 2026 22:15:46 +0100 Subject: [PATCH 4/5] fix: several merge messes --- .../vue/components/ChoiceCombobox.vue | 32 +++++++++------ .../vue/components/choiceComboboxHelpers.js | 33 +++++++++++++++ .../components/choiceComboboxHelpers.test.mjs | 40 +++++++++++++++++++ .../multipleDropdowns/multipleDropdowns.js | 14 ++++--- service/Makefile | 4 +- .../internal/config/config_reloader_test.go | 40 ++++++++++++------- var/windows/build-msi.sh | 6 +-- var/windows/generate-resources.sh | 8 ++-- var/windows/upload-msi-release.sh | 2 +- 9 files changed, 136 insertions(+), 43 deletions(-) create mode 100644 frontend/resources/vue/components/choiceComboboxHelpers.js create mode 100644 frontend/resources/vue/components/choiceComboboxHelpers.test.mjs diff --git a/frontend/resources/vue/components/ChoiceCombobox.vue b/frontend/resources/vue/components/ChoiceCombobox.vue index e2488b9..d19ec35 100644 --- a/frontend/resources/vue/components/ChoiceCombobox.vue +++ b/frontend/resources/vue/components/ChoiceCombobox.vue @@ -52,6 +52,10 @@