feat: Show config issues in diagnostics (#1087)
This commit is contained in:
commit
ecc5c64046
|
|
@ -1,11 +1,29 @@
|
||||||
= Diagnostics
|
= Diagnostics
|
||||||
|
|
||||||
OliveTin provides a built-in diagnostics page that can be used to help check how OliveTin is running and help to troubleshoot issues. It's mainly designed for checking SSH configuration at the moment.
|
OliveTin provides a built-in diagnostics page that can be used to help check how OliveTin is running and help to troubleshoot issues.
|
||||||
|
|
||||||
This is a screenshot of the diagnostics page, which can be accessed by clicking the "Diagnostics" link in the navigation bar:
|
This is a screenshot of the diagnostics page, which can be accessed by clicking the "Diagnostics" link in the navigation bar:
|
||||||
|
|
||||||
image::advanced_configuration/diagnostics/diagnostics.png[]
|
image::advanced_configuration/diagnostics/diagnostics.png[]
|
||||||
|
|
||||||
|
== Configuration issues
|
||||||
|
|
||||||
|
The Diagnostics page includes a **Configuration issues** section. OliveTin checks the loaded configuration for problems such as:
|
||||||
|
|
||||||
|
* Filesystem watch paths that cannot be created (for example missing directories used by `execOnFileCreatedInDir`)
|
||||||
|
* Argument templates that fail to parse
|
||||||
|
* Missing entity files or empty entity data for entity-bound actions
|
||||||
|
* Invalid cron schedules
|
||||||
|
* Entity-bound actions that also use `execOnCron` (cron cannot bind an entity)
|
||||||
|
* Unset environment variables referenced from configuration
|
||||||
|
* Other sanitize-time warnings (for example unknown action groups)
|
||||||
|
|
||||||
|
When one or more issues exist, the Diagnostics navigation link shows a count badge with the number of issues so operators notice without opening the page first. The list and badge refresh after configuration reload and after entity file changes.
|
||||||
|
|
||||||
|
Issues that refer to a specific action are only shown to users who are allowed to view that action. The badge count uses the same per-user filtering. Issues that are not tied to an action (for example unset environment variables) remain visible to anyone who can open Diagnostics.
|
||||||
|
|
||||||
|
Where possible, each issue includes the configuration source file that defined the related action or entity (for example a file under an `include` directory).
|
||||||
|
|
||||||
== Disabling Diagnostics
|
== Disabling Diagnostics
|
||||||
|
|
||||||
The diagnostics page is enabled by default, but you can disable it by using the OliveTin xref::security/acl.adoc#_acls_and_policies_global[security policy configuration], using the defaults, or via an ACL. Examples are shown below for each of these methods.
|
The diagnostics page is enabled by default, but you can disable it by using the OliveTin xref::security/acl.adoc#_acls_and_policies_global[security policy configuration], using the defaults, or via an ACL. Examples are shown below for each of these methods.
|
||||||
|
|
|
||||||
|
|
@ -199,6 +199,11 @@ async function handleConfigChangedEvent (j) {
|
||||||
window.dispatchEvent(j)
|
window.dispatchEvent(j)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleEntityChangedEvent (j) {
|
||||||
|
await refreshInitAfterConfigChange()
|
||||||
|
window.dispatchEvent(j)
|
||||||
|
}
|
||||||
|
|
||||||
const eventCaseToTypeName = {
|
const eventCaseToTypeName = {
|
||||||
entityChanged: 'EventEntityChanged',
|
entityChanged: 'EventEntityChanged',
|
||||||
configChanged: 'EventConfigChanged',
|
configChanged: 'EventConfigChanged',
|
||||||
|
|
@ -230,9 +235,13 @@ function handleEvent (msg) {
|
||||||
case 'EventHeartbeat':
|
case 'EventHeartbeat':
|
||||||
break
|
break
|
||||||
case 'EventOutputChunk':
|
case 'EventOutputChunk':
|
||||||
case 'EventEntityChanged':
|
|
||||||
window.dispatchEvent(j)
|
window.dispatchEvent(j)
|
||||||
break
|
break
|
||||||
|
case 'EventEntityChanged':
|
||||||
|
handleEntityChangedEvent(j).catch((err) => {
|
||||||
|
console.error('EventEntityChanged handler failed:', err)
|
||||||
|
})
|
||||||
|
break
|
||||||
case 'EventExecutionFinished':
|
case 'EventExecutionFinished':
|
||||||
case 'EventExecutionStarted':
|
case 'EventExecutionStarted':
|
||||||
window.dispatchEvent(j)
|
window.dispatchEvent(j)
|
||||||
|
|
|
||||||
|
|
@ -11,25 +11,25 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@connectrpc/connect": "^2.1.2",
|
"@connectrpc/connect": "^2.1.2",
|
||||||
"@connectrpc/connect-web": "^2.1.2",
|
"@connectrpc/connect-web": "^2.1.2",
|
||||||
"@hugeicons/core-free-icons": "^4.2.2",
|
"@hugeicons/core-free-icons": "^4.2.3",
|
||||||
"@hugeicons/vue": "^1.0.7",
|
"@hugeicons/vue": "^1.0.7",
|
||||||
"@vitejs/plugin-vue": "^6.0.7",
|
"@vitejs/plugin-vue": "^6.0.8",
|
||||||
"@xterm/addon-fit": "^0.11.0",
|
"@xterm/addon-fit": "^0.11.0",
|
||||||
"@xterm/addon-web-links": "^0.12.0",
|
"@xterm/addon-web-links": "^0.12.0",
|
||||||
"@xterm/xterm": "^6.0.0",
|
"@xterm/xterm": "^6.0.0",
|
||||||
"iconify-icon": "^3.0.2",
|
"iconify-icon": "^3.0.2",
|
||||||
"picocrank": "^1.18.2",
|
"picocrank": "^1.21.0",
|
||||||
"standard": "^17.1.2",
|
"standard": "^17.1.2",
|
||||||
"unplugin-vue-components": "^32.1.0",
|
"unplugin-vue-components": "^32.1.0",
|
||||||
"vite": "^8.1.3",
|
"vite": "^8.1.5",
|
||||||
"vue": "^3.5.39",
|
"vue": "^3.5.40",
|
||||||
"vue-i18n": "^11.4.6",
|
"vue-i18n": "^11.4.7",
|
||||||
"vue-router": "^5.1.0"
|
"vue-router": "^5.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"eslint-plugin-vue": "^10.9.2",
|
"eslint-plugin-vue": "^10.10.0",
|
||||||
"process": "^0.11.10",
|
"process": "^0.11.10",
|
||||||
"stylelint": "^17.14.0",
|
"stylelint": "^17.14.1",
|
||||||
"stylelint-config-standard": "^40.0.0"
|
"stylelint-config-standard": "^40.0.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|
@ -52,13 +52,13 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/generator": {
|
"node_modules/@babel/generator": {
|
||||||
"version": "8.0.0-rc.5",
|
"version": "8.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-8.0.0-rc.5.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-8.0.0.tgz",
|
||||||
"integrity": "sha512-nFZPWz3FHIS7y6rMIVoa/WBwjdutfIaRJIBQjzn+t3RnecZoRNlGmGcyR2wb0T/IgSd50Kz/6dG8/LvMCRunjg==",
|
"integrity": "sha512-NT9NrVwJsbSV6Y2FSstWa71EETOnzrjkL5/wX3D2mYHtKM+qvqB1DvR4D0Setb/gDBsHzRICifwEWMO8CnTF6g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/parser": "^8.0.0-rc.5",
|
"@babel/parser": "^8.0.0",
|
||||||
"@babel/types": "^8.0.0-rc.5",
|
"@babel/types": "^8.0.0",
|
||||||
"@jridgewell/gen-mapping": "^0.3.12",
|
"@jridgewell/gen-mapping": "^0.3.12",
|
||||||
"@jridgewell/trace-mapping": "^0.3.28",
|
"@jridgewell/trace-mapping": "^0.3.28",
|
||||||
"@types/jsesc": "^2.5.0",
|
"@types/jsesc": "^2.5.0",
|
||||||
|
|
@ -69,30 +69,30 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/generator/node_modules/@babel/helper-string-parser": {
|
"node_modules/@babel/generator/node_modules/@babel/helper-string-parser": {
|
||||||
"version": "8.0.0-rc.5",
|
"version": "8.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-8.0.0-rc.5.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-8.0.0.tgz",
|
||||||
"integrity": "sha512-sN7R8rBvDurfaziNfDEIjIntlazmlkCDGO4SNl2RJ3wRCn+QxspLV7hzYAE8WWVd2joVuT8sUxeePdLp2idI1A==",
|
"integrity": "sha512-6mJgmFFFIIO82vvoLt9XtRC7/TkzXfts1t/SpRX4IHSzMgqoPYCWesVu1udUPUWioAE/2fcG6WuI8zrkE1gwrg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^22.18.0 || >=24.11.0"
|
"node": "^22.18.0 || >=24.11.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/generator/node_modules/@babel/helper-validator-identifier": {
|
"node_modules/@babel/generator/node_modules/@babel/helper-validator-identifier": {
|
||||||
"version": "8.0.0-rc.5",
|
"version": "8.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-8.0.0-rc.5.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-8.0.4.tgz",
|
||||||
"integrity": "sha512-ehJDxHvtbZ85RtX/L2fi0h9AGsBNqB5Euv1EB8RMAvGYvD+2X+QbpzzOpbklnNXO+WSZJNOaetw2BBj27xsWVg==",
|
"integrity": "sha512-4wFaiLd0bVo4cIoTXI3zKI038NIWE/cr3jvBjejOVYVxV/m8Ltav1USiGzG1fmS5J2RhgEOgXNNK46cRPnRsrg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^22.18.0 || >=24.11.0"
|
"node": "^22.18.0 || >=24.11.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/generator/node_modules/@babel/parser": {
|
"node_modules/@babel/generator/node_modules/@babel/parser": {
|
||||||
"version": "8.0.0-rc.5",
|
"version": "8.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-8.0.0-rc.5.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-8.0.4.tgz",
|
||||||
"integrity": "sha512-/Mfg83rK3+jsRbl4Vbd0jqxc6M1A1/WNFtgrowRM1unEsD3XcNnrBdMM0JWakd0/RN9lseQKwPduW1TiEwKOlQ==",
|
"integrity": "sha512-srpptsAkEbbNIC/q8nT7o+m6CQe8CJUTV/t7MYc9NnWlgYVtHOb7JH6SorxMhN0kuRJjVqXbKClG6xSbPtzz+g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/types": "^8.0.0-rc.5"
|
"@babel/types": "^8.0.4"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"parser": "bin/babel-parser.js"
|
"parser": "bin/babel-parser.js"
|
||||||
|
|
@ -102,13 +102,13 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/generator/node_modules/@babel/types": {
|
"node_modules/@babel/generator/node_modules/@babel/types": {
|
||||||
"version": "8.0.0-rc.5",
|
"version": "8.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-8.0.0-rc.5.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/types/-/types-8.0.4.tgz",
|
||||||
"integrity": "sha512-JeSVu/m8x/zpp4CLjYHVNXuhEyOkhPXuxM8YOXjh6L4LlvQNKuUNOTo5KdBuKAcTDHw8DquToTaEkhsBqPXOaA==",
|
"integrity": "sha512-eY+Yn3dCqTGmyiq2QRU66lA5FL8lqqqvecHt0fF3uHONIa7ToYsaCiWV8lOKqAs0Rb2SjixiKFROngnulPtt2g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-string-parser": "^8.0.0-rc.5",
|
"@babel/helper-string-parser": "^8.0.0",
|
||||||
"@babel/helper-validator-identifier": "^8.0.0-rc.5"
|
"@babel/helper-validator-identifier": "^8.0.4"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^22.18.0 || >=24.11.0"
|
"node": "^22.18.0 || >=24.11.0"
|
||||||
|
|
@ -168,13 +168,13 @@
|
||||||
"peer": true
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/@cacheable/memory": {
|
"node_modules/@cacheable/memory": {
|
||||||
"version": "2.0.9",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.0.9.tgz",
|
"resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.2.0.tgz",
|
||||||
"integrity": "sha512-HdMx6DoGywB30vacDbBsITbIX4pgFqj1zsrV58jZBUw3klzkNoXhj7qOqAgledhxG7YZI5rBSJg7Zp8/VG0DuA==",
|
"integrity": "sha512-CTLKqLItRCEixEAewD3/j9DB3/o96gpTPD4eJ1v+DGOlxZRZncRQkGYqqnAGCscYd6RNeXfGeiuCphsPtqyIfQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cacheable/utils": "^2.4.1",
|
"@cacheable/utils": "^2.5.0",
|
||||||
"@keyv/bigmap": "^1.3.1",
|
"@keyv/bigmap": "^1.3.1",
|
||||||
"hookified": "^1.15.1",
|
"hookified": "^1.15.1",
|
||||||
"keyv": "^5.6.0"
|
"keyv": "^5.6.0"
|
||||||
|
|
@ -208,9 +208,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@cacheable/utils": {
|
"node_modules/@cacheable/utils": {
|
||||||
"version": "2.4.1",
|
"version": "2.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.5.0.tgz",
|
||||||
"integrity": "sha512-eiFgzCbIneyMlLOmNG4g9xzF7Hv3Mga4LjxjcSC/ues6VYq2+gUbQI8JqNuw/ZM8tJIeIaBGpswAsqV2V7ApgA==",
|
"integrity": "sha512-buipgOVDkkPXNR5+xBpDw7Zk2n1EvU7qBJCNUcL7rhQ//kfpOXPAvQ511Os0vpLYJ1pZnvudNytkQt2hst3wqA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
@ -295,9 +295,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@csstools/css-syntax-patches-for-csstree": {
|
"node_modules/@csstools/css-syntax-patches-for-csstree": {
|
||||||
"version": "1.1.5",
|
"version": "1.1.7",
|
||||||
"resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.7.tgz",
|
||||||
"integrity": "sha512-oNjBvzLq2GPZtJphCjLqXow/cHySHSgtxvKZb7OqSZ/xHgw6NWNhfad+6AB9cLeVm6eA9d/qMll3JdEHjy6M+A==",
|
"integrity": "sha512-fQ+05118eQS1cofO3aJpB5efgpBZMvIzwr/sbC8kDLVA5XLG8q1kJV5yzrUAI1f7lvhPnm8fgIjzFB8/O/5Dig==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
|
|
@ -883,9 +883,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint-community/eslint-utils": {
|
"node_modules/@eslint-community/eslint-utils": {
|
||||||
"version": "4.9.0",
|
"version": "4.10.1",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz",
|
||||||
"integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==",
|
"integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"eslint-visitor-keys": "^3.4.3"
|
"eslint-visitor-keys": "^3.4.3"
|
||||||
|
|
@ -942,9 +942,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@hugeicons/core-free-icons": {
|
"node_modules/@hugeicons/core-free-icons": {
|
||||||
"version": "4.2.2",
|
"version": "4.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/@hugeicons/core-free-icons/-/core-free-icons-4.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/@hugeicons/core-free-icons/-/core-free-icons-4.2.3.tgz",
|
||||||
"integrity": "sha512-fjQW3p6cwoJmwK3oCnV8Z3PC5sHihDvr2jkAHax8cxqp62GaV/D7B/Rnao+JwicW8fmLZUQ6QrzWfoyMFOEQlQ==",
|
"integrity": "sha512-fpiU0qFZkv4ABi23xJ2m4qNr0BBfuIaYjPboN8WDA6aj9D+OzXa7eykxKOpACZRuGbDz1U9S5DsNx1d5KNnsgA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@hugeicons/vue": {
|
"node_modules/@hugeicons/vue": {
|
||||||
|
|
@ -998,14 +998,14 @@
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@intlify/core-base": {
|
"node_modules/@intlify/core-base": {
|
||||||
"version": "11.4.6",
|
"version": "11.4.7",
|
||||||
"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-11.4.6.tgz",
|
"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-11.4.7.tgz",
|
||||||
"integrity": "sha512-EOeHO95XESK9IFHgHeZXunsM/WBAoCA0DlaWODvx14vKmetAuS97t+l6Xe9hTUqntPpF93vtVSjjUDafw3wXMw==",
|
"integrity": "sha512-MSB/sBKwEWJTILvQIhg2rnIcwPpLayo3wGwvVA+dJTNeUBD9GoqQgAaSOLdI9iOPDHCm9YoVnLqpfzza98MpkQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@intlify/devtools-types": "11.4.6",
|
"@intlify/devtools-types": "11.4.7",
|
||||||
"@intlify/message-compiler": "11.4.6",
|
"@intlify/message-compiler": "11.4.7",
|
||||||
"@intlify/shared": "11.4.6"
|
"@intlify/shared": "11.4.7"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 22"
|
"node": ">= 22"
|
||||||
|
|
@ -1015,13 +1015,13 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@intlify/devtools-types": {
|
"node_modules/@intlify/devtools-types": {
|
||||||
"version": "11.4.6",
|
"version": "11.4.7",
|
||||||
"resolved": "https://registry.npmjs.org/@intlify/devtools-types/-/devtools-types-11.4.6.tgz",
|
"resolved": "https://registry.npmjs.org/@intlify/devtools-types/-/devtools-types-11.4.7.tgz",
|
||||||
"integrity": "sha512-wowQPpNem56b2d43IJmqbrzG2FeBKe5f/kUGlpNuBmXs6OSqncF8m1+1lxHuW8ISZJF0ma2RkW3iLkw0g0G4VA==",
|
"integrity": "sha512-GSz+J+hqH+AEpAHIYya6fSufS30OaMnG39HiZX7DmGKi3+aaLvassCfsXENEc4Wr4m68q2YP0QdMdB3D9UeAXg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@intlify/core-base": "11.4.6",
|
"@intlify/core-base": "11.4.7",
|
||||||
"@intlify/shared": "11.4.6"
|
"@intlify/shared": "11.4.7"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 22"
|
"node": ">= 22"
|
||||||
|
|
@ -1031,12 +1031,12 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@intlify/message-compiler": {
|
"node_modules/@intlify/message-compiler": {
|
||||||
"version": "11.4.6",
|
"version": "11.4.7",
|
||||||
"resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-11.4.6.tgz",
|
"resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-11.4.7.tgz",
|
||||||
"integrity": "sha512-5nj3jULqeTAC1WovwMs1LQWgatTa2pM/rXN9T3XW8rdOtXW9ZF6/GLSNFTKDQmPLwclhPdgUWLJ/4w3fMeeC/Q==",
|
"integrity": "sha512-bHxmh7n94N4N1evADeb7XTkc3jTw6Ki5biMFZVSX6Jmk+iehy8/maeH2XUsBI27rtKIK+Hzc6QnVAKggUwylKw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@intlify/shared": "11.4.6",
|
"@intlify/shared": "11.4.7",
|
||||||
"source-map-js": "^1.0.2"
|
"source-map-js": "^1.0.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|
@ -1047,9 +1047,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@intlify/shared": {
|
"node_modules/@intlify/shared": {
|
||||||
"version": "11.4.6",
|
"version": "11.4.7",
|
||||||
"resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-11.4.6.tgz",
|
"resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-11.4.7.tgz",
|
||||||
"integrity": "sha512-m1p1HHAMLhqSpTRH7VnXdrN0CQ4y+9vunFkpLkbD8soIuBsnQdawZXqMCgvwI2UVF9Ww7sVaw7g9tV2VO7shoA==",
|
"integrity": "sha512-OtjPZan3No2OZZFnMUiCVsXC6+j+XRwEywaFDk0AoayAbLuPesyDloXhJZLl9JUl5vHZeQUkYSbEA8VX+CWMjg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 22"
|
"node": ">= 22"
|
||||||
|
|
@ -1481,9 +1481,9 @@
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/@vitejs/plugin-vue": {
|
"node_modules/@vitejs/plugin-vue": {
|
||||||
"version": "6.0.7",
|
"version": "6.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.8.tgz",
|
||||||
"integrity": "sha512-km+p+XdSz9Sxm5rqUbqcSfZYaAniKxWBj1KURl+Jr7UaPvvX7BmaWMdP69I5rrFDeQGyxAG7NXdc57vz+snhWg==",
|
"integrity": "sha512-0ZjgOg7oO6farnNGup7yvoM/YXZV84OZxHAwtflItNa/6zzQyVb5LNxyea3FEKEX2XlagIKzrlH7wwxkKgtiew==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@rolldown/pluginutils": "^1.0.1"
|
"@rolldown/pluginutils": "^1.0.1"
|
||||||
|
|
@ -1497,9 +1497,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue-macros/common": {
|
"node_modules/@vue-macros/common": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/@vue-macros/common/-/common-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@vue-macros/common/-/common-3.1.4.tgz",
|
||||||
"integrity": "sha512-h9t4ArDdniO9ekYHAD95t9AZcAbb19lEGK+26iAjUODOIJKmObDNBSe4+6ELQAA3vtYiFPPBtHh7+cQCKi3Dng==",
|
"integrity": "sha512-/5Fv+6DgIcM9ajY05ZmKBv+LMX1M9A0X+IUwDRVdt67ciw8OV9bvG2r34p3RiEadlsQybjhKPRKNXDC8Bp23cw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/compiler-sfc": "^3.5.22",
|
"@vue/compiler-sfc": "^3.5.22",
|
||||||
|
|
@ -1524,53 +1524,53 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/compiler-core": {
|
"node_modules/@vue/compiler-core": {
|
||||||
"version": "3.5.39",
|
"version": "3.5.40",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.39.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.40.tgz",
|
||||||
"integrity": "sha512-16KBTEXAJCpDr0mwlw+AZyhu8iyC7R3S2vBwsI7QnWJU6X3WKc9VKeNEZpiMdZ569qWhz9574L3vV55qRL0Vtw==",
|
"integrity": "sha512-39E8IgOhTbVDnoJFMKc2DvYnypcZwUqgUhQkccva/0m6FUwtIKSGV7n1hpVmYcFaoRAwf9pBcwnKlCEsN63ZEQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/parser": "^7.29.7",
|
"@babel/parser": "^7.29.7",
|
||||||
"@vue/shared": "3.5.39",
|
"@vue/shared": "3.5.40",
|
||||||
"entities": "^7.0.1",
|
"entities": "^7.0.1",
|
||||||
"estree-walker": "^2.0.2",
|
"estree-walker": "^2.0.2",
|
||||||
"source-map-js": "^1.2.1"
|
"source-map-js": "^1.2.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/compiler-dom": {
|
"node_modules/@vue/compiler-dom": {
|
||||||
"version": "3.5.39",
|
"version": "3.5.40",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.39.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.40.tgz",
|
||||||
"integrity": "sha512-oQPigALqYbNxTNPvNgSOe+czwVExfbVF02lz8jP0S3AXJiu3jxYDygNUiqSep4ezzW8XgnubqH63My2A7JR/vg==",
|
"integrity": "sha512-pwkx4vqlqOspFstrcmzwkKLePVMD3PT65imRzLhanU2V1Fj4K13g6OXjanOyzw3aTAuRk84BOmY8f3rEHqPaVA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/compiler-core": "3.5.39",
|
"@vue/compiler-core": "3.5.40",
|
||||||
"@vue/shared": "3.5.39"
|
"@vue/shared": "3.5.40"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/compiler-sfc": {
|
"node_modules/@vue/compiler-sfc": {
|
||||||
"version": "3.5.39",
|
"version": "3.5.40",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.39.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.40.tgz",
|
||||||
"integrity": "sha512-d0ki86iOyN8LoZPBmk5SJWNwHP19CnDDCfuo//+2WJa2g5Ke0Jay983PIBIcSSzldC68I8DrD5GrHV3OSDfodg==",
|
"integrity": "sha512-gIf497P4kpuALcvs5n3AEg1Vdn0pSY4XbjASIfHNYF1/MP3T2Mf2STERTubysBxCRxzJGJYtF/O7vwJrxFB3Vw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/parser": "^7.29.7",
|
"@babel/parser": "^7.29.7",
|
||||||
"@vue/compiler-core": "3.5.39",
|
"@vue/compiler-core": "3.5.40",
|
||||||
"@vue/compiler-dom": "3.5.39",
|
"@vue/compiler-dom": "3.5.40",
|
||||||
"@vue/compiler-ssr": "3.5.39",
|
"@vue/compiler-ssr": "3.5.40",
|
||||||
"@vue/shared": "3.5.39",
|
"@vue/shared": "3.5.40",
|
||||||
"estree-walker": "^2.0.2",
|
"estree-walker": "^2.0.2",
|
||||||
"magic-string": "^0.30.21",
|
"magic-string": "^0.30.21",
|
||||||
"postcss": "^8.5.15",
|
"postcss": "^8.5.19",
|
||||||
"source-map-js": "^1.2.1"
|
"source-map-js": "^1.2.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/compiler-ssr": {
|
"node_modules/@vue/compiler-ssr": {
|
||||||
"version": "3.5.39",
|
"version": "3.5.40",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.39.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.40.tgz",
|
||||||
"integrity": "sha512-Ce7/wvwMHai74bdszfXExdazFigYnlF9zgCmEQUcM1j0fOymlouZ7XilTYNo8oUjhlnjYOZbGrcYKuqjz89Ucw==",
|
"integrity": "sha512-rrE5xiXG663+vHCHa3J9p2z5OcBRjXmoqenprJxAFQxg5pSshzeBiCE6pu46axapRJ2Adk0YDA2BRZVjiHXnhg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/compiler-dom": "3.5.39",
|
"@vue/compiler-dom": "3.5.40",
|
||||||
"@vue/shared": "3.5.39"
|
"@vue/shared": "3.5.40"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/devtools-api": {
|
"node_modules/@vue/devtools-api": {
|
||||||
|
|
@ -1580,71 +1580,69 @@
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@vue/devtools-kit": {
|
"node_modules/@vue/devtools-kit": {
|
||||||
"version": "8.1.2",
|
"version": "8.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-8.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-8.1.5.tgz",
|
||||||
"integrity": "sha512-f75/upc+GCyjXErpgPGz4582ujS0L/adAltGy+tqXMGUJpgAcfGr6CxnnhpZY8BHuMYt6KpbF8uaFrrQG66rGQ==",
|
"integrity": "sha512-FcSAxsi4eWuXLCB7Rv9lj0aIVHHPNVQ2BazGf4RJTc2JCqb4BQg0hk87ZFhminCfl+mD5OUI0rX2cgyu4kJOGA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/devtools-shared": "^8.1.2",
|
"@vue/devtools-shared": "^8.1.5",
|
||||||
"birpc": "^2.6.1",
|
"birpc": "^2.6.1",
|
||||||
"hookable": "^5.5.3",
|
"hookable": "^5.5.3",
|
||||||
"perfect-debounce": "^2.0.0"
|
"perfect-debounce": "^2.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/devtools-shared": {
|
"node_modules/@vue/devtools-shared": {
|
||||||
"version": "8.1.2",
|
"version": "8.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-8.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-8.1.5.tgz",
|
||||||
"integrity": "sha512-X9RyVFYAdkBe4IUf5v48TxBF/6QPmF8CmWrDAjXzfUHrgQ/HGfTC1A6TqgXqZ03ye66l3AD51BAGD69IvKM9sw==",
|
"integrity": "sha512-mhT4zcPFhF+Xk1O4BfhhrbXzpmfqY03fS6xGpcllbQG7lDjhQf8pQHcTIhqQIYx1hfwtHmk/6jM96ele0UxPqQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@vue/reactivity": {
|
"node_modules/@vue/reactivity": {
|
||||||
"version": "3.5.39",
|
"version": "3.5.40",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.39.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.40.tgz",
|
||||||
"integrity": "sha512-TpsuBJ9gGlZa5d23XcM2y8EXanz9dZeVDQBXRwzy46ItgvM+rWpzs+UVM0wcRLxGvcav0HE5jz2gNL53xlRAog==",
|
"integrity": "sha512-B7ot9UlUZOi1zbq61/LvE88ZLTV8IlajTdiZTAEiDQgrnIMIZoPr9kGw0Zw46ObW62O9+H/Be3kMbfb7kYPQZA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/shared": "3.5.39"
|
"@vue/shared": "3.5.40"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/runtime-core": {
|
"node_modules/@vue/runtime-core": {
|
||||||
"version": "3.5.39",
|
"version": "3.5.40",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.39.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.40.tgz",
|
||||||
"integrity": "sha512-9GLtNyRvPAUMbX+7ono0RC2j0guo2LXVi8LvcmAooImACUKm0oFf0jjwbX8/H0AE/t1nxhAkn8RSl9PMCzzxZw==",
|
"integrity": "sha512-KAZLweuZ6uUJPK1PMSQPgBU5gCjgrrfjUhSglmU9NhH+Zjepa8cnwSydPWDWHDwOgY4g3VcZ+PljbiHlURNCbw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/reactivity": "3.5.39",
|
"@vue/reactivity": "3.5.40",
|
||||||
"@vue/shared": "3.5.39"
|
"@vue/shared": "3.5.40"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/runtime-dom": {
|
"node_modules/@vue/runtime-dom": {
|
||||||
"version": "3.5.39",
|
"version": "3.5.40",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.39.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.40.tgz",
|
||||||
"integrity": "sha512-7Y6aAGboKcXAZ3ECuUy7RrS5yy2r47dhTp2SKaJmYxjopImaVFaNa5Ne66NwGovsrxVAl5S5rwc7m22UG7Lmww==",
|
"integrity": "sha512-ZfrX8ssZQds900L9pr8AuK05ddnMsR4MPMZr8cPN9GoqoPWcXLhjvvbIA2SMv+7a97sJ1vv9pj/zxK0Cq/eEFQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/reactivity": "3.5.39",
|
"@vue/reactivity": "3.5.40",
|
||||||
"@vue/runtime-core": "3.5.39",
|
"@vue/runtime-core": "3.5.40",
|
||||||
"@vue/shared": "3.5.39",
|
"@vue/shared": "3.5.40",
|
||||||
"csstype": "^3.2.3"
|
"csstype": "^3.2.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/server-renderer": {
|
"node_modules/@vue/server-renderer": {
|
||||||
"version": "3.5.39",
|
"version": "3.5.40",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.39.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.40.tgz",
|
||||||
"integrity": "sha512-yZSakiAGw85rZfG7UM8akMnIF+FmeiNk47uvHf2nVBBSe+dIKUhZuZq9+XgJhbV3nS5Z4ALH23/MpXofW+mbcw==",
|
"integrity": "sha512-XNJym9WpevhTVt1HuwOrCRJ5Q+9z4BjTMrDtjTrvx74SmUll8spNTw6whWJa9mEkO4PKn5TihI/bm/8ds2QVJw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/compiler-ssr": "3.5.39",
|
"@vue/compiler-ssr": "3.5.40",
|
||||||
"@vue/shared": "3.5.39"
|
"@vue/runtime-dom": "3.5.40",
|
||||||
},
|
"@vue/shared": "3.5.40"
|
||||||
"peerDependencies": {
|
|
||||||
"vue": "3.5.39"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/shared": {
|
"node_modules/@vue/shared": {
|
||||||
"version": "3.5.39",
|
"version": "3.5.40",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.39.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.40.tgz",
|
||||||
"integrity": "sha512-l1rrBtBfTnmxvtsvdQDXltUUy8S1Y+ZaqdfUzmAnJkTd8Z8rv5v/ytW+TKiqEOWyHPoqtPlNFSs0lhRmYVSHVA==",
|
"integrity": "sha512-WxnBtruIqOoV3rA4jeKDWzrYI5h7Cp4+pjwDi8kWGHz+IslhiN+wguLVVhtv2l8VoU02rzDCVfDjgCl1lNpZVg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@xterm/addon-fit": {
|
"node_modules/@xterm/addon-fit": {
|
||||||
|
|
@ -1974,9 +1972,9 @@
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/brace-expansion": {
|
"node_modules/brace-expansion": {
|
||||||
"version": "1.1.14",
|
"version": "1.1.16",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz",
|
||||||
"integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==",
|
"integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"balanced-match": "^1.0.0",
|
"balanced-match": "^1.0.0",
|
||||||
|
|
@ -2018,14 +2016,14 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/cacheable": {
|
"node_modules/cacheable": {
|
||||||
"version": "2.3.5",
|
"version": "2.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.5.0.tgz",
|
||||||
"integrity": "sha512-EQfaKe09tl615iNvq/TBRWTFf1AKJNXYQSsMx0Z3EI0nA+pVsVPS8wJhnRlkbdacKPh1d0qVIhwTc2zsQNFEEg==",
|
"integrity": "sha512-60cyAOytib/OzBw1JNSoSV/boK1AtHryDIjvVBk7XbN4ugfkM3+Sry7fEjNgPMGgOjuaZPAp8ruZ0Cxafwyq9g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cacheable/memory": "^2.0.8",
|
"@cacheable/memory": "^2.2.0",
|
||||||
"@cacheable/utils": "^2.4.1",
|
"@cacheable/utils": "^2.5.0",
|
||||||
"hookified": "^1.15.0",
|
"hookified": "^1.15.0",
|
||||||
"keyv": "^5.6.0",
|
"keyv": "^5.6.0",
|
||||||
"qified": "^0.10.1"
|
"qified": "^0.10.1"
|
||||||
|
|
@ -3015,18 +3013,18 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint-plugin-vue": {
|
"node_modules/eslint-plugin-vue": {
|
||||||
"version": "10.9.2",
|
"version": "10.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-10.9.2.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-10.10.0.tgz",
|
||||||
"integrity": "sha512-4g7ZP3pYcuqd7Zp0pzUKcos0W+RkjBz4EGdhJ92FcYk6v03Ti/GK5NwjgsjxHK+98eXDbHeK7VtX1az7/8doZA==",
|
"integrity": "sha512-dL9x9rBHqqNcByWiLOHK6L0SB97V82/NC0cZRn9cXPjM7pCuWlpQQP9bFH4vjBv80ej1ZpzAkuD8zWH1o9bZbA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.4.0",
|
"@eslint-community/eslint-utils": "^4.9.1",
|
||||||
"natural-compare": "^1.4.0",
|
"natural-compare": "^1.4.0",
|
||||||
"nth-check": "^2.1.1",
|
"nth-check": "^2.1.1",
|
||||||
"postcss-selector-parser": "^7.1.0",
|
"postcss-selector-parser": "^7.1.4",
|
||||||
"semver": "^7.6.3",
|
"semver": "^7.8.5",
|
||||||
"xml-name-validator": "^4.0.0"
|
"xml-name-validator": "^5.0.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
|
|
@ -3232,9 +3230,9 @@
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/fast-uri": {
|
"node_modules/fast-uri": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.4.tgz",
|
||||||
"integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==",
|
"integrity": "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
|
|
@ -3597,9 +3595,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/globby": {
|
"node_modules/globby": {
|
||||||
"version": "16.2.0",
|
"version": "16.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/globby/-/globby-16.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/globby/-/globby-16.2.2.tgz",
|
||||||
"integrity": "sha512-QrJia2qDf5BB/V6HYlDTs0I0lBahyjLzpGQg3KT7FnCdTonAyPy2RtY802m2k4ALx6Dp752f82WsOczEVr3l6Q==",
|
"integrity": "sha512-NLvV9ubZ6NDsJaOpKPy3cQeJpKi9DcWiyCiFUpJPA0YihRqiE6RWaLUmgNNPr8MgPpLZjnBjSmou7uZBRJv9wA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
@ -3618,9 +3616,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/globby/node_modules/ignore": {
|
"node_modules/globby/node_modules/ignore": {
|
||||||
"version": "7.0.5",
|
"version": "7.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz",
|
||||||
"integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
|
"integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|
@ -4311,9 +4309,9 @@
|
||||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
|
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
|
||||||
},
|
},
|
||||||
"node_modules/js-yaml": {
|
"node_modules/js-yaml": {
|
||||||
"version": "4.2.0",
|
"version": "4.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz",
|
||||||
"integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==",
|
"integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
|
|
@ -4934,9 +4932,9 @@
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/nanoid": {
|
"node_modules/nanoid": {
|
||||||
"version": "3.3.12",
|
"version": "3.3.16",
|
||||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz",
|
||||||
"integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
|
"integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
|
|
@ -4966,6 +4964,12 @@
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/nostics": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/nostics/-/nostics-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-FGqEfhQjrvo1lL8KFifdTQiNwwQHJxC1jtYE1Rc54qF/jxONUNL+kC9gS1krX8Q65PgrQ5fCqH/I4NhWBvdSqg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/nth-check": {
|
"node_modules/nth-check": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
|
||||||
|
|
@ -5268,9 +5272,9 @@
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/picocrank": {
|
"node_modules/picocrank": {
|
||||||
"version": "1.18.2",
|
"version": "1.21.0",
|
||||||
"resolved": "https://registry.npmjs.org/picocrank/-/picocrank-1.18.2.tgz",
|
"resolved": "https://registry.npmjs.org/picocrank/-/picocrank-1.21.0.tgz",
|
||||||
"integrity": "sha512-YR1Y00KnEPjH6OCrijSxzTHoyHLhKrsPhkA7ubOkbvcUuD7wtVKbRxfPkjAkA838dGymjoG//iFca2oLnNT5tg==",
|
"integrity": "sha512-KM2xyrHfgrKwAwFtyr/o4cuHYqSeTS5sNAOpSUBw/zFvTGPBUWP/K82cYXRU4Tuz7NoQD6D0VbwAs/Q+rvEjkA==",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hugeicons/core-free-icons": "^4.2.2",
|
"@hugeicons/core-free-icons": "^4.2.2",
|
||||||
|
|
@ -5400,9 +5404,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/postcss": {
|
"node_modules/postcss": {
|
||||||
"version": "8.5.16",
|
"version": "8.5.22",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.22.tgz",
|
||||||
"integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==",
|
"integrity": "sha512-KBDEIpLrvpv16pp3K0Fw+UCoZfopFjjgeB+0tA/aaThfEE74kKDLrgg603YvOWJyg3+WYtyq3xYsQWsIyZlPqQ==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
|
|
@ -5419,7 +5423,7 @@
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.12",
|
"nanoid": "^3.3.16",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
"source-map-js": "^1.2.1"
|
"source-map-js": "^1.2.1"
|
||||||
},
|
},
|
||||||
|
|
@ -6233,9 +6237,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/stylelint": {
|
"node_modules/stylelint": {
|
||||||
"version": "17.14.0",
|
"version": "17.14.1",
|
||||||
"resolved": "https://registry.npmjs.org/stylelint/-/stylelint-17.14.0.tgz",
|
"resolved": "https://registry.npmjs.org/stylelint/-/stylelint-17.14.1.tgz",
|
||||||
"integrity": "sha512-8xkHPpdqYryeIsOgfsYTmr6cIeC4nLYWk5S8BPxpodq8mIuepggkMljsHewWfuAjj/+qpRKou2QerhjMH3iasg==",
|
"integrity": "sha512-xVQwyiuxALUBNB2fBe0tmNemg9KqLtdj3T64mioFDar79B2cU8LIyz+3KL6LdiHs9NkeNfwxpKSaIVOY8f112g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
|
|
@ -6251,7 +6255,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@csstools/css-calc": "^3.2.1",
|
"@csstools/css-calc": "^3.2.1",
|
||||||
"@csstools/css-parser-algorithms": "^4.0.0",
|
"@csstools/css-parser-algorithms": "^4.0.0",
|
||||||
"@csstools/css-syntax-patches-for-csstree": "^1.1.5",
|
"@csstools/css-syntax-patches-for-csstree": "^1.1.6",
|
||||||
"@csstools/css-tokenizer": "^4.0.0",
|
"@csstools/css-tokenizer": "^4.0.0",
|
||||||
"@csstools/media-query-list-parser": "^5.0.0",
|
"@csstools/media-query-list-parser": "^5.0.0",
|
||||||
"@csstools/selector-resolve-nested": "^4.0.0",
|
"@csstools/selector-resolve-nested": "^4.0.0",
|
||||||
|
|
@ -6263,9 +6267,9 @@
|
||||||
"debug": "^4.4.3",
|
"debug": "^4.4.3",
|
||||||
"fast-glob": "^3.3.3",
|
"fast-glob": "^3.3.3",
|
||||||
"fastest-levenshtein": "^1.0.16",
|
"fastest-levenshtein": "^1.0.16",
|
||||||
"file-entry-cache": "^11.1.3",
|
"file-entry-cache": "^11.1.5",
|
||||||
"global-modules": "^2.0.0",
|
"global-modules": "^2.0.0",
|
||||||
"globby": "^16.2.0",
|
"globby": "^16.2.1",
|
||||||
"globjoin": "^0.1.4",
|
"globjoin": "^0.1.4",
|
||||||
"html-tags": "^5.1.0",
|
"html-tags": "^5.1.0",
|
||||||
"ignore": "^7.0.5",
|
"ignore": "^7.0.5",
|
||||||
|
|
@ -6275,12 +6279,12 @@
|
||||||
"micromatch": "^4.0.8",
|
"micromatch": "^4.0.8",
|
||||||
"normalize-path": "^3.0.0",
|
"normalize-path": "^3.0.0",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
"postcss": "^8.5.15",
|
"postcss": "^8.5.16",
|
||||||
"postcss-safe-parser": "^7.0.1",
|
"postcss-safe-parser": "^7.0.1",
|
||||||
"postcss-selector-parser": "^7.1.4",
|
"postcss-selector-parser": "^7.1.4",
|
||||||
"postcss-value-parser": "^4.2.0",
|
"postcss-value-parser": "^4.2.0",
|
||||||
"string-width": "^8.2.1",
|
"string-width": "^8.2.1",
|
||||||
"supports-hyperlinks": "^4.4.0",
|
"supports-hyperlinks": "^4.5.0",
|
||||||
"svg-tags": "^1.0.0",
|
"svg-tags": "^1.0.0",
|
||||||
"table": "^6.9.0",
|
"table": "^6.9.0",
|
||||||
"write-file-atomic": "^7.0.1"
|
"write-file-atomic": "^7.0.1"
|
||||||
|
|
@ -6355,23 +6359,23 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/stylelint/node_modules/file-entry-cache": {
|
"node_modules/stylelint/node_modules/file-entry-cache": {
|
||||||
"version": "11.1.3",
|
"version": "11.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-11.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-11.1.5.tgz",
|
||||||
"integrity": "sha512-oMbq0PD6VIiIwMF6LIa7MEwd/l9huKwmqRKXqmrkqIZv8CvRbfowL+L0ryAl8h//HfAS0zS+4SbYoRyAoA6BJA==",
|
"integrity": "sha512-+PFTHITI08JIGhnNpGNI8T8inUpgZfk3GNEqfT9R2zZV2iFXg3CvqzSl/uEhs7TSGujYRELEANyDvS8Fj7+S7Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"flat-cache": "^6.1.22"
|
"flat-cache": "^6.1.23"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/stylelint/node_modules/flat-cache": {
|
"node_modules/stylelint/node_modules/flat-cache": {
|
||||||
"version": "6.1.22",
|
"version": "6.1.23",
|
||||||
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.22.tgz",
|
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.23.tgz",
|
||||||
"integrity": "sha512-N2dnzVJIphnNsjHcrxGW7DePckJ6haPrSFqpsBUhHYgwtKGVq4JrBGielEGD2fCVnsGm1zlBVZ8wGhkyuetgug==",
|
"integrity": "sha512-f++BY9pTk+983xK1FLzlLpmM0i0z+jHmx3QESGkURMXujQZz1k5wzwX6hjnQ8goaD0B+sYnDK1yZ6MTyZfUaqA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cacheable": "^2.3.4",
|
"cacheable": "^2.5.0",
|
||||||
"flatted": "^3.4.2",
|
"flatted": "^3.4.2",
|
||||||
"hookified": "^1.15.0"
|
"hookified": "^1.15.0"
|
||||||
}
|
}
|
||||||
|
|
@ -6431,9 +6435,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/supports-hyperlinks": {
|
"node_modules/supports-hyperlinks": {
|
||||||
"version": "4.4.0",
|
"version": "4.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-4.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-4.5.0.tgz",
|
||||||
"integrity": "sha512-UKbpT93hN5Nr9go5UY7bopIB9YQlMz9nm/ct4IXt/irb5YRkn9WaqrOBJGZ5Pwvsd5FQzSVeYlGdXoCAPQZrPg==",
|
"integrity": "sha512-ZW2OvfeCXrNTbLakPUzjQG922EeGCOteFSVoek5DKStTh898wf7zgtuFlzQN8HfZCxC3Eh02yJVrRW51hADf+w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
@ -6750,27 +6754,67 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/unplugin": {
|
"node_modules/unplugin": {
|
||||||
"version": "3.0.0",
|
"version": "3.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/unplugin/-/unplugin-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/unplugin/-/unplugin-3.3.0.tgz",
|
||||||
"integrity": "sha512-0Mqk3AT2TZCXWKdcoaufeXNukv2mTrEZExeXlHIOZXdqYoHHr4n51pymnwV8x2BOVxwXbK2HLlI7usrqMpycdg==",
|
"integrity": "sha512-qa66K+crbfyE6JK10GjvbJeRrOsuC/JpbnHctfyp/i4oBTxWOzJfRZyDiOk1PtErMFRu8JhsU/wPvOdBNWe5Rg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/remapping": "^2.3.5",
|
"@jridgewell/remapping": "^2.3.5",
|
||||||
"picomatch": "^4.0.3",
|
"picomatch": "^4.0.4",
|
||||||
"webpack-virtual-modules": "^0.6.2"
|
"webpack-virtual-modules": "^0.6.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^20.19.0 || >=22.12.0"
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@farmfe/core": "*",
|
||||||
|
"@rspack/core": "*",
|
||||||
|
"bun-types-no-globals": "*",
|
||||||
|
"esbuild": "*",
|
||||||
|
"rolldown": "*",
|
||||||
|
"rollup": "*",
|
||||||
|
"unloader": "*",
|
||||||
|
"vite": "*",
|
||||||
|
"webpack": "*"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@farmfe/core": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@rspack/core": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"bun-types-no-globals": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"esbuild": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"rolldown": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"rollup": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"unloader": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"vite": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"webpack": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/unplugin-utils": {
|
"node_modules/unplugin-utils": {
|
||||||
"version": "0.3.1",
|
"version": "0.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/unplugin-utils/-/unplugin-utils-0.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/unplugin-utils/-/unplugin-utils-0.3.2.tgz",
|
||||||
"integrity": "sha512-5lWVjgi6vuHhJ526bI4nlCOmkCIF3nnfXkCMDeMJrtdvxTs6ZFCM8oNufGTsDbKv/tJ/xj8RpvXjRuPBZJuJog==",
|
"integrity": "sha512-xVToRh2CTmLk2HnEG7ac4rl1MJTT3RFkpS8B++/SnB0kXvuaavD+n3m/vrzyWQOdJNSZQACnbz01pnppbwV5BA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"pathe": "^2.0.3",
|
"pathe": "^2.0.3",
|
||||||
"picomatch": "^4.0.3"
|
"picomatch": "^4.0.4"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20.19.0"
|
"node": ">=20.19.0"
|
||||||
|
|
@ -6872,15 +6916,15 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "8.1.3",
|
"version": "8.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-8.1.5.tgz",
|
||||||
"integrity": "sha512-Ds+gBRbj0lwRO2Y5hwnUBdxSwlAve9LeRyU4sNnAr0ewW0gWF0n5bgXgUzbgZ49MV9BVUAQUFYVcDUcilUExMA==",
|
"integrity": "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"lightningcss": "^1.32.0",
|
"lightningcss": "^1.32.0",
|
||||||
"picomatch": "^4.0.4",
|
"picomatch": "^4.0.5",
|
||||||
"postcss": "^8.5.16",
|
"postcss": "^8.5.17",
|
||||||
"rolldown": "~1.1.3",
|
"rolldown": "~1.1.5",
|
||||||
"tinyglobby": "^0.2.17"
|
"tinyglobby": "^0.2.17"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|
@ -6949,9 +6993,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vite/node_modules/picomatch": {
|
"node_modules/vite/node_modules/picomatch": {
|
||||||
"version": "4.0.4",
|
"version": "4.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
|
||||||
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
"integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
|
|
@ -6961,16 +7005,16 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vue": {
|
"node_modules/vue": {
|
||||||
"version": "3.5.39",
|
"version": "3.5.40",
|
||||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.39.tgz",
|
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.40.tgz",
|
||||||
"integrity": "sha512-xmZCYabFGcirU8r0fTuvl/LICc1OU620rnqepaJDL/a141ZigkG7AyaxQLdqJ02ZRYzWe6YPaDHeQx7MfknQfA==",
|
"integrity": "sha512-+8PJ4SJXdn/cHGImF4CKdxlWHIN5Dkt7DoufRREM6h6uVCx2m7QxgcEQmmzyOK8A9mcafg7sFbJFYsdFVubTig==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/compiler-dom": "3.5.39",
|
"@vue/compiler-dom": "3.5.40",
|
||||||
"@vue/compiler-sfc": "3.5.39",
|
"@vue/compiler-sfc": "3.5.40",
|
||||||
"@vue/runtime-dom": "3.5.39",
|
"@vue/runtime-dom": "3.5.40",
|
||||||
"@vue/server-renderer": "3.5.39",
|
"@vue/server-renderer": "3.5.40",
|
||||||
"@vue/shared": "3.5.39"
|
"@vue/shared": "3.5.40"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"typescript": "*"
|
"typescript": "*"
|
||||||
|
|
@ -7074,14 +7118,14 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vue-i18n": {
|
"node_modules/vue-i18n": {
|
||||||
"version": "11.4.6",
|
"version": "11.4.7",
|
||||||
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.4.6.tgz",
|
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.4.7.tgz",
|
||||||
"integrity": "sha512-l0gE7Rfy0phCa5ChKYkOq543Wgd39BCK6hkktfr1Ed4D99oRkgPK9ffShASZdeC8OJxGfdWmpYoAaAH6iLEuIg==",
|
"integrity": "sha512-j6RyshdPPzqLiMAUpnpvZGFPM+rRoWi14Sl5yTsquvoW0/56DWyvhAj2o9TO2YXGvb6teg8T0xrYO9jR3urvdw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@intlify/core-base": "11.4.6",
|
"@intlify/core-base": "11.4.7",
|
||||||
"@intlify/devtools-types": "11.4.6",
|
"@intlify/devtools-types": "11.4.7",
|
||||||
"@intlify/shared": "11.4.6",
|
"@intlify/shared": "11.4.7",
|
||||||
"@vue/devtools-api": "^6.5.0"
|
"@vue/devtools-api": "^6.5.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|
@ -7095,27 +7139,28 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vue-router": {
|
"node_modules/vue-router": {
|
||||||
"version": "5.1.0",
|
"version": "5.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-5.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-5.2.0.tgz",
|
||||||
"integrity": "sha512-HAbiLzLEHQwxPgvsbOJDAwtavszEgLwri6XfyrsPECIFez8+59xc9LofWVdc/HEaSRT822lJ8H9Ns38VVond5g==",
|
"integrity": "sha512-QAC5i0LEb1GLG0LXDQmHu8L7FX12j0KwU/JTKmLQUJMrn04gQdKP6Du+p0QwpHb3iy71vBlqnHQ8WAfOSAWhqw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/generator": "^8.0.0-rc.4",
|
"@babel/generator": "^8.0.0",
|
||||||
"@vue-macros/common": "^3.1.1",
|
"@vue-macros/common": "^3.1.3",
|
||||||
"@vue/devtools-api": "^8.1.2",
|
"@vue/devtools-api": "^8.1.5",
|
||||||
"ast-walker-scope": "^0.9.0",
|
"ast-walker-scope": "^0.9.0",
|
||||||
"chokidar": "^5.0.0",
|
"chokidar": "^5.0.0",
|
||||||
"json5": "^2.2.3",
|
"json5": "^2.2.3",
|
||||||
"local-pkg": "^1.1.2",
|
"local-pkg": "^1.2.1",
|
||||||
"magic-string": "^0.30.21",
|
"magic-string": "^0.30.21",
|
||||||
"mlly": "^1.8.2",
|
"mlly": "^1.8.2",
|
||||||
"muggle-string": "^0.4.1",
|
"muggle-string": "^0.4.1",
|
||||||
|
"nostics": "^1.1.4",
|
||||||
"pathe": "^2.0.3",
|
"pathe": "^2.0.3",
|
||||||
"picomatch": "^4.0.3",
|
"picomatch": "^4.0.5",
|
||||||
"scule": "^1.3.0",
|
"scule": "^1.3.0",
|
||||||
"tinyglobby": "^0.2.16",
|
"tinyglobby": "^0.2.17",
|
||||||
"unplugin": "^3.0.0",
|
"unplugin": "^3.3.0",
|
||||||
"unplugin-utils": "^0.3.1",
|
"unplugin-utils": "^0.3.2",
|
||||||
"yaml": "^2.9.0"
|
"yaml": "^2.9.0"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
|
|
@ -7123,10 +7168,10 @@
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@pinia/colada": ">=0.21.2",
|
"@pinia/colada": ">=0.21.2",
|
||||||
"@vue/compiler-sfc": "^3.5.34",
|
"@vue/compiler-sfc": "^3.5.34 || ^4.0.0",
|
||||||
"pinia": "^3.0.4",
|
"pinia": "^3.0.4 || ^4.0.2",
|
||||||
"vite": "^7.0.0 || ^8.0.0",
|
"vite": "^7.3.0 || ^8.0.0",
|
||||||
"vue": "^3.5.34"
|
"vue": "^3.5.34 || ^4.0.0"
|
||||||
},
|
},
|
||||||
"peerDependenciesMeta": {
|
"peerDependenciesMeta": {
|
||||||
"@pinia/colada": {
|
"@pinia/colada": {
|
||||||
|
|
@ -7144,12 +7189,12 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vue-router/node_modules/@vue/devtools-api": {
|
"node_modules/vue-router/node_modules/@vue/devtools-api": {
|
||||||
"version": "8.1.2",
|
"version": "8.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-8.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-8.1.5.tgz",
|
||||||
"integrity": "sha512-vA0O112YqyDuNA1s7Yb2gCgToQ/OxOWiFDO5ThLCcDy0ldHnSd1dUTaSYhOldbqoNgumE4dxtGAoAaSUKUD1Zg==",
|
"integrity": "sha512-YJipMVAKe5wT5CWf5kTYCaNV7NMNjFVxJkIkJaJ4W/nCxEBzlZzrOsYKeCymdCrFZmBS/+wTWFoUs3Jf/Q6XSQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/devtools-kit": "^8.1.2"
|
"@vue/devtools-kit": "^8.1.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vue-router/node_modules/json5": {
|
"node_modules/vue-router/node_modules/json5": {
|
||||||
|
|
@ -7165,9 +7210,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vue-router/node_modules/picomatch": {
|
"node_modules/vue-router/node_modules/picomatch": {
|
||||||
"version": "4.0.4",
|
"version": "4.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
|
||||||
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
"integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
|
|
@ -7320,13 +7365,13 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/xml-name-validator": {
|
"node_modules/xml-name-validator": {
|
||||||
"version": "4.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
|
||||||
"integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==",
|
"integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/yaml": {
|
"node_modules/yaml": {
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,9 @@
|
||||||
"repository": "https://github.com/OliveTin/OliveTin",
|
"repository": "https://github.com/OliveTin/OliveTin",
|
||||||
"source": "index.html",
|
"source": "index.html",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"eslint-plugin-vue": "^10.9.2",
|
"eslint-plugin-vue": "^10.10.0",
|
||||||
"process": "^0.11.10",
|
"process": "^0.11.10",
|
||||||
"stylelint": "^17.14.0",
|
"stylelint": "^17.14.1",
|
||||||
"stylelint-config-standard": "^40.0.0"
|
"stylelint-config-standard": "^40.0.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
@ -25,20 +25,20 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@connectrpc/connect": "^2.1.2",
|
"@connectrpc/connect": "^2.1.2",
|
||||||
"@connectrpc/connect-web": "^2.1.2",
|
"@connectrpc/connect-web": "^2.1.2",
|
||||||
"@hugeicons/core-free-icons": "^4.2.2",
|
"@hugeicons/core-free-icons": "^4.2.3",
|
||||||
"@hugeicons/vue": "^1.0.7",
|
"@hugeicons/vue": "^1.0.7",
|
||||||
"@vitejs/plugin-vue": "^6.0.7",
|
"@vitejs/plugin-vue": "^6.0.8",
|
||||||
"@xterm/addon-fit": "^0.11.0",
|
"@xterm/addon-fit": "^0.11.0",
|
||||||
"@xterm/addon-web-links": "^0.12.0",
|
"@xterm/addon-web-links": "^0.12.0",
|
||||||
"@xterm/xterm": "^6.0.0",
|
"@xterm/xterm": "^6.0.0",
|
||||||
"iconify-icon": "^3.0.2",
|
"iconify-icon": "^3.0.2",
|
||||||
"picocrank": "^1.18.2",
|
"picocrank": "^1.21.0",
|
||||||
"standard": "^17.1.2",
|
"standard": "^17.1.2",
|
||||||
"unplugin-vue-components": "^32.1.0",
|
"unplugin-vue-components": "^32.1.0",
|
||||||
"vite": "^8.1.3",
|
"vite": "^8.1.5",
|
||||||
"vue": "^3.5.39",
|
"vue": "^3.5.40",
|
||||||
"vue-i18n": "^11.4.6",
|
"vue-i18n": "^11.4.7",
|
||||||
"vue-router": "^5.1.0"
|
"vue-router": "^5.2.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^20.19.0 || >=22.12.0"
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
// @generated by protoc-gen-es v2.12.1
|
// @generated by protoc-gen-es v2.13.0
|
||||||
// @generated from file olivetin/api/v1/olivetin.proto (package olivetin.api.v1, syntax proto3)
|
// @generated from file olivetin/api/v1/olivetin.proto (package olivetin.api.v1, syntax proto3)
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
|
|
||||||
|
|
@ -1666,6 +1666,57 @@ export declare type GetDiagnosticsRequest = Message<"olivetin.api.v1.GetDiagnost
|
||||||
*/
|
*/
|
||||||
export declare const GetDiagnosticsRequestSchema: GenMessage<GetDiagnosticsRequest>;
|
export declare const GetDiagnosticsRequestSchema: GenMessage<GetDiagnosticsRequest>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @generated from message olivetin.api.v1.ConfigIssue
|
||||||
|
*/
|
||||||
|
export declare type ConfigIssue = Message<"olivetin.api.v1.ConfigIssue"> & {
|
||||||
|
/**
|
||||||
|
* @generated from field: string severity = 1;
|
||||||
|
*/
|
||||||
|
severity: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @generated from field: string code = 2;
|
||||||
|
*/
|
||||||
|
code: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @generated from field: string message = 3;
|
||||||
|
*/
|
||||||
|
message: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @generated from field: string action_id = 4;
|
||||||
|
*/
|
||||||
|
actionId: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @generated from field: string action_title = 5;
|
||||||
|
*/
|
||||||
|
actionTitle: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @generated from field: string argument_name = 6;
|
||||||
|
*/
|
||||||
|
argumentName: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @generated from field: string source = 7;
|
||||||
|
*/
|
||||||
|
source: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @generated from field: string config_file = 8;
|
||||||
|
*/
|
||||||
|
configFile: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Describes the message olivetin.api.v1.ConfigIssue.
|
||||||
|
* Use `create(ConfigIssueSchema)` to create a new message.
|
||||||
|
*/
|
||||||
|
export declare const ConfigIssueSchema: GenMessage<ConfigIssue>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @generated from message olivetin.api.v1.GetDiagnosticsResponse
|
* @generated from message olivetin.api.v1.GetDiagnosticsResponse
|
||||||
*/
|
*/
|
||||||
|
|
@ -1679,6 +1730,11 @@ export declare type GetDiagnosticsResponse = Message<"olivetin.api.v1.GetDiagnos
|
||||||
* @generated from field: string SshFoundConfig = 2;
|
* @generated from field: string SshFoundConfig = 2;
|
||||||
*/
|
*/
|
||||||
SshFoundConfig: string;
|
SshFoundConfig: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @generated from field: repeated olivetin.api.v1.ConfigIssue config_issues = 3;
|
||||||
|
*/
|
||||||
|
configIssues: ConfigIssue[];
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -1829,6 +1885,11 @@ export declare type InitResponse = Message<"olivetin.api.v1.InitResponse"> & {
|
||||||
* @generated from field: bool show_navigate_on_start_icons = 25;
|
* @generated from field: bool show_navigate_on_start_icons = 25;
|
||||||
*/
|
*/
|
||||||
showNavigateOnStartIcons: boolean;
|
showNavigateOnStartIcons: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @generated from field: int32 config_issue_count = 26;
|
||||||
|
*/
|
||||||
|
configIssueCount: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -345,8 +345,8 @@ function renderNavigation () {
|
||||||
|
|
||||||
const rootDashboards = window.initResponse?.rootDashboards || []
|
const rootDashboards = window.initResponse?.rootDashboards || []
|
||||||
|
|
||||||
if (typeof navigation.value.clear === 'function') {
|
if (typeof navigation.value.clearNavigationLinks === 'function') {
|
||||||
navigation.value.clear()
|
navigation.value.clearNavigationLinks()
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const rootDashboard of rootDashboards) {
|
for (const rootDashboard of rootDashboards) {
|
||||||
|
|
@ -367,7 +367,10 @@ function renderNavigation () {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showDiagnostics.value) {
|
if (showDiagnostics.value) {
|
||||||
navigation.value.addRouterLink('Diagnostics', t('nav.diagnostics'))
|
const issueCount = window.initResponse?.configIssueCount || 0
|
||||||
|
navigation.value.addRouterLink('Diagnostics', t('nav.diagnostics'), {
|
||||||
|
count: issueCount
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,28 @@
|
||||||
<template>
|
<template>
|
||||||
|
<Section :title="t('diagnostics.config-issues')">
|
||||||
|
<p>{{ t('diagnostics.config-issues-description') }}</p>
|
||||||
|
|
||||||
|
<p v-if="!loading && configIssues.length === 0">
|
||||||
|
{{ t('diagnostics.config-issues-none') }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<Table
|
||||||
|
v-else
|
||||||
|
:data="configIssueRows"
|
||||||
|
:headers="configIssueHeaders"
|
||||||
|
:show-pagination="false"
|
||||||
|
>
|
||||||
|
<template #cell-severity="{ value }">
|
||||||
|
<div
|
||||||
|
class="tag"
|
||||||
|
:class="value === 'error' ? 'fg-bad' : 'fg-warning'"
|
||||||
|
>
|
||||||
|
{{ value }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Table>
|
||||||
|
</Section>
|
||||||
|
|
||||||
<Section :title="t('diagnostics.get-support')">
|
<Section :title="t('diagnostics.get-support')">
|
||||||
<p>
|
<p>
|
||||||
{{ t('diagnostics.get-support-description') }}
|
{{ t('diagnostics.get-support-description') }}
|
||||||
|
|
@ -84,19 +108,41 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||||
import Section from 'picocrank/vue/components/Section.vue'
|
import Section from 'picocrank/vue/components/Section.vue'
|
||||||
|
import Table from 'picocrank/vue/components/Table.vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
const { t, locale } = useI18n()
|
const { t, locale } = useI18n()
|
||||||
|
|
||||||
const diagnostics = ref({})
|
const diagnostics = ref({})
|
||||||
|
const configIssues = ref([])
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const serverDiagnostics = ref('')
|
const serverDiagnostics = ref('')
|
||||||
const browserInfo = ref('')
|
const browserInfo = ref('')
|
||||||
const serverDiagnosticsCopied = ref(false)
|
const serverDiagnosticsCopied = ref(false)
|
||||||
const browserInfoCopied = ref(false)
|
const browserInfoCopied = ref(false)
|
||||||
|
|
||||||
|
const configIssueHeaders = computed(() => [
|
||||||
|
{ key: 'severity', label: t('diagnostics.config-issue-severity'), sortable: true, width: '7rem' },
|
||||||
|
{ key: 'code', label: t('diagnostics.config-issue-code'), sortable: true, width: '12rem' },
|
||||||
|
{ key: 'message', label: t('diagnostics.config-issue-message'), sortable: false },
|
||||||
|
{ key: 'actionTitle', label: t('diagnostics.config-issue-action'), sortable: true, width: '10rem' },
|
||||||
|
{ key: 'argumentName', label: t('diagnostics.config-issue-argument'), sortable: true, width: '8rem' },
|
||||||
|
{ key: 'configFile', label: t('diagnostics.config-issue-config-file'), sortable: true, width: '14rem' },
|
||||||
|
{ key: 'source', label: t('diagnostics.config-issue-source'), sortable: false, width: '12rem' }
|
||||||
|
])
|
||||||
|
|
||||||
|
const configIssueRows = computed(() => configIssues.value.map((issue) => ({
|
||||||
|
severity: issue.severity || '',
|
||||||
|
code: issue.code || '',
|
||||||
|
message: issue.message || '',
|
||||||
|
actionTitle: issue.actionTitle || '',
|
||||||
|
argumentName: issue.argumentName || '',
|
||||||
|
configFile: issue.configFile || '',
|
||||||
|
source: issue.source || ''
|
||||||
|
})))
|
||||||
|
|
||||||
async function fetchDiagnostics () {
|
async function fetchDiagnostics () {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
|
|
||||||
|
|
@ -106,12 +152,14 @@ async function fetchDiagnostics () {
|
||||||
sshFoundKey: response.SshFoundKey,
|
sshFoundKey: response.SshFoundKey,
|
||||||
sshFoundConfig: response.SshFoundConfig
|
sshFoundConfig: response.SshFoundConfig
|
||||||
}
|
}
|
||||||
|
configIssues.value = response.configIssues || []
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to fetch diagnostics:', err)
|
console.error('Failed to fetch diagnostics:', err)
|
||||||
diagnostics.value = {
|
diagnostics.value = {
|
||||||
sshFoundKey: t('diagnostics.unknown'),
|
sshFoundKey: t('diagnostics.unknown'),
|
||||||
sshFoundConfig: t('diagnostics.unknown')
|
sshFoundConfig: t('diagnostics.unknown')
|
||||||
}
|
}
|
||||||
|
configIssues.value = []
|
||||||
}
|
}
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
|
|
@ -263,85 +311,12 @@ async function copyBrowserInfo () {
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchDiagnostics()
|
fetchDiagnostics()
|
||||||
|
window.addEventListener('EventConfigChanged', fetchDiagnostics)
|
||||||
|
window.addEventListener('EventEntityChanged', fetchDiagnostics)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener('EventConfigChanged', fetchDiagnostics)
|
||||||
|
window.removeEventListener('EventEntityChanged', fetchDiagnostics)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.diagnostics-view {
|
|
||||||
padding: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.diagnostics-content {
|
|
||||||
max-width: 800px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.note {
|
|
||||||
background: #f8f9fa;
|
|
||||||
border-left: 4px solid #007bff;
|
|
||||||
padding: 1rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
border-radius: 0 4px 4px 0;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
color: #495057;
|
|
||||||
}
|
|
||||||
|
|
||||||
.note a {
|
|
||||||
color: #007bff;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.note a:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.diagnostics-table {
|
|
||||||
width: 100%;
|
|
||||||
border-collapse: collapse;
|
|
||||||
}
|
|
||||||
|
|
||||||
.diagnostics-table td {
|
|
||||||
padding: 0.75rem 1rem;
|
|
||||||
border-bottom: 1px solid #f1f3f4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.diagnostics-table td:first-child {
|
|
||||||
font-weight: 500;
|
|
||||||
color: #495057;
|
|
||||||
background: #f8f9fa;
|
|
||||||
}
|
|
||||||
|
|
||||||
.diagnostics-table tr:last-child td {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-list {
|
|
||||||
padding: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-item {
|
|
||||||
background: #f8d7da;
|
|
||||||
color: #721c24;
|
|
||||||
padding: 0.75rem;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
border-radius: 4px;
|
|
||||||
border-left: 4px solid #dc3545;
|
|
||||||
font-family: monospace;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-item:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flex-col {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-content {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1em;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ describe('config: dashboards with basic fieldsets', function () {
|
||||||
await openSidebar()
|
await openSidebar()
|
||||||
|
|
||||||
const navigationLinks = await getNavigationLinks()
|
const navigationLinks = await getNavigationLinks()
|
||||||
assert.equal(navigationLinks.length, 5, 'Expected the nav to only have 5 links') // test dashboard + logs + diagnostics + entities + separator
|
assert.equal(navigationLinks.length, 4, 'Expected the nav to only have 4 links') // test dashboard + entities + logs + diagnostics
|
||||||
|
|
||||||
const firstLink = await navigationLinks[0]
|
const firstLink = await navigationLinks[0]
|
||||||
|
|
||||||
|
|
@ -44,7 +44,7 @@ describe('config: dashboards with basic fieldsets', function () {
|
||||||
// Check that we have the expected number of fieldsets
|
// Check that we have the expected number of fieldsets
|
||||||
const dashboardRows = await webdriver.findElements(By.css('.dashboard-row'))
|
const dashboardRows = await webdriver.findElements(By.css('.dashboard-row'))
|
||||||
expect(dashboardRows).to.have.length(3, 'Expected 3 dashboard rows total')
|
expect(dashboardRows).to.have.length(3, 'Expected 3 dashboard rows total')
|
||||||
|
|
||||||
// Check that we have fieldsets with the expected titles
|
// Check that we have fieldsets with the expected titles
|
||||||
const fieldsetTitles = []
|
const fieldsetTitles = []
|
||||||
for (let i = 0; i < dashboardRows.length; i++) {
|
for (let i = 0; i < dashboardRows.length; i++) {
|
||||||
|
|
@ -54,7 +54,7 @@ describe('config: dashboards with basic fieldsets', function () {
|
||||||
fieldsetTitles.push(title)
|
fieldsetTitles.push(title)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// We should have fieldsets for: Fieldset 1, Fieldset 2, and Actions fieldsets
|
// We should have fieldsets for: Fieldset 1, Fieldset 2, and Actions fieldsets
|
||||||
expect(fieldsetTitles).to.include('Fieldset 1')
|
expect(fieldsetTitles).to.include('Fieldset 1')
|
||||||
expect(fieldsetTitles).to.include('Fieldset 2')
|
expect(fieldsetTitles).to.include('Fieldset 2')
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,6 @@ describe('config: empty dashboards are hidden', function () {
|
||||||
|
|
||||||
const navigationLinks = await getNavigationLinks()
|
const navigationLinks = await getNavigationLinks()
|
||||||
expect(navigationLinks).to.not.be.empty
|
expect(navigationLinks).to.not.be.empty
|
||||||
expect(navigationLinks.length).to.be.equal(4, 'Expected the nav to only have 4 links')
|
expect(navigationLinks.length).to.be.equal(3, 'Expected the nav to only have 3 links') // entities + logs + diagnostics
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,17 @@
|
||||||
"connected": "Verbunden",
|
"connected": "Verbunden",
|
||||||
"diagnostics.browser-info": "Browser-Informationen",
|
"diagnostics.browser-info": "Browser-Informationen",
|
||||||
"diagnostics.browser-info-description": "Dieser Abschnitt ermöglicht es Ihnen, einen detaillierten Bericht über Ihre Browser-Informationen zu erstellen. Dies kann bei der Fehlerbehebung von browser-spezifischen Problemen hilfreich sein.",
|
"diagnostics.browser-info-description": "Dieser Abschnitt ermöglicht es Ihnen, einen detaillierten Bericht über Ihre Browser-Informationen zu erstellen. Dies kann bei der Fehlerbehebung von browser-spezifischen Problemen hilfreich sein.",
|
||||||
|
"diagnostics.config-issue-action": "Aktion",
|
||||||
|
"diagnostics.config-issue-argument": "Argument",
|
||||||
|
"diagnostics.config-issue-code": "Code",
|
||||||
|
"diagnostics.config-issue-config-file": "Konfigurationsdatei",
|
||||||
|
"diagnostics.config-issue-detail": "Detail",
|
||||||
|
"diagnostics.config-issue-message": "Meldung",
|
||||||
|
"diagnostics.config-issue-severity": "Schweregrad",
|
||||||
|
"diagnostics.config-issue-source": "Quelle",
|
||||||
|
"diagnostics.config-issues": "Konfigurationsprobleme",
|
||||||
|
"diagnostics.config-issues-description": "In der aktuellen OliveTin-Konfiguration erkannte Probleme. Beheben Sie diese in Ihren Konfigurationsdateien und laden Sie OliveTin neu.",
|
||||||
|
"diagnostics.config-issues-none": "Keine Konfigurationsprobleme erkannt.",
|
||||||
"diagnostics.copied": "Kopiert!",
|
"diagnostics.copied": "Kopiert!",
|
||||||
"diagnostics.copy-to-clipboard": "In Zwischenablage kopieren",
|
"diagnostics.copy-to-clipboard": "In Zwischenablage kopieren",
|
||||||
"diagnostics.found-config": "Konfiguration gefunden",
|
"diagnostics.found-config": "Konfiguration gefunden",
|
||||||
|
|
@ -84,6 +95,17 @@
|
||||||
"connected": "Connected",
|
"connected": "Connected",
|
||||||
"diagnostics.browser-info": "Browser Info",
|
"diagnostics.browser-info": "Browser Info",
|
||||||
"diagnostics.browser-info-description": "This section allows you to generate a detailed report of your browser information. This can be helpful when troubleshooting browser-specific issues.",
|
"diagnostics.browser-info-description": "This section allows you to generate a detailed report of your browser information. This can be helpful when troubleshooting browser-specific issues.",
|
||||||
|
"diagnostics.config-issue-action": "Action",
|
||||||
|
"diagnostics.config-issue-argument": "Argument",
|
||||||
|
"diagnostics.config-issue-code": "Code",
|
||||||
|
"diagnostics.config-issue-config-file": "Config file",
|
||||||
|
"diagnostics.config-issue-detail": "Detail",
|
||||||
|
"diagnostics.config-issue-message": "Message",
|
||||||
|
"diagnostics.config-issue-severity": "Severity",
|
||||||
|
"diagnostics.config-issue-source": "Source",
|
||||||
|
"diagnostics.config-issues": "Configuration issues",
|
||||||
|
"diagnostics.config-issues-description": "Problems detected in the current OliveTin configuration. Fix these in your config files and reload OliveTin.",
|
||||||
|
"diagnostics.config-issues-none": "No configuration issues detected.",
|
||||||
"diagnostics.copied": "Copied!",
|
"diagnostics.copied": "Copied!",
|
||||||
"diagnostics.copy-to-clipboard": "Copy to Clipboard",
|
"diagnostics.copy-to-clipboard": "Copy to Clipboard",
|
||||||
"diagnostics.found-config": "Found Config",
|
"diagnostics.found-config": "Found Config",
|
||||||
|
|
@ -163,6 +185,17 @@
|
||||||
"connected": "Conectado",
|
"connected": "Conectado",
|
||||||
"diagnostics.browser-info": "Información del navegador",
|
"diagnostics.browser-info": "Información del navegador",
|
||||||
"diagnostics.browser-info-description": "Esta sección le permite generar un informe detallado de su información del navegador. Esto puede ser útil al solucionar problemas específicos del navegador.",
|
"diagnostics.browser-info-description": "Esta sección le permite generar un informe detallado de su información del navegador. Esto puede ser útil al solucionar problemas específicos del navegador.",
|
||||||
|
"diagnostics.config-issue-action": "Acción",
|
||||||
|
"diagnostics.config-issue-argument": "Argumento",
|
||||||
|
"diagnostics.config-issue-code": "Código",
|
||||||
|
"diagnostics.config-issue-config-file": "Archivo de configuración",
|
||||||
|
"diagnostics.config-issue-detail": "Detalle",
|
||||||
|
"diagnostics.config-issue-message": "Mensaje",
|
||||||
|
"diagnostics.config-issue-severity": "Severidad",
|
||||||
|
"diagnostics.config-issue-source": "Origen",
|
||||||
|
"diagnostics.config-issues": "Problemas de configuración",
|
||||||
|
"diagnostics.config-issues-description": "Problemas detectados en la configuración actual de OliveTin. Corríjalos en sus archivos de configuración y vuelva a cargar OliveTin.",
|
||||||
|
"diagnostics.config-issues-none": "No se detectaron problemas de configuración.",
|
||||||
"diagnostics.copied": "¡Copiado!",
|
"diagnostics.copied": "¡Copiado!",
|
||||||
"diagnostics.copy-to-clipboard": "Copiar al portapapeles",
|
"diagnostics.copy-to-clipboard": "Copiar al portapapeles",
|
||||||
"diagnostics.found-config": "Configuración encontrada",
|
"diagnostics.found-config": "Configuración encontrada",
|
||||||
|
|
@ -242,6 +275,17 @@
|
||||||
"connected": "Connesso",
|
"connected": "Connesso",
|
||||||
"diagnostics.browser-info": "Informazioni del browser",
|
"diagnostics.browser-info": "Informazioni del browser",
|
||||||
"diagnostics.browser-info-description": "Questa sezione ti consente di generare un rapporto dettagliato delle informazioni del tuo browser. Questo può essere utile durante la risoluzione dei problemi specifici del browser.",
|
"diagnostics.browser-info-description": "Questa sezione ti consente di generare un rapporto dettagliato delle informazioni del tuo browser. Questo può essere utile durante la risoluzione dei problemi specifici del browser.",
|
||||||
|
"diagnostics.config-issue-action": "Azione",
|
||||||
|
"diagnostics.config-issue-argument": "Argomento",
|
||||||
|
"diagnostics.config-issue-code": "Codice",
|
||||||
|
"diagnostics.config-issue-config-file": "File di configurazione",
|
||||||
|
"diagnostics.config-issue-detail": "Dettaglio",
|
||||||
|
"diagnostics.config-issue-message": "Messaggio",
|
||||||
|
"diagnostics.config-issue-severity": "Gravità",
|
||||||
|
"diagnostics.config-issue-source": "Origine",
|
||||||
|
"diagnostics.config-issues": "Problemi di configurazione",
|
||||||
|
"diagnostics.config-issues-description": "Problemi rilevati nella configurazione corrente di OliveTin. Correggili nei file di configurazione e ricarica OliveTin.",
|
||||||
|
"diagnostics.config-issues-none": "Nessun problema di configurazione rilevato.",
|
||||||
"diagnostics.copied": "Copiato!",
|
"diagnostics.copied": "Copiato!",
|
||||||
"diagnostics.copy-to-clipboard": "Copia negli appunti",
|
"diagnostics.copy-to-clipboard": "Copia negli appunti",
|
||||||
"diagnostics.found-config": "Configurazione trovata",
|
"diagnostics.found-config": "Configurazione trovata",
|
||||||
|
|
@ -321,6 +365,17 @@
|
||||||
"connected": "已连接",
|
"connected": "已连接",
|
||||||
"diagnostics.browser-info": "浏览器信息",
|
"diagnostics.browser-info": "浏览器信息",
|
||||||
"diagnostics.browser-info-description": "此部分允许您生成浏览器信息的详细报告。这在排查浏览器特定问题时很有帮助。",
|
"diagnostics.browser-info-description": "此部分允许您生成浏览器信息的详细报告。这在排查浏览器特定问题时很有帮助。",
|
||||||
|
"diagnostics.config-issue-action": "操作",
|
||||||
|
"diagnostics.config-issue-argument": "参数",
|
||||||
|
"diagnostics.config-issue-code": "代码",
|
||||||
|
"diagnostics.config-issue-config-file": "配置文件",
|
||||||
|
"diagnostics.config-issue-detail": "详情",
|
||||||
|
"diagnostics.config-issue-message": "消息",
|
||||||
|
"diagnostics.config-issue-severity": "严重程度",
|
||||||
|
"diagnostics.config-issue-source": "来源",
|
||||||
|
"diagnostics.config-issues": "配置问题",
|
||||||
|
"diagnostics.config-issues-description": "当前 OliveTin 配置中检测到的问题。请在配置文件中修复这些问题并重新加载 OliveTin。",
|
||||||
|
"diagnostics.config-issues-none": "未检测到配置问题。",
|
||||||
"diagnostics.copied": "已复制!",
|
"diagnostics.copied": "已复制!",
|
||||||
"diagnostics.copy-to-clipboard": "复制到剪贴板",
|
"diagnostics.copy-to-clipboard": "复制到剪贴板",
|
||||||
"diagnostics.found-config": "找到配置",
|
"diagnostics.found-config": "找到配置",
|
||||||
|
|
@ -400,6 +455,17 @@
|
||||||
"connected": "已連線",
|
"connected": "已連線",
|
||||||
"diagnostics.browser-info": "瀏覽器資訊",
|
"diagnostics.browser-info": "瀏覽器資訊",
|
||||||
"diagnostics.browser-info-description": "此區塊可讓您產生瀏覽器資訊的詳細報告。這在排解特定瀏覽器問題時相當有幫助。",
|
"diagnostics.browser-info-description": "此區塊可讓您產生瀏覽器資訊的詳細報告。這在排解特定瀏覽器問題時相當有幫助。",
|
||||||
|
"diagnostics.config-issue-action": "動作",
|
||||||
|
"diagnostics.config-issue-argument": "參數",
|
||||||
|
"diagnostics.config-issue-code": "代碼",
|
||||||
|
"diagnostics.config-issue-config-file": "設定檔",
|
||||||
|
"diagnostics.config-issue-detail": "詳細資料",
|
||||||
|
"diagnostics.config-issue-message": "訊息",
|
||||||
|
"diagnostics.config-issue-severity": "嚴重程度",
|
||||||
|
"diagnostics.config-issue-source": "來源",
|
||||||
|
"diagnostics.config-issues": "設定問題",
|
||||||
|
"diagnostics.config-issues-description": "目前 OliveTin 設定中偵測到的問題。請在設定檔中修正這些問題並重新載入 OliveTin。",
|
||||||
|
"diagnostics.config-issues-none": "未偵測到設定問題。",
|
||||||
"diagnostics.copied": "已複製!",
|
"diagnostics.copied": "已複製!",
|
||||||
"diagnostics.copy-to-clipboard": "複製到剪貼簿",
|
"diagnostics.copy-to-clipboard": "複製到剪貼簿",
|
||||||
"diagnostics.found-config": "找到設定檔 (Config)",
|
"diagnostics.found-config": "找到設定檔 (Config)",
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,17 @@ translations:
|
||||||
diagnostics.get-support: Unterstützung erhalten Unterstützung erhalten
|
diagnostics.get-support: Unterstützung erhalten Unterstützung erhalten
|
||||||
diagnostics.get-support-description: Wenn Sie Probleme mit OliveTin haben und eine Support-Anfrage stellen möchten, wäre es sehr hilfreich, Server-Diagnostik von dieser Seite einzufügen.
|
diagnostics.get-support-description: Wenn Sie Probleme mit OliveTin haben und eine Support-Anfrage stellen möchten, wäre es sehr hilfreich, Server-Diagnostik von dieser Seite einzufügen.
|
||||||
diagnostics.where-to-find-help: Wo Sie Hilfe finden
|
diagnostics.where-to-find-help: Wo Sie Hilfe finden
|
||||||
|
diagnostics.config-issues: Konfigurationsprobleme
|
||||||
|
diagnostics.config-issues-description: In der aktuellen OliveTin-Konfiguration erkannte Probleme. Beheben Sie diese in Ihren Konfigurationsdateien und laden Sie OliveTin neu.
|
||||||
|
diagnostics.config-issues-none: Keine Konfigurationsprobleme erkannt.
|
||||||
|
diagnostics.config-issue-severity: Schweregrad
|
||||||
|
diagnostics.config-issue-code: Code
|
||||||
|
diagnostics.config-issue-message: Meldung
|
||||||
|
diagnostics.config-issue-action: Aktion
|
||||||
|
diagnostics.config-issue-argument: Argument
|
||||||
|
diagnostics.config-issue-config-file: Konfigurationsdatei
|
||||||
|
diagnostics.config-issue-detail: Detail
|
||||||
|
diagnostics.config-issue-source: Quelle
|
||||||
diagnostics.ssh: SSH
|
diagnostics.ssh: SSH
|
||||||
diagnostics.found-key: Schlüssel gefunden
|
diagnostics.found-key: Schlüssel gefunden
|
||||||
diagnostics.found-config: Konfiguration gefunden
|
diagnostics.found-config: Konfiguration gefunden
|
||||||
|
|
|
||||||
11
lang/en.yaml
11
lang/en.yaml
|
|
@ -54,6 +54,17 @@ translations:
|
||||||
diagnostics.get-support: Get support
|
diagnostics.get-support: Get support
|
||||||
diagnostics.get-support-description: If you are having problems with OliveTin and want to raise a support request, it would be very helpful to include Server Diagnostics from this page.
|
diagnostics.get-support-description: If you are having problems with OliveTin and want to raise a support request, it would be very helpful to include Server Diagnostics from this page.
|
||||||
diagnostics.where-to-find-help: Where to find help
|
diagnostics.where-to-find-help: Where to find help
|
||||||
|
diagnostics.config-issues: Configuration issues
|
||||||
|
diagnostics.config-issues-description: Problems detected in the current OliveTin configuration. Fix these in your config files and reload OliveTin.
|
||||||
|
diagnostics.config-issues-none: No configuration issues detected.
|
||||||
|
diagnostics.config-issue-severity: Severity
|
||||||
|
diagnostics.config-issue-code: Code
|
||||||
|
diagnostics.config-issue-message: Message
|
||||||
|
diagnostics.config-issue-action: Action
|
||||||
|
diagnostics.config-issue-argument: Argument
|
||||||
|
diagnostics.config-issue-config-file: Config file
|
||||||
|
diagnostics.config-issue-detail: Detail
|
||||||
|
diagnostics.config-issue-source: Source
|
||||||
diagnostics.ssh: SSH
|
diagnostics.ssh: SSH
|
||||||
diagnostics.found-key: Found Key
|
diagnostics.found-key: Found Key
|
||||||
diagnostics.found-config: Found Config
|
diagnostics.found-config: Found Config
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,17 @@ translations:
|
||||||
diagnostics.get-support: Obtener soporte Obtener soporte
|
diagnostics.get-support: Obtener soporte Obtener soporte
|
||||||
diagnostics.get-support-description: Si tiene problemas con OliveTin y desea presentar una solicitud de soporte, sería muy útil incluir diagnósticos del servidor desde esta página.
|
diagnostics.get-support-description: Si tiene problemas con OliveTin y desea presentar una solicitud de soporte, sería muy útil incluir diagnósticos del servidor desde esta página.
|
||||||
diagnostics.where-to-find-help: Dónde encontrar ayuda
|
diagnostics.where-to-find-help: Dónde encontrar ayuda
|
||||||
|
diagnostics.config-issues: Problemas de configuración
|
||||||
|
diagnostics.config-issues-description: Problemas detectados en la configuración actual de OliveTin. Corríjalos en sus archivos de configuración y vuelva a cargar OliveTin.
|
||||||
|
diagnostics.config-issues-none: No se detectaron problemas de configuración.
|
||||||
|
diagnostics.config-issue-severity: Severidad
|
||||||
|
diagnostics.config-issue-code: Código
|
||||||
|
diagnostics.config-issue-message: Mensaje
|
||||||
|
diagnostics.config-issue-action: Acción
|
||||||
|
diagnostics.config-issue-argument: Argumento
|
||||||
|
diagnostics.config-issue-config-file: Archivo de configuración
|
||||||
|
diagnostics.config-issue-detail: Detalle
|
||||||
|
diagnostics.config-issue-source: Origen
|
||||||
diagnostics.ssh: SSH
|
diagnostics.ssh: SSH
|
||||||
diagnostics.found-key: Clave encontrada
|
diagnostics.found-key: Clave encontrada
|
||||||
diagnostics.found-config: Configuración encontrada
|
diagnostics.found-config: Configuración encontrada
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,17 @@ translations:
|
||||||
diagnostics.get-support: Ottenere supporto Ottenere supporto
|
diagnostics.get-support: Ottenere supporto Ottenere supporto
|
||||||
diagnostics.get-support-description: Se hai problemi con OliveTin e vuoi presentare una richiesta di supporto, sarebbe molto utile includere la diagnostica del server da questa pagina.
|
diagnostics.get-support-description: Se hai problemi con OliveTin e vuoi presentare una richiesta di supporto, sarebbe molto utile includere la diagnostica del server da questa pagina.
|
||||||
diagnostics.where-to-find-help: Dove trovare aiuto
|
diagnostics.where-to-find-help: Dove trovare aiuto
|
||||||
|
diagnostics.config-issues: Problemi di configurazione
|
||||||
|
diagnostics.config-issues-description: Problemi rilevati nella configurazione corrente di OliveTin. Correggili nei file di configurazione e ricarica OliveTin.
|
||||||
|
diagnostics.config-issues-none: Nessun problema di configurazione rilevato.
|
||||||
|
diagnostics.config-issue-severity: Gravità
|
||||||
|
diagnostics.config-issue-code: Codice
|
||||||
|
diagnostics.config-issue-message: Messaggio
|
||||||
|
diagnostics.config-issue-action: Azione
|
||||||
|
diagnostics.config-issue-argument: Argomento
|
||||||
|
diagnostics.config-issue-config-file: File di configurazione
|
||||||
|
diagnostics.config-issue-detail: Dettaglio
|
||||||
|
diagnostics.config-issue-source: Origine
|
||||||
diagnostics.ssh: SSH
|
diagnostics.ssh: SSH
|
||||||
diagnostics.found-key: Chiave trovata
|
diagnostics.found-key: Chiave trovata
|
||||||
diagnostics.found-config: Configurazione trovata
|
diagnostics.found-config: Configurazione trovata
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,17 @@ translations:
|
||||||
diagnostics.get-support: 获取支持 获取支持
|
diagnostics.get-support: 获取支持 获取支持
|
||||||
diagnostics.get-support-description: 如果您在使用 OliveTin 时遇到问题并希望提交支持请求,从本页面包含服务器诊断将非常有帮助。
|
diagnostics.get-support-description: 如果您在使用 OliveTin 时遇到问题并希望提交支持请求,从本页面包含服务器诊断将非常有帮助。
|
||||||
diagnostics.where-to-find-help: 在哪里找到帮助
|
diagnostics.where-to-find-help: 在哪里找到帮助
|
||||||
|
diagnostics.config-issues: 配置问题
|
||||||
|
diagnostics.config-issues-description: 当前 OliveTin 配置中检测到的问题。请在配置文件中修复这些问题并重新加载 OliveTin。
|
||||||
|
diagnostics.config-issues-none: 未检测到配置问题。
|
||||||
|
diagnostics.config-issue-severity: 严重程度
|
||||||
|
diagnostics.config-issue-code: 代码
|
||||||
|
diagnostics.config-issue-message: 消息
|
||||||
|
diagnostics.config-issue-action: 操作
|
||||||
|
diagnostics.config-issue-argument: 参数
|
||||||
|
diagnostics.config-issue-config-file: 配置文件
|
||||||
|
diagnostics.config-issue-detail: 详情
|
||||||
|
diagnostics.config-issue-source: 来源
|
||||||
diagnostics.ssh: SSH
|
diagnostics.ssh: SSH
|
||||||
diagnostics.found-key: 找到密钥
|
diagnostics.found-key: 找到密钥
|
||||||
diagnostics.found-config: 找到配置
|
diagnostics.found-config: 找到配置
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,17 @@ translations:
|
||||||
diagnostics.get-support: 尋求技術支援
|
diagnostics.get-support: 尋求技術支援
|
||||||
diagnostics.get-support-description: 如果您在使用 OliveTin 時遇到問題並希望提出支援請求,提供此頁面的「伺服器診斷資訊」將會有很大的幫助。
|
diagnostics.get-support-description: 如果您在使用 OliveTin 時遇到問題並希望提出支援請求,提供此頁面的「伺服器診斷資訊」將會有很大的幫助。
|
||||||
diagnostics.where-to-find-help: 哪裡可以尋求協助
|
diagnostics.where-to-find-help: 哪裡可以尋求協助
|
||||||
|
diagnostics.config-issues: 設定問題
|
||||||
|
diagnostics.config-issues-description: 目前 OliveTin 設定中偵測到的問題。請在設定檔中修正這些問題並重新載入 OliveTin。
|
||||||
|
diagnostics.config-issues-none: 未偵測到設定問題。
|
||||||
|
diagnostics.config-issue-severity: 嚴重程度
|
||||||
|
diagnostics.config-issue-code: 代碼
|
||||||
|
diagnostics.config-issue-message: 訊息
|
||||||
|
diagnostics.config-issue-action: 動作
|
||||||
|
diagnostics.config-issue-argument: 參數
|
||||||
|
diagnostics.config-issue-config-file: 設定檔
|
||||||
|
diagnostics.config-issue-detail: 詳細資料
|
||||||
|
diagnostics.config-issue-source: 來源
|
||||||
diagnostics.ssh: SSH
|
diagnostics.ssh: SSH
|
||||||
diagnostics.found-key: 找到金鑰 (Key)
|
diagnostics.found-key: 找到金鑰 (Key)
|
||||||
diagnostics.found-config: 找到設定檔 (Config)
|
diagnostics.found-config: 找到設定檔 (Config)
|
||||||
|
|
|
||||||
|
|
@ -379,9 +379,21 @@ message LogoutResponse {
|
||||||
message GetDiagnosticsRequest {
|
message GetDiagnosticsRequest {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message ConfigIssue {
|
||||||
|
string severity = 1;
|
||||||
|
string code = 2;
|
||||||
|
string message = 3;
|
||||||
|
string action_id = 4;
|
||||||
|
string action_title = 5;
|
||||||
|
string argument_name = 6;
|
||||||
|
string source = 7;
|
||||||
|
string config_file = 8;
|
||||||
|
}
|
||||||
|
|
||||||
message GetDiagnosticsResponse {
|
message GetDiagnosticsResponse {
|
||||||
string SshFoundKey = 1;
|
string SshFoundKey = 1;
|
||||||
string SshFoundConfig = 2;
|
string SshFoundConfig = 2;
|
||||||
|
repeated ConfigIssue config_issues = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
message InitRequest {}
|
message InitRequest {}
|
||||||
|
|
@ -412,6 +424,7 @@ message InitResponse {
|
||||||
bool login_required = 23;
|
bool login_required = 23;
|
||||||
repeated string available_themes = 24; // List of available theme names
|
repeated string available_themes = 24; // List of available theme names
|
||||||
bool show_navigate_on_start_icons = 25;
|
bool show_navigate_on_start_icons = 25;
|
||||||
|
int32 config_issue_count = 26;
|
||||||
}
|
}
|
||||||
|
|
||||||
message AdditionalLink {
|
message AdditionalLink {
|
||||||
|
|
|
||||||
|
|
@ -3871,17 +3871,118 @@ func (*GetDiagnosticsRequest) Descriptor() ([]byte, []int) {
|
||||||
return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{64}
|
return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{64}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ConfigIssue struct {
|
||||||
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
|
Severity string `protobuf:"bytes,1,opt,name=severity,proto3" json:"severity,omitempty"`
|
||||||
|
Code string `protobuf:"bytes,2,opt,name=code,proto3" json:"code,omitempty"`
|
||||||
|
Message string `protobuf:"bytes,3,opt,name=message,proto3" json:"message,omitempty"`
|
||||||
|
ActionId string `protobuf:"bytes,4,opt,name=action_id,json=actionId,proto3" json:"action_id,omitempty"`
|
||||||
|
ActionTitle string `protobuf:"bytes,5,opt,name=action_title,json=actionTitle,proto3" json:"action_title,omitempty"`
|
||||||
|
ArgumentName string `protobuf:"bytes,6,opt,name=argument_name,json=argumentName,proto3" json:"argument_name,omitempty"`
|
||||||
|
Source string `protobuf:"bytes,7,opt,name=source,proto3" json:"source,omitempty"`
|
||||||
|
ConfigFile string `protobuf:"bytes,8,opt,name=config_file,json=configFile,proto3" json:"config_file,omitempty"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ConfigIssue) Reset() {
|
||||||
|
*x = ConfigIssue{}
|
||||||
|
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[65]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ConfigIssue) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*ConfigIssue) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *ConfigIssue) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[65]
|
||||||
|
if x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use ConfigIssue.ProtoReflect.Descriptor instead.
|
||||||
|
func (*ConfigIssue) Descriptor() ([]byte, []int) {
|
||||||
|
return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{65}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ConfigIssue) GetSeverity() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Severity
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ConfigIssue) GetCode() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Code
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ConfigIssue) GetMessage() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Message
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ConfigIssue) GetActionId() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.ActionId
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ConfigIssue) GetActionTitle() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.ActionTitle
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ConfigIssue) GetArgumentName() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.ArgumentName
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ConfigIssue) GetSource() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Source
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ConfigIssue) GetConfigFile() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.ConfigFile
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
type GetDiagnosticsResponse struct {
|
type GetDiagnosticsResponse struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
SshFoundKey string `protobuf:"bytes,1,opt,name=SshFoundKey,proto3" json:"SshFoundKey,omitempty"`
|
SshFoundKey string `protobuf:"bytes,1,opt,name=SshFoundKey,proto3" json:"SshFoundKey,omitempty"`
|
||||||
SshFoundConfig string `protobuf:"bytes,2,opt,name=SshFoundConfig,proto3" json:"SshFoundConfig,omitempty"`
|
SshFoundConfig string `protobuf:"bytes,2,opt,name=SshFoundConfig,proto3" json:"SshFoundConfig,omitempty"`
|
||||||
|
ConfigIssues []*ConfigIssue `protobuf:"bytes,3,rep,name=config_issues,json=configIssues,proto3" json:"config_issues,omitempty"`
|
||||||
unknownFields protoimpl.UnknownFields
|
unknownFields protoimpl.UnknownFields
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *GetDiagnosticsResponse) Reset() {
|
func (x *GetDiagnosticsResponse) Reset() {
|
||||||
*x = GetDiagnosticsResponse{}
|
*x = GetDiagnosticsResponse{}
|
||||||
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[65]
|
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[66]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
|
|
@ -3893,7 +3994,7 @@ func (x *GetDiagnosticsResponse) String() string {
|
||||||
func (*GetDiagnosticsResponse) ProtoMessage() {}
|
func (*GetDiagnosticsResponse) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *GetDiagnosticsResponse) ProtoReflect() protoreflect.Message {
|
func (x *GetDiagnosticsResponse) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[65]
|
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[66]
|
||||||
if x != nil {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
|
@ -3906,7 +4007,7 @@ func (x *GetDiagnosticsResponse) ProtoReflect() protoreflect.Message {
|
||||||
|
|
||||||
// Deprecated: Use GetDiagnosticsResponse.ProtoReflect.Descriptor instead.
|
// Deprecated: Use GetDiagnosticsResponse.ProtoReflect.Descriptor instead.
|
||||||
func (*GetDiagnosticsResponse) Descriptor() ([]byte, []int) {
|
func (*GetDiagnosticsResponse) Descriptor() ([]byte, []int) {
|
||||||
return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{65}
|
return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{66}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *GetDiagnosticsResponse) GetSshFoundKey() string {
|
func (x *GetDiagnosticsResponse) GetSshFoundKey() string {
|
||||||
|
|
@ -3923,6 +4024,13 @@ func (x *GetDiagnosticsResponse) GetSshFoundConfig() string {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (x *GetDiagnosticsResponse) GetConfigIssues() []*ConfigIssue {
|
||||||
|
if x != nil {
|
||||||
|
return x.ConfigIssues
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type InitRequest struct {
|
type InitRequest struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
unknownFields protoimpl.UnknownFields
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
@ -3931,7 +4039,7 @@ type InitRequest struct {
|
||||||
|
|
||||||
func (x *InitRequest) Reset() {
|
func (x *InitRequest) Reset() {
|
||||||
*x = InitRequest{}
|
*x = InitRequest{}
|
||||||
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[66]
|
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[67]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
|
|
@ -3943,7 +4051,7 @@ func (x *InitRequest) String() string {
|
||||||
func (*InitRequest) ProtoMessage() {}
|
func (*InitRequest) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *InitRequest) ProtoReflect() protoreflect.Message {
|
func (x *InitRequest) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[66]
|
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[67]
|
||||||
if x != nil {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
|
@ -3956,7 +4064,7 @@ func (x *InitRequest) ProtoReflect() protoreflect.Message {
|
||||||
|
|
||||||
// Deprecated: Use InitRequest.ProtoReflect.Descriptor instead.
|
// Deprecated: Use InitRequest.ProtoReflect.Descriptor instead.
|
||||||
func (*InitRequest) Descriptor() ([]byte, []int) {
|
func (*InitRequest) Descriptor() ([]byte, []int) {
|
||||||
return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{66}
|
return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{67}
|
||||||
}
|
}
|
||||||
|
|
||||||
type InitResponse struct {
|
type InitResponse struct {
|
||||||
|
|
@ -3986,13 +4094,14 @@ type InitResponse struct {
|
||||||
LoginRequired bool `protobuf:"varint,23,opt,name=login_required,json=loginRequired,proto3" json:"login_required,omitempty"`
|
LoginRequired bool `protobuf:"varint,23,opt,name=login_required,json=loginRequired,proto3" json:"login_required,omitempty"`
|
||||||
AvailableThemes []string `protobuf:"bytes,24,rep,name=available_themes,json=availableThemes,proto3" json:"available_themes,omitempty"` // List of available theme names
|
AvailableThemes []string `protobuf:"bytes,24,rep,name=available_themes,json=availableThemes,proto3" json:"available_themes,omitempty"` // List of available theme names
|
||||||
ShowNavigateOnStartIcons bool `protobuf:"varint,25,opt,name=show_navigate_on_start_icons,json=showNavigateOnStartIcons,proto3" json:"show_navigate_on_start_icons,omitempty"`
|
ShowNavigateOnStartIcons bool `protobuf:"varint,25,opt,name=show_navigate_on_start_icons,json=showNavigateOnStartIcons,proto3" json:"show_navigate_on_start_icons,omitempty"`
|
||||||
|
ConfigIssueCount int32 `protobuf:"varint,26,opt,name=config_issue_count,json=configIssueCount,proto3" json:"config_issue_count,omitempty"`
|
||||||
unknownFields protoimpl.UnknownFields
|
unknownFields protoimpl.UnknownFields
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *InitResponse) Reset() {
|
func (x *InitResponse) Reset() {
|
||||||
*x = InitResponse{}
|
*x = InitResponse{}
|
||||||
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[67]
|
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[68]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
|
|
@ -4004,7 +4113,7 @@ func (x *InitResponse) String() string {
|
||||||
func (*InitResponse) ProtoMessage() {}
|
func (*InitResponse) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *InitResponse) ProtoReflect() protoreflect.Message {
|
func (x *InitResponse) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[67]
|
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[68]
|
||||||
if x != nil {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
|
@ -4017,7 +4126,7 @@ func (x *InitResponse) ProtoReflect() protoreflect.Message {
|
||||||
|
|
||||||
// Deprecated: Use InitResponse.ProtoReflect.Descriptor instead.
|
// Deprecated: Use InitResponse.ProtoReflect.Descriptor instead.
|
||||||
func (*InitResponse) Descriptor() ([]byte, []int) {
|
func (*InitResponse) Descriptor() ([]byte, []int) {
|
||||||
return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{67}
|
return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{68}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *InitResponse) GetShowFooter() bool {
|
func (x *InitResponse) GetShowFooter() bool {
|
||||||
|
|
@ -4195,6 +4304,13 @@ func (x *InitResponse) GetShowNavigateOnStartIcons() bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (x *InitResponse) GetConfigIssueCount() int32 {
|
||||||
|
if x != nil {
|
||||||
|
return x.ConfigIssueCount
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
type AdditionalLink struct {
|
type AdditionalLink struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
Title string `protobuf:"bytes,1,opt,name=title,proto3" json:"title,omitempty"`
|
Title string `protobuf:"bytes,1,opt,name=title,proto3" json:"title,omitempty"`
|
||||||
|
|
@ -4205,7 +4321,7 @@ type AdditionalLink struct {
|
||||||
|
|
||||||
func (x *AdditionalLink) Reset() {
|
func (x *AdditionalLink) Reset() {
|
||||||
*x = AdditionalLink{}
|
*x = AdditionalLink{}
|
||||||
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[68]
|
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[69]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
|
|
@ -4217,7 +4333,7 @@ func (x *AdditionalLink) String() string {
|
||||||
func (*AdditionalLink) ProtoMessage() {}
|
func (*AdditionalLink) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *AdditionalLink) ProtoReflect() protoreflect.Message {
|
func (x *AdditionalLink) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[68]
|
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[69]
|
||||||
if x != nil {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
|
@ -4230,7 +4346,7 @@ func (x *AdditionalLink) ProtoReflect() protoreflect.Message {
|
||||||
|
|
||||||
// Deprecated: Use AdditionalLink.ProtoReflect.Descriptor instead.
|
// Deprecated: Use AdditionalLink.ProtoReflect.Descriptor instead.
|
||||||
func (*AdditionalLink) Descriptor() ([]byte, []int) {
|
func (*AdditionalLink) Descriptor() ([]byte, []int) {
|
||||||
return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{68}
|
return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{69}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *AdditionalLink) GetTitle() string {
|
func (x *AdditionalLink) GetTitle() string {
|
||||||
|
|
@ -4258,7 +4374,7 @@ type OAuth2Provider struct {
|
||||||
|
|
||||||
func (x *OAuth2Provider) Reset() {
|
func (x *OAuth2Provider) Reset() {
|
||||||
*x = OAuth2Provider{}
|
*x = OAuth2Provider{}
|
||||||
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[69]
|
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[70]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
|
|
@ -4270,7 +4386,7 @@ func (x *OAuth2Provider) String() string {
|
||||||
func (*OAuth2Provider) ProtoMessage() {}
|
func (*OAuth2Provider) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *OAuth2Provider) ProtoReflect() protoreflect.Message {
|
func (x *OAuth2Provider) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[69]
|
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[70]
|
||||||
if x != nil {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
|
@ -4283,7 +4399,7 @@ func (x *OAuth2Provider) ProtoReflect() protoreflect.Message {
|
||||||
|
|
||||||
// Deprecated: Use OAuth2Provider.ProtoReflect.Descriptor instead.
|
// Deprecated: Use OAuth2Provider.ProtoReflect.Descriptor instead.
|
||||||
func (*OAuth2Provider) Descriptor() ([]byte, []int) {
|
func (*OAuth2Provider) Descriptor() ([]byte, []int) {
|
||||||
return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{69}
|
return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{70}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *OAuth2Provider) GetTitle() string {
|
func (x *OAuth2Provider) GetTitle() string {
|
||||||
|
|
@ -4316,7 +4432,7 @@ type GetActionBindingRequest struct {
|
||||||
|
|
||||||
func (x *GetActionBindingRequest) Reset() {
|
func (x *GetActionBindingRequest) Reset() {
|
||||||
*x = GetActionBindingRequest{}
|
*x = GetActionBindingRequest{}
|
||||||
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[70]
|
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[71]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
|
|
@ -4328,7 +4444,7 @@ func (x *GetActionBindingRequest) String() string {
|
||||||
func (*GetActionBindingRequest) ProtoMessage() {}
|
func (*GetActionBindingRequest) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *GetActionBindingRequest) ProtoReflect() protoreflect.Message {
|
func (x *GetActionBindingRequest) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[70]
|
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[71]
|
||||||
if x != nil {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
|
@ -4341,7 +4457,7 @@ func (x *GetActionBindingRequest) ProtoReflect() protoreflect.Message {
|
||||||
|
|
||||||
// Deprecated: Use GetActionBindingRequest.ProtoReflect.Descriptor instead.
|
// Deprecated: Use GetActionBindingRequest.ProtoReflect.Descriptor instead.
|
||||||
func (*GetActionBindingRequest) Descriptor() ([]byte, []int) {
|
func (*GetActionBindingRequest) Descriptor() ([]byte, []int) {
|
||||||
return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{70}
|
return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{71}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *GetActionBindingRequest) GetBindingId() string {
|
func (x *GetActionBindingRequest) GetBindingId() string {
|
||||||
|
|
@ -4361,7 +4477,7 @@ type GetActionBindingResponse struct {
|
||||||
|
|
||||||
func (x *GetActionBindingResponse) Reset() {
|
func (x *GetActionBindingResponse) Reset() {
|
||||||
*x = GetActionBindingResponse{}
|
*x = GetActionBindingResponse{}
|
||||||
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[71]
|
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[72]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
|
|
@ -4373,7 +4489,7 @@ func (x *GetActionBindingResponse) String() string {
|
||||||
func (*GetActionBindingResponse) ProtoMessage() {}
|
func (*GetActionBindingResponse) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *GetActionBindingResponse) ProtoReflect() protoreflect.Message {
|
func (x *GetActionBindingResponse) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[71]
|
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[72]
|
||||||
if x != nil {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
|
@ -4386,7 +4502,7 @@ func (x *GetActionBindingResponse) ProtoReflect() protoreflect.Message {
|
||||||
|
|
||||||
// Deprecated: Use GetActionBindingResponse.ProtoReflect.Descriptor instead.
|
// Deprecated: Use GetActionBindingResponse.ProtoReflect.Descriptor instead.
|
||||||
func (*GetActionBindingResponse) Descriptor() ([]byte, []int) {
|
func (*GetActionBindingResponse) Descriptor() ([]byte, []int) {
|
||||||
return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{71}
|
return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{72}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *GetActionBindingResponse) GetAction() *Action {
|
func (x *GetActionBindingResponse) GetAction() *Action {
|
||||||
|
|
@ -4415,7 +4531,7 @@ type GetEntitiesRequest struct {
|
||||||
|
|
||||||
func (x *GetEntitiesRequest) Reset() {
|
func (x *GetEntitiesRequest) Reset() {
|
||||||
*x = GetEntitiesRequest{}
|
*x = GetEntitiesRequest{}
|
||||||
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[72]
|
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[73]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
|
|
@ -4427,7 +4543,7 @@ func (x *GetEntitiesRequest) String() string {
|
||||||
func (*GetEntitiesRequest) ProtoMessage() {}
|
func (*GetEntitiesRequest) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *GetEntitiesRequest) ProtoReflect() protoreflect.Message {
|
func (x *GetEntitiesRequest) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[72]
|
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[73]
|
||||||
if x != nil {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
|
@ -4440,7 +4556,7 @@ func (x *GetEntitiesRequest) ProtoReflect() protoreflect.Message {
|
||||||
|
|
||||||
// Deprecated: Use GetEntitiesRequest.ProtoReflect.Descriptor instead.
|
// Deprecated: Use GetEntitiesRequest.ProtoReflect.Descriptor instead.
|
||||||
func (*GetEntitiesRequest) Descriptor() ([]byte, []int) {
|
func (*GetEntitiesRequest) Descriptor() ([]byte, []int) {
|
||||||
return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{72}
|
return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{73}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *GetEntitiesRequest) GetEntityType() string {
|
func (x *GetEntitiesRequest) GetEntityType() string {
|
||||||
|
|
@ -4480,7 +4596,7 @@ type GetEntitiesResponse struct {
|
||||||
|
|
||||||
func (x *GetEntitiesResponse) Reset() {
|
func (x *GetEntitiesResponse) Reset() {
|
||||||
*x = GetEntitiesResponse{}
|
*x = GetEntitiesResponse{}
|
||||||
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[73]
|
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[74]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
|
|
@ -4492,7 +4608,7 @@ func (x *GetEntitiesResponse) String() string {
|
||||||
func (*GetEntitiesResponse) ProtoMessage() {}
|
func (*GetEntitiesResponse) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *GetEntitiesResponse) ProtoReflect() protoreflect.Message {
|
func (x *GetEntitiesResponse) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[73]
|
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[74]
|
||||||
if x != nil {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
|
@ -4505,7 +4621,7 @@ func (x *GetEntitiesResponse) ProtoReflect() protoreflect.Message {
|
||||||
|
|
||||||
// Deprecated: Use GetEntitiesResponse.ProtoReflect.Descriptor instead.
|
// Deprecated: Use GetEntitiesResponse.ProtoReflect.Descriptor instead.
|
||||||
func (*GetEntitiesResponse) Descriptor() ([]byte, []int) {
|
func (*GetEntitiesResponse) Descriptor() ([]byte, []int) {
|
||||||
return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{73}
|
return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{74}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *GetEntitiesResponse) GetEntityDefinitions() []*EntityDefinition {
|
func (x *GetEntitiesResponse) GetEntityDefinitions() []*EntityDefinition {
|
||||||
|
|
@ -4529,7 +4645,7 @@ type EntityDefinition struct {
|
||||||
|
|
||||||
func (x *EntityDefinition) Reset() {
|
func (x *EntityDefinition) Reset() {
|
||||||
*x = EntityDefinition{}
|
*x = EntityDefinition{}
|
||||||
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[74]
|
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[75]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
|
|
@ -4541,7 +4657,7 @@ func (x *EntityDefinition) String() string {
|
||||||
func (*EntityDefinition) ProtoMessage() {}
|
func (*EntityDefinition) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *EntityDefinition) ProtoReflect() protoreflect.Message {
|
func (x *EntityDefinition) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[74]
|
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[75]
|
||||||
if x != nil {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
|
@ -4554,7 +4670,7 @@ func (x *EntityDefinition) ProtoReflect() protoreflect.Message {
|
||||||
|
|
||||||
// Deprecated: Use EntityDefinition.ProtoReflect.Descriptor instead.
|
// Deprecated: Use EntityDefinition.ProtoReflect.Descriptor instead.
|
||||||
func (*EntityDefinition) Descriptor() ([]byte, []int) {
|
func (*EntityDefinition) Descriptor() ([]byte, []int) {
|
||||||
return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{74}
|
return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{75}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *EntityDefinition) GetTitle() string {
|
func (x *EntityDefinition) GetTitle() string {
|
||||||
|
|
@ -4609,7 +4725,7 @@ type EntityProperty struct {
|
||||||
|
|
||||||
func (x *EntityProperty) Reset() {
|
func (x *EntityProperty) Reset() {
|
||||||
*x = EntityProperty{}
|
*x = EntityProperty{}
|
||||||
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[75]
|
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[76]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
|
|
@ -4621,7 +4737,7 @@ func (x *EntityProperty) String() string {
|
||||||
func (*EntityProperty) ProtoMessage() {}
|
func (*EntityProperty) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *EntityProperty) ProtoReflect() protoreflect.Message {
|
func (x *EntityProperty) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[75]
|
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[76]
|
||||||
if x != nil {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
|
@ -4634,7 +4750,7 @@ func (x *EntityProperty) ProtoReflect() protoreflect.Message {
|
||||||
|
|
||||||
// Deprecated: Use EntityProperty.ProtoReflect.Descriptor instead.
|
// Deprecated: Use EntityProperty.ProtoReflect.Descriptor instead.
|
||||||
func (*EntityProperty) Descriptor() ([]byte, []int) {
|
func (*EntityProperty) Descriptor() ([]byte, []int) {
|
||||||
return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{75}
|
return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{76}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *EntityProperty) GetName() string {
|
func (x *EntityProperty) GetName() string {
|
||||||
|
|
@ -4661,7 +4777,7 @@ type GetEntityRequest struct {
|
||||||
|
|
||||||
func (x *GetEntityRequest) Reset() {
|
func (x *GetEntityRequest) Reset() {
|
||||||
*x = GetEntityRequest{}
|
*x = GetEntityRequest{}
|
||||||
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[76]
|
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[77]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
|
|
@ -4673,7 +4789,7 @@ func (x *GetEntityRequest) String() string {
|
||||||
func (*GetEntityRequest) ProtoMessage() {}
|
func (*GetEntityRequest) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *GetEntityRequest) ProtoReflect() protoreflect.Message {
|
func (x *GetEntityRequest) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[76]
|
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[77]
|
||||||
if x != nil {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
|
@ -4686,7 +4802,7 @@ func (x *GetEntityRequest) ProtoReflect() protoreflect.Message {
|
||||||
|
|
||||||
// Deprecated: Use GetEntityRequest.ProtoReflect.Descriptor instead.
|
// Deprecated: Use GetEntityRequest.ProtoReflect.Descriptor instead.
|
||||||
func (*GetEntityRequest) Descriptor() ([]byte, []int) {
|
func (*GetEntityRequest) Descriptor() ([]byte, []int) {
|
||||||
return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{76}
|
return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{77}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *GetEntityRequest) GetUniqueKey() string {
|
func (x *GetEntityRequest) GetUniqueKey() string {
|
||||||
|
|
@ -4712,7 +4828,7 @@ type RestartActionRequest struct {
|
||||||
|
|
||||||
func (x *RestartActionRequest) Reset() {
|
func (x *RestartActionRequest) Reset() {
|
||||||
*x = RestartActionRequest{}
|
*x = RestartActionRequest{}
|
||||||
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[77]
|
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[78]
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
ms.StoreMessageInfo(mi)
|
ms.StoreMessageInfo(mi)
|
||||||
}
|
}
|
||||||
|
|
@ -4724,7 +4840,7 @@ func (x *RestartActionRequest) String() string {
|
||||||
func (*RestartActionRequest) ProtoMessage() {}
|
func (*RestartActionRequest) ProtoMessage() {}
|
||||||
|
|
||||||
func (x *RestartActionRequest) ProtoReflect() protoreflect.Message {
|
func (x *RestartActionRequest) ProtoReflect() protoreflect.Message {
|
||||||
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[77]
|
mi := &file_olivetin_api_v1_olivetin_proto_msgTypes[78]
|
||||||
if x != nil {
|
if x != nil {
|
||||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
if ms.LoadMessageInfo() == nil {
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
|
@ -4737,7 +4853,7 @@ func (x *RestartActionRequest) ProtoReflect() protoreflect.Message {
|
||||||
|
|
||||||
// Deprecated: Use RestartActionRequest.ProtoReflect.Descriptor instead.
|
// Deprecated: Use RestartActionRequest.ProtoReflect.Descriptor instead.
|
||||||
func (*RestartActionRequest) Descriptor() ([]byte, []int) {
|
func (*RestartActionRequest) Descriptor() ([]byte, []int) {
|
||||||
return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{77}
|
return file_olivetin_api_v1_olivetin_proto_rawDescGZIP(), []int{78}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *RestartActionRequest) GetExecutionTrackingId() string {
|
func (x *RestartActionRequest) GetExecutionTrackingId() string {
|
||||||
|
|
@ -5044,11 +5160,22 @@ const file_olivetin_api_v1_olivetin_proto_rawDesc = "" +
|
||||||
"\x04hash\x18\x01 \x01(\tR\x04hash\"\x0f\n" +
|
"\x04hash\x18\x01 \x01(\tR\x04hash\"\x0f\n" +
|
||||||
"\rLogoutRequest\"\x10\n" +
|
"\rLogoutRequest\"\x10\n" +
|
||||||
"\x0eLogoutResponse\"\x17\n" +
|
"\x0eLogoutResponse\"\x17\n" +
|
||||||
"\x15GetDiagnosticsRequest\"b\n" +
|
"\x15GetDiagnosticsRequest\"\xf5\x01\n" +
|
||||||
|
"\vConfigIssue\x12\x1a\n" +
|
||||||
|
"\bseverity\x18\x01 \x01(\tR\bseverity\x12\x12\n" +
|
||||||
|
"\x04code\x18\x02 \x01(\tR\x04code\x12\x18\n" +
|
||||||
|
"\amessage\x18\x03 \x01(\tR\amessage\x12\x1b\n" +
|
||||||
|
"\taction_id\x18\x04 \x01(\tR\bactionId\x12!\n" +
|
||||||
|
"\faction_title\x18\x05 \x01(\tR\vactionTitle\x12#\n" +
|
||||||
|
"\rargument_name\x18\x06 \x01(\tR\fargumentName\x12\x16\n" +
|
||||||
|
"\x06source\x18\a \x01(\tR\x06source\x12\x1f\n" +
|
||||||
|
"\vconfig_file\x18\b \x01(\tR\n" +
|
||||||
|
"configFile\"\xa5\x01\n" +
|
||||||
"\x16GetDiagnosticsResponse\x12 \n" +
|
"\x16GetDiagnosticsResponse\x12 \n" +
|
||||||
"\vSshFoundKey\x18\x01 \x01(\tR\vSshFoundKey\x12&\n" +
|
"\vSshFoundKey\x18\x01 \x01(\tR\vSshFoundKey\x12&\n" +
|
||||||
"\x0eSshFoundConfig\x18\x02 \x01(\tR\x0eSshFoundConfig\"\r\n" +
|
"\x0eSshFoundConfig\x18\x02 \x01(\tR\x0eSshFoundConfig\x12A\n" +
|
||||||
"\vInitRequest\"\x8d\t\n" +
|
"\rconfig_issues\x18\x03 \x03(\v2\x1c.olivetin.api.v1.ConfigIssueR\fconfigIssues\"\r\n" +
|
||||||
|
"\vInitRequest\"\xbb\t\n" +
|
||||||
"\fInitResponse\x12\x1e\n" +
|
"\fInitResponse\x12\x1e\n" +
|
||||||
"\n" +
|
"\n" +
|
||||||
"showFooter\x18\x01 \x01(\bR\n" +
|
"showFooter\x18\x01 \x01(\bR\n" +
|
||||||
|
|
@ -5078,7 +5205,8 @@ const file_olivetin_api_v1_olivetin_proto_rawDesc = "" +
|
||||||
"\rshow_log_list\x18\x16 \x01(\bR\vshowLogList\x12%\n" +
|
"\rshow_log_list\x18\x16 \x01(\bR\vshowLogList\x12%\n" +
|
||||||
"\x0elogin_required\x18\x17 \x01(\bR\rloginRequired\x12)\n" +
|
"\x0elogin_required\x18\x17 \x01(\bR\rloginRequired\x12)\n" +
|
||||||
"\x10available_themes\x18\x18 \x03(\tR\x0favailableThemes\x12>\n" +
|
"\x10available_themes\x18\x18 \x03(\tR\x0favailableThemes\x12>\n" +
|
||||||
"\x1cshow_navigate_on_start_icons\x18\x19 \x01(\bR\x18showNavigateOnStartIcons\"8\n" +
|
"\x1cshow_navigate_on_start_icons\x18\x19 \x01(\bR\x18showNavigateOnStartIcons\x12,\n" +
|
||||||
|
"\x12config_issue_count\x18\x1a \x01(\x05R\x10configIssueCount\"8\n" +
|
||||||
"\x0eAdditionalLink\x12\x14\n" +
|
"\x0eAdditionalLink\x12\x14\n" +
|
||||||
"\x05title\x18\x01 \x01(\tR\x05title\x12\x10\n" +
|
"\x05title\x18\x01 \x01(\tR\x05title\x12\x10\n" +
|
||||||
"\x03url\x18\x02 \x01(\tR\x03url\"L\n" +
|
"\x03url\x18\x02 \x01(\tR\x03url\"L\n" +
|
||||||
|
|
@ -5159,7 +5287,7 @@ func file_olivetin_api_v1_olivetin_proto_rawDescGZIP() []byte {
|
||||||
return file_olivetin_api_v1_olivetin_proto_rawDescData
|
return file_olivetin_api_v1_olivetin_proto_rawDescData
|
||||||
}
|
}
|
||||||
|
|
||||||
var file_olivetin_api_v1_olivetin_proto_msgTypes = make([]protoimpl.MessageInfo, 85)
|
var file_olivetin_api_v1_olivetin_proto_msgTypes = make([]protoimpl.MessageInfo, 86)
|
||||||
var file_olivetin_api_v1_olivetin_proto_goTypes = []any{
|
var file_olivetin_api_v1_olivetin_proto_goTypes = []any{
|
||||||
(*Action)(nil), // 0: olivetin.api.v1.Action
|
(*Action)(nil), // 0: olivetin.api.v1.Action
|
||||||
(*ActionGroupMembership)(nil), // 1: olivetin.api.v1.ActionGroupMembership
|
(*ActionGroupMembership)(nil), // 1: olivetin.api.v1.ActionGroupMembership
|
||||||
|
|
@ -5226,38 +5354,39 @@ var file_olivetin_api_v1_olivetin_proto_goTypes = []any{
|
||||||
(*LogoutRequest)(nil), // 62: olivetin.api.v1.LogoutRequest
|
(*LogoutRequest)(nil), // 62: olivetin.api.v1.LogoutRequest
|
||||||
(*LogoutResponse)(nil), // 63: olivetin.api.v1.LogoutResponse
|
(*LogoutResponse)(nil), // 63: olivetin.api.v1.LogoutResponse
|
||||||
(*GetDiagnosticsRequest)(nil), // 64: olivetin.api.v1.GetDiagnosticsRequest
|
(*GetDiagnosticsRequest)(nil), // 64: olivetin.api.v1.GetDiagnosticsRequest
|
||||||
(*GetDiagnosticsResponse)(nil), // 65: olivetin.api.v1.GetDiagnosticsResponse
|
(*ConfigIssue)(nil), // 65: olivetin.api.v1.ConfigIssue
|
||||||
(*InitRequest)(nil), // 66: olivetin.api.v1.InitRequest
|
(*GetDiagnosticsResponse)(nil), // 66: olivetin.api.v1.GetDiagnosticsResponse
|
||||||
(*InitResponse)(nil), // 67: olivetin.api.v1.InitResponse
|
(*InitRequest)(nil), // 67: olivetin.api.v1.InitRequest
|
||||||
(*AdditionalLink)(nil), // 68: olivetin.api.v1.AdditionalLink
|
(*InitResponse)(nil), // 68: olivetin.api.v1.InitResponse
|
||||||
(*OAuth2Provider)(nil), // 69: olivetin.api.v1.OAuth2Provider
|
(*AdditionalLink)(nil), // 69: olivetin.api.v1.AdditionalLink
|
||||||
(*GetActionBindingRequest)(nil), // 70: olivetin.api.v1.GetActionBindingRequest
|
(*OAuth2Provider)(nil), // 70: olivetin.api.v1.OAuth2Provider
|
||||||
(*GetActionBindingResponse)(nil), // 71: olivetin.api.v1.GetActionBindingResponse
|
(*GetActionBindingRequest)(nil), // 71: olivetin.api.v1.GetActionBindingRequest
|
||||||
(*GetEntitiesRequest)(nil), // 72: olivetin.api.v1.GetEntitiesRequest
|
(*GetActionBindingResponse)(nil), // 72: olivetin.api.v1.GetActionBindingResponse
|
||||||
(*GetEntitiesResponse)(nil), // 73: olivetin.api.v1.GetEntitiesResponse
|
(*GetEntitiesRequest)(nil), // 73: olivetin.api.v1.GetEntitiesRequest
|
||||||
(*EntityDefinition)(nil), // 74: olivetin.api.v1.EntityDefinition
|
(*GetEntitiesResponse)(nil), // 74: olivetin.api.v1.GetEntitiesResponse
|
||||||
(*EntityProperty)(nil), // 75: olivetin.api.v1.EntityProperty
|
(*EntityDefinition)(nil), // 75: olivetin.api.v1.EntityDefinition
|
||||||
(*GetEntityRequest)(nil), // 76: olivetin.api.v1.GetEntityRequest
|
(*EntityProperty)(nil), // 76: olivetin.api.v1.EntityProperty
|
||||||
(*RestartActionRequest)(nil), // 77: olivetin.api.v1.RestartActionRequest
|
(*GetEntityRequest)(nil), // 77: olivetin.api.v1.GetEntityRequest
|
||||||
nil, // 78: olivetin.api.v1.ActionWebhookExecHint.MatchHeadersEntry
|
(*RestartActionRequest)(nil), // 78: olivetin.api.v1.RestartActionRequest
|
||||||
nil, // 79: olivetin.api.v1.ActionWebhookExecHint.MatchQueryEntry
|
nil, // 79: olivetin.api.v1.ActionWebhookExecHint.MatchHeadersEntry
|
||||||
nil, // 80: olivetin.api.v1.ActionArgument.SuggestionsEntry
|
nil, // 80: olivetin.api.v1.ActionWebhookExecHint.MatchQueryEntry
|
||||||
nil, // 81: olivetin.api.v1.EntityRelatedAction.PrefilledArgumentsEntry
|
nil, // 81: olivetin.api.v1.ActionArgument.SuggestionsEntry
|
||||||
nil, // 82: olivetin.api.v1.Entity.FieldsEntry
|
nil, // 82: olivetin.api.v1.EntityRelatedAction.PrefilledArgumentsEntry
|
||||||
nil, // 83: olivetin.api.v1.DumpVarsResponse.ContentsEntry
|
nil, // 83: olivetin.api.v1.Entity.FieldsEntry
|
||||||
nil, // 84: olivetin.api.v1.DumpPublicIdActionMapResponse.ContentsEntry
|
nil, // 84: olivetin.api.v1.DumpVarsResponse.ContentsEntry
|
||||||
|
nil, // 85: olivetin.api.v1.DumpPublicIdActionMapResponse.ContentsEntry
|
||||||
}
|
}
|
||||||
var file_olivetin_api_v1_olivetin_proto_depIdxs = []int32{
|
var file_olivetin_api_v1_olivetin_proto_depIdxs = []int32{
|
||||||
3, // 0: olivetin.api.v1.Action.arguments:type_name -> olivetin.api.v1.ActionArgument
|
3, // 0: olivetin.api.v1.Action.arguments:type_name -> olivetin.api.v1.ActionArgument
|
||||||
2, // 1: olivetin.api.v1.Action.exec_on_webhooks:type_name -> olivetin.api.v1.ActionWebhookExecHint
|
2, // 1: olivetin.api.v1.Action.exec_on_webhooks:type_name -> olivetin.api.v1.ActionWebhookExecHint
|
||||||
1, // 2: olivetin.api.v1.Action.groups:type_name -> olivetin.api.v1.ActionGroupMembership
|
1, // 2: olivetin.api.v1.Action.groups:type_name -> olivetin.api.v1.ActionGroupMembership
|
||||||
78, // 3: olivetin.api.v1.ActionWebhookExecHint.match_headers:type_name -> olivetin.api.v1.ActionWebhookExecHint.MatchHeadersEntry
|
79, // 3: olivetin.api.v1.ActionWebhookExecHint.match_headers:type_name -> olivetin.api.v1.ActionWebhookExecHint.MatchHeadersEntry
|
||||||
79, // 4: olivetin.api.v1.ActionWebhookExecHint.match_query:type_name -> olivetin.api.v1.ActionWebhookExecHint.MatchQueryEntry
|
80, // 4: olivetin.api.v1.ActionWebhookExecHint.match_query:type_name -> olivetin.api.v1.ActionWebhookExecHint.MatchQueryEntry
|
||||||
4, // 5: olivetin.api.v1.ActionArgument.choices:type_name -> olivetin.api.v1.ActionArgumentChoice
|
4, // 5: olivetin.api.v1.ActionArgument.choices:type_name -> olivetin.api.v1.ActionArgumentChoice
|
||||||
80, // 6: olivetin.api.v1.ActionArgument.suggestions:type_name -> olivetin.api.v1.ActionArgument.SuggestionsEntry
|
81, // 6: olivetin.api.v1.ActionArgument.suggestions:type_name -> olivetin.api.v1.ActionArgument.SuggestionsEntry
|
||||||
0, // 7: olivetin.api.v1.EntityRelatedAction.action:type_name -> olivetin.api.v1.Action
|
0, // 7: olivetin.api.v1.EntityRelatedAction.action:type_name -> olivetin.api.v1.Action
|
||||||
81, // 8: olivetin.api.v1.EntityRelatedAction.prefilled_arguments:type_name -> olivetin.api.v1.EntityRelatedAction.PrefilledArgumentsEntry
|
82, // 8: olivetin.api.v1.EntityRelatedAction.prefilled_arguments:type_name -> olivetin.api.v1.EntityRelatedAction.PrefilledArgumentsEntry
|
||||||
82, // 9: olivetin.api.v1.Entity.fields:type_name -> olivetin.api.v1.Entity.FieldsEntry
|
83, // 9: olivetin.api.v1.Entity.fields:type_name -> olivetin.api.v1.Entity.FieldsEntry
|
||||||
5, // 10: olivetin.api.v1.Entity.related_actions:type_name -> olivetin.api.v1.EntityRelatedAction
|
5, // 10: olivetin.api.v1.Entity.related_actions:type_name -> olivetin.api.v1.EntityRelatedAction
|
||||||
10, // 11: olivetin.api.v1.GetDashboardResponse.dashboard:type_name -> olivetin.api.v1.Dashboard
|
10, // 11: olivetin.api.v1.GetDashboardResponse.dashboard:type_name -> olivetin.api.v1.Dashboard
|
||||||
11, // 12: olivetin.api.v1.Dashboard.contents:type_name -> olivetin.api.v1.DashboardComponent
|
11, // 12: olivetin.api.v1.Dashboard.contents:type_name -> olivetin.api.v1.DashboardComponent
|
||||||
|
|
@ -5275,8 +5404,8 @@ var file_olivetin_api_v1_olivetin_proto_depIdxs = []int32{
|
||||||
28, // 24: olivetin.api.v1.GetExecutionQueueResponse.groups:type_name -> olivetin.api.v1.ExecutionQueueGroup
|
28, // 24: olivetin.api.v1.GetExecutionQueueResponse.groups:type_name -> olivetin.api.v1.ExecutionQueueGroup
|
||||||
22, // 25: olivetin.api.v1.ExecutionStatusResponse.log_entry:type_name -> olivetin.api.v1.LogEntry
|
22, // 25: olivetin.api.v1.ExecutionStatusResponse.log_entry:type_name -> olivetin.api.v1.LogEntry
|
||||||
35, // 26: olivetin.api.v1.ExecutionStatusResponse.back_to_dashboards:type_name -> olivetin.api.v1.DashboardNavigationTarget
|
35, // 26: olivetin.api.v1.ExecutionStatusResponse.back_to_dashboards:type_name -> olivetin.api.v1.DashboardNavigationTarget
|
||||||
83, // 27: olivetin.api.v1.DumpVarsResponse.contents:type_name -> olivetin.api.v1.DumpVarsResponse.ContentsEntry
|
84, // 27: olivetin.api.v1.DumpVarsResponse.contents:type_name -> olivetin.api.v1.DumpVarsResponse.ContentsEntry
|
||||||
84, // 28: olivetin.api.v1.DumpPublicIdActionMapResponse.contents:type_name -> olivetin.api.v1.DumpPublicIdActionMapResponse.ContentsEntry
|
85, // 28: olivetin.api.v1.DumpPublicIdActionMapResponse.contents:type_name -> olivetin.api.v1.DumpPublicIdActionMapResponse.ContentsEntry
|
||||||
51, // 29: olivetin.api.v1.EventStreamResponse.entity_changed:type_name -> olivetin.api.v1.EventEntityChanged
|
51, // 29: olivetin.api.v1.EventStreamResponse.entity_changed:type_name -> olivetin.api.v1.EventEntityChanged
|
||||||
52, // 30: olivetin.api.v1.EventStreamResponse.config_changed:type_name -> olivetin.api.v1.EventConfigChanged
|
52, // 30: olivetin.api.v1.EventStreamResponse.config_changed:type_name -> olivetin.api.v1.EventConfigChanged
|
||||||
54, // 31: olivetin.api.v1.EventStreamResponse.execution_finished:type_name -> olivetin.api.v1.EventExecutionFinished
|
54, // 31: olivetin.api.v1.EventStreamResponse.execution_finished:type_name -> olivetin.api.v1.EventExecutionFinished
|
||||||
|
|
@ -5285,72 +5414,73 @@ var file_olivetin_api_v1_olivetin_proto_depIdxs = []int32{
|
||||||
53, // 34: olivetin.api.v1.EventStreamResponse.heartbeat:type_name -> olivetin.api.v1.EventHeartbeat
|
53, // 34: olivetin.api.v1.EventStreamResponse.heartbeat:type_name -> olivetin.api.v1.EventHeartbeat
|
||||||
22, // 35: olivetin.api.v1.EventExecutionFinished.log_entry:type_name -> olivetin.api.v1.LogEntry
|
22, // 35: olivetin.api.v1.EventExecutionFinished.log_entry:type_name -> olivetin.api.v1.LogEntry
|
||||||
22, // 36: olivetin.api.v1.EventExecutionStarted.log_entry:type_name -> olivetin.api.v1.LogEntry
|
22, // 36: olivetin.api.v1.EventExecutionStarted.log_entry:type_name -> olivetin.api.v1.LogEntry
|
||||||
69, // 37: olivetin.api.v1.InitResponse.oAuth2Providers:type_name -> olivetin.api.v1.OAuth2Provider
|
65, // 37: olivetin.api.v1.GetDiagnosticsResponse.config_issues:type_name -> olivetin.api.v1.ConfigIssue
|
||||||
68, // 38: olivetin.api.v1.InitResponse.additionalLinks:type_name -> olivetin.api.v1.AdditionalLink
|
70, // 38: olivetin.api.v1.InitResponse.oAuth2Providers:type_name -> olivetin.api.v1.OAuth2Provider
|
||||||
8, // 39: olivetin.api.v1.InitResponse.effective_policy:type_name -> olivetin.api.v1.EffectivePolicy
|
69, // 39: olivetin.api.v1.InitResponse.additionalLinks:type_name -> olivetin.api.v1.AdditionalLink
|
||||||
0, // 40: olivetin.api.v1.GetActionBindingResponse.action:type_name -> olivetin.api.v1.Action
|
8, // 40: olivetin.api.v1.InitResponse.effective_policy:type_name -> olivetin.api.v1.EffectivePolicy
|
||||||
35, // 41: olivetin.api.v1.GetActionBindingResponse.back_to_dashboards:type_name -> olivetin.api.v1.DashboardNavigationTarget
|
0, // 41: olivetin.api.v1.GetActionBindingResponse.action:type_name -> olivetin.api.v1.Action
|
||||||
74, // 42: olivetin.api.v1.GetEntitiesResponse.entity_definitions:type_name -> olivetin.api.v1.EntityDefinition
|
35, // 42: olivetin.api.v1.GetActionBindingResponse.back_to_dashboards:type_name -> olivetin.api.v1.DashboardNavigationTarget
|
||||||
6, // 43: olivetin.api.v1.EntityDefinition.instances:type_name -> olivetin.api.v1.Entity
|
75, // 43: olivetin.api.v1.GetEntitiesResponse.entity_definitions:type_name -> olivetin.api.v1.EntityDefinition
|
||||||
75, // 44: olivetin.api.v1.EntityDefinition.properties:type_name -> olivetin.api.v1.EntityProperty
|
6, // 44: olivetin.api.v1.EntityDefinition.instances:type_name -> olivetin.api.v1.Entity
|
||||||
43, // 45: olivetin.api.v1.DumpPublicIdActionMapResponse.ContentsEntry.value:type_name -> olivetin.api.v1.DebugBinding
|
76, // 45: olivetin.api.v1.EntityDefinition.properties:type_name -> olivetin.api.v1.EntityProperty
|
||||||
9, // 46: olivetin.api.v1.OliveTinApiService.GetDashboard:input_type -> olivetin.api.v1.GetDashboardRequest
|
43, // 46: olivetin.api.v1.DumpPublicIdActionMapResponse.ContentsEntry.value:type_name -> olivetin.api.v1.DebugBinding
|
||||||
12, // 47: olivetin.api.v1.OliveTinApiService.StartAction:input_type -> olivetin.api.v1.StartActionRequest
|
9, // 47: olivetin.api.v1.OliveTinApiService.GetDashboard:input_type -> olivetin.api.v1.GetDashboardRequest
|
||||||
15, // 48: olivetin.api.v1.OliveTinApiService.StartActionAndWait:input_type -> olivetin.api.v1.StartActionAndWaitRequest
|
12, // 48: olivetin.api.v1.OliveTinApiService.StartAction:input_type -> olivetin.api.v1.StartActionRequest
|
||||||
17, // 49: olivetin.api.v1.OliveTinApiService.StartActionByGet:input_type -> olivetin.api.v1.StartActionByGetRequest
|
15, // 49: olivetin.api.v1.OliveTinApiService.StartActionAndWait:input_type -> olivetin.api.v1.StartActionAndWaitRequest
|
||||||
19, // 50: olivetin.api.v1.OliveTinApiService.StartActionByGetAndWait:input_type -> olivetin.api.v1.StartActionByGetAndWaitRequest
|
17, // 50: olivetin.api.v1.OliveTinApiService.StartActionByGet:input_type -> olivetin.api.v1.StartActionByGetRequest
|
||||||
77, // 51: olivetin.api.v1.OliveTinApiService.RestartAction:input_type -> olivetin.api.v1.RestartActionRequest
|
19, // 51: olivetin.api.v1.OliveTinApiService.StartActionByGetAndWait:input_type -> olivetin.api.v1.StartActionByGetAndWaitRequest
|
||||||
56, // 52: olivetin.api.v1.OliveTinApiService.KillAction:input_type -> olivetin.api.v1.KillActionRequest
|
78, // 52: olivetin.api.v1.OliveTinApiService.RestartAction:input_type -> olivetin.api.v1.RestartActionRequest
|
||||||
34, // 53: olivetin.api.v1.OliveTinApiService.ExecutionStatus:input_type -> olivetin.api.v1.ExecutionStatusRequest
|
56, // 53: olivetin.api.v1.OliveTinApiService.KillAction:input_type -> olivetin.api.v1.KillActionRequest
|
||||||
21, // 54: olivetin.api.v1.OliveTinApiService.GetLogs:input_type -> olivetin.api.v1.GetLogsRequest
|
34, // 54: olivetin.api.v1.OliveTinApiService.ExecutionStatus:input_type -> olivetin.api.v1.ExecutionStatusRequest
|
||||||
24, // 55: olivetin.api.v1.OliveTinApiService.GetActionLogs:input_type -> olivetin.api.v1.GetActionLogsRequest
|
21, // 55: olivetin.api.v1.OliveTinApiService.GetLogs:input_type -> olivetin.api.v1.GetLogsRequest
|
||||||
26, // 56: olivetin.api.v1.OliveTinApiService.GetExecutionQueue:input_type -> olivetin.api.v1.GetExecutionQueueRequest
|
24, // 56: olivetin.api.v1.OliveTinApiService.GetActionLogs:input_type -> olivetin.api.v1.GetActionLogsRequest
|
||||||
30, // 57: olivetin.api.v1.OliveTinApiService.ValidateArgumentType:input_type -> olivetin.api.v1.ValidateArgumentTypeRequest
|
26, // 57: olivetin.api.v1.OliveTinApiService.GetExecutionQueue:input_type -> olivetin.api.v1.GetExecutionQueueRequest
|
||||||
37, // 58: olivetin.api.v1.OliveTinApiService.WhoAmI:input_type -> olivetin.api.v1.WhoAmIRequest
|
30, // 58: olivetin.api.v1.OliveTinApiService.ValidateArgumentType:input_type -> olivetin.api.v1.ValidateArgumentTypeRequest
|
||||||
39, // 59: olivetin.api.v1.OliveTinApiService.ServerDiagnostics:input_type -> olivetin.api.v1.ServerDiagnosticsRequest
|
37, // 59: olivetin.api.v1.OliveTinApiService.WhoAmI:input_type -> olivetin.api.v1.WhoAmIRequest
|
||||||
41, // 60: olivetin.api.v1.OliveTinApiService.DumpVars:input_type -> olivetin.api.v1.DumpVarsRequest
|
39, // 60: olivetin.api.v1.OliveTinApiService.ServerDiagnostics:input_type -> olivetin.api.v1.ServerDiagnosticsRequest
|
||||||
44, // 61: olivetin.api.v1.OliveTinApiService.DumpPublicIdActionMap:input_type -> olivetin.api.v1.DumpPublicIdActionMapRequest
|
41, // 61: olivetin.api.v1.OliveTinApiService.DumpVars:input_type -> olivetin.api.v1.DumpVarsRequest
|
||||||
46, // 62: olivetin.api.v1.OliveTinApiService.GetReadyz:input_type -> olivetin.api.v1.GetReadyzRequest
|
44, // 62: olivetin.api.v1.OliveTinApiService.DumpPublicIdActionMap:input_type -> olivetin.api.v1.DumpPublicIdActionMapRequest
|
||||||
58, // 63: olivetin.api.v1.OliveTinApiService.LocalUserLogin:input_type -> olivetin.api.v1.LocalUserLoginRequest
|
46, // 63: olivetin.api.v1.OliveTinApiService.GetReadyz:input_type -> olivetin.api.v1.GetReadyzRequest
|
||||||
60, // 64: olivetin.api.v1.OliveTinApiService.PasswordHash:input_type -> olivetin.api.v1.PasswordHashRequest
|
58, // 64: olivetin.api.v1.OliveTinApiService.LocalUserLogin:input_type -> olivetin.api.v1.LocalUserLoginRequest
|
||||||
62, // 65: olivetin.api.v1.OliveTinApiService.Logout:input_type -> olivetin.api.v1.LogoutRequest
|
60, // 65: olivetin.api.v1.OliveTinApiService.PasswordHash:input_type -> olivetin.api.v1.PasswordHashRequest
|
||||||
48, // 66: olivetin.api.v1.OliveTinApiService.EventStream:input_type -> olivetin.api.v1.EventStreamRequest
|
62, // 66: olivetin.api.v1.OliveTinApiService.Logout:input_type -> olivetin.api.v1.LogoutRequest
|
||||||
64, // 67: olivetin.api.v1.OliveTinApiService.GetDiagnostics:input_type -> olivetin.api.v1.GetDiagnosticsRequest
|
48, // 67: olivetin.api.v1.OliveTinApiService.EventStream:input_type -> olivetin.api.v1.EventStreamRequest
|
||||||
66, // 68: olivetin.api.v1.OliveTinApiService.Init:input_type -> olivetin.api.v1.InitRequest
|
64, // 68: olivetin.api.v1.OliveTinApiService.GetDiagnostics:input_type -> olivetin.api.v1.GetDiagnosticsRequest
|
||||||
70, // 69: olivetin.api.v1.OliveTinApiService.GetActionBinding:input_type -> olivetin.api.v1.GetActionBindingRequest
|
67, // 69: olivetin.api.v1.OliveTinApiService.Init:input_type -> olivetin.api.v1.InitRequest
|
||||||
72, // 70: olivetin.api.v1.OliveTinApiService.GetEntities:input_type -> olivetin.api.v1.GetEntitiesRequest
|
71, // 70: olivetin.api.v1.OliveTinApiService.GetActionBinding:input_type -> olivetin.api.v1.GetActionBindingRequest
|
||||||
76, // 71: olivetin.api.v1.OliveTinApiService.GetEntity:input_type -> olivetin.api.v1.GetEntityRequest
|
73, // 71: olivetin.api.v1.OliveTinApiService.GetEntities:input_type -> olivetin.api.v1.GetEntitiesRequest
|
||||||
7, // 72: olivetin.api.v1.OliveTinApiService.GetDashboard:output_type -> olivetin.api.v1.GetDashboardResponse
|
77, // 72: olivetin.api.v1.OliveTinApiService.GetEntity:input_type -> olivetin.api.v1.GetEntityRequest
|
||||||
14, // 73: olivetin.api.v1.OliveTinApiService.StartAction:output_type -> olivetin.api.v1.StartActionResponse
|
7, // 73: olivetin.api.v1.OliveTinApiService.GetDashboard:output_type -> olivetin.api.v1.GetDashboardResponse
|
||||||
16, // 74: olivetin.api.v1.OliveTinApiService.StartActionAndWait:output_type -> olivetin.api.v1.StartActionAndWaitResponse
|
14, // 74: olivetin.api.v1.OliveTinApiService.StartAction:output_type -> olivetin.api.v1.StartActionResponse
|
||||||
18, // 75: olivetin.api.v1.OliveTinApiService.StartActionByGet:output_type -> olivetin.api.v1.StartActionByGetResponse
|
16, // 75: olivetin.api.v1.OliveTinApiService.StartActionAndWait:output_type -> olivetin.api.v1.StartActionAndWaitResponse
|
||||||
20, // 76: olivetin.api.v1.OliveTinApiService.StartActionByGetAndWait:output_type -> olivetin.api.v1.StartActionByGetAndWaitResponse
|
18, // 76: olivetin.api.v1.OliveTinApiService.StartActionByGet:output_type -> olivetin.api.v1.StartActionByGetResponse
|
||||||
14, // 77: olivetin.api.v1.OliveTinApiService.RestartAction:output_type -> olivetin.api.v1.StartActionResponse
|
20, // 77: olivetin.api.v1.OliveTinApiService.StartActionByGetAndWait:output_type -> olivetin.api.v1.StartActionByGetAndWaitResponse
|
||||||
57, // 78: olivetin.api.v1.OliveTinApiService.KillAction:output_type -> olivetin.api.v1.KillActionResponse
|
14, // 78: olivetin.api.v1.OliveTinApiService.RestartAction:output_type -> olivetin.api.v1.StartActionResponse
|
||||||
36, // 79: olivetin.api.v1.OliveTinApiService.ExecutionStatus:output_type -> olivetin.api.v1.ExecutionStatusResponse
|
57, // 79: olivetin.api.v1.OliveTinApiService.KillAction:output_type -> olivetin.api.v1.KillActionResponse
|
||||||
23, // 80: olivetin.api.v1.OliveTinApiService.GetLogs:output_type -> olivetin.api.v1.GetLogsResponse
|
36, // 80: olivetin.api.v1.OliveTinApiService.ExecutionStatus:output_type -> olivetin.api.v1.ExecutionStatusResponse
|
||||||
25, // 81: olivetin.api.v1.OliveTinApiService.GetActionLogs:output_type -> olivetin.api.v1.GetActionLogsResponse
|
23, // 81: olivetin.api.v1.OliveTinApiService.GetLogs:output_type -> olivetin.api.v1.GetLogsResponse
|
||||||
29, // 82: olivetin.api.v1.OliveTinApiService.GetExecutionQueue:output_type -> olivetin.api.v1.GetExecutionQueueResponse
|
25, // 82: olivetin.api.v1.OliveTinApiService.GetActionLogs:output_type -> olivetin.api.v1.GetActionLogsResponse
|
||||||
31, // 83: olivetin.api.v1.OliveTinApiService.ValidateArgumentType:output_type -> olivetin.api.v1.ValidateArgumentTypeResponse
|
29, // 83: olivetin.api.v1.OliveTinApiService.GetExecutionQueue:output_type -> olivetin.api.v1.GetExecutionQueueResponse
|
||||||
38, // 84: olivetin.api.v1.OliveTinApiService.WhoAmI:output_type -> olivetin.api.v1.WhoAmIResponse
|
31, // 84: olivetin.api.v1.OliveTinApiService.ValidateArgumentType:output_type -> olivetin.api.v1.ValidateArgumentTypeResponse
|
||||||
40, // 85: olivetin.api.v1.OliveTinApiService.ServerDiagnostics:output_type -> olivetin.api.v1.ServerDiagnosticsResponse
|
38, // 85: olivetin.api.v1.OliveTinApiService.WhoAmI:output_type -> olivetin.api.v1.WhoAmIResponse
|
||||||
42, // 86: olivetin.api.v1.OliveTinApiService.DumpVars:output_type -> olivetin.api.v1.DumpVarsResponse
|
40, // 86: olivetin.api.v1.OliveTinApiService.ServerDiagnostics:output_type -> olivetin.api.v1.ServerDiagnosticsResponse
|
||||||
45, // 87: olivetin.api.v1.OliveTinApiService.DumpPublicIdActionMap:output_type -> olivetin.api.v1.DumpPublicIdActionMapResponse
|
42, // 87: olivetin.api.v1.OliveTinApiService.DumpVars:output_type -> olivetin.api.v1.DumpVarsResponse
|
||||||
47, // 88: olivetin.api.v1.OliveTinApiService.GetReadyz:output_type -> olivetin.api.v1.GetReadyzResponse
|
45, // 88: olivetin.api.v1.OliveTinApiService.DumpPublicIdActionMap:output_type -> olivetin.api.v1.DumpPublicIdActionMapResponse
|
||||||
59, // 89: olivetin.api.v1.OliveTinApiService.LocalUserLogin:output_type -> olivetin.api.v1.LocalUserLoginResponse
|
47, // 89: olivetin.api.v1.OliveTinApiService.GetReadyz:output_type -> olivetin.api.v1.GetReadyzResponse
|
||||||
61, // 90: olivetin.api.v1.OliveTinApiService.PasswordHash:output_type -> olivetin.api.v1.PasswordHashResponse
|
59, // 90: olivetin.api.v1.OliveTinApiService.LocalUserLogin:output_type -> olivetin.api.v1.LocalUserLoginResponse
|
||||||
63, // 91: olivetin.api.v1.OliveTinApiService.Logout:output_type -> olivetin.api.v1.LogoutResponse
|
61, // 91: olivetin.api.v1.OliveTinApiService.PasswordHash:output_type -> olivetin.api.v1.PasswordHashResponse
|
||||||
49, // 92: olivetin.api.v1.OliveTinApiService.EventStream:output_type -> olivetin.api.v1.EventStreamResponse
|
63, // 92: olivetin.api.v1.OliveTinApiService.Logout:output_type -> olivetin.api.v1.LogoutResponse
|
||||||
65, // 93: olivetin.api.v1.OliveTinApiService.GetDiagnostics:output_type -> olivetin.api.v1.GetDiagnosticsResponse
|
49, // 93: olivetin.api.v1.OliveTinApiService.EventStream:output_type -> olivetin.api.v1.EventStreamResponse
|
||||||
67, // 94: olivetin.api.v1.OliveTinApiService.Init:output_type -> olivetin.api.v1.InitResponse
|
66, // 94: olivetin.api.v1.OliveTinApiService.GetDiagnostics:output_type -> olivetin.api.v1.GetDiagnosticsResponse
|
||||||
71, // 95: olivetin.api.v1.OliveTinApiService.GetActionBinding:output_type -> olivetin.api.v1.GetActionBindingResponse
|
68, // 95: olivetin.api.v1.OliveTinApiService.Init:output_type -> olivetin.api.v1.InitResponse
|
||||||
73, // 96: olivetin.api.v1.OliveTinApiService.GetEntities:output_type -> olivetin.api.v1.GetEntitiesResponse
|
72, // 96: olivetin.api.v1.OliveTinApiService.GetActionBinding:output_type -> olivetin.api.v1.GetActionBindingResponse
|
||||||
6, // 97: olivetin.api.v1.OliveTinApiService.GetEntity:output_type -> olivetin.api.v1.Entity
|
74, // 97: olivetin.api.v1.OliveTinApiService.GetEntities:output_type -> olivetin.api.v1.GetEntitiesResponse
|
||||||
72, // [72:98] is the sub-list for method output_type
|
6, // 98: olivetin.api.v1.OliveTinApiService.GetEntity:output_type -> olivetin.api.v1.Entity
|
||||||
46, // [46:72] is the sub-list for method input_type
|
73, // [73:99] is the sub-list for method output_type
|
||||||
46, // [46:46] is the sub-list for extension type_name
|
47, // [47:73] is the sub-list for method input_type
|
||||||
46, // [46:46] is the sub-list for extension extendee
|
47, // [47:47] is the sub-list for extension type_name
|
||||||
0, // [0:46] is the sub-list for field type_name
|
47, // [47:47] is the sub-list for extension extendee
|
||||||
|
0, // [0:47] is the sub-list for field type_name
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() { file_olivetin_api_v1_olivetin_proto_init() }
|
func init() { file_olivetin_api_v1_olivetin_proto_init() }
|
||||||
|
|
@ -5372,7 +5502,7 @@ func file_olivetin_api_v1_olivetin_proto_init() {
|
||||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_olivetin_api_v1_olivetin_proto_rawDesc), len(file_olivetin_api_v1_olivetin_proto_rawDesc)),
|
RawDescriptor: unsafe.Slice(unsafe.StringData(file_olivetin_api_v1_olivetin_proto_rawDesc), len(file_olivetin_api_v1_olivetin_proto_rawDesc)),
|
||||||
NumEnums: 0,
|
NumEnums: 0,
|
||||||
NumMessages: 85,
|
NumMessages: 86,
|
||||||
NumExtensions: 0,
|
NumExtensions: 0,
|
||||||
NumServices: 1,
|
NumServices: 1,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1200,6 +1200,7 @@ func (api *oliveTinAPI) GetDiagnostics(ctx ctx.Context, req *connect.Request[api
|
||||||
res := &apiv1.GetDiagnosticsResponse{
|
res := &apiv1.GetDiagnosticsResponse{
|
||||||
SshFoundKey: installationinfo.Runtime.SshFoundKey,
|
SshFoundKey: installationinfo.Runtime.SshFoundKey,
|
||||||
SshFoundConfig: installationinfo.Runtime.SshFoundConfig,
|
SshFoundConfig: installationinfo.Runtime.SshFoundConfig,
|
||||||
|
ConfigIssues: api.buildConfigIssuesForUser(user),
|
||||||
}
|
}
|
||||||
return connect.NewResponse(res), nil
|
return connect.NewResponse(res), nil
|
||||||
}
|
}
|
||||||
|
|
@ -1242,6 +1243,7 @@ func (api *oliveTinAPI) Init(ctx ctx.Context, req *connect.Request[apiv1.InitReq
|
||||||
LoginRequired: loginRequired,
|
LoginRequired: loginRequired,
|
||||||
AvailableThemes: discoverAvailableThemes(api.cfg),
|
AvailableThemes: discoverAvailableThemes(api.cfg),
|
||||||
ShowNavigateOnStartIcons: api.cfg.ShowNavigateOnStartIcons,
|
ShowNavigateOnStartIcons: api.cfg.ShowNavigateOnStartIcons,
|
||||||
|
ConfigIssueCount: configIssueCountForUser(api, user),
|
||||||
}
|
}
|
||||||
|
|
||||||
return connect.NewResponse(res), nil
|
return connect.NewResponse(res), nil
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,197 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"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/configissues"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetDiagnosticsReturnsConfigIssues(t *testing.T) {
|
||||||
|
cfg := config.DefaultConfig()
|
||||||
|
cfg.Actions = []*config.Action{
|
||||||
|
{
|
||||||
|
Title: "Deploy",
|
||||||
|
ID: "deploy",
|
||||||
|
Entity: "host",
|
||||||
|
Arguments: []config.ActionArgument{
|
||||||
|
{
|
||||||
|
Name: "ansible_host",
|
||||||
|
Type: "ascii_identifier",
|
||||||
|
Default: `{{ host.name | default "All" }}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ExecOnCron: []string{"0 1 * * *"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
testServer, client := getNewTestServerAndClient(cfg)
|
||||||
|
defer testServer.Close()
|
||||||
|
|
||||||
|
res, err := client.GetDiagnostics(context.Background(), connect.NewRequest(&apiv1.GetDiagnosticsRequest{}))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, res.Msg)
|
||||||
|
require.NotEmpty(t, res.Msg.ConfigIssues)
|
||||||
|
|
||||||
|
codes := map[string]bool{}
|
||||||
|
for _, issue := range res.Msg.ConfigIssues {
|
||||||
|
codes[issue.Code] = true
|
||||||
|
}
|
||||||
|
assert.True(t, codes[configissues.CodeTemplateParse])
|
||||||
|
assert.True(t, codes[configissues.CodeCronEntityBinding])
|
||||||
|
|
||||||
|
initRes, err := client.Init(context.Background(), connect.NewRequest(&apiv1.InitRequest{}))
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, int32(len(res.Msg.ConfigIssues)), initRes.Msg.ConfigIssueCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInitHidesConfigIssueCountWithoutDiagnostics(t *testing.T) {
|
||||||
|
cfg := config.DefaultConfig()
|
||||||
|
cfg.DefaultPolicy.ShowDiagnostics = false
|
||||||
|
cfg.Actions = []*config.Action{
|
||||||
|
{
|
||||||
|
Title: "Deploy",
|
||||||
|
ID: "deploy",
|
||||||
|
Entity: "host",
|
||||||
|
Arguments: []config.ActionArgument{
|
||||||
|
{
|
||||||
|
Name: "ansible_host",
|
||||||
|
Type: "ascii_identifier",
|
||||||
|
Default: `{{ host.name | default "All" }}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
testServer, client := getNewTestServerAndClient(cfg)
|
||||||
|
defer testServer.Close()
|
||||||
|
|
||||||
|
initRes, err := client.Init(context.Background(), connect.NewRequest(&apiv1.InitRequest{}))
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, int32(0), initRes.Msg.ConfigIssueCount)
|
||||||
|
|
||||||
|
_, err = client.GetDiagnostics(context.Background(), connect.NewRequest(&apiv1.GetDiagnosticsRequest{}))
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Equal(t, connect.CodePermissionDenied, connect.CodeOf(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
func attachBrokenTemplateArgToSecretAction(cfg *config.Config) {
|
||||||
|
for i := range cfg.Actions {
|
||||||
|
if cfg.Actions[i].ID != "secret_action" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cfg.Actions[i].Arguments = []config.ActionArgument{
|
||||||
|
{
|
||||||
|
Name: "target",
|
||||||
|
Type: "ascii_identifier",
|
||||||
|
Default: `{{ host.name | default "All" }}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertNoActionScopedConfigIssues(t *testing.T, issues []*apiv1.ConfigIssue) {
|
||||||
|
t.Helper()
|
||||||
|
for _, issue := range issues {
|
||||||
|
assert.Empty(t, issue.ActionId,
|
||||||
|
"user with view:false must not see action-scoped config issues")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func configIssuesContainActionID(issues []*apiv1.ConfigIssue, actionID string) bool {
|
||||||
|
for _, issue := range issues {
|
||||||
|
if issue.ActionId == actionID {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigIssuesHideActionsWithoutViewPermission(t *testing.T) {
|
||||||
|
cfg, _, _ := buildViewPermissionTestConfig(t)
|
||||||
|
cfg.AuthHttpHeaderUsername = "X-Ot-User"
|
||||||
|
attachBrokenTemplateArgToSecretAction(cfg)
|
||||||
|
|
||||||
|
testServer, client := getNewTestServerAndClient(cfg)
|
||||||
|
defer testServer.Close()
|
||||||
|
|
||||||
|
configissues.Report(configissues.Issue{
|
||||||
|
Severity: configissues.SeverityWarning,
|
||||||
|
Code: configissues.CodeEnvUnset,
|
||||||
|
Message: "TEST_GLOBAL_ENV is not set",
|
||||||
|
Source: "TEST_GLOBAL_ENV",
|
||||||
|
})
|
||||||
|
|
||||||
|
lowReq := connect.NewRequest(&apiv1.GetDiagnosticsRequest{})
|
||||||
|
lowReq.Header().Set("X-Ot-User", "low")
|
||||||
|
lowRes, err := client.GetDiagnostics(context.Background(), lowReq)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assertNoActionScopedConfigIssues(t, lowRes.Msg.ConfigIssues)
|
||||||
|
require.NotEmpty(t, lowRes.Msg.ConfigIssues, "low user should still see global config issues")
|
||||||
|
|
||||||
|
lowInit := connect.NewRequest(&apiv1.InitRequest{})
|
||||||
|
lowInit.Header().Set("X-Ot-User", "low")
|
||||||
|
lowInitRes, err := client.Init(context.Background(), lowInit)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, int32(len(lowRes.Msg.ConfigIssues)), lowInitRes.Msg.ConfigIssueCount)
|
||||||
|
|
||||||
|
adminReq := connect.NewRequest(&apiv1.GetDiagnosticsRequest{})
|
||||||
|
adminReq.Header().Set("X-Ot-User", "admin")
|
||||||
|
adminRes, err := client.GetDiagnostics(context.Background(), adminReq)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.True(t, configIssuesContainActionID(adminRes.Msg.ConfigIssues, "secret_action"),
|
||||||
|
"admin must still see config issues for secret_action")
|
||||||
|
assert.Greater(t, len(adminRes.Msg.ConfigIssues), len(lowRes.Msg.ConfigIssues))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigIssuesHideRuntimeWatcherFailuresWithoutViewPermission(t *testing.T) {
|
||||||
|
cfg, _, _ := buildViewPermissionTestConfig(t)
|
||||||
|
cfg.AuthHttpHeaderUsername = "X-Ot-User"
|
||||||
|
|
||||||
|
testServer, client := getNewTestServerAndClient(cfg)
|
||||||
|
defer testServer.Close()
|
||||||
|
|
||||||
|
configissues.Report(configissues.Issue{
|
||||||
|
Severity: configissues.SeverityError,
|
||||||
|
Code: configissues.CodeWatcherPath,
|
||||||
|
Message: `Could not create watcher for "/secret/path": permission denied`,
|
||||||
|
ActionID: "secret_action",
|
||||||
|
ActionTitle: "Secret Action",
|
||||||
|
Source: "/secret/path",
|
||||||
|
ConfigFile: "/etc/OliveTin/secret.yaml",
|
||||||
|
})
|
||||||
|
|
||||||
|
lowReq := connect.NewRequest(&apiv1.GetDiagnosticsRequest{})
|
||||||
|
lowReq.Header().Set("X-Ot-User", "low")
|
||||||
|
lowRes, err := client.GetDiagnostics(context.Background(), lowReq)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
for _, issue := range lowRes.Msg.ConfigIssues {
|
||||||
|
assert.NotEqual(t, configissues.CodeWatcherPath, issue.Code,
|
||||||
|
"user with view:false must not see action-scoped watcher_path issues")
|
||||||
|
assert.NotContains(t, issue.Source, "/secret/path")
|
||||||
|
}
|
||||||
|
|
||||||
|
adminReq := connect.NewRequest(&apiv1.GetDiagnosticsRequest{})
|
||||||
|
adminReq.Header().Set("X-Ot-User", "admin")
|
||||||
|
adminRes, err := client.GetDiagnostics(context.Background(), adminReq)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
foundWatcher := false
|
||||||
|
for _, issue := range adminRes.Msg.ConfigIssues {
|
||||||
|
if issue.Code == configissues.CodeWatcherPath && issue.ActionId == "secret_action" {
|
||||||
|
foundWatcher = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert.True(t, foundWatcher, "admin must see action-scoped watcher_path issues")
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
apiv1 "github.com/OliveTin/OliveTin/gen/olivetin/api/v1"
|
||||||
|
authpublic "github.com/OliveTin/OliveTin/internal/auth/authpublic"
|
||||||
|
"github.com/OliveTin/OliveTin/internal/config"
|
||||||
|
"github.com/OliveTin/OliveTin/internal/configissues"
|
||||||
|
)
|
||||||
|
|
||||||
|
func configIssueCountForUser(api *oliveTinAPI, user *authpublic.AuthenticatedUser) int32 {
|
||||||
|
return int32(len(api.buildConfigIssuesForUser(user)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *oliveTinAPI) buildConfigIssuesForUser(user *authpublic.AuthenticatedUser) []*apiv1.ConfigIssue {
|
||||||
|
if !userCanSeeDiagnostics(user) {
|
||||||
|
return []*apiv1.ConfigIssue{}
|
||||||
|
}
|
||||||
|
|
||||||
|
return filterConfigIssuesForUser(api, user, configissues.List())
|
||||||
|
}
|
||||||
|
|
||||||
|
func userCanSeeDiagnostics(user *authpublic.AuthenticatedUser) bool {
|
||||||
|
return user != nil && user.EffectivePolicy != nil && user.EffectivePolicy.ShowDiagnostics
|
||||||
|
}
|
||||||
|
|
||||||
|
func filterConfigIssuesForUser(api *oliveTinAPI, user *authpublic.AuthenticatedUser, src []configissues.Issue) []*apiv1.ConfigIssue {
|
||||||
|
out := make([]*apiv1.ConfigIssue, 0, len(src))
|
||||||
|
for _, issue := range src {
|
||||||
|
if api.includeConfigIssueForUser(user, issue) {
|
||||||
|
out = append(out, toProtoConfigIssue(issue))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *oliveTinAPI) includeConfigIssueForUser(user *authpublic.AuthenticatedUser, issue configissues.Issue) bool {
|
||||||
|
if issue.ActionID == "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return api.canViewActionID(user, issue.ActionID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *oliveTinAPI) canViewActionID(user *authpublic.AuthenticatedUser, actionID string) bool {
|
||||||
|
action := findActionInList(api.cfg.Actions, actionID)
|
||||||
|
if action == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return api.userCanViewAction(user, action)
|
||||||
|
}
|
||||||
|
|
||||||
|
func findActionInList(actions []*config.Action, actionID string) *config.Action {
|
||||||
|
for _, action := range actions {
|
||||||
|
if actionHasID(action, actionID) {
|
||||||
|
return action
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func actionHasID(action *config.Action, actionID string) bool {
|
||||||
|
return action != nil && action.ID == actionID
|
||||||
|
}
|
||||||
|
|
||||||
|
func toProtoConfigIssue(issue configissues.Issue) *apiv1.ConfigIssue {
|
||||||
|
return &apiv1.ConfigIssue{
|
||||||
|
Severity: issue.Severity,
|
||||||
|
Code: issue.Code,
|
||||||
|
Message: issue.Message,
|
||||||
|
ActionId: issue.ActionID,
|
||||||
|
ActionTitle: issue.ActionTitle,
|
||||||
|
ArgumentName: issue.ArgumentName,
|
||||||
|
Source: issue.Source,
|
||||||
|
ConfigFile: issue.ConfigFile,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -39,6 +39,8 @@ type Action struct {
|
||||||
EnabledExpression string `koanf:"enabledExpression"`
|
EnabledExpression string `koanf:"enabledExpression"`
|
||||||
Groups []string `koanf:"groups"`
|
Groups []string `koanf:"groups"`
|
||||||
Justification string `koanf:"justification"`
|
Justification string `koanf:"justification"`
|
||||||
|
// SourceFile is set by OliveTin when loading config (not user YAML).
|
||||||
|
SourceFile string `koanf:"x-olivetin-source-file"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (action *Action) RequiresJustification() bool {
|
func (action *Action) RequiresJustification() bool {
|
||||||
|
|
@ -110,6 +112,8 @@ type EntityFile struct {
|
||||||
Name string `koanf:"name"`
|
Name string `koanf:"name"`
|
||||||
Icon string `koanf:"icon"`
|
Icon string `koanf:"icon"`
|
||||||
Properties []EntityProperty `koanf:"properties"`
|
Properties []EntityProperty `koanf:"properties"`
|
||||||
|
// SourceFile is set by OliveTin when loading config (not user YAML).
|
||||||
|
SourceFile string `koanf:"x-olivetin-source-file"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// EntityProperty defines a column shown when listing entity instances in the UI.
|
// EntityProperty defines a column shown when listing entity instances in the UI.
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
|
@ -8,6 +9,7 @@ import (
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/OliveTin/OliveTin/internal/configissues"
|
||||||
"github.com/go-viper/mapstructure/v2"
|
"github.com/go-viper/mapstructure/v2"
|
||||||
"github.com/knadh/koanf/parsers/yaml"
|
"github.com/knadh/koanf/parsers/yaml"
|
||||||
"github.com/knadh/koanf/providers/file"
|
"github.com/knadh/koanf/providers/file"
|
||||||
|
|
@ -40,6 +42,8 @@ func AppendSource(cfg *Config, k *koanf.Koanf, configPath string) {
|
||||||
"configPath": configPath,
|
"configPath": configPath,
|
||||||
}).Info("Appending cfg source")
|
}).Info("Appending cfg source")
|
||||||
|
|
||||||
|
configissues.BeginConfigLoad()
|
||||||
|
stampLoadedConfigSources(k, configPath)
|
||||||
loadIncludedConfigsFromDir(k, configPath)
|
loadIncludedConfigsFromDir(k, configPath)
|
||||||
|
|
||||||
if !unmarshalRoot(k, cfg) {
|
if !unmarshalRoot(k, cfg) {
|
||||||
|
|
@ -172,7 +176,7 @@ func listYamlFiles(includePath string) ([]string, bool) {
|
||||||
func loadAndMergeIncludedFile(k *koanf.Koanf, includePath, filename string) {
|
func loadAndMergeIncludedFile(k *koanf.Koanf, includePath, filename string) {
|
||||||
filePath := filepath.Join(includePath, filename)
|
filePath := filepath.Join(includePath, filename)
|
||||||
|
|
||||||
if err := k.Load(file.Provider(filePath), yaml.Parser(), koanf.WithMergeFunc(mergeFunc)); err != nil {
|
if err := k.Load(file.Provider(filePath), yaml.Parser(), koanf.WithMergeFunc(mergeFuncForSource(filePath))); err != nil {
|
||||||
log.Errorf("Error loading included config file %s: %v", filePath, err)
|
log.Errorf("Error loading included config file %s: %v", filePath, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -182,8 +186,15 @@ func loadAndMergeIncludedFile(k *koanf.Koanf, includePath, filename string) {
|
||||||
}).Info("Successfully loaded included config file")
|
}).Info("Successfully loaded included config file")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func mergeFuncForSource(sourceFile string) func(src, dest map[string]interface{}) error {
|
||||||
|
return func(src map[string]interface{}, dest map[string]interface{}) error {
|
||||||
|
return mergeFunc(src, dest, sourceFile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// mergeActionsWhenBothExist merges actions when both src and dest have actions.
|
// mergeActionsWhenBothExist merges actions when both src and dest have actions.
|
||||||
func mergeActionsWhenBothExist(srcActions interface{}, destActions interface{}, dest map[string]interface{}) {
|
func mergeActionsWhenBothExist(srcActions interface{}, destActions interface{}, dest map[string]interface{}, sourceFile string) {
|
||||||
|
stampSourceOnMaps(srcActions, sourceFile)
|
||||||
srcSlice, ok1 := srcActions.([]interface{})
|
srcSlice, ok1 := srcActions.([]interface{})
|
||||||
destSlice, ok2 := destActions.([]interface{})
|
destSlice, ok2 := destActions.([]interface{})
|
||||||
if ok1 && ok2 {
|
if ok1 && ok2 {
|
||||||
|
|
@ -194,10 +205,11 @@ func mergeActionsWhenBothExist(srcActions interface{}, destActions interface{},
|
||||||
}
|
}
|
||||||
|
|
||||||
// mergeActionsFromSource merges actions from source into destination.
|
// mergeActionsFromSource merges actions from source into destination.
|
||||||
func mergeActionsFromSource(srcActions interface{}, dest map[string]interface{}) {
|
func mergeActionsFromSource(srcActions interface{}, dest map[string]interface{}, sourceFile string) {
|
||||||
if destActions, ok := dest["actions"]; ok {
|
if destActions, ok := dest["actions"]; ok {
|
||||||
mergeActionsWhenBothExist(srcActions, destActions, dest)
|
mergeActionsWhenBothExist(srcActions, destActions, dest, sourceFile)
|
||||||
} else {
|
} else {
|
||||||
|
stampSourceOnMaps(srcActions, sourceFile)
|
||||||
dest["actions"] = srcActions
|
dest["actions"] = srcActions
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -223,7 +235,8 @@ func mergeDashboardsFromSource(srcDashboards interface{}, dest map[string]interf
|
||||||
}
|
}
|
||||||
|
|
||||||
// mergeEntitiesWhenBothExist merges entities when both src and dest have entities.
|
// mergeEntitiesWhenBothExist merges entities when both src and dest have entities.
|
||||||
func mergeEntitiesWhenBothExist(srcEntities interface{}, destEntities interface{}, dest map[string]interface{}) {
|
func mergeEntitiesWhenBothExist(srcEntities interface{}, destEntities interface{}, dest map[string]interface{}, sourceFile string) {
|
||||||
|
stampSourceOnMaps(srcEntities, sourceFile)
|
||||||
srcSlice, ok1 := srcEntities.([]interface{})
|
srcSlice, ok1 := srcEntities.([]interface{})
|
||||||
destSlice, ok2 := destEntities.([]interface{})
|
destSlice, ok2 := destEntities.([]interface{})
|
||||||
if ok1 && ok2 {
|
if ok1 && ok2 {
|
||||||
|
|
@ -234,17 +247,18 @@ func mergeEntitiesWhenBothExist(srcEntities interface{}, destEntities interface{
|
||||||
}
|
}
|
||||||
|
|
||||||
// mergeEntitiesFromSource merges entities from source into destination.
|
// mergeEntitiesFromSource merges entities from source into destination.
|
||||||
func mergeEntitiesFromSource(srcEntities interface{}, dest map[string]interface{}) {
|
func mergeEntitiesFromSource(srcEntities interface{}, dest map[string]interface{}, sourceFile string) {
|
||||||
if destEntities, ok := dest["entities"]; ok {
|
if destEntities, ok := dest["entities"]; ok {
|
||||||
mergeEntitiesWhenBothExist(srcEntities, destEntities, dest)
|
mergeEntitiesWhenBothExist(srcEntities, destEntities, dest, sourceFile)
|
||||||
} else {
|
} else {
|
||||||
|
stampSourceOnMaps(srcEntities, sourceFile)
|
||||||
dest["entities"] = srcEntities
|
dest["entities"] = srcEntities
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func mergeFunc(src map[string]interface{}, dest map[string]interface{}) error {
|
func mergeFunc(src map[string]interface{}, dest map[string]interface{}, sourceFile string) error {
|
||||||
if srcActions, ok := src["actions"]; ok {
|
if srcActions, ok := src["actions"]; ok {
|
||||||
mergeActionsFromSource(srcActions, dest)
|
mergeActionsFromSource(srcActions, dest, sourceFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
if srcDashboards, ok := src["dashboards"]; ok {
|
if srcDashboards, ok := src["dashboards"]; ok {
|
||||||
|
|
@ -252,7 +266,7 @@ func mergeFunc(src map[string]interface{}, dest map[string]interface{}) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if srcEntities, ok := src["entities"]; ok {
|
if srcEntities, ok := src["entities"]; ok {
|
||||||
mergeEntitiesFromSource(srcEntities, dest)
|
mergeEntitiesFromSource(srcEntities, dest, sourceFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -285,7 +299,12 @@ func envDecodeHookFunc(from reflect.Type, to reflect.Type, data any) (any, error
|
||||||
val, set := os.LookupEnv(key)
|
val, set := os.LookupEnv(key)
|
||||||
log.Debugf("Environment variable %q: set=%v, value=%q", key, set, val)
|
log.Debugf("Environment variable %q: set=%v, value=%q", key, set, val)
|
||||||
if !set {
|
if !set {
|
||||||
log.Warnf("Config file references unset environment variable: \"%s\"", key)
|
configissues.ReportSticky(configissues.Issue{
|
||||||
|
Severity: configissues.SeverityWarning,
|
||||||
|
Code: configissues.CodeEnvUnset,
|
||||||
|
Message: fmt.Sprintf("Config file references unset environment variable %q", key),
|
||||||
|
Source: key,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
return val
|
return val
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"text/template"
|
"text/template"
|
||||||
|
|
||||||
|
"github.com/OliveTin/OliveTin/internal/configissues"
|
||||||
"github.com/OliveTin/OliveTin/internal/env"
|
"github.com/OliveTin/OliveTin/internal/env"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
@ -29,7 +30,6 @@ func (cfg *Config) Sanitize() {
|
||||||
cfg.sanitizeDashboardsForInlineActions()
|
cfg.sanitizeDashboardsForInlineActions()
|
||||||
|
|
||||||
cfg.sanitizeActionGroups()
|
cfg.sanitizeActionGroups()
|
||||||
cfg.sanitizeActionGroupReferences()
|
|
||||||
cfg.sanitizeEntities()
|
cfg.sanitizeEntities()
|
||||||
|
|
||||||
if err := cfg.validateReservedActionArgumentNames(); err != nil {
|
if err := cfg.validateReservedActionArgumentNames(); err != nil {
|
||||||
|
|
@ -238,7 +238,7 @@ func (action *Action) sanitize(cfg *Config) {
|
||||||
action.Groups = dedupeStrings(action.Groups)
|
action.Groups = dedupeStrings(action.Groups)
|
||||||
|
|
||||||
for idx := range action.Arguments {
|
for idx := range action.Arguments {
|
||||||
action.Arguments[idx].sanitize()
|
action.Arguments[idx].sanitize(action)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -283,14 +283,6 @@ func (cfg *Config) sanitizeActionGroups() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cfg *Config) sanitizeActionGroupReferences() {
|
|
||||||
for _, action := range cfg.Actions {
|
|
||||||
for _, groupName := range action.Groups {
|
|
||||||
cfg.warnInvalidActionGroupReference(action, groupName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cfg *Config) sanitizeEntities() {
|
func (cfg *Config) sanitizeEntities() {
|
||||||
for _, entityFile := range cfg.Entities {
|
for _, entityFile := range cfg.Entities {
|
||||||
if entityFile == nil {
|
if entityFile == nil {
|
||||||
|
|
@ -310,24 +302,6 @@ func sanitizeEntityProperties(entityFile *EntityFile) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cfg *Config) warnInvalidActionGroupReference(action *Action, groupName string) {
|
|
||||||
group, found := cfg.ActionGroups[groupName]
|
|
||||||
if !found {
|
|
||||||
log.WithFields(log.Fields{
|
|
||||||
"actionTitle": action.Title,
|
|
||||||
"groupName": groupName,
|
|
||||||
}).Warn("Action references unknown action group")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if group == nil || group.MaxConcurrent < 1 {
|
|
||||||
log.WithFields(log.Fields{
|
|
||||||
"actionTitle": action.Title,
|
|
||||||
"groupName": groupName,
|
|
||||||
}).Warn("Action references action group that will not be enforced at runtime")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cfg *Config) sanitizeAuthRequireGuestsToLogin() {
|
func (cfg *Config) sanitizeAuthRequireGuestsToLogin() {
|
||||||
if cfg.AuthRequireGuestsToLogin {
|
if cfg.AuthRequireGuestsToLogin {
|
||||||
log.Infof("AuthRequireGuestsToLogin is enabled. All defaultPermissions will be set to false")
|
log.Infof("AuthRequireGuestsToLogin is enabled. All defaultPermissions will be set to false")
|
||||||
|
|
@ -505,7 +479,7 @@ func (cfg *Config) sanitizeOnClickDefaults() {
|
||||||
cfg.DefaultPopupOnStart = cfg.DefaultOnClick
|
cfg.DefaultPopupOnStart = cfg.DefaultOnClick
|
||||||
}
|
}
|
||||||
|
|
||||||
func (arg *ActionArgument) sanitize() {
|
func (arg *ActionArgument) sanitize(action *Action) {
|
||||||
if arg.Title == "" {
|
if arg.Title == "" {
|
||||||
arg.Title = arg.Name
|
arg.Title = arg.Name
|
||||||
}
|
}
|
||||||
|
|
@ -516,45 +490,24 @@ func (arg *ActionArgument) sanitize() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
arg.sanitizeNoType()
|
arg.sanitizeNoType(action)
|
||||||
arg.sanitizeChecklist()
|
// Checklist configuration problems are reported by configcheck.
|
||||||
|
|
||||||
// Default value validation runs in executor at config load (validateArgumentDefaults).
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (arg *ActionArgument) sanitizeChecklist() {
|
func (arg *ActionArgument) sanitizeNoType(action *Action) {
|
||||||
if arg.Type != "checklist" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
arg.warnMissingChecklistChoices()
|
|
||||||
arg.warnInvalidChecklistEntityTemplate()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (arg *ActionArgument) warnMissingChecklistChoices() {
|
|
||||||
if len(arg.Choices) == 0 {
|
|
||||||
log.WithFields(log.Fields{
|
|
||||||
"arg": arg.Name,
|
|
||||||
}).Warn("Checklist argument has no choices defined")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (arg *ActionArgument) warnInvalidChecklistEntityTemplate() {
|
|
||||||
if arg.Entity == "" || len(arg.Choices) == 1 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.WithFields(log.Fields{
|
|
||||||
"arg": arg.Name,
|
|
||||||
"entity": arg.Entity,
|
|
||||||
}).Warn("Checklist argument with entity should define exactly one choice template")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (arg *ActionArgument) sanitizeNoType() {
|
|
||||||
if len(arg.Choices) == 0 && arg.Type == "" {
|
if len(arg.Choices) == 0 && arg.Type == "" {
|
||||||
log.WithFields(log.Fields{
|
issue := configissues.Issue{
|
||||||
"arg": arg.Name,
|
Severity: configissues.SeverityWarning,
|
||||||
}).Warn("Argument type isn't set, will default to 'ascii' but this may not be safe. You should set a type specifically.")
|
Code: configissues.CodeArgTypeUnset,
|
||||||
|
Message: "Argument type isn't set, will default to 'ascii' but this may not be safe",
|
||||||
|
ArgumentName: arg.Name,
|
||||||
|
}
|
||||||
|
if action != nil {
|
||||||
|
issue.ActionID = action.ID
|
||||||
|
issue.ActionTitle = action.Title
|
||||||
|
issue.ConfigFile = action.SourceFile
|
||||||
|
}
|
||||||
|
configissues.ReportSticky(issue)
|
||||||
arg.Type = "ascii"
|
arg.Type = "ascii"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/knadh/koanf/v2"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
const sourceFileKey = "x-olivetin-source-file"
|
||||||
|
|
||||||
|
// stampSourceOnMaps sets the OliveTin source-file marker on each map in a
|
||||||
|
// koanf slice value (actions or entities).
|
||||||
|
func stampSourceOnMaps(raw any, sourceFile string) any {
|
||||||
|
if sourceFile == "" {
|
||||||
|
return raw
|
||||||
|
}
|
||||||
|
|
||||||
|
items, ok := raw.([]interface{})
|
||||||
|
if !ok {
|
||||||
|
return raw
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, item := range items {
|
||||||
|
stampSourceOnMap(item, sourceFile)
|
||||||
|
}
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
|
func stampSourceOnMap(item any, sourceFile string) {
|
||||||
|
m, ok := item.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if existing, ok := m[sourceFileKey].(string); ok && existing != "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m[sourceFileKey] = sourceFile
|
||||||
|
}
|
||||||
|
|
||||||
|
func stampLoadedConfigSources(k *koanf.Koanf, configPath string) {
|
||||||
|
stampConfigKey(k, "actions", configPath)
|
||||||
|
stampConfigKey(k, "entities", configPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func stampConfigKey(k *koanf.Koanf, key, configPath string) {
|
||||||
|
if err := k.Set(key, stampSourceOnMaps(k.Get(key), configPath)); err != nil {
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"key": key,
|
||||||
|
"configPath": configPath,
|
||||||
|
}).Errorf("Failed to persist source stamps: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
package config_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/OliveTin/OliveTin/internal/config"
|
||||||
|
"github.com/knadh/koanf/parsers/yaml"
|
||||||
|
"github.com/knadh/koanf/providers/file"
|
||||||
|
"github.com/knadh/koanf/v2"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAppendSourceStampsActionSourceFiles(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
includeDir := filepath.Join(dir, "config.d")
|
||||||
|
require.NoError(t, os.Mkdir(includeDir, 0o755))
|
||||||
|
|
||||||
|
basePath := filepath.Join(dir, "config.yaml")
|
||||||
|
require.NoError(t, os.WriteFile(basePath, []byte(`
|
||||||
|
include: config.d
|
||||||
|
actions:
|
||||||
|
- title: From base
|
||||||
|
shell: echo base
|
||||||
|
`), 0o644))
|
||||||
|
|
||||||
|
includePath := filepath.Join(includeDir, "10-extra.yaml")
|
||||||
|
require.NoError(t, os.WriteFile(includePath, []byte(`
|
||||||
|
actions:
|
||||||
|
- title: From include
|
||||||
|
shell: echo include
|
||||||
|
entities:
|
||||||
|
- name: host
|
||||||
|
file: hosts.yaml
|
||||||
|
`), 0o644))
|
||||||
|
|
||||||
|
k := koanf.New(".")
|
||||||
|
require.NoError(t, k.Load(file.Provider(basePath), yaml.Parser()))
|
||||||
|
|
||||||
|
cfg := config.DefaultConfig()
|
||||||
|
config.AppendSource(cfg, k, basePath)
|
||||||
|
|
||||||
|
require.Len(t, cfg.Actions, 2)
|
||||||
|
assert.Equal(t, basePath, cfg.Actions[0].SourceFile)
|
||||||
|
assert.Equal(t, includePath, cfg.Actions[1].SourceFile)
|
||||||
|
|
||||||
|
require.Len(t, cfg.Entities, 1)
|
||||||
|
assert.Equal(t, includePath, cfg.Entities[0].SourceFile)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,486 @@
|
||||||
|
package configcheck
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/OliveTin/OliveTin/internal/config"
|
||||||
|
"github.com/OliveTin/OliveTin/internal/configissues"
|
||||||
|
"github.com/OliveTin/OliveTin/internal/entities"
|
||||||
|
"github.com/OliveTin/OliveTin/internal/tpl"
|
||||||
|
"github.com/robfig/cron/v3"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Rebuild clears and rebuilds the issue list from the current config, entity
|
||||||
|
// state, sticky load-time reports, and any extra issues provided by callers
|
||||||
|
// (for example literal argument default validation from the executor).
|
||||||
|
func Rebuild(cfg *config.Config, extra ...configissues.Issue) {
|
||||||
|
if cfg == nil {
|
||||||
|
configissues.Replace(extra)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
collected := make([]configissues.Issue, 0)
|
||||||
|
collected = append(collected, configissues.CopySticky()...)
|
||||||
|
collected = append(collected, collectActionGroupIssues(cfg)...)
|
||||||
|
collected = append(collected, collectArgumentIssues(cfg)...)
|
||||||
|
collected = append(collected, collectIncludeIssues(cfg)...)
|
||||||
|
collected = append(collected, collectTemplateParseIssues(cfg)...)
|
||||||
|
collected = append(collected, collectEntityFileIssues(cfg)...)
|
||||||
|
collected = append(collected, collectEntityEmptyIssues(cfg)...)
|
||||||
|
collected = append(collected, collectCronIssues(cfg)...)
|
||||||
|
collected = append(collected, collectWatcherPathIssues(cfg)...)
|
||||||
|
collected = append(collected, extra...)
|
||||||
|
configissues.Replace(collected)
|
||||||
|
}
|
||||||
|
|
||||||
|
func collectActionGroupIssues(cfg *config.Config) []configissues.Issue {
|
||||||
|
out := make([]configissues.Issue, 0)
|
||||||
|
for _, action := range cfg.Actions {
|
||||||
|
if action == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, groupName := range action.Groups {
|
||||||
|
out = append(out, actionGroupIssue(cfg, action, groupName)...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func actionGroupIssue(cfg *config.Config, action *config.Action, groupName string) []configissues.Issue {
|
||||||
|
group, found := cfg.ActionGroups[groupName]
|
||||||
|
if !found {
|
||||||
|
return []configissues.Issue{actionIssue(action, configissues.SeverityWarning, configissues.CodeActionGroupUnknown,
|
||||||
|
fmt.Sprintf("Action references unknown action group %q", groupName), groupName, "")}
|
||||||
|
}
|
||||||
|
|
||||||
|
if group == nil || group.MaxConcurrent < 1 {
|
||||||
|
return []configissues.Issue{actionIssue(action, configissues.SeverityWarning, configissues.CodeActionGroupUnenforced,
|
||||||
|
fmt.Sprintf("Action references action group %q that will not be enforced at runtime", groupName), groupName, "")}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func actionIssue(action *config.Action, severity, code, message, source, argName string) configissues.Issue {
|
||||||
|
return configissues.Issue{
|
||||||
|
Severity: severity,
|
||||||
|
Code: code,
|
||||||
|
Message: message,
|
||||||
|
ActionID: action.ID,
|
||||||
|
ActionTitle: action.Title,
|
||||||
|
ArgumentName: argName,
|
||||||
|
Source: source,
|
||||||
|
ConfigFile: action.SourceFile,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func collectArgumentIssues(cfg *config.Config) []configissues.Issue {
|
||||||
|
out := make([]configissues.Issue, 0)
|
||||||
|
for _, action := range cfg.Actions {
|
||||||
|
if action == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for i := range action.Arguments {
|
||||||
|
out = append(out, argumentIssues(action, &action.Arguments[i])...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func argumentIssues(action *config.Action, arg *config.ActionArgument) []configissues.Issue {
|
||||||
|
if arg.Type != "checklist" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
out := make([]configissues.Issue, 0)
|
||||||
|
out = append(out, checklistNoChoicesIssue(action, arg)...)
|
||||||
|
out = append(out, checklistEntityChoicesIssue(action, arg)...)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func checklistNoChoicesIssue(action *config.Action, arg *config.ActionArgument) []configissues.Issue {
|
||||||
|
if len(arg.Choices) > 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return []configissues.Issue{actionIssue(action, configissues.SeverityWarning, configissues.CodeChecklistNoChoices,
|
||||||
|
"Checklist argument has no choices defined", "", arg.Name)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func checklistEntityChoicesIssue(action *config.Action, arg *config.ActionArgument) []configissues.Issue {
|
||||||
|
if arg.Entity == "" || len(arg.Choices) <= 1 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return []configissues.Issue{actionIssue(action, configissues.SeverityWarning, configissues.CodeChecklistEntityChoices,
|
||||||
|
"Checklist argument with entity should define exactly one choice template", arg.Entity, arg.Name)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func collectIncludeIssues(cfg *config.Config) []configissues.Issue {
|
||||||
|
if cfg.Include == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
includePath := cfg.Include
|
||||||
|
if !filepath.IsAbs(includePath) {
|
||||||
|
includePath = filepath.Join(cfg.GetDir(), cfg.Include)
|
||||||
|
}
|
||||||
|
info, err := os.Stat(includePath)
|
||||||
|
if err != nil {
|
||||||
|
return []configissues.Issue{{
|
||||||
|
Severity: configissues.SeverityWarning,
|
||||||
|
Code: configissues.CodeIncludeMissing,
|
||||||
|
Message: fmt.Sprintf("Include directory not found: %s", includePath),
|
||||||
|
Source: includePath,
|
||||||
|
ConfigFile: includePath,
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !info.IsDir() {
|
||||||
|
return []configissues.Issue{{
|
||||||
|
Severity: configissues.SeverityWarning,
|
||||||
|
Code: configissues.CodeIncludeMissing,
|
||||||
|
Message: fmt.Sprintf("Include path is not a directory: %s", includePath),
|
||||||
|
Source: includePath,
|
||||||
|
ConfigFile: includePath,
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func collectTemplateParseIssues(cfg *config.Config) []configissues.Issue {
|
||||||
|
out := make([]configissues.Issue, 0)
|
||||||
|
for _, action := range cfg.Actions {
|
||||||
|
if action == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for i := range action.Arguments {
|
||||||
|
arg := &action.Arguments[i]
|
||||||
|
out = append(out, templateIssuesForArg(action, arg)...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func templateIssuesForArg(action *config.Action, arg *config.ActionArgument) []configissues.Issue {
|
||||||
|
out := make([]configissues.Issue, 0)
|
||||||
|
|
||||||
|
if looksLikeTemplate(arg.Default) {
|
||||||
|
if err := tpl.CheckActionTemplateParses(arg.Default); err != nil {
|
||||||
|
out = append(out, actionIssue(action, configissues.SeverityError, configissues.CodeTemplateParse,
|
||||||
|
fmt.Sprintf("Argument default template failed to parse: %v", err), arg.Default, arg.Name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, choice := range arg.Choices {
|
||||||
|
out = append(out, templateIssueForChoice(action, arg, choice.Title, "title")...)
|
||||||
|
out = append(out, templateIssueForChoice(action, arg, choice.Value, "value")...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func templateIssueForChoice(action *config.Action, arg *config.ActionArgument, value, field string) []configissues.Issue {
|
||||||
|
if !looksLikeTemplate(value) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tpl.CheckActionTemplateParses(value); err != nil {
|
||||||
|
return []configissues.Issue{actionIssue(action, configissues.SeverityError, configissues.CodeTemplateParse,
|
||||||
|
fmt.Sprintf("Argument choice %s template failed to parse: %v", field, err), value, arg.Name)}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func looksLikeTemplate(value string) bool {
|
||||||
|
return strings.Contains(value, "{{")
|
||||||
|
}
|
||||||
|
|
||||||
|
func collectEntityFileIssues(cfg *config.Config) []configissues.Issue {
|
||||||
|
out := make([]configissues.Issue, 0)
|
||||||
|
baseDir := entities.ResolveEntitiesBaseDir(cfg.GetDir())
|
||||||
|
|
||||||
|
for _, ef := range cfg.Entities {
|
||||||
|
out = append(out, entityFileIssuesFor(ef, baseDir)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func entityFileIssuesFor(ef *config.EntityFile, baseDir string) []configissues.Issue {
|
||||||
|
if ef == nil || ef.File == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
path := resolveEntityPath(ef.File, baseDir)
|
||||||
|
issue := entityFileIssue(ef, path)
|
||||||
|
if issue == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return []configissues.Issue{*issue}
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveEntityPath(file, baseDir string) string {
|
||||||
|
if filepath.IsAbs(file) {
|
||||||
|
return file
|
||||||
|
}
|
||||||
|
return filepath.Join(baseDir, file)
|
||||||
|
}
|
||||||
|
|
||||||
|
func entityFileIssue(ef *config.EntityFile, path string) *configissues.Issue {
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return entityReadIssue(ef, path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(strings.TrimSpace(string(data))) == 0 {
|
||||||
|
return &configissues.Issue{
|
||||||
|
Severity: configissues.SeverityWarning,
|
||||||
|
Code: configissues.CodeEntityFile,
|
||||||
|
Message: fmt.Sprintf("Entity %q file is empty", ef.Name),
|
||||||
|
Source: path,
|
||||||
|
ConfigFile: ef.SourceFile,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasSuffix(path, ".json") {
|
||||||
|
return entityJSONParseIssue(ef, path, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
return entityYAMLParseIssue(ef, path, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func entityReadIssue(ef *config.EntityFile, path string, err error) *configissues.Issue {
|
||||||
|
return &configissues.Issue{
|
||||||
|
Severity: configissues.SeverityError,
|
||||||
|
Code: configissues.CodeEntityFile,
|
||||||
|
Message: fmt.Sprintf("Entity %q file could not be read: %v", ef.Name, err),
|
||||||
|
Source: path,
|
||||||
|
ConfigFile: ef.SourceFile,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func entityJSONParseIssue(ef *config.EntityFile, path string, data []byte) *configissues.Issue {
|
||||||
|
decoder := json.NewDecoder(bytes.NewReader(data))
|
||||||
|
for decoder.More() {
|
||||||
|
d := make(map[string]any)
|
||||||
|
if err := decoder.Decode(&d); err != nil {
|
||||||
|
return &configissues.Issue{
|
||||||
|
Severity: configissues.SeverityError,
|
||||||
|
Code: configissues.CodeEntityFile,
|
||||||
|
Message: fmt.Sprintf("Entity %q file could not be parsed as JSON: %v", ef.Name, err),
|
||||||
|
Source: path,
|
||||||
|
ConfigFile: ef.SourceFile,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func entityYAMLParseIssue(ef *config.EntityFile, path string, data []byte) *configissues.Issue {
|
||||||
|
var parsed []map[string]any
|
||||||
|
if err := yaml.Unmarshal(data, &parsed); err != nil {
|
||||||
|
return &configissues.Issue{
|
||||||
|
Severity: configissues.SeverityError,
|
||||||
|
Code: configissues.CodeEntityFile,
|
||||||
|
Message: fmt.Sprintf("Entity %q file could not be parsed as YAML: %v", ef.Name, err),
|
||||||
|
Source: path,
|
||||||
|
ConfigFile: ef.SourceFile,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func collectEntityEmptyIssues(cfg *config.Config) []configissues.Issue {
|
||||||
|
out := make([]configissues.Issue, 0)
|
||||||
|
for _, action := range cfg.Actions {
|
||||||
|
out = append(out, entityEmptyIssueFor(cfg, action)...)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func entityEmptyIssueFor(cfg *config.Config, action *config.Action) []configissues.Issue {
|
||||||
|
if !shouldReportEntityEmpty(cfg, action) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return []configissues.Issue{actionIssue(action, configissues.SeverityWarning, configissues.CodeEntityEmpty,
|
||||||
|
fmt.Sprintf("Entity-bound action has no instances of entity %q", action.Entity), action.Entity, "")}
|
||||||
|
}
|
||||||
|
|
||||||
|
func shouldReportEntityEmpty(cfg *config.Config, action *config.Action) bool {
|
||||||
|
if action == nil || action.Entity == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if len(entities.GetEntityInstances(action.Entity)) > 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return entityEmptyLoadGatePassed(cfg, action.Entity)
|
||||||
|
}
|
||||||
|
|
||||||
|
func entityEmptyLoadGatePassed(cfg *config.Config, entityName string) bool {
|
||||||
|
// Avoid startup false positives: entity files load after the first action-map rebuild.
|
||||||
|
if entityTypeConfigured(cfg, entityName) && !entities.HasEntityLoadAttempted(entityName) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func entityTypeConfigured(cfg *config.Config, entityName string) bool {
|
||||||
|
for _, ef := range cfg.Entities {
|
||||||
|
if ef != nil && ef.Name == entityName {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func collectCronIssues(cfg *config.Config) []configissues.Issue {
|
||||||
|
out := make([]configissues.Issue, 0)
|
||||||
|
parser := cronParser(cfg)
|
||||||
|
|
||||||
|
for _, action := range cfg.Actions {
|
||||||
|
out = append(out, cronIssuesForAction(action, parser)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func cronIssuesForAction(action *config.Action, parser cron.Parser) []configissues.Issue {
|
||||||
|
if action == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
out := make([]configissues.Issue, 0)
|
||||||
|
out = append(out, cronEntityBindingIssue(action)...)
|
||||||
|
out = append(out, cronScheduleIssues(action, parser)...)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func cronEntityBindingIssue(action *config.Action) []configissues.Issue {
|
||||||
|
if action.Entity == "" || len(action.ExecOnCron) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return []configissues.Issue{actionIssue(action, configissues.SeverityWarning, configissues.CodeCronEntityBinding,
|
||||||
|
"Entity-bound action uses execOnCron; cron runs without an entity binding and will not execute", action.Entity, "")}
|
||||||
|
}
|
||||||
|
|
||||||
|
func cronScheduleIssues(action *config.Action, parser cron.Parser) []configissues.Issue {
|
||||||
|
out := make([]configissues.Issue, 0)
|
||||||
|
for _, cronline := range action.ExecOnCron {
|
||||||
|
out = append(out, cronScheduleIssue(action, parser, cronline)...)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func cronScheduleIssue(action *config.Action, parser cron.Parser, cronline string) []configissues.Issue {
|
||||||
|
_, err := parser.Parse(cronline)
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return []configissues.Issue{actionIssue(action, configissues.SeverityError, configissues.CodeCronInvalid,
|
||||||
|
fmt.Sprintf("Invalid cron schedule %q: %v", cronline, err), cronline, "")}
|
||||||
|
}
|
||||||
|
|
||||||
|
func cronParser(cfg *config.Config) cron.Parser {
|
||||||
|
if cfg.CronSupportForSeconds {
|
||||||
|
return cron.NewParser(cron.Second | cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor)
|
||||||
|
}
|
||||||
|
return cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor)
|
||||||
|
}
|
||||||
|
|
||||||
|
func collectWatcherPathIssues(cfg *config.Config) []configissues.Issue {
|
||||||
|
out := make([]configissues.Issue, 0)
|
||||||
|
for _, action := range cfg.Actions {
|
||||||
|
out = append(out, watcherPathIssuesForAction(action)...)
|
||||||
|
}
|
||||||
|
out = append(out, watcherPathIssuesForEntities(cfg)...)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func watcherPathIssuesForAction(action *config.Action) []configissues.Issue {
|
||||||
|
if action == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
out := make([]configissues.Issue, 0)
|
||||||
|
out = append(out, watcherCreatedDirIssues(action)...)
|
||||||
|
out = append(out, watcherChangedDirIssues(action)...)
|
||||||
|
out = append(out, watcherCalendarFileIssues(action)...)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func watcherCreatedDirIssues(action *config.Action) []configissues.Issue {
|
||||||
|
out := make([]configissues.Issue, 0)
|
||||||
|
for _, dir := range action.ExecOnFileCreatedInDir {
|
||||||
|
out = append(out, watcherDirIssue(action, dir)...)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func watcherChangedDirIssues(action *config.Action) []configissues.Issue {
|
||||||
|
out := make([]configissues.Issue, 0)
|
||||||
|
for _, dir := range action.ExecOnFileChangedInDir {
|
||||||
|
out = append(out, watcherDirIssue(action, dir)...)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func watcherCalendarFileIssues(action *config.Action) []configissues.Issue {
|
||||||
|
return watcherFileIssue(action, action.ExecOnCalendarFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
func watcherPathIssuesForEntities(cfg *config.Config) []configissues.Issue {
|
||||||
|
out := make([]configissues.Issue, 0)
|
||||||
|
baseDir := entities.ResolveEntitiesBaseDir(cfg.GetDir())
|
||||||
|
|
||||||
|
for _, ef := range cfg.Entities {
|
||||||
|
if ef == nil || ef.File == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
path := resolveEntityPath(ef.File, baseDir)
|
||||||
|
out = append(out, missingWatchDirIssue(path, filepath.Dir(path), ef.SourceFile, "", "")...)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func watcherDirIssue(action *config.Action, dir string) []configissues.Issue {
|
||||||
|
if dir == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return missingWatchDirIssue(dir, dir, action.SourceFile, action.ID, action.Title)
|
||||||
|
}
|
||||||
|
|
||||||
|
func watcherFileIssue(action *config.Action, filePath string) []configissues.Issue {
|
||||||
|
if filePath == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return missingWatchDirIssue(filePath, filepath.Dir(filePath), action.SourceFile, action.ID, action.Title)
|
||||||
|
}
|
||||||
|
|
||||||
|
func missingWatchDirIssue(displayPath, dirToStat, configFile, actionID, actionTitle string) []configissues.Issue {
|
||||||
|
_, err := os.Stat(dirToStat)
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return []configissues.Issue{{
|
||||||
|
Severity: configissues.SeverityError,
|
||||||
|
Code: configissues.CodeWatcherPath,
|
||||||
|
Message: fmt.Sprintf("Could not create watcher for %q: %v", displayPath, err),
|
||||||
|
ActionID: actionID,
|
||||||
|
ActionTitle: actionTitle,
|
||||||
|
Source: displayPath,
|
||||||
|
ConfigFile: configFile,
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,201 @@
|
||||||
|
package configcheck_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/OliveTin/OliveTin/internal/config"
|
||||||
|
"github.com/OliveTin/OliveTin/internal/configcheck"
|
||||||
|
"github.com/OliveTin/OliveTin/internal/configissues"
|
||||||
|
"github.com/OliveTin/OliveTin/internal/entities"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRebuildTemplateParseDefault(t *testing.T) {
|
||||||
|
configissues.BeginConfigLoad()
|
||||||
|
cfg := config.DefaultConfig()
|
||||||
|
cfg.Actions = []*config.Action{
|
||||||
|
{
|
||||||
|
Title: "Deploy",
|
||||||
|
ID: "deploy",
|
||||||
|
Entity: "host",
|
||||||
|
Arguments: []config.ActionArgument{
|
||||||
|
{
|
||||||
|
Name: "ansible_host",
|
||||||
|
Type: "ascii_identifier",
|
||||||
|
Default: `{{ host.name | default "All" }}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
configcheck.Rebuild(cfg)
|
||||||
|
|
||||||
|
issues := configissues.List()
|
||||||
|
require.NotEmpty(t, issues)
|
||||||
|
assert.True(t, hasCode(issues, configissues.CodeTemplateParse))
|
||||||
|
assert.True(t, hasCode(issues, configissues.CodeEntityEmpty))
|
||||||
|
assert.False(t, hasCode(issues, configissues.CodeCronEntityBinding))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRebuildValidTemplateDefault(t *testing.T) {
|
||||||
|
configissues.BeginConfigLoad()
|
||||||
|
cfg := config.DefaultConfig()
|
||||||
|
cfg.Actions = []*config.Action{
|
||||||
|
{
|
||||||
|
Title: "Sleep",
|
||||||
|
ID: "sleep",
|
||||||
|
Arguments: []config.ActionArgument{
|
||||||
|
{
|
||||||
|
Name: "ssh_host",
|
||||||
|
Type: "ascii_identifier",
|
||||||
|
Default: `{{ host.hostname }}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
configcheck.Rebuild(cfg)
|
||||||
|
|
||||||
|
assert.False(t, hasCode(configissues.List(), configissues.CodeTemplateParse))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRebuildCronEntityBinding(t *testing.T) {
|
||||||
|
configissues.BeginConfigLoad()
|
||||||
|
cfg := config.DefaultConfig()
|
||||||
|
cfg.Actions = []*config.Action{
|
||||||
|
{
|
||||||
|
Title: "Nightly",
|
||||||
|
ID: "nightly",
|
||||||
|
Entity: "host",
|
||||||
|
ExecOnCron: []string{"0 1 * * *"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
configcheck.Rebuild(cfg)
|
||||||
|
|
||||||
|
assert.True(t, hasCode(configissues.List(), configissues.CodeCronEntityBinding))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRebuildInvalidCron(t *testing.T) {
|
||||||
|
configissues.BeginConfigLoad()
|
||||||
|
cfg := config.DefaultConfig()
|
||||||
|
cfg.Actions = []*config.Action{
|
||||||
|
{
|
||||||
|
Title: "Broken cron",
|
||||||
|
ID: "broken",
|
||||||
|
ExecOnCron: []string{"not a cron line"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
configcheck.Rebuild(cfg)
|
||||||
|
|
||||||
|
assert.True(t, hasCode(configissues.List(), configissues.CodeCronInvalid))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRebuildMissingEntityFile(t *testing.T) {
|
||||||
|
configissues.BeginConfigLoad()
|
||||||
|
cfg := config.DefaultConfig()
|
||||||
|
dir := t.TempDir()
|
||||||
|
cfg.SetDir(dir)
|
||||||
|
cfg.Entities = []*config.EntityFile{
|
||||||
|
{Name: "host", File: "missing-hosts.yaml"},
|
||||||
|
}
|
||||||
|
|
||||||
|
configcheck.Rebuild(cfg)
|
||||||
|
|
||||||
|
assert.True(t, hasCode(configissues.List(), configissues.CodeEntityFile))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRebuildEntityFileLoadsAndClearsEmpty(t *testing.T) {
|
||||||
|
configissues.BeginConfigLoad()
|
||||||
|
cfg := config.DefaultConfig()
|
||||||
|
dir := t.TempDir()
|
||||||
|
cfg.SetDir(dir)
|
||||||
|
|
||||||
|
entityPath := filepath.Join(dir, "hosts.yaml")
|
||||||
|
require.NoError(t, os.WriteFile(entityPath, []byte("- name: cradle\n"), 0o644))
|
||||||
|
|
||||||
|
cfg.Entities = []*config.EntityFile{
|
||||||
|
{Name: "host", File: "hosts.yaml"},
|
||||||
|
}
|
||||||
|
cfg.Actions = []*config.Action{
|
||||||
|
{Title: "Wake", ID: "wake", Entity: "host"},
|
||||||
|
}
|
||||||
|
|
||||||
|
entities.AddEntity("host", "0", map[string]any{"name": "cradle"})
|
||||||
|
t.Cleanup(func() { entities.ClearEntitiesOfType("host") })
|
||||||
|
|
||||||
|
configcheck.Rebuild(cfg)
|
||||||
|
|
||||||
|
issues := configissues.List()
|
||||||
|
assert.False(t, hasCode(issues, configissues.CodeEntityFile))
|
||||||
|
assert.False(t, hasCode(issues, configissues.CodeEntityEmpty))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRebuildIncludesActionConfigFile(t *testing.T) {
|
||||||
|
configissues.BeginConfigLoad()
|
||||||
|
cfg := config.DefaultConfig()
|
||||||
|
cfg.Actions = []*config.Action{
|
||||||
|
{
|
||||||
|
Title: "Nightly",
|
||||||
|
ID: "nightly",
|
||||||
|
Entity: "host",
|
||||||
|
SourceFile: "/etc/OliveTin/config.d/16-gitops-sr-soe.yaml",
|
||||||
|
ExecOnCron: []string{"0 1 * * *"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
configcheck.Rebuild(cfg)
|
||||||
|
|
||||||
|
var found bool
|
||||||
|
for _, issue := range configissues.List() {
|
||||||
|
if issue.Code == configissues.CodeCronEntityBinding {
|
||||||
|
assert.Equal(t, "/etc/OliveTin/config.d/16-gitops-sr-soe.yaml", issue.ConfigFile)
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert.True(t, found)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRebuildSkipsEntityEmptyBeforeLoadAttempt(t *testing.T) {
|
||||||
|
entities.ResetEntityLoadAttempts()
|
||||||
|
configissues.BeginConfigLoad()
|
||||||
|
cfg := config.DefaultConfig()
|
||||||
|
cfg.Entities = []*config.EntityFile{{Name: "container", File: "containers.json"}}
|
||||||
|
cfg.Actions = []*config.Action{
|
||||||
|
{Title: "Start container", ID: "start", Entity: "container"},
|
||||||
|
}
|
||||||
|
|
||||||
|
configcheck.Rebuild(cfg)
|
||||||
|
assert.False(t, hasCode(configissues.List(), configissues.CodeEntityEmpty))
|
||||||
|
|
||||||
|
entities.MarkEntityLoadAttempted("container")
|
||||||
|
configcheck.Rebuild(cfg)
|
||||||
|
assert.True(t, hasCode(configissues.List(), configissues.CodeEntityEmpty))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStickyEnvUnsetSurvivesRebuild(t *testing.T) {
|
||||||
|
configissues.BeginConfigLoad()
|
||||||
|
configissues.ReportSticky(configissues.Issue{
|
||||||
|
Severity: configissues.SeverityWarning,
|
||||||
|
Code: configissues.CodeEnvUnset,
|
||||||
|
Message: "unset",
|
||||||
|
Source: "MISSING_VAR",
|
||||||
|
})
|
||||||
|
|
||||||
|
configcheck.Rebuild(config.DefaultConfig())
|
||||||
|
|
||||||
|
assert.True(t, hasCode(configissues.List(), configissues.CodeEnvUnset))
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasCode(issues []configissues.Issue, code string) bool {
|
||||||
|
for _, issue := range issues {
|
||||||
|
if issue.Code == code {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
package configcheck_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/OliveTin/OliveTin/internal/config"
|
||||||
|
"github.com/OliveTin/OliveTin/internal/configcheck"
|
||||||
|
"github.com/OliveTin/OliveTin/internal/configissues"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRebuildWatcherPathMissingDir(t *testing.T) {
|
||||||
|
configissues.BeginConfigLoad()
|
||||||
|
configissues.Replace(nil)
|
||||||
|
|
||||||
|
missing := filepath.Join(t.TempDir(), "does-not-exist")
|
||||||
|
cfg := config.DefaultConfig()
|
||||||
|
cfg.Actions = []*config.Action{
|
||||||
|
{
|
||||||
|
Title: "On create",
|
||||||
|
ID: "on_create",
|
||||||
|
SourceFile: "/etc/OliveTin/config.yaml",
|
||||||
|
ExecOnFileCreatedInDir: []string{missing},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
configcheck.Rebuild(cfg)
|
||||||
|
|
||||||
|
issues := configissues.List()
|
||||||
|
require.True(t, hasCode(issues, configissues.CodeWatcherPath))
|
||||||
|
|
||||||
|
var found *configissues.Issue
|
||||||
|
for i := range issues {
|
||||||
|
if issues[i].Code == configissues.CodeWatcherPath {
|
||||||
|
found = &issues[i]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
require.NotNil(t, found)
|
||||||
|
assert.Equal(t, missing, found.Source)
|
||||||
|
assert.Equal(t, "/etc/OliveTin/config.yaml", found.ConfigFile)
|
||||||
|
assert.Contains(t, found.Message, missing)
|
||||||
|
assert.Contains(t, found.Message, "Could not create watcher")
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
package configissues
|
||||||
|
|
||||||
|
const (
|
||||||
|
SeverityWarning = "warning"
|
||||||
|
SeverityError = "error"
|
||||||
|
|
||||||
|
CodeActionGroupUnknown = "action_group_unknown"
|
||||||
|
CodeActionGroupUnenforced = "action_group_unenforced"
|
||||||
|
CodeArgTypeUnset = "arg_type_unset"
|
||||||
|
CodeChecklistNoChoices = "checklist_no_choices"
|
||||||
|
CodeChecklistEntityChoices = "checklist_entity_choices"
|
||||||
|
CodeEnvUnset = "env_unset"
|
||||||
|
CodeIncludeMissing = "include_missing"
|
||||||
|
CodeTemplateParse = "template_parse"
|
||||||
|
CodeArgDefaultInvalid = "arg_default_invalid"
|
||||||
|
CodeEntityFile = "entity_file"
|
||||||
|
CodeEntityEmpty = "entity_empty"
|
||||||
|
CodeCronInvalid = "cron_invalid"
|
||||||
|
CodeCronEntityBinding = "cron_entity_binding"
|
||||||
|
CodeWatcherPath = "watcher_path"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Issue is a configuration warning or error surfaced on Diagnostics.
|
||||||
|
type Issue struct {
|
||||||
|
Severity string
|
||||||
|
Code string
|
||||||
|
Message string
|
||||||
|
ActionID string
|
||||||
|
ActionTitle string
|
||||||
|
ArgumentName string
|
||||||
|
Source string
|
||||||
|
ConfigFile string
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,125 @@
|
||||||
|
package configissues
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
storeMu sync.RWMutex
|
||||||
|
issues []Issue
|
||||||
|
|
||||||
|
stickyMu sync.Mutex
|
||||||
|
sticky []Issue
|
||||||
|
)
|
||||||
|
|
||||||
|
// BeginConfigLoad clears sticky issues captured during config decode/include load.
|
||||||
|
func BeginConfigLoad() {
|
||||||
|
stickyMu.Lock()
|
||||||
|
defer stickyMu.Unlock()
|
||||||
|
sticky = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReportSticky records an issue that cannot be re-derived after config decode
|
||||||
|
// (for example unset environment variables that were already expanded).
|
||||||
|
func ReportSticky(issue Issue) {
|
||||||
|
stickyMu.Lock()
|
||||||
|
defer stickyMu.Unlock()
|
||||||
|
sticky = append(sticky, issue)
|
||||||
|
}
|
||||||
|
|
||||||
|
func copySticky() []Issue {
|
||||||
|
stickyMu.Lock()
|
||||||
|
defer stickyMu.Unlock()
|
||||||
|
out := make([]Issue, len(sticky))
|
||||||
|
copy(out, sticky)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// CopySticky returns a copy of sticky load-time issues.
|
||||||
|
func CopySticky() []Issue {
|
||||||
|
return copySticky()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace sets the current issue list and logs only newly appeared issues.
|
||||||
|
func Replace(next []Issue) {
|
||||||
|
storeMu.Lock()
|
||||||
|
defer storeMu.Unlock()
|
||||||
|
|
||||||
|
previous := make(map[string]struct{}, len(issues))
|
||||||
|
for _, issue := range issues {
|
||||||
|
previous[issueFingerprint(issue)] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
issues = make([]Issue, len(next))
|
||||||
|
copy(issues, next)
|
||||||
|
|
||||||
|
for _, issue := range issues {
|
||||||
|
if _, exists := previous[issueFingerprint(issue)]; exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
logNewIssue(issue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func issueFingerprint(issue Issue) string {
|
||||||
|
return issue.Severity + "\x00" +
|
||||||
|
issue.Code + "\x00" +
|
||||||
|
issue.ActionID + "\x00" +
|
||||||
|
issue.ActionTitle + "\x00" +
|
||||||
|
issue.ArgumentName + "\x00" +
|
||||||
|
issue.Source + "\x00" +
|
||||||
|
issue.ConfigFile + "\x00" +
|
||||||
|
issue.Message
|
||||||
|
}
|
||||||
|
|
||||||
|
func logNewIssue(issue Issue) {
|
||||||
|
fields := log.Fields{
|
||||||
|
"code": issue.Code,
|
||||||
|
"actionTitle": issue.ActionTitle,
|
||||||
|
"argumentName": issue.ArgumentName,
|
||||||
|
"configFile": issue.ConfigFile,
|
||||||
|
"source": issue.Source,
|
||||||
|
}
|
||||||
|
|
||||||
|
if issue.Severity == SeverityError {
|
||||||
|
log.WithFields(fields).Error(issue.Message)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.WithFields(fields).Warn(issue.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Report adds a live issue if it is not already present and logs it when new.
|
||||||
|
// Use this for runtime discoveries that happen outside Rebuild (for example
|
||||||
|
// filesystem watcher setup failures).
|
||||||
|
func Report(issue Issue) {
|
||||||
|
storeMu.Lock()
|
||||||
|
defer storeMu.Unlock()
|
||||||
|
|
||||||
|
issueFingerprintValue := issueFingerprint(issue)
|
||||||
|
for _, existing := range issues {
|
||||||
|
if issueFingerprint(existing) == issueFingerprintValue {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
issues = append(issues, issue)
|
||||||
|
logNewIssue(issue)
|
||||||
|
}
|
||||||
|
|
||||||
|
// List returns a copy of the current issues.
|
||||||
|
func List() []Issue {
|
||||||
|
storeMu.RLock()
|
||||||
|
defer storeMu.RUnlock()
|
||||||
|
out := make([]Issue, len(issues))
|
||||||
|
copy(out, issues)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count returns the number of current issues.
|
||||||
|
func Count() int {
|
||||||
|
storeMu.RLock()
|
||||||
|
defer storeMu.RUnlock()
|
||||||
|
return len(issues)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
package configissues_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/OliveTin/OliveTin/internal/configissues"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestStoreReplaceListCount(t *testing.T) {
|
||||||
|
configissues.BeginConfigLoad()
|
||||||
|
configissues.Replace([]configissues.Issue{{
|
||||||
|
Severity: configissues.SeverityError,
|
||||||
|
Code: configissues.CodeTemplateParse,
|
||||||
|
Message: "broken",
|
||||||
|
}})
|
||||||
|
|
||||||
|
assert.Equal(t, 1, configissues.Count())
|
||||||
|
assert.Equal(t, configissues.CodeTemplateParse, configissues.List()[0].Code)
|
||||||
|
|
||||||
|
configissues.Replace(nil)
|
||||||
|
assert.Equal(t, 0, configissues.Count())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStickyCopy(t *testing.T) {
|
||||||
|
configissues.BeginConfigLoad()
|
||||||
|
configissues.ReportSticky(configissues.Issue{
|
||||||
|
Code: configissues.CodeEnvUnset,
|
||||||
|
Message: "missing",
|
||||||
|
Source: "FOO",
|
||||||
|
})
|
||||||
|
|
||||||
|
copied := configissues.CopySticky()
|
||||||
|
assert.Len(t, copied, 1)
|
||||||
|
assert.Equal(t, configissues.CodeEnvUnset, copied[0].Code)
|
||||||
|
|
||||||
|
configissues.BeginConfigLoad()
|
||||||
|
assert.Empty(t, configissues.CopySticky())
|
||||||
|
}
|
||||||
|
|
@ -30,13 +30,18 @@ func AddListener(l func()) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func SetupEntityFileWatchers(cfg *config.Config) {
|
func SetupEntityFileWatchers(cfg *config.Config) {
|
||||||
baseDir := resolveEntitiesBaseDir(cfg.GetDir())
|
baseDir := ResolveEntitiesBaseDir(cfg.GetDir())
|
||||||
for i := range cfg.Entities { // #337 - iterate by key, not by value
|
for i := range cfg.Entities { // #337 - iterate by key, not by value
|
||||||
ef := cfg.Entities[i]
|
ef := cfg.Entities[i]
|
||||||
watchAndLoadEntity(baseDir, ef)
|
watchAndLoadEntity(baseDir, ef)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ResolveEntitiesBaseDir returns the directory used to resolve relative entity file paths.
|
||||||
|
func ResolveEntitiesBaseDir(configDir string) string {
|
||||||
|
return resolveEntitiesBaseDir(configDir)
|
||||||
|
}
|
||||||
|
|
||||||
//gocyclo:ignore
|
//gocyclo:ignore
|
||||||
func resolveEntitiesBaseDir(configDir string) string {
|
func resolveEntitiesBaseDir(configDir string) string {
|
||||||
absConfigDir, err := filepath.Abs(configDir)
|
absConfigDir, err := filepath.Abs(configDir)
|
||||||
|
|
@ -64,15 +69,28 @@ func watchAndLoadEntity(baseDir string, ef *config.EntityFile) {
|
||||||
p = filepath.Join(baseDir, p)
|
p = filepath.Join(baseDir, p)
|
||||||
log.WithFields(log.Fields{"entityFile": p}).Debugf("Adding config dir to entity file path")
|
log.WithFields(log.Fields{"entityFile": p}).Debugf("Adding config dir to entity file path")
|
||||||
}
|
}
|
||||||
go filehelper.WatchFileWrite(p, func(filename string) { loadEntityFile(p, ef.Name) })
|
go filehelper.WatchFileWrite(p, func(filename string) { loadEntityFile(p, ef.Name) }, filehelper.WatchMeta{
|
||||||
|
ConfigFile: ef.SourceFile,
|
||||||
|
})
|
||||||
loadEntityFile(p, ef.Name)
|
loadEntityFile(p, ef.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadEntityFile(filename string, entityname string) {
|
func loadEntityFile(filename string, entityname string) {
|
||||||
|
defer func() {
|
||||||
|
MarkEntityLoadAttempted(entityname)
|
||||||
|
notifyEntityListeners()
|
||||||
|
}()
|
||||||
|
|
||||||
if strings.HasSuffix(filename, ".json") {
|
if strings.HasSuffix(filename, ".json") {
|
||||||
loadEntityFileJson(filename, entityname)
|
loadEntityFileJson(filename, entityname)
|
||||||
} else {
|
return
|
||||||
loadEntityFileYaml(filename, entityname)
|
}
|
||||||
|
loadEntityFileYaml(filename, entityname)
|
||||||
|
}
|
||||||
|
|
||||||
|
func notifyEntityListeners() {
|
||||||
|
for _, l := range listeners {
|
||||||
|
l()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -86,6 +104,7 @@ func loadEntityFileJson(filename string, entityname string) {
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("ReadIn: %v", err)
|
log.Errorf("ReadIn: %v", err)
|
||||||
|
ClearEntitiesOfType(entityname)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -100,13 +119,14 @@ func loadEntityFileJson(filename string, entityname string) {
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("%v", err)
|
log.Errorf("%v", err)
|
||||||
|
ClearEntitiesOfType(entityname)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
data = append(data, d)
|
data = append(data, d)
|
||||||
}
|
}
|
||||||
|
|
||||||
updateSvFromFile(entityname, data)
|
replaceEntitiesFromFile(entityname, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadEntityFileYaml(filename string, entityname string) {
|
func loadEntityFileYaml(filename string, entityname string) {
|
||||||
|
|
@ -119,6 +139,7 @@ func loadEntityFileYaml(filename string, entityname string) {
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("ReadIn: %v", err)
|
log.Errorf("ReadIn: %v", err)
|
||||||
|
ClearEntitiesOfType(entityname)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -128,21 +149,31 @@ func loadEntityFileYaml(filename string, entityname string) {
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("Unmarshal: %v", err)
|
log.Errorf("Unmarshal: %v", err)
|
||||||
|
ClearEntitiesOfType(entityname)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
updateSvFromFile(entityname, data)
|
replaceEntitiesFromFile(entityname, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateSvFromFile(entityname string, data []map[string]any) {
|
func replaceEntitiesFromFile(entityname string, data []map[string]any) {
|
||||||
ClearEntitiesOfType(entityname)
|
rwmutex.Lock()
|
||||||
|
defer rwmutex.Unlock()
|
||||||
|
|
||||||
for i, mapp := range data {
|
delete(entities, entityname)
|
||||||
AddEntity(entityname, fmt.Sprintf("%d", i), mapp)
|
|
||||||
|
if len(data) == 0 {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, l := range listeners {
|
entities[entityname] = make(entityInstancesByKey, 0)
|
||||||
l()
|
for i, mapp := range data {
|
||||||
|
entityKey := fmt.Sprintf("%d", i)
|
||||||
|
entities[entityname][entityKey] = &Entity{
|
||||||
|
Data: mapp,
|
||||||
|
UniqueKey: entityKey,
|
||||||
|
Title: findEntityTitle(mapp),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
package entities
|
||||||
|
|
||||||
|
import "sync"
|
||||||
|
|
||||||
|
var (
|
||||||
|
loadAttemptedMu sync.RWMutex
|
||||||
|
loadAttempted = map[string]bool{}
|
||||||
|
)
|
||||||
|
|
||||||
|
// MarkEntityLoadAttempted records that OliveTin has tried to load this entity
|
||||||
|
// type from its entity file (successfully or not).
|
||||||
|
func MarkEntityLoadAttempted(entityName string) {
|
||||||
|
if entityName == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
loadAttemptedMu.Lock()
|
||||||
|
defer loadAttemptedMu.Unlock()
|
||||||
|
loadAttempted[entityName] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasEntityLoadAttempted reports whether a load has been attempted for the entity type.
|
||||||
|
func HasEntityLoadAttempted(entityName string) bool {
|
||||||
|
loadAttemptedMu.RLock()
|
||||||
|
defer loadAttemptedMu.RUnlock()
|
||||||
|
return loadAttempted[entityName]
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResetEntityLoadAttempts clears load-attempt tracking (used by tests).
|
||||||
|
func ResetEntityLoadAttempts() {
|
||||||
|
loadAttemptedMu.Lock()
|
||||||
|
defer loadAttemptedMu.Unlock()
|
||||||
|
loadAttempted = map[string]bool{}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
package executor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
config "github.com/OliveTin/OliveTin/internal/config"
|
||||||
|
"github.com/OliveTin/OliveTin/internal/configissues"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestValidateArgumentDefaultSkipsTemplates(t *testing.T) {
|
||||||
|
action := &config.Action{Title: "Deploy", ID: "deploy"}
|
||||||
|
arg := &config.ActionArgument{
|
||||||
|
Name: "host",
|
||||||
|
Type: "ascii_identifier",
|
||||||
|
Default: `{{ host.name }}`,
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Nil(t, validateArgumentDefault(action, arg))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateArgumentDefaultLiteralInvalid(t *testing.T) {
|
||||||
|
action := &config.Action{Title: "Deploy", ID: "deploy"}
|
||||||
|
arg := &config.ActionArgument{
|
||||||
|
Name: "host",
|
||||||
|
Type: "ascii_identifier",
|
||||||
|
Default: "not a valid id!!",
|
||||||
|
}
|
||||||
|
|
||||||
|
issue := validateArgumentDefault(action, arg)
|
||||||
|
require.NotNil(t, issue)
|
||||||
|
assert.Equal(t, configissues.CodeArgDefaultInvalid, issue.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateArgumentDefaultLiteralValid(t *testing.T) {
|
||||||
|
action := &config.Action{Title: "Deploy", ID: "deploy"}
|
||||||
|
arg := &config.ActionArgument{
|
||||||
|
Name: "host",
|
||||||
|
Type: "ascii_identifier",
|
||||||
|
Default: "example.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Nil(t, validateArgumentDefault(action, arg))
|
||||||
|
}
|
||||||
|
|
@ -3,10 +3,12 @@ package executor
|
||||||
import (
|
import (
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
config "github.com/OliveTin/OliveTin/internal/config"
|
"github.com/OliveTin/OliveTin/internal/config"
|
||||||
|
"github.com/OliveTin/OliveTin/internal/configcheck"
|
||||||
|
"github.com/OliveTin/OliveTin/internal/configissues"
|
||||||
"github.com/OliveTin/OliveTin/internal/entities"
|
"github.com/OliveTin/OliveTin/internal/entities"
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (e *Executor) FindBindingByID(id string) *ActionBinding {
|
func (e *Executor) FindBindingByID(id string) *ActionBinding {
|
||||||
|
|
@ -40,40 +42,54 @@ type RebuildActionMapRequest struct {
|
||||||
dashboardTargets *dashboardTargetIndex
|
dashboardTargets *dashboardTargetIndex
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateArgumentDefaults(cfg *config.Config) {
|
func validateArgumentDefaults(cfg *config.Config) []configissues.Issue {
|
||||||
if cfg == nil {
|
if cfg == nil {
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
|
out := make([]configissues.Issue, 0)
|
||||||
for _, action := range cfg.Actions {
|
for _, action := range cfg.Actions {
|
||||||
validateActionArgumentDefaults(action)
|
out = append(out, validateActionArgumentDefaults(action)...)
|
||||||
}
|
}
|
||||||
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateActionArgumentDefaults(action *config.Action) {
|
func validateActionArgumentDefaults(action *config.Action) []configissues.Issue {
|
||||||
if action == nil {
|
if action == nil {
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
|
out := make([]configissues.Issue, 0)
|
||||||
for i := range action.Arguments {
|
for i := range action.Arguments {
|
||||||
validateArgumentDefault(action, &action.Arguments[i])
|
if issue := validateArgumentDefault(action, &action.Arguments[i]); issue != nil {
|
||||||
|
out = append(out, *issue)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateArgumentDefault(action *config.Action, arg *config.ActionArgument) {
|
func validateArgumentDefault(action *config.Action, arg *config.ActionArgument) *configissues.Issue {
|
||||||
if arg.Default == "" {
|
if arg.Default == "" {
|
||||||
return
|
return nil
|
||||||
|
}
|
||||||
|
if strings.Contains(arg.Default, "{{") {
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
if err := ValidateArgument(arg, arg.Default, action); err != nil {
|
if err := ValidateArgument(arg, arg.Default, action); err != nil {
|
||||||
log.WithFields(log.Fields{
|
return &configissues.Issue{
|
||||||
"actionTitle": action.Title,
|
Severity: configissues.SeverityWarning,
|
||||||
"argName": arg.Name,
|
Code: configissues.CodeArgDefaultInvalid,
|
||||||
"default": arg.Default,
|
Message: fmt.Sprintf("Argument default value failed validation: %v", err),
|
||||||
"error": err,
|
ActionID: action.ID,
|
||||||
}).Warn("Argument default value failed validation")
|
ActionTitle: action.Title,
|
||||||
|
ArgumentName: arg.Name,
|
||||||
|
Source: arg.Default,
|
||||||
|
ConfigFile: action.SourceFile,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Executor) RebuildActionMap() {
|
func (e *Executor) RebuildActionMap() {
|
||||||
validateArgumentDefaults(e.Cfg)
|
defaultIssues := validateArgumentDefaults(e.Cfg)
|
||||||
|
|
||||||
e.MapActionBindingsLock.Lock()
|
e.MapActionBindingsLock.Lock()
|
||||||
|
|
||||||
|
|
@ -94,6 +110,8 @@ func (e *Executor) RebuildActionMap() {
|
||||||
|
|
||||||
e.MapActionBindingsLock.Unlock()
|
e.MapActionBindingsLock.Unlock()
|
||||||
|
|
||||||
|
configcheck.Rebuild(e.Cfg, defaultIssues...)
|
||||||
|
|
||||||
for _, l := range e.copyListeners() {
|
for _, l := range e.copyListeners() {
|
||||||
l.OnActionMapRebuilt()
|
l.OnActionMapRebuilt()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,14 @@
|
||||||
package filehelper
|
package filehelper
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/fsnotify/fsnotify"
|
"fmt"
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"sync"
|
"github.com/OliveTin/OliveTin/internal/configissues"
|
||||||
|
"github.com/fsnotify/fsnotify"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|
@ -28,33 +30,44 @@ const (
|
||||||
debounceDelay = 300 * time.Millisecond
|
debounceDelay = 300 * time.Millisecond
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// WatchMeta attaches action context to watcher setup failures so Diagnostics
|
||||||
|
// can apply the same action view ACL as other configuration issues.
|
||||||
|
type WatchMeta struct {
|
||||||
|
ActionID string
|
||||||
|
ActionTitle string
|
||||||
|
ConfigFile string
|
||||||
|
}
|
||||||
|
|
||||||
type watchContext struct {
|
type watchContext struct {
|
||||||
filename string
|
filename string
|
||||||
filedir string
|
filedir string
|
||||||
callback func(filename string)
|
callback func(filename string)
|
||||||
interestedEvent fsnotify.Op
|
interestedEvent fsnotify.Op
|
||||||
event *fsnotify.Event
|
event *fsnotify.Event
|
||||||
|
meta WatchMeta
|
||||||
}
|
}
|
||||||
|
|
||||||
func WatchDirectoryCreate(fullpath string, callback func(filename string)) {
|
func WatchDirectoryCreate(fullpath string, callback func(filename string), meta WatchMeta) {
|
||||||
watchPath(&watchContext{
|
watchPath(&watchContext{
|
||||||
filedir: fullpath,
|
filedir: fullpath,
|
||||||
filename: "",
|
filename: "",
|
||||||
callback: callback,
|
callback: callback,
|
||||||
interestedEvent: fsnotify.Create,
|
interestedEvent: fsnotify.Create,
|
||||||
|
meta: meta,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func WatchDirectoryWrite(fullpath string, callback func(filename string)) {
|
func WatchDirectoryWrite(fullpath string, callback func(filename string), meta WatchMeta) {
|
||||||
watchPath(&watchContext{
|
watchPath(&watchContext{
|
||||||
filedir: fullpath,
|
filedir: fullpath,
|
||||||
filename: "",
|
filename: "",
|
||||||
callback: callback,
|
callback: callback,
|
||||||
interestedEvent: fsnotify.Write,
|
interestedEvent: fsnotify.Write,
|
||||||
|
meta: meta,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func WatchFileWrite(fullpath string, callback func(filename string)) {
|
func WatchFileWrite(fullpath string, callback func(filename string), meta WatchMeta) {
|
||||||
filename := filepath.Base(fullpath)
|
filename := filepath.Base(fullpath)
|
||||||
filedir := filepath.Dir(fullpath)
|
filedir := filepath.Dir(fullpath)
|
||||||
|
|
||||||
|
|
@ -63,69 +76,94 @@ func WatchFileWrite(fullpath string, callback func(filename string)) {
|
||||||
filename: filename,
|
filename: filename,
|
||||||
callback: callback,
|
callback: callback,
|
||||||
interestedEvent: fsnotify.Write,
|
interestedEvent: fsnotify.Write,
|
||||||
|
meta: meta,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func watchPath(ctx *watchContext) {
|
func watchPath(ctx *watchContext) {
|
||||||
watcher, err := fsnotify.NewWatcher()
|
watcher, err := fsnotify.NewWatcher()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("Could not watch for files being created: %v", err)
|
reportWatcherFailure(ctx, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
defer func() {
|
defer closeWatcher(watcher)
|
||||||
if err := watcher.Close(); err != nil {
|
|
||||||
log.Errorf("Failed to close file watcher: %v", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
done := make(chan bool)
|
if err := watcher.Add(ctx.filedir); err != nil {
|
||||||
|
reportWatcherFailure(ctx, err)
|
||||||
go func() {
|
return
|
||||||
for {
|
|
||||||
processEvent(ctx, watcher)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
err = watcher.Add(ctx.filedir)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("Could not create watcher: %v", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
<-done
|
for processEvent(ctx, watcher) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func processEvent(ctx *watchContext, watcher *fsnotify.Watcher) {
|
func closeWatcher(watcher *fsnotify.Watcher) {
|
||||||
|
if err := watcher.Close(); err != nil {
|
||||||
|
log.Errorf("Failed to close file watcher: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func watchTarget(ctx *watchContext) string {
|
||||||
|
if ctx.filename == "" {
|
||||||
|
return ctx.filedir
|
||||||
|
}
|
||||||
|
return filepath.Join(ctx.filedir, ctx.filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
func reportWatcherFailure(ctx *watchContext, err error) {
|
||||||
|
path := watchTarget(ctx)
|
||||||
|
message := fmt.Sprintf("Could not create watcher for %q: %v", path, err)
|
||||||
|
configissues.Report(configissues.Issue{
|
||||||
|
Severity: configissues.SeverityError,
|
||||||
|
Code: configissues.CodeWatcherPath,
|
||||||
|
Message: message,
|
||||||
|
ActionID: ctx.meta.ActionID,
|
||||||
|
ActionTitle: ctx.meta.ActionTitle,
|
||||||
|
Source: path,
|
||||||
|
ConfigFile: ctx.meta.ConfigFile,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// processEvent waits for one watcher event. It returns false when the watcher
|
||||||
|
// channels are closed so the caller can stop looping.
|
||||||
|
func processEvent(ctx *watchContext, watcher *fsnotify.Watcher) bool {
|
||||||
select {
|
select {
|
||||||
case event, ok := <-watcher.Events:
|
case event, ok := <-watcher.Events:
|
||||||
ctx.event = &event
|
return handleWatcherEvent(ctx, event, ok)
|
||||||
|
case err, ok := <-watcher.Errors:
|
||||||
if !consumeEvent(ok, ctx) {
|
return handleWatcherError(ctx, err, ok)
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
break
|
|
||||||
case err := <-watcher.Errors:
|
|
||||||
log.Errorf("Error in fsnotify: %v", err)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func consumeEvent(ok bool, ctx *watchContext) bool {
|
func handleWatcherEvent(ctx *watchContext, event fsnotify.Event, ok bool) bool {
|
||||||
if !ok {
|
if !ok {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
ctx.event = &event
|
||||||
|
consumeEvent(ctx)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleWatcherError(ctx *watchContext, err error, ok bool) bool {
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"path": watchTarget(ctx),
|
||||||
|
}).Errorf("Error in fsnotify: %v", err)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func consumeEvent(ctx *watchContext) {
|
||||||
if ctx.filename != "" && filepath.Base(ctx.event.Name) != ctx.filename {
|
if ctx.filename != "" && filepath.Base(ctx.event.Name) != ctx.filename {
|
||||||
log.Tracef("fsnotify irreleventa event different file %+v", ctx.event)
|
log.Tracef("fsnotify irreleventa event different file %+v", ctx.event)
|
||||||
return true
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
consumeRelevantEvents(ctx)
|
consumeRelevantEvents(ctx)
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func consumeRelevantEvents(ctx *watchContext) {
|
func consumeRelevantEvents(ctx *watchContext) {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
package filehelper_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/OliveTin/OliveTin/internal/configissues"
|
||||||
|
"github.com/OliveTin/OliveTin/internal/filehelper"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestWatchDirectoryCreateReportsMissingPath(t *testing.T) {
|
||||||
|
configissues.Replace(nil)
|
||||||
|
|
||||||
|
missing := filepath.Join(t.TempDir(), "missing-watch-dir")
|
||||||
|
done := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
filehelper.WatchDirectoryCreate(missing, func(string) {}, filehelper.WatchMeta{})
|
||||||
|
close(done)
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
case <-time.After(2 * time.Second):
|
||||||
|
t.Fatal("WatchDirectoryCreate did not return after watcher failure")
|
||||||
|
}
|
||||||
|
|
||||||
|
issues := configissues.List()
|
||||||
|
require.Len(t, issues, 1)
|
||||||
|
assert.Equal(t, configissues.CodeWatcherPath, issues[0].Code)
|
||||||
|
assert.Equal(t, missing, issues[0].Source)
|
||||||
|
assert.Contains(t, issues[0].Message, missing)
|
||||||
|
assert.Empty(t, issues[0].ActionID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWatchDirectoryCreateReportsActionMeta(t *testing.T) {
|
||||||
|
configissues.Replace(nil)
|
||||||
|
|
||||||
|
missing := filepath.Join(t.TempDir(), "missing-secret-watch")
|
||||||
|
done := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
filehelper.WatchDirectoryCreate(missing, func(string) {}, filehelper.WatchMeta{
|
||||||
|
ActionID: "secret_action",
|
||||||
|
ActionTitle: "Secret Action",
|
||||||
|
ConfigFile: "/etc/OliveTin/secret.yaml",
|
||||||
|
})
|
||||||
|
close(done)
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
case <-time.After(2 * time.Second):
|
||||||
|
t.Fatal("WatchDirectoryCreate did not return after watcher failure")
|
||||||
|
}
|
||||||
|
|
||||||
|
issues := configissues.List()
|
||||||
|
require.Len(t, issues, 1)
|
||||||
|
assert.Equal(t, "secret_action", issues[0].ActionID)
|
||||||
|
assert.Equal(t, "Secret Action", issues[0].ActionTitle)
|
||||||
|
assert.Equal(t, "/etc/OliveTin/secret.yaml", issues[0].ConfigFile)
|
||||||
|
}
|
||||||
|
|
@ -37,7 +37,11 @@ func Schedule(cfg *config.Config, ex *executor.Executor) {
|
||||||
parseCalendarFile(captured, cfg, ex, filename)
|
parseCalendarFile(captured, cfg, ex, filename)
|
||||||
}
|
}
|
||||||
|
|
||||||
go filehelper.WatchFileWrite(action.ExecOnCalendarFile, x)
|
go filehelper.WatchFileWrite(action.ExecOnCalendarFile, x, filehelper.WatchMeta{
|
||||||
|
ActionID: action.ID,
|
||||||
|
ActionTitle: action.Title,
|
||||||
|
ConfigFile: action.SourceFile,
|
||||||
|
})
|
||||||
|
|
||||||
x(action.ExecOnCalendarFile)
|
x(action.ExecOnCalendarFile)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ func WatchFilesInDirectory(cfg *config.Config, ex *executor.Executor) {
|
||||||
go func(act *config.Action, dir string) {
|
go func(act *config.Action, dir string) {
|
||||||
filehelper.WatchDirectoryCreate(dir, func(filename string) {
|
filehelper.WatchDirectoryCreate(dir, func(filename string) {
|
||||||
scheduleExec(act, cfg, ex, filename)
|
scheduleExec(act, cfg, ex, filename)
|
||||||
})
|
}, watchMetaForAction(act))
|
||||||
}(action, dirname)
|
}(action, dirname)
|
||||||
}
|
}
|
||||||
for _, dirname := range action.ExecOnFileChangedInDir {
|
for _, dirname := range action.ExecOnFileChangedInDir {
|
||||||
|
|
@ -27,12 +27,20 @@ func WatchFilesInDirectory(cfg *config.Config, ex *executor.Executor) {
|
||||||
go func(act *config.Action, dir string) {
|
go func(act *config.Action, dir string) {
|
||||||
filehelper.WatchDirectoryWrite(dir, func(filename string) {
|
filehelper.WatchDirectoryWrite(dir, func(filename string) {
|
||||||
scheduleExec(act, cfg, ex, filename)
|
scheduleExec(act, cfg, ex, filename)
|
||||||
})
|
}, watchMetaForAction(act))
|
||||||
}(action, dirname)
|
}(action, dirname)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func watchMetaForAction(action *config.Action) filehelper.WatchMeta {
|
||||||
|
return filehelper.WatchMeta{
|
||||||
|
ActionID: action.ID,
|
||||||
|
ActionTitle: action.Title,
|
||||||
|
ConfigFile: action.SourceFile,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func scheduleExec(action *config.Action, cfg *config.Config, ex *executor.Executor, path string) {
|
func scheduleExec(action *config.Action, cfg *config.Config, ex *executor.Executor, path string) {
|
||||||
args := map[string]string{
|
args := map[string]string{
|
||||||
"filepath": path,
|
"filepath": path,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
package tpl_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/OliveTin/OliveTin/internal/tpl"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCheckActionTemplateParses(t *testing.T) {
|
||||||
|
assert.NoError(t, tpl.CheckActionTemplateParses(`{{ host.name }}`))
|
||||||
|
assert.NoError(t, tpl.CheckActionTemplateParses(`{{ host.hostname }}`))
|
||||||
|
|
||||||
|
err := tpl.CheckActionTemplateParses(`{{ host.name | default "All" }}`)
|
||||||
|
require.Error(t, err)
|
||||||
|
}
|
||||||
|
|
@ -219,6 +219,25 @@ func ParseTemplateOfActionBeforeExec(source string, ent *entities.Entity) string
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CheckActionTemplateParses migrates legacy template syntax and reports parse
|
||||||
|
// errors only (execution against entity/argument data is not attempted).
|
||||||
|
func CheckActionTemplateParses(source string) error {
|
||||||
|
if source == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
source = migrateLegacyArgumentNames(source)
|
||||||
|
source = migrateLegacyEntityProperties(source)
|
||||||
|
|
||||||
|
clone, err := tpl.Clone()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = clone.Parse(source)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
func ParseTemplateBoolWith(source string, ent *entities.Entity) bool {
|
func ParseTemplateBoolWith(source string, ent *entities.Entity) bool {
|
||||||
source = strings.TrimSpace(source)
|
source = strings.TrimSpace(source)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue