Next (#862)
This commit is contained in:
commit
6b72348310
|
|
@ -1,10 +1,8 @@
|
||||||
---
|
---
|
||||||
#branches:
|
# Only allow releases on the main branch (for 3k)
|
||||||
# - name: main
|
# releases for 2k are published manually.
|
||||||
# range: '3000.x.x'
|
branches:
|
||||||
|
- name: main
|
||||||
# - name: release/2k
|
|
||||||
# range: '>=2000.0.0 <3000.0.0'
|
|
||||||
|
|
||||||
plugins:
|
plugins:
|
||||||
- '@semantic-release/commit-analyzer'
|
- '@semantic-release/commit-analyzer'
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -6,7 +6,7 @@
|
||||||
"source": "index.html",
|
"source": "index.html",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"process": "^0.11.10",
|
"process": "^0.11.10",
|
||||||
"stylelint": "^17.0.0",
|
"stylelint": "^17.3.0",
|
||||||
"stylelint-config-standard": "^40.0.0"
|
"stylelint-config-standard": "^40.0.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
@ -26,16 +26,16 @@
|
||||||
"@connectrpc/connect-web": "^2.1.1",
|
"@connectrpc/connect-web": "^2.1.1",
|
||||||
"@hugeicons/core-free-icons": "^3.1.1",
|
"@hugeicons/core-free-icons": "^3.1.1",
|
||||||
"@hugeicons/vue": "^1.0.4",
|
"@hugeicons/vue": "^1.0.4",
|
||||||
"@vitejs/plugin-vue": "^6.0.3",
|
"@vitejs/plugin-vue": "^6.0.4",
|
||||||
"@xterm/addon-fit": "^0.11.0",
|
"@xterm/addon-fit": "^0.11.0",
|
||||||
"@xterm/xterm": "^6.0.0",
|
"@xterm/xterm": "^6.0.0",
|
||||||
"iconify-icon": "^3.0.2",
|
"iconify-icon": "^3.0.2",
|
||||||
"picocrank": "^1.13.1",
|
"picocrank": "^1.14.0",
|
||||||
"standard": "^17.1.2",
|
"standard": "^17.1.2",
|
||||||
"unplugin-vue-components": "^31.0.0",
|
"unplugin-vue-components": "^31.0.0",
|
||||||
"vite": "^7.3.1",
|
"vite": "^7.3.1",
|
||||||
"vue": "^3.5.27",
|
"vue": "^3.5.28",
|
||||||
"vue-i18n": "^11.2.8",
|
"vue-i18n": "^11.2.8",
|
||||||
"vue-router": "^4.6.4"
|
"vue-router": "^5.0.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
// @generated by protoc-gen-es v2.10.2
|
// @generated by protoc-gen-es v2.11.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 */
|
||||||
|
|
||||||
|
|
@ -1458,6 +1458,11 @@ export declare type InitResponse = Message<"olivetin.api.v1.InitResponse"> & {
|
||||||
* @generated from field: repeated string available_themes = 24;
|
* @generated from field: repeated string available_themes = 24;
|
||||||
*/
|
*/
|
||||||
availableThemes: string[];
|
availableThemes: string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @generated from field: bool show_navigate_on_start_icons = 25;
|
||||||
|
*/
|
||||||
|
showNavigateOnStartIcons: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -1841,4 +1846,3 @@ export declare const OliveTinApiService: GenService<{
|
||||||
output: typeof EntitySchema;
|
output: typeof EntitySchema;
|
||||||
},
|
},
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -3,7 +3,7 @@
|
||||||
<button :id="`actionButtonInner-${bindingId}`" :title="title" :disabled="!canExec || isDisabled"
|
<button :id="`actionButtonInner-${bindingId}`" :title="title" :disabled="!canExec || isDisabled"
|
||||||
:class="combinedClasses" @click="handleClick">
|
:class="combinedClasses" @click="handleClick">
|
||||||
|
|
||||||
<div class="navigate-on-start-container">
|
<div v-if="showNavigateOnStartIcons" class="navigate-on-start-container">
|
||||||
<div v-if="navigateOnStart == 'pop'" class="navigate-on-start" title="Opens a popup dialog on start">
|
<div v-if="navigateOnStart == 'pop'" class="navigate-on-start" title="Opens a popup dialog on start">
|
||||||
<HugeiconsIcon :icon="ComputerTerminal01Icon" />
|
<HugeiconsIcon :icon="ComputerTerminal01Icon" />
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -69,6 +69,11 @@ let rateLimitInterval = null
|
||||||
// Animation classes
|
// Animation classes
|
||||||
const buttonClasses = ref([])
|
const buttonClasses = ref([])
|
||||||
|
|
||||||
|
// Show navigate on start icons - defaults to true if not set
|
||||||
|
const showNavigateOnStartIcons = computed(() => {
|
||||||
|
return window.initResponse?.showNavigateOnStartIcons ?? true
|
||||||
|
})
|
||||||
|
|
||||||
// Combined classes including custom cssClass
|
// Combined classes including custom cssClass
|
||||||
const combinedClasses = computed(() => {
|
const combinedClasses = computed(() => {
|
||||||
const classes = [...buttonClasses.value]
|
const classes = [...buttonClasses.value]
|
||||||
|
|
|
||||||
|
|
@ -361,6 +361,7 @@ function applyTheme() {
|
||||||
document.head.appendChild(themeStyle)
|
document.head.appendChild(themeStyle)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load theme into @layer theme so it takes precedence over @layer components
|
||||||
if (themePreference.value && themePreference.value !== '') {
|
if (themePreference.value && themePreference.value !== '') {
|
||||||
themeStyle.textContent = `@import url('/custom-webui/themes/${themePreference.value}/theme.css') layer(theme);`
|
themeStyle.textContent = `@import url('/custom-webui/themes/${themePreference.value}/theme.css') layer(theme);`
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ a:hover {
|
||||||
|
|
||||||
links.value = [];
|
links.value = [];
|
||||||
matched.forEach((record) => {
|
matched.forEach((record) => {
|
||||||
|
if (!record) return;
|
||||||
if (record.meta && record.meta.breadcrumb) {
|
if (record.meta && record.meta.breadcrumb) {
|
||||||
record.meta.breadcrumb.forEach((item) => {
|
record.meta.breadcrumb.forEach((item) => {
|
||||||
links.value.push({
|
links.value.push({
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
#
|
||||||
|
# Integration Test Config: cssClass on dashboard components (#804)
|
||||||
|
#
|
||||||
|
|
||||||
|
listenAddressSingleHTTPFrontend: 0.0.0.0:1337
|
||||||
|
|
||||||
|
logLevel: "DEBUG"
|
||||||
|
checkForUpdates: false
|
||||||
|
|
||||||
|
# Custom theme used to verify theme CSS applies to cssClass (e.g. action button background)
|
||||||
|
themeName: cssclass-theme
|
||||||
|
|
||||||
|
actions: []
|
||||||
|
|
||||||
|
dashboards:
|
||||||
|
- title: CssClass Dashboard
|
||||||
|
contents:
|
||||||
|
- title: Button with custom class
|
||||||
|
type: link
|
||||||
|
cssClass: test-custom-class
|
||||||
|
inlineAction:
|
||||||
|
shell: echo ok
|
||||||
|
icon: ping
|
||||||
|
- title: Display with custom class
|
||||||
|
type: display
|
||||||
|
cssClass: test-display-class
|
||||||
|
contents: []
|
||||||
|
|
@ -0,0 +1,77 @@
|
||||||
|
import { describe, it, before, after, afterEach } from 'mocha'
|
||||||
|
import { expect } from 'chai'
|
||||||
|
import { By } from 'selenium-webdriver'
|
||||||
|
import {
|
||||||
|
getRootAndWait,
|
||||||
|
takeScreenshotOnFailure,
|
||||||
|
} from '../../lib/elements.js'
|
||||||
|
|
||||||
|
describe('config: cssClass', function () {
|
||||||
|
before(async function () {
|
||||||
|
await runner.start('cssClass')
|
||||||
|
})
|
||||||
|
|
||||||
|
after(async () => {
|
||||||
|
await runner.stop()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(function () {
|
||||||
|
takeScreenshotOnFailure(this.currentTest, webdriver)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('cssClass is applied to action button (link component)', async function () {
|
||||||
|
await getRootAndWait()
|
||||||
|
|
||||||
|
const buttonWithClass = await webdriver.findElements(By.css('.action-button button.test-custom-class'))
|
||||||
|
expect(buttonWithClass).to.have.length.at.least(1, 'Action button should have cssClass test-custom-class on the button')
|
||||||
|
|
||||||
|
const classAttr = await buttonWithClass[0].getAttribute('class')
|
||||||
|
expect(classAttr).to.include('test-custom-class')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('custom theme applies background color to action button via cssClass', async function () {
|
||||||
|
await getRootAndWait()
|
||||||
|
|
||||||
|
const buttonWithClass = await webdriver.findElements(By.css('.action-button button.test-custom-class'))
|
||||||
|
expect(buttonWithClass).to.have.length.at.least(1, 'Action button with test-custom-class should exist')
|
||||||
|
|
||||||
|
const bgColor = await buttonWithClass[0].getCssValue('background-color')
|
||||||
|
expect(bgColor, 'Theme theme.css should set .action-button button.test-custom-class background to rgb(32, 64, 128)')
|
||||||
|
.to.match(/rgba?\(\s*32\s*,\s*64\s*,\s*128\s*(,\s*1)?\s*\)/)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('cssClass override: style rule targeting custom class wins over component styles', async function () {
|
||||||
|
await getRootAndWait()
|
||||||
|
|
||||||
|
const buttonWithClass = await webdriver.findElements(By.css('.action-button button.test-custom-class'))
|
||||||
|
expect(buttonWithClass).to.have.length.at.least(1)
|
||||||
|
|
||||||
|
const beforePx = await buttonWithClass[0].getCssValue('border-top-width')
|
||||||
|
await webdriver.executeScript(`
|
||||||
|
var style = document.getElementById('cssclass-test-override-style');
|
||||||
|
if (!style) {
|
||||||
|
style = document.createElement('style');
|
||||||
|
style.id = 'cssclass-test-override-style';
|
||||||
|
style.textContent = '.test-custom-class { border-top-width: 31px !important; }';
|
||||||
|
document.head.appendChild(style);
|
||||||
|
} else {
|
||||||
|
style.textContent = '.test-custom-class { border-top-width: 31px !important; }';
|
||||||
|
}
|
||||||
|
`)
|
||||||
|
await webdriver.sleep(150)
|
||||||
|
|
||||||
|
const afterPx = await buttonWithClass[0].getCssValue('border-top-width')
|
||||||
|
const afterNum = parseInt(afterPx, 10)
|
||||||
|
expect(afterNum).to.be.greaterThan(10, 'Override targeting cssClass should win over component 1px (before=' + beforePx + ' after=' + afterPx + ') (#804)')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('cssClass is applied to display component', async function () {
|
||||||
|
await getRootAndWait()
|
||||||
|
|
||||||
|
const displayElements = await webdriver.findElements(By.css('.display.test-display-class'))
|
||||||
|
expect(displayElements).to.have.length.at.least(1, 'Display component with cssClass test-display-class should be in DOM')
|
||||||
|
|
||||||
|
const classAttr = await displayElements[0].getAttribute('class')
|
||||||
|
expect(classAttr).to.include('test-display-class')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
/* Theme for cssClass integration test: set a distinct background on the action button */
|
||||||
|
.action-button button.test-custom-class {
|
||||||
|
background-color: rgb(32, 64, 128);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
#
|
||||||
|
# Integration Test Config: Log Persistence
|
||||||
|
#
|
||||||
|
|
||||||
|
listenAddressSingleHTTPFrontend: 0.0.0.0:1337
|
||||||
|
|
||||||
|
logLevel: "DEBUG"
|
||||||
|
checkForUpdates: false
|
||||||
|
|
||||||
|
saveLogs:
|
||||||
|
resultsDirectory: /tmp/olivetin-test-logs
|
||||||
|
|
||||||
|
actions:
|
||||||
|
- title: Echo Test
|
||||||
|
shell: echo "Hello from persisted log test"
|
||||||
|
icon: test
|
||||||
|
|
@ -0,0 +1,258 @@
|
||||||
|
import { describe, it, before, after } from 'mocha'
|
||||||
|
import { expect } from 'chai'
|
||||||
|
import { By, Condition } from 'selenium-webdriver'
|
||||||
|
import fs from 'fs'
|
||||||
|
import path from 'path'
|
||||||
|
import {
|
||||||
|
getRootAndWait,
|
||||||
|
getActionButtons,
|
||||||
|
takeScreenshotOnFailure,
|
||||||
|
} from '../../lib/elements.js'
|
||||||
|
|
||||||
|
describe('config: logPersistence', function () {
|
||||||
|
const logsDir = '/tmp/olivetin-test-logs'
|
||||||
|
let firstExecutionTrackingId = null
|
||||||
|
|
||||||
|
before(async function () {
|
||||||
|
// Clean up any existing test logs
|
||||||
|
if (fs.existsSync(logsDir)) {
|
||||||
|
fs.rmSync(logsDir, { recursive: true, force: true })
|
||||||
|
}
|
||||||
|
fs.mkdirSync(logsDir, { recursive: true })
|
||||||
|
|
||||||
|
await runner.start('logPersistence')
|
||||||
|
})
|
||||||
|
|
||||||
|
after(async () => {
|
||||||
|
await runner.stop()
|
||||||
|
|
||||||
|
// Clean up test logs directory
|
||||||
|
if (fs.existsSync(logsDir)) {
|
||||||
|
fs.rmSync(logsDir, { recursive: true, force: true })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(function () {
|
||||||
|
takeScreenshotOnFailure(this.currentTest, webdriver)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Execute action and verify log is saved to disk', async function () {
|
||||||
|
this.timeout(30000)
|
||||||
|
await getRootAndWait()
|
||||||
|
|
||||||
|
// Get initial log file count
|
||||||
|
const initialLogCount = fs.existsSync(logsDir)
|
||||||
|
? fs.readdirSync(logsDir).filter(f => f.endsWith('.yaml')).length
|
||||||
|
: 0
|
||||||
|
|
||||||
|
// Wait for action button to be available
|
||||||
|
await webdriver.wait(
|
||||||
|
new Condition('wait for Echo Test button', async () => {
|
||||||
|
const buttons = await webdriver.findElements(By.css('.action-button button'))
|
||||||
|
for (const btn of buttons) {
|
||||||
|
const text = await btn.getText()
|
||||||
|
if (text.includes('Echo Test')) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}),
|
||||||
|
10000
|
||||||
|
)
|
||||||
|
|
||||||
|
// Find and click the Echo Test button
|
||||||
|
const buttons = await webdriver.findElements(By.css('.action-button button'))
|
||||||
|
let echoButton = null
|
||||||
|
for (const btn of buttons) {
|
||||||
|
const text = await btn.getText()
|
||||||
|
if (text.includes('Echo Test')) {
|
||||||
|
echoButton = btn
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
expect(echoButton).to.not.be.null
|
||||||
|
|
||||||
|
// Click the button to execute the action
|
||||||
|
await echoButton.click()
|
||||||
|
|
||||||
|
// Wait for the log file to be written to disk
|
||||||
|
await webdriver.wait(
|
||||||
|
new Condition('wait for log file to appear', async () => {
|
||||||
|
if (!fs.existsSync(logsDir)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const logFiles = fs.readdirSync(logsDir).filter(f => f.endsWith('.yaml'))
|
||||||
|
return logFiles.length > initialLogCount
|
||||||
|
}),
|
||||||
|
10000
|
||||||
|
)
|
||||||
|
|
||||||
|
// Wait a bit more to ensure file is fully written
|
||||||
|
await webdriver.sleep(1000)
|
||||||
|
|
||||||
|
// Get the newest log file
|
||||||
|
const logFiles = fs.readdirSync(logsDir).filter(f => f.endsWith('.yaml'))
|
||||||
|
expect(logFiles.length).to.be.greaterThan(initialLogCount, 'At least one new log file should be saved')
|
||||||
|
|
||||||
|
// Sort by modification time to get the newest
|
||||||
|
const logFilesWithStats = logFiles.map(f => {
|
||||||
|
const filePath = path.join(logsDir, f)
|
||||||
|
return {
|
||||||
|
name: f,
|
||||||
|
path: filePath,
|
||||||
|
mtime: fs.statSync(filePath).mtime
|
||||||
|
}
|
||||||
|
}).sort((a, b) => b.mtime - a.mtime)
|
||||||
|
|
||||||
|
const newestLogFile = logFilesWithStats[0]
|
||||||
|
expect(newestLogFile).to.not.be.undefined
|
||||||
|
|
||||||
|
// Read the log file to extract the tracking ID
|
||||||
|
const logFileContent = fs.readFileSync(newestLogFile.path, 'utf8')
|
||||||
|
|
||||||
|
// Verify the log file contains expected content (action title might be in different fields)
|
||||||
|
expect(logFileContent.length).to.be.greaterThan(0, 'Log file should not be empty')
|
||||||
|
|
||||||
|
// Extract tracking ID from filename first (most reliable)
|
||||||
|
// Filename format: <title>.<timestamp>.<trackingId>.yaml
|
||||||
|
// Tracking IDs are UUIDs, so match UUID pattern at the end before .yaml
|
||||||
|
let uuidMatch = newestLogFile.name.match(/([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})\.yaml$/)
|
||||||
|
|
||||||
|
if (uuidMatch) {
|
||||||
|
firstExecutionTrackingId = uuidMatch[1]
|
||||||
|
} else {
|
||||||
|
// Fallback: split by dots and get the last part before .yaml
|
||||||
|
const parts = newestLogFile.name.replace('.yaml', '').split('.')
|
||||||
|
if (parts.length >= 3) {
|
||||||
|
// The last part should be the tracking ID
|
||||||
|
firstExecutionTrackingId = parts[parts.length - 1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If still not found, try to extract from YAML content
|
||||||
|
// Try different possible field name variations
|
||||||
|
if (!firstExecutionTrackingId) {
|
||||||
|
const patterns = [
|
||||||
|
/executionTrackingID:\s*([^\s\n]+)/i,
|
||||||
|
/execution_tracking_id:\s*([^\s\n]+)/i,
|
||||||
|
/ExecutionTrackingID:\s*([^\s\n]+)/,
|
||||||
|
/executionTrackingId:\s*([^\s\n]+)/i,
|
||||||
|
]
|
||||||
|
|
||||||
|
for (const pattern of patterns) {
|
||||||
|
const match = logFileContent.match(pattern)
|
||||||
|
if (match) {
|
||||||
|
firstExecutionTrackingId = match[1].trim()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(firstExecutionTrackingId).to.not.be.null
|
||||||
|
expect(firstExecutionTrackingId.length).to.be.greaterThan(0)
|
||||||
|
|
||||||
|
// Verify the log file name contains the tracking ID
|
||||||
|
expect(newestLogFile.name).to.include(firstExecutionTrackingId)
|
||||||
|
|
||||||
|
// Verify the log file content contains the action (might be in actionTitle, actionConfigTitle, or title field)
|
||||||
|
const hasActionReference = logFileContent.includes('Echo Test') ||
|
||||||
|
logFileContent.includes('echo') ||
|
||||||
|
logFileContent.includes('actionTitle') ||
|
||||||
|
logFileContent.includes('actionConfigTitle')
|
||||||
|
expect(hasActionReference).to.be.true
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Restart service and verify logs are loaded from disk', async function () {
|
||||||
|
this.timeout(60000)
|
||||||
|
|
||||||
|
// Skip if first test didn't set the tracking ID
|
||||||
|
if (!firstExecutionTrackingId) {
|
||||||
|
this.skip()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify log file exists before restart
|
||||||
|
const logFilesBeforeRestart = fs.readdirSync(logsDir).filter(f => f.endsWith('.yaml'))
|
||||||
|
expect(logFilesBeforeRestart.length).to.be.greaterThan(0, 'Log file should exist before restart')
|
||||||
|
|
||||||
|
// Find the log file for this execution
|
||||||
|
const matchingLogFileBefore = logFilesBeforeRestart.find(f => f.includes(firstExecutionTrackingId))
|
||||||
|
expect(matchingLogFileBefore).to.not.be.undefined
|
||||||
|
|
||||||
|
// Stop the current service instance
|
||||||
|
await runner.stop()
|
||||||
|
|
||||||
|
// Wait a moment to ensure the process has fully stopped
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 2000))
|
||||||
|
|
||||||
|
// Verify log file still exists after stop (should not be deleted)
|
||||||
|
const logFilesAfterStop = fs.readdirSync(logsDir).filter(f => f.endsWith('.yaml'))
|
||||||
|
expect(logFilesAfterStop.length).to.be.greaterThan(0, 'Log file should still exist after service stop')
|
||||||
|
|
||||||
|
const matchingLogFileAfter = logFilesAfterStop.find(f => f.includes(firstExecutionTrackingId))
|
||||||
|
expect(matchingLogFileAfter).to.not.be.undefined
|
||||||
|
|
||||||
|
// Start a new service instance (logs should be loaded from disk)
|
||||||
|
await runner.start('logPersistence')
|
||||||
|
|
||||||
|
// Wait for the service to fully start and load logs
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 3000))
|
||||||
|
|
||||||
|
await getRootAndWait()
|
||||||
|
|
||||||
|
// Navigate directly to the specific log entry (this verifies the log was loaded)
|
||||||
|
await webdriver.get(runner.baseUrl() + 'logs/' + firstExecutionTrackingId)
|
||||||
|
|
||||||
|
// Wait for the log details page to load
|
||||||
|
await webdriver.wait(
|
||||||
|
new Condition('wait for log details to load', async () => {
|
||||||
|
try {
|
||||||
|
const body = await webdriver.findElement(By.tagName('body'))
|
||||||
|
const text = await body.getText()
|
||||||
|
// The log should contain the output from the echo command
|
||||||
|
return text.includes('Hello from persisted log test') || text.includes(firstExecutionTrackingId)
|
||||||
|
} catch (e) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
15000
|
||||||
|
)
|
||||||
|
|
||||||
|
// Verify the log content is displayed
|
||||||
|
const body = await webdriver.findElement(By.tagName('body'))
|
||||||
|
const bodyText = await body.getText()
|
||||||
|
|
||||||
|
// The persisted log should be accessible and contain the expected output
|
||||||
|
expect(bodyText).to.include('Hello from persisted log test')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Verify log file still exists after restart', async function () {
|
||||||
|
// Skip if first test didn't set the tracking ID
|
||||||
|
if (!firstExecutionTrackingId) {
|
||||||
|
this.skip()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the log file still exists on disk
|
||||||
|
const logFiles = fs.readdirSync(logsDir).filter(f => f.endsWith('.yaml'))
|
||||||
|
expect(logFiles.length).to.be.greaterThan(0, 'Log files should still exist after restart')
|
||||||
|
|
||||||
|
// Find the log file for the first execution
|
||||||
|
const matchingLogFile = logFiles.find(f => f.includes(firstExecutionTrackingId))
|
||||||
|
expect(matchingLogFile).to.not.be.undefined
|
||||||
|
expect(matchingLogFile).to.not.be.null
|
||||||
|
|
||||||
|
// Verify the log file content is still valid
|
||||||
|
const logFilePath = path.join(logsDir, matchingLogFile)
|
||||||
|
const logFileContent = fs.readFileSync(logFilePath, 'utf8')
|
||||||
|
expect(logFileContent.length).to.be.greaterThan(0, 'Log file should not be empty')
|
||||||
|
|
||||||
|
// The filename contains the tracking ID, so verify that
|
||||||
|
expect(matchingLogFile).to.include(firstExecutionTrackingId)
|
||||||
|
|
||||||
|
// Verify the file contains some expected content (action reference)
|
||||||
|
const hasActionReference = logFileContent.includes('Echo Test') ||
|
||||||
|
logFileContent.includes('echo') ||
|
||||||
|
logFileContent.includes('actionTitle') ||
|
||||||
|
logFileContent.includes('actionConfigTitle')
|
||||||
|
expect(hasActionReference).to.be.true
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -333,6 +333,7 @@ message InitResponse {
|
||||||
bool show_log_list = 22;
|
bool show_log_list = 22;
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
message AdditionalLink {
|
message AdditionalLink {
|
||||||
|
|
|
||||||
|
|
@ -3237,6 +3237,7 @@ type InitResponse struct {
|
||||||
ShowLogList bool `protobuf:"varint,22,opt,name=show_log_list,json=showLogList,proto3" json:"show_log_list,omitempty"`
|
ShowLogList bool `protobuf:"varint,22,opt,name=show_log_list,json=showLogList,proto3" json:"show_log_list,omitempty"`
|
||||||
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"`
|
||||||
unknownFields protoimpl.UnknownFields
|
unknownFields protoimpl.UnknownFields
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
}
|
}
|
||||||
|
|
@ -3439,6 +3440,13 @@ func (x *InitResponse) GetAvailableThemes() []string {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (x *InitResponse) GetShowNavigateOnStartIcons() bool {
|
||||||
|
if x != nil {
|
||||||
|
return x.ShowNavigateOnStartIcons
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
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"`
|
||||||
|
|
@ -4096,7 +4104,7 @@ const file_olivetin_api_v1_olivetin_proto_rawDesc = "" +
|
||||||
"\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\"\r\n" +
|
||||||
"\vInitRequest\"\xcd\b\n" +
|
"\vInitRequest\"\x8d\t\n" +
|
||||||
"\fInitResponse\x12\x1e\n" +
|
"\fInitResponse\x12\x1e\n" +
|
||||||
"\n" +
|
"\n" +
|
||||||
"showFooter\x18\x01 \x01(\bR\n" +
|
"showFooter\x18\x01 \x01(\bR\n" +
|
||||||
|
|
@ -4125,7 +4133,8 @@ const file_olivetin_api_v1_olivetin_proto_rawDesc = "" +
|
||||||
"\x10show_diagnostics\x18\x15 \x01(\bR\x0fshowDiagnostics\x12\"\n" +
|
"\x10show_diagnostics\x18\x15 \x01(\bR\x0fshowDiagnostics\x12\"\n" +
|
||||||
"\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\"8\n" +
|
"\x10available_themes\x18\x18 \x03(\tR\x0favailableThemes\x12>\n" +
|
||||||
|
"\x1cshow_navigate_on_start_icons\x18\x19 \x01(\bR\x18showNavigateOnStartIcons\"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" +
|
||||||
|
|
|
||||||
|
|
@ -1,37 +1,35 @@
|
||||||
module github.com/OliveTin/OliveTin
|
module github.com/OliveTin/OliveTin
|
||||||
|
|
||||||
go 1.24.0
|
go 1.25.0
|
||||||
|
|
||||||
toolchain go1.24.9
|
|
||||||
|
|
||||||
exclude google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884
|
exclude google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884
|
||||||
|
|
||||||
require (
|
require (
|
||||||
connectrpc.com/connect v1.19.1
|
connectrpc.com/connect v1.19.1
|
||||||
github.com/Masterminds/semver v1.5.0
|
github.com/Masterminds/semver v1.5.0
|
||||||
github.com/MicahParks/keyfunc/v3 v3.7.0
|
github.com/MicahParks/keyfunc/v3 v3.8.0
|
||||||
github.com/PaesslerAG/jsonpath v0.1.1
|
github.com/PaesslerAG/jsonpath v0.1.1
|
||||||
github.com/alexedwards/argon2id v1.0.0
|
github.com/alexedwards/argon2id v1.0.0
|
||||||
github.com/bufbuild/buf v1.64.0
|
github.com/bufbuild/buf v1.65.0
|
||||||
github.com/fsnotify/fsnotify v1.9.0
|
github.com/fsnotify/fsnotify v1.9.0
|
||||||
github.com/fzipp/gocyclo v0.6.0
|
github.com/fzipp/gocyclo v0.6.0
|
||||||
github.com/go-critic/go-critic v0.14.3
|
github.com/go-critic/go-critic v0.14.3
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.0
|
github.com/golang-jwt/jwt/v5 v5.3.1
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/jamesread/golure v0.0.0-20260104005024-ad0d6ec8c0ac
|
github.com/jamesread/golure v0.0.0-20260104005024-ad0d6ec8c0ac
|
||||||
github.com/knadh/koanf/parsers/yaml v1.1.0
|
github.com/knadh/koanf/parsers/yaml v1.1.0
|
||||||
github.com/knadh/koanf/providers/env v1.1.0
|
github.com/knadh/koanf/providers/env v1.1.0
|
||||||
github.com/knadh/koanf/providers/file v1.2.1
|
github.com/knadh/koanf/providers/file v1.2.1
|
||||||
github.com/knadh/koanf/providers/rawbytes v1.0.0
|
github.com/knadh/koanf/providers/rawbytes v1.0.0
|
||||||
github.com/knadh/koanf/v2 v2.3.0
|
github.com/knadh/koanf/v2 v2.3.2
|
||||||
github.com/prometheus/client_golang v1.23.2
|
github.com/prometheus/client_golang v1.23.2
|
||||||
github.com/robfig/cron/v3 v3.0.1
|
github.com/robfig/cron/v3 v3.0.1
|
||||||
github.com/sirupsen/logrus v1.9.4
|
github.com/sirupsen/logrus v1.9.4
|
||||||
github.com/stretchr/testify v1.11.1
|
github.com/stretchr/testify v1.11.1
|
||||||
go.akshayshah.org/connectproto v0.6.0
|
go.akshayshah.org/connectproto v0.6.0
|
||||||
golang.org/x/exp v0.0.0-20260112195511-716be5621a96
|
golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a
|
||||||
golang.org/x/oauth2 v0.34.0
|
golang.org/x/oauth2 v0.35.0
|
||||||
golang.org/x/sys v0.40.0
|
golang.org/x/sys v0.41.0
|
||||||
google.golang.org/protobuf v1.36.11
|
google.golang.org/protobuf v1.36.11
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
@ -39,15 +37,15 @@ require (
|
||||||
require (
|
require (
|
||||||
buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.11-20250718181942-e35f9b667443.1 // indirect
|
buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.11-20250718181942-e35f9b667443.1 // indirect
|
||||||
buf.build/gen/go/bufbuild/protodescriptor/protocolbuffers/go v1.36.11-20250109164928-1da0de137947.1 // indirect
|
buf.build/gen/go/bufbuild/protodescriptor/protocolbuffers/go v1.36.11-20250109164928-1da0de137947.1 // indirect
|
||||||
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20251209175733-2a1774d88802.1 // indirect
|
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260209202127-80ab13bee0bf.1 // indirect
|
||||||
buf.build/gen/go/bufbuild/registry/connectrpc/go v1.19.1-20251202164234-62b14f0b533c.2 // indirect
|
buf.build/gen/go/bufbuild/registry/connectrpc/go v1.19.1-20260126144947-819582968857.2 // indirect
|
||||||
buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.11-20251202164234-62b14f0b533c.1 // indirect
|
buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.11-20260126144947-819582968857.1 // indirect
|
||||||
buf.build/gen/go/pluginrpc/pluginrpc/protocolbuffers/go v1.36.11-20241007202033-cf42259fcbfc.1 // indirect
|
buf.build/gen/go/pluginrpc/pluginrpc/protocolbuffers/go v1.36.11-20241007202033-cf42259fcbfc.1 // indirect
|
||||||
buf.build/go/app v0.2.0 // indirect
|
buf.build/go/app v0.2.0 // indirect
|
||||||
buf.build/go/bufplugin v0.9.0 // indirect
|
buf.build/go/bufplugin v0.9.0 // indirect
|
||||||
buf.build/go/bufprivateusage v0.1.0 // indirect
|
buf.build/go/bufprivateusage v0.1.0 // indirect
|
||||||
buf.build/go/interrupt v1.1.0 // indirect
|
buf.build/go/interrupt v1.1.0 // indirect
|
||||||
buf.build/go/protovalidate v1.1.0 // indirect
|
buf.build/go/protovalidate v1.1.2 // indirect
|
||||||
buf.build/go/protoyaml v0.6.0 // indirect
|
buf.build/go/protoyaml v0.6.0 // indirect
|
||||||
buf.build/go/spdx v0.2.0 // indirect
|
buf.build/go/spdx v0.2.0 // indirect
|
||||||
buf.build/go/standard v0.1.0 // indirect
|
buf.build/go/standard v0.1.0 // indirect
|
||||||
|
|
@ -59,25 +57,25 @@ require (
|
||||||
github.com/PaesslerAG/gval v1.2.4 // indirect
|
github.com/PaesslerAG/gval v1.2.4 // indirect
|
||||||
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
|
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/bufbuild/protocompile v0.14.2-0.20260120135352-a3ed5cd7a608 // indirect
|
github.com/bufbuild/protocompile v0.14.2-0.20260130195850-5c64bed4577e // indirect
|
||||||
github.com/bufbuild/protoplugin v0.0.0-20250218205857-750e09ce93e1 // indirect
|
github.com/bufbuild/protoplugin v0.0.0-20250218205857-750e09ce93e1 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/cli/browser v1.3.0 // indirect
|
github.com/cli/browser v1.3.0 // indirect
|
||||||
github.com/containerd/errdefs v1.0.0 // indirect
|
github.com/containerd/errdefs v1.0.0 // indirect
|
||||||
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
||||||
github.com/containerd/stargz-snapshotter/estargz v0.18.1 // indirect
|
github.com/containerd/stargz-snapshotter/estargz v0.18.2 // indirect
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
|
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
|
||||||
github.com/cristalhq/acmd v0.12.0 // indirect
|
github.com/cristalhq/acmd v0.12.0 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/distribution/reference v0.6.0 // indirect
|
github.com/distribution/reference v0.6.0 // indirect
|
||||||
github.com/docker/cli v29.1.5+incompatible // indirect
|
github.com/docker/cli v29.2.1+incompatible // indirect
|
||||||
github.com/docker/distribution v2.8.3+incompatible // indirect
|
github.com/docker/distribution v2.8.3+incompatible // indirect
|
||||||
github.com/docker/docker v28.5.2+incompatible // indirect
|
github.com/docker/docker v28.5.2+incompatible // indirect
|
||||||
github.com/docker/docker-credential-helpers v0.9.5 // indirect
|
github.com/docker/docker-credential-helpers v0.9.5 // indirect
|
||||||
github.com/docker/go-connections v0.6.0 // indirect
|
github.com/docker/go-connections v0.6.0 // indirect
|
||||||
github.com/docker/go-units v0.5.0 // indirect
|
github.com/docker/go-units v0.5.0 // indirect
|
||||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||||
github.com/go-chi/chi/v5 v5.2.4 // indirect
|
github.com/go-chi/chi/v5 v5.2.5 // indirect
|
||||||
github.com/go-logr/logr v1.4.3 // indirect
|
github.com/go-logr/logr v1.4.3 // indirect
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
github.com/go-toolsmith/astcast v1.1.0 // indirect
|
github.com/go-toolsmith/astcast v1.1.0 // indirect
|
||||||
|
|
@ -90,13 +88,13 @@ require (
|
||||||
github.com/go-toolsmith/typep v1.1.0 // indirect
|
github.com/go-toolsmith/typep v1.1.0 // indirect
|
||||||
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
||||||
github.com/gofrs/flock v0.13.0 // indirect
|
github.com/gofrs/flock v0.13.0 // indirect
|
||||||
github.com/google/cel-go v0.26.1 // indirect
|
github.com/google/cel-go v0.27.0 // indirect
|
||||||
github.com/google/go-cmp v0.7.0 // indirect
|
github.com/google/go-cmp v0.7.0 // indirect
|
||||||
github.com/google/go-containerregistry v0.20.7 // indirect
|
github.com/google/go-containerregistry v0.20.7 // indirect
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/jdx/go-netrc v1.0.0 // indirect
|
github.com/jdx/go-netrc v1.0.0 // indirect
|
||||||
github.com/klauspost/compress v1.18.3 // indirect
|
github.com/klauspost/compress v1.18.4 // indirect
|
||||||
github.com/klauspost/pgzip v1.2.6 // indirect
|
github.com/klauspost/pgzip v1.2.6 // indirect
|
||||||
github.com/knadh/koanf/maps v0.1.2 // indirect
|
github.com/knadh/koanf/maps v0.1.2 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
|
|
@ -139,26 +137,26 @@ require (
|
||||||
go.lsp.dev/protocol v0.12.0 // indirect
|
go.lsp.dev/protocol v0.12.0 // indirect
|
||||||
go.lsp.dev/uri v0.3.0 // indirect
|
go.lsp.dev/uri v0.3.0 // indirect
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 // indirect
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 // indirect
|
||||||
go.opentelemetry.io/otel v1.39.0 // indirect
|
go.opentelemetry.io/otel v1.40.0 // indirect
|
||||||
go.opentelemetry.io/otel/metric v1.39.0 // indirect
|
go.opentelemetry.io/otel/metric v1.40.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.39.0 // indirect
|
go.opentelemetry.io/otel/trace v1.40.0 // indirect
|
||||||
go.uber.org/mock v0.6.0 // indirect
|
go.uber.org/mock v0.6.0 // indirect
|
||||||
go.uber.org/multierr v1.11.0 // indirect
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
go.uber.org/zap v1.27.1 // indirect
|
go.uber.org/zap v1.27.1 // indirect
|
||||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||||
golang.org/x/crypto v0.47.0 // indirect
|
golang.org/x/crypto v0.48.0 // indirect
|
||||||
golang.org/x/exp/typeparams v0.0.0-20260112195511-716be5621a96 // indirect
|
golang.org/x/exp/typeparams v0.0.0-20260212183809-81e46e3db34a // indirect
|
||||||
golang.org/x/mod v0.32.0 // indirect
|
golang.org/x/mod v0.33.0 // indirect
|
||||||
golang.org/x/net v0.49.0 // indirect
|
golang.org/x/net v0.50.0 // indirect
|
||||||
golang.org/x/sync v0.19.0 // indirect
|
golang.org/x/sync v0.19.0 // indirect
|
||||||
golang.org/x/term v0.39.0 // indirect
|
golang.org/x/term v0.40.0 // indirect
|
||||||
golang.org/x/text v0.33.0 // indirect
|
golang.org/x/text v0.34.0 // indirect
|
||||||
golang.org/x/time v0.14.0 // indirect
|
golang.org/x/time v0.14.0 // indirect
|
||||||
golang.org/x/tools v0.41.0 // indirect
|
golang.org/x/tools v0.42.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20260120221211-b8f7ae30c516 // indirect
|
google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect
|
||||||
google.golang.org/grpc v1.75.1 // indirect
|
google.golang.org/grpc v1.75.1 // indirect
|
||||||
mvdan.cc/xurls/v2 v2.6.0 // indirect
|
mvdan.cc/xurls/v2 v2.6.0 // indirect
|
||||||
pluginrpc.com/pluginrpc v0.5.0 // indirect
|
pluginrpc.com/pluginrpc v0.5.0 // indirect
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,20 @@ buf.build/gen/go/bufbuild/protodescriptor/protocolbuffers/go v1.36.11-2025010916
|
||||||
buf.build/gen/go/bufbuild/protodescriptor/protocolbuffers/go v1.36.11-20250109164928-1da0de137947.1/go.mod h1:8PRKXhgNes29Tjrnv8KdZzg3I1QceOkzibW1QK7EXv0=
|
buf.build/gen/go/bufbuild/protodescriptor/protocolbuffers/go v1.36.11-20250109164928-1da0de137947.1/go.mod h1:8PRKXhgNes29Tjrnv8KdZzg3I1QceOkzibW1QK7EXv0=
|
||||||
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20251209175733-2a1774d88802.1 h1:j9yeqTWEFrtimt8Nng2MIeRrpoCvQzM9/g25XTvqUGg=
|
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20251209175733-2a1774d88802.1 h1:j9yeqTWEFrtimt8Nng2MIeRrpoCvQzM9/g25XTvqUGg=
|
||||||
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20251209175733-2a1774d88802.1/go.mod h1:tvtbpgaVXZX4g6Pn+AnzFycuRK3MOz5HJfEGeEllXYM=
|
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20251209175733-2a1774d88802.1/go.mod h1:tvtbpgaVXZX4g6Pn+AnzFycuRK3MOz5HJfEGeEllXYM=
|
||||||
|
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260209202127-80ab13bee0bf.1 h1:PMmTMyvHScV9Mn8wc6ASge9uRcHy0jtqPd+fM35LmsQ=
|
||||||
|
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260209202127-80ab13bee0bf.1/go.mod h1:tvtbpgaVXZX4g6Pn+AnzFycuRK3MOz5HJfEGeEllXYM=
|
||||||
buf.build/gen/go/bufbuild/registry/connectrpc/go v1.19.1-20251202164234-62b14f0b533c.2 h1:eQ6XRVUaYYZFOZvBsyrOYLWbw6464s5dVnHscxa0b8w=
|
buf.build/gen/go/bufbuild/registry/connectrpc/go v1.19.1-20251202164234-62b14f0b533c.2 h1:eQ6XRVUaYYZFOZvBsyrOYLWbw6464s5dVnHscxa0b8w=
|
||||||
buf.build/gen/go/bufbuild/registry/connectrpc/go v1.19.1-20251202164234-62b14f0b533c.2/go.mod h1:omxVRch3jEPMINnUipLsuRWoEhND6LPXELKBG7xzyDw=
|
buf.build/gen/go/bufbuild/registry/connectrpc/go v1.19.1-20251202164234-62b14f0b533c.2/go.mod h1:omxVRch3jEPMINnUipLsuRWoEhND6LPXELKBG7xzyDw=
|
||||||
|
buf.build/gen/go/bufbuild/registry/connectrpc/go v1.19.1-20260122161138-ab4e39a3c3bc.2 h1:cMzWbIukJ5uk1M58CtqmBE7Ojacg/t2nAg4AbS78uX8=
|
||||||
|
buf.build/gen/go/bufbuild/registry/connectrpc/go v1.19.1-20260122161138-ab4e39a3c3bc.2/go.mod h1:GL3rFhQQsaI3PCBa0y5X71UHs6q5E/Xf9Q8WXBxE7a8=
|
||||||
|
buf.build/gen/go/bufbuild/registry/connectrpc/go v1.19.1-20260126144947-819582968857.2 h1:XPrWCd9ydEo5Ofv1aNJVJaxndMXLQjRO9vVzsJG3jL8=
|
||||||
|
buf.build/gen/go/bufbuild/registry/connectrpc/go v1.19.1-20260126144947-819582968857.2/go.mod h1:mpsjeEaxOYPIJV2cz4IagLghZufRvx+NPVtInjEeoQ8=
|
||||||
buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.11-20251202164234-62b14f0b533c.1 h1:PdfIJUbUVKdajMVYuMdvr2Wvo+wmzGnlPEYA4bhFaWI=
|
buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.11-20251202164234-62b14f0b533c.1 h1:PdfIJUbUVKdajMVYuMdvr2Wvo+wmzGnlPEYA4bhFaWI=
|
||||||
buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.11-20251202164234-62b14f0b533c.1/go.mod h1:1JJi9jvOqRxSMa+JxiZSm57doB+db/1WYCIa2lHfc40=
|
buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.11-20251202164234-62b14f0b533c.1/go.mod h1:1JJi9jvOqRxSMa+JxiZSm57doB+db/1WYCIa2lHfc40=
|
||||||
|
buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.11-20260122161138-ab4e39a3c3bc.1 h1:yWmrELGX6l1GphG9kPVcrMQLjWfXGI5bLDxwE+SfbDw=
|
||||||
|
buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.11-20260122161138-ab4e39a3c3bc.1/go.mod h1:1JJi9jvOqRxSMa+JxiZSm57doB+db/1WYCIa2lHfc40=
|
||||||
|
buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.11-20260126144947-819582968857.1 h1:Yreby6Ypa58wdQUEm9Fnc5g8n/jP487Dq3aK5yBYwfk=
|
||||||
|
buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.11-20260126144947-819582968857.1/go.mod h1:1JJi9jvOqRxSMa+JxiZSm57doB+db/1WYCIa2lHfc40=
|
||||||
buf.build/gen/go/pluginrpc/pluginrpc/protocolbuffers/go v1.36.11-20241007202033-cf42259fcbfc.1 h1:iGPvEJltOXUMANWf0zajcRcbiOXLD90ZwPUFvbcuv6Q=
|
buf.build/gen/go/pluginrpc/pluginrpc/protocolbuffers/go v1.36.11-20241007202033-cf42259fcbfc.1 h1:iGPvEJltOXUMANWf0zajcRcbiOXLD90ZwPUFvbcuv6Q=
|
||||||
buf.build/gen/go/pluginrpc/pluginrpc/protocolbuffers/go v1.36.11-20241007202033-cf42259fcbfc.1/go.mod h1:nWVKKRA29zdt4uvkjka3i/y4mkrswyWwiu0TbdX0zts=
|
buf.build/gen/go/pluginrpc/pluginrpc/protocolbuffers/go v1.36.11-20241007202033-cf42259fcbfc.1/go.mod h1:nWVKKRA29zdt4uvkjka3i/y4mkrswyWwiu0TbdX0zts=
|
||||||
buf.build/go/app v0.2.0 h1:NYaH13A+RzPb7M5vO8uZYZ2maBZI5+MS9A9tQm66fy8=
|
buf.build/go/app v0.2.0 h1:NYaH13A+RzPb7M5vO8uZYZ2maBZI5+MS9A9tQm66fy8=
|
||||||
|
|
@ -20,6 +30,8 @@ buf.build/go/interrupt v1.1.0 h1:olBuhgv9Sav4/9pkSLoxgiOsZDgM5VhRhvRpn3DL0lE=
|
||||||
buf.build/go/interrupt v1.1.0/go.mod h1:ql56nXPG1oHlvZa6efNC7SKAQ/tUjS6z0mhJl0gyeRM=
|
buf.build/go/interrupt v1.1.0/go.mod h1:ql56nXPG1oHlvZa6efNC7SKAQ/tUjS6z0mhJl0gyeRM=
|
||||||
buf.build/go/protovalidate v1.1.0 h1:pQqEQRpOo4SqS60qkvmhLTTQU9JwzEvdyiqAtXa5SeY=
|
buf.build/go/protovalidate v1.1.0 h1:pQqEQRpOo4SqS60qkvmhLTTQU9JwzEvdyiqAtXa5SeY=
|
||||||
buf.build/go/protovalidate v1.1.0/go.mod h1:bGZcPiAQDC3ErCHK3t74jSoJDFOs2JH3d7LWuTEIdss=
|
buf.build/go/protovalidate v1.1.0/go.mod h1:bGZcPiAQDC3ErCHK3t74jSoJDFOs2JH3d7LWuTEIdss=
|
||||||
|
buf.build/go/protovalidate v1.1.2 h1:83vYHoY8f34hB8MeitGaYE3CGVPFxwdEUuskh5qQpA0=
|
||||||
|
buf.build/go/protovalidate v1.1.2/go.mod h1:Ez3z+w4c+wG+EpW8ovgZaZPnPl2XVF6kaxgcv1NG/QE=
|
||||||
buf.build/go/protoyaml v0.6.0 h1:Nzz1lvcXF8YgNZXk+voPPwdU8FjDPTUV4ndNTXN0n2w=
|
buf.build/go/protoyaml v0.6.0 h1:Nzz1lvcXF8YgNZXk+voPPwdU8FjDPTUV4ndNTXN0n2w=
|
||||||
buf.build/go/protoyaml v0.6.0/go.mod h1:RgUOsBu/GYKLDSIRgQXniXbNgFlGEZnQpRAUdLAFV2Q=
|
buf.build/go/protoyaml v0.6.0/go.mod h1:RgUOsBu/GYKLDSIRgQXniXbNgFlGEZnQpRAUdLAFV2Q=
|
||||||
buf.build/go/spdx v0.2.0 h1:IItqM0/cMxvFJJumcBuP8NrsIzMs/UYjp/6WSpq8LTw=
|
buf.build/go/spdx v0.2.0 h1:IItqM0/cMxvFJJumcBuP8NrsIzMs/UYjp/6WSpq8LTw=
|
||||||
|
|
@ -42,6 +54,8 @@ github.com/MicahParks/jwkset v0.11.0 h1:yc0zG+jCvZpWgFDFmvs8/8jqqVBG9oyIbmBtmjOh
|
||||||
github.com/MicahParks/jwkset v0.11.0/go.mod h1:U2oRhRaLgDCLjtpGL2GseNKGmZtLs/3O7p+OZaL5vo0=
|
github.com/MicahParks/jwkset v0.11.0/go.mod h1:U2oRhRaLgDCLjtpGL2GseNKGmZtLs/3O7p+OZaL5vo0=
|
||||||
github.com/MicahParks/keyfunc/v3 v3.7.0 h1:pdafUNyq+p3ZlvjJX1HWFP7MA3+cLpDtg69U3kITJGM=
|
github.com/MicahParks/keyfunc/v3 v3.7.0 h1:pdafUNyq+p3ZlvjJX1HWFP7MA3+cLpDtg69U3kITJGM=
|
||||||
github.com/MicahParks/keyfunc/v3 v3.7.0/go.mod h1:z66bkCviwqfg2YUp+Jcc/xRE9IXLcMq6DrgV/+Htru0=
|
github.com/MicahParks/keyfunc/v3 v3.7.0/go.mod h1:z66bkCviwqfg2YUp+Jcc/xRE9IXLcMq6DrgV/+Htru0=
|
||||||
|
github.com/MicahParks/keyfunc/v3 v3.8.0 h1:Hx2dgIjAXGk9slakM6rV9BOeaWDPEXXZ4Us8guNBfds=
|
||||||
|
github.com/MicahParks/keyfunc/v3 v3.8.0/go.mod h1:z66bkCviwqfg2YUp+Jcc/xRE9IXLcMq6DrgV/+Htru0=
|
||||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||||
github.com/PaesslerAG/gval v1.0.0 h1:GEKnRwkWDdf9dOmKcNrar9EA1bz1z9DqPIO1+iLzhd8=
|
github.com/PaesslerAG/gval v1.0.0 h1:GEKnRwkWDdf9dOmKcNrar9EA1bz1z9DqPIO1+iLzhd8=
|
||||||
|
|
@ -59,6 +73,7 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
github.com/bmatcuk/doublestar/v4 v4.9.1 h1:X8jg9rRZmJd4yRy7ZeNDRnM+T3ZfHv15JiBJ/avrEXE=
|
github.com/bmatcuk/doublestar/v4 v4.9.1 h1:X8jg9rRZmJd4yRy7ZeNDRnM+T3ZfHv15JiBJ/avrEXE=
|
||||||
github.com/bmatcuk/doublestar/v4 v4.9.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
|
github.com/bmatcuk/doublestar/v4 v4.9.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
|
||||||
|
github.com/bmatcuk/doublestar/v4 v4.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6TYWexEs=
|
||||||
github.com/brianvoe/gofakeit/v6 v6.28.0 h1:Xib46XXuQfmlLS2EXRuJpqcw8St6qSZz75OUo0tgAW4=
|
github.com/brianvoe/gofakeit/v6 v6.28.0 h1:Xib46XXuQfmlLS2EXRuJpqcw8St6qSZz75OUo0tgAW4=
|
||||||
github.com/brianvoe/gofakeit/v6 v6.28.0/go.mod h1:Xj58BMSnFqcn/fAQeSK+/PLtC5kSb7FJIq4JyGa8vEs=
|
github.com/brianvoe/gofakeit/v6 v6.28.0/go.mod h1:Xj58BMSnFqcn/fAQeSK+/PLtC5kSb7FJIq4JyGa8vEs=
|
||||||
github.com/bufbuild/buf v1.61.0 h1:JPaK/RM2eoheyzznW+1LxaFgN6xjBCi8s25q2kUbH9A=
|
github.com/bufbuild/buf v1.61.0 h1:JPaK/RM2eoheyzznW+1LxaFgN6xjBCi8s25q2kUbH9A=
|
||||||
|
|
@ -67,12 +82,16 @@ github.com/bufbuild/buf v1.63.0 h1:vMIRozWqYcOU992FqcGgAp8LjoWVTzr52qEolUT+xi4=
|
||||||
github.com/bufbuild/buf v1.63.0/go.mod h1:IWF+TIxwmk4DeyDmguN8WhxKFKQitcX2WnP+RlJlDiY=
|
github.com/bufbuild/buf v1.63.0/go.mod h1:IWF+TIxwmk4DeyDmguN8WhxKFKQitcX2WnP+RlJlDiY=
|
||||||
github.com/bufbuild/buf v1.64.0 h1:puHWFcVKmZFSu4KuaN0kZiQ32n7VVc3un1FeLU77XUs=
|
github.com/bufbuild/buf v1.64.0 h1:puHWFcVKmZFSu4KuaN0kZiQ32n7VVc3un1FeLU77XUs=
|
||||||
github.com/bufbuild/buf v1.64.0/go.mod h1:U4ISwkjZXRLMaCkPG9zp1xY3xHEIwhCFwyNAaA56SGw=
|
github.com/bufbuild/buf v1.64.0/go.mod h1:U4ISwkjZXRLMaCkPG9zp1xY3xHEIwhCFwyNAaA56SGw=
|
||||||
|
github.com/bufbuild/buf v1.65.0 h1:f2BzeCY9rRh9P5KD340ZoPAaFLTkssoUTHx7lpqozgg=
|
||||||
|
github.com/bufbuild/buf v1.65.0/go.mod h1:7SAs2YqGpPXHqBBXBeYQbCzY0OQq4Jbg6XCqirEiYvQ=
|
||||||
github.com/bufbuild/protocompile v0.14.2-0.20251223142729-db46c1b9d34e h1:LQA+1MyiPkolGHJGC2GMDC5Xu+0RDVH6jGMKech7Exs=
|
github.com/bufbuild/protocompile v0.14.2-0.20251223142729-db46c1b9d34e h1:LQA+1MyiPkolGHJGC2GMDC5Xu+0RDVH6jGMKech7Exs=
|
||||||
github.com/bufbuild/protocompile v0.14.2-0.20251223142729-db46c1b9d34e/go.mod h1:5UUj46Eu+U+C59C5N6YilaMI7WWfP2bW9xGcOkme2DI=
|
github.com/bufbuild/protocompile v0.14.2-0.20251223142729-db46c1b9d34e/go.mod h1:5UUj46Eu+U+C59C5N6YilaMI7WWfP2bW9xGcOkme2DI=
|
||||||
github.com/bufbuild/protocompile v0.14.2-0.20260105175043-4d8d90b1c6b8 h1:cQYwUyAzyMmYr7AyJU1C6pVCpUrJJBkmx7UunZosxxs=
|
github.com/bufbuild/protocompile v0.14.2-0.20260105175043-4d8d90b1c6b8 h1:cQYwUyAzyMmYr7AyJU1C6pVCpUrJJBkmx7UunZosxxs=
|
||||||
github.com/bufbuild/protocompile v0.14.2-0.20260105175043-4d8d90b1c6b8/go.mod h1:5UUj46Eu+U+C59C5N6YilaMI7WWfP2bW9xGcOkme2DI=
|
github.com/bufbuild/protocompile v0.14.2-0.20260105175043-4d8d90b1c6b8/go.mod h1:5UUj46Eu+U+C59C5N6YilaMI7WWfP2bW9xGcOkme2DI=
|
||||||
github.com/bufbuild/protocompile v0.14.2-0.20260120135352-a3ed5cd7a608 h1:3aRREBMDRbAajlaYTtD4uC9f2UYbqqyhaveDqJ35G/w=
|
github.com/bufbuild/protocompile v0.14.2-0.20260120135352-a3ed5cd7a608 h1:3aRREBMDRbAajlaYTtD4uC9f2UYbqqyhaveDqJ35G/w=
|
||||||
github.com/bufbuild/protocompile v0.14.2-0.20260120135352-a3ed5cd7a608/go.mod h1:5UUj46Eu+U+C59C5N6YilaMI7WWfP2bW9xGcOkme2DI=
|
github.com/bufbuild/protocompile v0.14.2-0.20260120135352-a3ed5cd7a608/go.mod h1:5UUj46Eu+U+C59C5N6YilaMI7WWfP2bW9xGcOkme2DI=
|
||||||
|
github.com/bufbuild/protocompile v0.14.2-0.20260130195850-5c64bed4577e h1:emH16Bf1w4C0cJ3ge4QtBAl4sIYJe23EfpWH0SpA9co=
|
||||||
|
github.com/bufbuild/protocompile v0.14.2-0.20260130195850-5c64bed4577e/go.mod h1:cxhE8h+14t0Yxq2H9MV/UggzQ1L0gh0t2tJobITWsBE=
|
||||||
github.com/bufbuild/protoplugin v0.0.0-20250218205857-750e09ce93e1 h1:V1xulAoqLqVg44rY97xOR+mQpD2N+GzhMHVwJ030WEU=
|
github.com/bufbuild/protoplugin v0.0.0-20250218205857-750e09ce93e1 h1:V1xulAoqLqVg44rY97xOR+mQpD2N+GzhMHVwJ030WEU=
|
||||||
github.com/bufbuild/protoplugin v0.0.0-20250218205857-750e09ce93e1/go.mod h1:c5D8gWRIZ2HLWO3gXYTtUfw/hbJyD8xikv2ooPxnklQ=
|
github.com/bufbuild/protoplugin v0.0.0-20250218205857-750e09ce93e1/go.mod h1:c5D8gWRIZ2HLWO3gXYTtUfw/hbJyD8xikv2ooPxnklQ=
|
||||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||||
|
|
@ -89,6 +108,8 @@ github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
||||||
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
||||||
github.com/containerd/stargz-snapshotter/estargz v0.18.1 h1:cy2/lpgBXDA3cDKSyEfNOFMA/c10O1axL69EU7iirO8=
|
github.com/containerd/stargz-snapshotter/estargz v0.18.1 h1:cy2/lpgBXDA3cDKSyEfNOFMA/c10O1axL69EU7iirO8=
|
||||||
github.com/containerd/stargz-snapshotter/estargz v0.18.1/go.mod h1:ALIEqa7B6oVDsrF37GkGN20SuvG/pIMm7FwP7ZmRb0Q=
|
github.com/containerd/stargz-snapshotter/estargz v0.18.1/go.mod h1:ALIEqa7B6oVDsrF37GkGN20SuvG/pIMm7FwP7ZmRb0Q=
|
||||||
|
github.com/containerd/stargz-snapshotter/estargz v0.18.2 h1:yXkZFYIzz3eoLwlTUZKz2iQ4MrckBxJjkmD16ynUTrw=
|
||||||
|
github.com/containerd/stargz-snapshotter/estargz v0.18.2/go.mod h1:XyVU5tcJ3PRpkA9XS2T5us6Eg35yM0214Y+wvrZTBrY=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
|
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||||
|
|
@ -105,6 +126,8 @@ github.com/docker/cli v29.1.3+incompatible h1:+kz9uDWgs+mAaIZojWfFt4d53/jv0ZUOOo
|
||||||
github.com/docker/cli v29.1.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
github.com/docker/cli v29.1.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||||
github.com/docker/cli v29.1.5+incompatible h1:GckbANUt3j+lsnQ6eCcQd70mNSOismSHWt8vk2AX8ao=
|
github.com/docker/cli v29.1.5+incompatible h1:GckbANUt3j+lsnQ6eCcQd70mNSOismSHWt8vk2AX8ao=
|
||||||
github.com/docker/cli v29.1.5+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
github.com/docker/cli v29.1.5+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||||
|
github.com/docker/cli v29.2.1+incompatible h1:n3Jt0QVCN65eiVBoUTZQM9mcQICCJt3akW4pKAbKdJg=
|
||||||
|
github.com/docker/cli v29.2.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||||
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
|
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
|
||||||
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||||
github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM=
|
github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM=
|
||||||
|
|
@ -127,6 +150,8 @@ github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE=
|
||||||
github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
|
github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
|
||||||
github.com/go-chi/chi/v5 v5.2.4 h1:WtFKPHwlywe8Srng8j2BhOD9312j9cGUxG1SP4V2cR4=
|
github.com/go-chi/chi/v5 v5.2.4 h1:WtFKPHwlywe8Srng8j2BhOD9312j9cGUxG1SP4V2cR4=
|
||||||
github.com/go-chi/chi/v5 v5.2.4/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
|
github.com/go-chi/chi/v5 v5.2.4/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
|
||||||
|
github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
|
||||||
|
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
|
||||||
github.com/go-critic/go-critic v0.14.2 h1:PMvP5f+LdR8p6B29npvChUXbD1vrNlKDf60NJtgMBOo=
|
github.com/go-critic/go-critic v0.14.2 h1:PMvP5f+LdR8p6B29npvChUXbD1vrNlKDf60NJtgMBOo=
|
||||||
github.com/go-critic/go-critic v0.14.2/go.mod h1:xwntfW6SYAd7h1OqDzmN6hBX/JxsEKl5up/Y2bsxgVQ=
|
github.com/go-critic/go-critic v0.14.2/go.mod h1:xwntfW6SYAd7h1OqDzmN6hBX/JxsEKl5up/Y2bsxgVQ=
|
||||||
github.com/go-critic/go-critic v0.14.3 h1:5R1qH2iFeo4I/RJU8vTezdqs08Egi4u5p6vOESA0pog=
|
github.com/go-critic/go-critic v0.14.3 h1:5R1qH2iFeo4I/RJU8vTezdqs08Egi4u5p6vOESA0pog=
|
||||||
|
|
@ -163,8 +188,12 @@ github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=
|
||||||
github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=
|
github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||||
github.com/google/cel-go v0.26.1 h1:iPbVVEdkhTX++hpe3lzSk7D3G3QSYqLGoHOcEio+UXQ=
|
github.com/google/cel-go v0.26.1 h1:iPbVVEdkhTX++hpe3lzSk7D3G3QSYqLGoHOcEio+UXQ=
|
||||||
github.com/google/cel-go v0.26.1/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM=
|
github.com/google/cel-go v0.26.1/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM=
|
||||||
|
github.com/google/cel-go v0.27.0 h1:e7ih85+4qVrBuqQWTW4FKSqZYokVuc3HnhH5keboFTo=
|
||||||
|
github.com/google/cel-go v0.27.0/go.mod h1:tTJ11FWqnhw5KKpnWpvW9CJC3Y9GK4EIS0WXnBbebzw=
|
||||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
|
|
@ -189,6 +218,8 @@ github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uq
|
||||||
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||||
github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw=
|
github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw=
|
||||||
github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||||
|
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
|
||||||
|
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||||
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
|
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
|
||||||
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
||||||
github.com/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpbo=
|
github.com/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpbo=
|
||||||
|
|
@ -203,6 +234,8 @@ github.com/knadh/koanf/providers/rawbytes v1.0.0 h1:MrKDh/HksJlKJmaZjgs4r8aVBb/z
|
||||||
github.com/knadh/koanf/providers/rawbytes v1.0.0/go.mod h1:KxwYJf1uezTKy6PBtfE+m725NGp4GPVA7XoNTJ/PtLo=
|
github.com/knadh/koanf/providers/rawbytes v1.0.0/go.mod h1:KxwYJf1uezTKy6PBtfE+m725NGp4GPVA7XoNTJ/PtLo=
|
||||||
github.com/knadh/koanf/v2 v2.3.0 h1:Qg076dDRFHvqnKG97ZEsi9TAg2/nFTa9hCdcSa1lvlM=
|
github.com/knadh/koanf/v2 v2.3.0 h1:Qg076dDRFHvqnKG97ZEsi9TAg2/nFTa9hCdcSa1lvlM=
|
||||||
github.com/knadh/koanf/v2 v2.3.0/go.mod h1:gRb40VRAbd4iJMYYD5IxZ6hfuopFcXBpc9bbQpZwo28=
|
github.com/knadh/koanf/v2 v2.3.0/go.mod h1:gRb40VRAbd4iJMYYD5IxZ6hfuopFcXBpc9bbQpZwo28=
|
||||||
|
github.com/knadh/koanf/v2 v2.3.2 h1:Ee6tuzQYFwcZXQpc2MiVeC6qHMandf5SMUJJNoFp/c4=
|
||||||
|
github.com/knadh/koanf/v2 v2.3.2/go.mod h1:gRb40VRAbd4iJMYYD5IxZ6hfuopFcXBpc9bbQpZwo28=
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
|
@ -333,8 +366,12 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 h1:ssfIgGNANqpVFCndZvcuyKbl0g+UAVcbBcqGkG28H0Y=
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 h1:ssfIgGNANqpVFCndZvcuyKbl0g+UAVcbBcqGkG28H0Y=
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0/go.mod h1:GQ/474YrbE4Jx8gZ4q5I4hrhUzM6UPzyrqJYV2AqPoQ=
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0/go.mod h1:GQ/474YrbE4Jx8gZ4q5I4hrhUzM6UPzyrqJYV2AqPoQ=
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8=
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0=
|
||||||
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
|
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
|
||||||
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
|
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
|
||||||
|
go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
|
||||||
|
go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 h1:f0cb2XPmrqn4XMy9PNliTgRKJgS5WcL/u0/WRYGz4t0=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 h1:f0cb2XPmrqn4XMy9PNliTgRKJgS5WcL/u0/WRYGz4t0=
|
||||||
|
|
@ -342,12 +379,18 @@ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0 h1:wpMfg
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0/go.mod h1:wAy0T/dUbs468uOlkT31xjvqQgEVXv58BRFWEgn5v/0=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0/go.mod h1:wAy0T/dUbs468uOlkT31xjvqQgEVXv58BRFWEgn5v/0=
|
||||||
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
|
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
|
||||||
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
|
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
|
||||||
|
go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=
|
||||||
|
go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=
|
||||||
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
|
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
|
||||||
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
|
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8=
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
|
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
|
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
|
||||||
|
go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw=
|
||||||
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
|
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
|
||||||
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
|
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
|
||||||
|
go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=
|
||||||
|
go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=
|
||||||
go.opentelemetry.io/proto/otlp v1.8.0 h1:fRAZQDcAFHySxpJ1TwlA1cJ4tvcrw7nXl9xWWC8N5CE=
|
go.opentelemetry.io/proto/otlp v1.8.0 h1:fRAZQDcAFHySxpJ1TwlA1cJ4tvcrw7nXl9xWWC8N5CE=
|
||||||
go.opentelemetry.io/proto/otlp v1.8.0/go.mod h1:tIeYOeNBU4cvmPqpaji1P+KbB4Oloai8wN4rWzRrFF0=
|
go.opentelemetry.io/proto/otlp v1.8.0/go.mod h1:tIeYOeNBU4cvmPqpaji1P+KbB4Oloai8wN4rWzRrFF0=
|
||||||
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
|
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
|
||||||
|
|
@ -370,22 +413,30 @@ golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||||
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||||
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
|
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
|
||||||
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
|
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
|
||||||
|
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||||
|
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||||
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0=
|
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0=
|
||||||
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU=
|
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU=
|
||||||
golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU=
|
golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU=
|
||||||
golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU=
|
golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU=
|
||||||
|
golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a h1:ovFr6Z0MNmU7nH8VaX5xqw+05ST2uO1exVfZPVqRC5o=
|
||||||
|
golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA=
|
||||||
golang.org/x/exp/typeparams v0.0.0-20220428152302-39d4317da171/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
|
golang.org/x/exp/typeparams v0.0.0-20220428152302-39d4317da171/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
|
||||||
golang.org/x/exp/typeparams v0.0.0-20230203172020-98cc5a0785f9/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
|
golang.org/x/exp/typeparams v0.0.0-20230203172020-98cc5a0785f9/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
|
||||||
golang.org/x/exp/typeparams v0.0.0-20251219203646-944ab1f22d93 h1:PbC785RGO6yPO051ItgbG/adwoKRWC0VS7kXXeD/iqk=
|
golang.org/x/exp/typeparams v0.0.0-20251219203646-944ab1f22d93 h1:PbC785RGO6yPO051ItgbG/adwoKRWC0VS7kXXeD/iqk=
|
||||||
golang.org/x/exp/typeparams v0.0.0-20251219203646-944ab1f22d93/go.mod h1:4Mzdyp/6jzw9auFDJ3OMF5qksa7UvPnzKqTVGcb04ms=
|
golang.org/x/exp/typeparams v0.0.0-20251219203646-944ab1f22d93/go.mod h1:4Mzdyp/6jzw9auFDJ3OMF5qksa7UvPnzKqTVGcb04ms=
|
||||||
golang.org/x/exp/typeparams v0.0.0-20260112195511-716be5621a96 h1:RMc8anw0hCPcg5CZYN2PEQ8nMwosk461R6vFwPrCFVg=
|
golang.org/x/exp/typeparams v0.0.0-20260112195511-716be5621a96 h1:RMc8anw0hCPcg5CZYN2PEQ8nMwosk461R6vFwPrCFVg=
|
||||||
golang.org/x/exp/typeparams v0.0.0-20260112195511-716be5621a96/go.mod h1:4Mzdyp/6jzw9auFDJ3OMF5qksa7UvPnzKqTVGcb04ms=
|
golang.org/x/exp/typeparams v0.0.0-20260112195511-716be5621a96/go.mod h1:4Mzdyp/6jzw9auFDJ3OMF5qksa7UvPnzKqTVGcb04ms=
|
||||||
|
golang.org/x/exp/typeparams v0.0.0-20260212183809-81e46e3db34a h1:n3SZDk8iNpMasCwQD7/0dIaCVf3gJiGZ9Rqa094jUN0=
|
||||||
|
golang.org/x/exp/typeparams v0.0.0-20260212183809-81e46e3db34a/go.mod h1:PqrXSW65cXDZH0k4IeUbhmg/bcAZDbzNz3byBpKCsXo=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
|
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
|
||||||
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
|
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
|
||||||
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
|
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
|
||||||
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
|
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
|
||||||
|
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
|
||||||
|
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
|
|
@ -395,8 +446,12 @@ golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||||
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||||
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
|
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
|
||||||
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
|
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
|
||||||
|
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
|
||||||
|
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
|
||||||
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
|
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
|
||||||
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||||
|
golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=
|
||||||
|
golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
|
@ -417,6 +472,8 @@ golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
|
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||||
|
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
|
|
@ -426,6 +483,8 @@ golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
|
||||||
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
|
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
|
||||||
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
|
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
|
||||||
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
|
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
|
||||||
|
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
|
||||||
|
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
|
@ -436,6 +495,8 @@ golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||||
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
|
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
|
||||||
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
|
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
|
||||||
|
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||||
|
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
|
@ -446,16 +507,26 @@ golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
|
||||||
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
|
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
|
||||||
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
|
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
|
||||||
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
|
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
|
||||||
|
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
|
||||||
|
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b h1:uA40e2M6fYRBf0+8uN5mLlqUtV192iiksiICIBkYJ1E=
|
google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b h1:uA40e2M6fYRBf0+8uN5mLlqUtV192iiksiICIBkYJ1E=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:Xa7le7qx2vmqB/SzWUBa7KdMjpdpAHlh5QCSnjessQk=
|
google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:Xa7le7qx2vmqB/SzWUBa7KdMjpdpAHlh5QCSnjessQk=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20260120221211-b8f7ae30c516 h1:vmC/ws+pLzWjj/gzApyoZuSVrDtF1aod4u/+bbj8hgM=
|
google.golang.org/genproto/googleapis/api v0.0.0-20260120221211-b8f7ae30c516 h1:vmC/ws+pLzWjj/gzApyoZuSVrDtF1aod4u/+bbj8hgM=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20260120221211-b8f7ae30c516/go.mod h1:p3MLuOwURrGBRoEyFHBT3GjUwaCQVKeNqqWxlcISGdw=
|
google.golang.org/genproto/googleapis/api v0.0.0-20260120221211-b8f7ae30c516/go.mod h1:p3MLuOwURrGBRoEyFHBT3GjUwaCQVKeNqqWxlcISGdw=
|
||||||
|
google.golang.org/genproto/googleapis/api v0.0.0-20260122232226-8e98ce8d340d h1:tUKoKfdZnSjTf5LW7xpG4c6SZ3Ozisn5eumcoTuMEN4=
|
||||||
|
google.golang.org/genproto/googleapis/api v0.0.0-20260122232226-8e98ce8d340d/go.mod h1:p3MLuOwURrGBRoEyFHBT3GjUwaCQVKeNqqWxlcISGdw=
|
||||||
|
google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 h1:JLQynH/LBHfCTSbDWl+py8C+Rg/k1OVH3xfcaiANuF0=
|
||||||
|
google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:kSJwQxqmFXeo79zOmbrALdflXQeAYcUbgS7PbpMknCY=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b h1:Mv8VFug0MP9e5vUxfBcE3vUkV6CImK3cMNMIDFjmzxU=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b h1:Mv8VFug0MP9e5vUxfBcE3vUkV6CImK3cMNMIDFjmzxU=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 h1:sNrWoksmOyF5bvJUcnmbeAmQi8baNhqg5IWaI3llQqU=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516 h1:sNrWoksmOyF5bvJUcnmbeAmQi8baNhqg5IWaI3llQqU=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260122232226-8e98ce8d340d h1:xXzuihhT3gL/ntduUZwHECzAn57E8dA6l8SOtYWdD8Q=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260122232226-8e98ce8d340d/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
|
||||||
google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI=
|
google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI=
|
||||||
google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
|
google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
|
||||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ import (
|
||||||
entities "github.com/OliveTin/OliveTin/internal/entities"
|
entities "github.com/OliveTin/OliveTin/internal/entities"
|
||||||
executor "github.com/OliveTin/OliveTin/internal/executor"
|
executor "github.com/OliveTin/OliveTin/internal/executor"
|
||||||
installationinfo "github.com/OliveTin/OliveTin/internal/installationinfo"
|
installationinfo "github.com/OliveTin/OliveTin/internal/installationinfo"
|
||||||
|
"github.com/OliveTin/OliveTin/internal/tpl"
|
||||||
connectproto "go.akshayshah.org/connectproto"
|
connectproto "go.akshayshah.org/connectproto"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -307,11 +308,11 @@ func (api *oliveTinAPI) internalLogEntryToPb(logEntry *executor.InternalLogEntry
|
||||||
ExecutionStarted: logEntry.ExecutionStarted,
|
ExecutionStarted: logEntry.ExecutionStarted,
|
||||||
ExecutionFinished: logEntry.ExecutionFinished,
|
ExecutionFinished: logEntry.ExecutionFinished,
|
||||||
User: logEntry.Username,
|
User: logEntry.Username,
|
||||||
BindingId: logEntry.Binding.ID,
|
BindingId: logEntry.GetBindingId(),
|
||||||
DatetimeRateLimitExpires: calculateRateLimitExpires(api, logEntry),
|
DatetimeRateLimitExpires: calculateRateLimitExpires(api, logEntry),
|
||||||
}
|
}
|
||||||
|
|
||||||
if !pble.ExecutionFinished {
|
if !pble.ExecutionFinished && logEntry.Binding != nil && logEntry.Binding.Action != nil {
|
||||||
pble.CanKill = acl.IsAllowedKill(api.cfg, authenticatedUser, logEntry.Binding.Action)
|
pble.CanKill = acl.IsAllowedKill(api.cfg, authenticatedUser, logEntry.Binding.Action)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -709,8 +710,12 @@ func (api *oliveTinAPI) DumpVars(ctx ctx.Context, req *connect.Request[apiv1.Dum
|
||||||
return connect.NewResponse(res), nil
|
return connect.NewResponse(res), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
jsonstring, _ := json.MarshalIndent(entities.GetAll(), "", " ")
|
jsonstring, err := json.MarshalIndent(tpl.GetNewGeneralTemplateContext(), "", " ")
|
||||||
fmt.Printf("%s", &jsonstring)
|
if err != nil {
|
||||||
|
log.WithError(err).Error("DumpVars: failed to marshal template context from GetNewGeneralTemplateContext")
|
||||||
|
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("dump vars: marshal template context: %w", err))
|
||||||
|
}
|
||||||
|
fmt.Printf("%s", jsonstring)
|
||||||
|
|
||||||
res.Alert = "Dumping variables has been enabled in the configuration. Please set InsecureAllowDumpVars = false again after you don't need it anymore"
|
res.Alert = "Dumping variables has been enabled in the configuration. Please set InsecureAllowDumpVars = false again after you don't need it anymore"
|
||||||
|
|
||||||
|
|
@ -905,6 +910,7 @@ func (api *oliveTinAPI) Init(ctx ctx.Context, req *connect.Request[apiv1.InitReq
|
||||||
ShowLogList: user.EffectivePolicy.ShowLogList,
|
ShowLogList: user.EffectivePolicy.ShowLogList,
|
||||||
LoginRequired: loginRequired,
|
LoginRequired: loginRequired,
|
||||||
AvailableThemes: discoverAvailableThemes(api.cfg),
|
AvailableThemes: discoverAvailableThemes(api.cfg),
|
||||||
|
ShowNavigateOnStartIcons: api.cfg.ShowNavigateOnStartIcons,
|
||||||
}
|
}
|
||||||
|
|
||||||
return connect.NewResponse(res), nil
|
return connect.NewResponse(res), nil
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import (
|
||||||
config "github.com/OliveTin/OliveTin/internal/config"
|
config "github.com/OliveTin/OliveTin/internal/config"
|
||||||
entities "github.com/OliveTin/OliveTin/internal/entities"
|
entities "github.com/OliveTin/OliveTin/internal/entities"
|
||||||
executor "github.com/OliveTin/OliveTin/internal/executor"
|
executor "github.com/OliveTin/OliveTin/internal/executor"
|
||||||
|
"github.com/OliveTin/OliveTin/internal/tpl"
|
||||||
)
|
)
|
||||||
|
|
||||||
type DashboardRenderRequest struct {
|
type DashboardRenderRequest struct {
|
||||||
|
|
@ -66,7 +67,7 @@ func evaluateEnabledExpression(action *config.Action, entity *entities.Entity) b
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
result := entities.ParseTemplateWith(action.EnabledExpression, entity)
|
result := tpl.ParseTemplateOfActionBeforeExec(action.EnabledExpression, entity)
|
||||||
result = strings.TrimSpace(result)
|
result = strings.TrimSpace(result)
|
||||||
|
|
||||||
if result == "" {
|
if result == "" {
|
||||||
|
|
@ -105,6 +106,16 @@ func evaluateResultValue(result string) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getDefaultArgumentValue(cfgArg config.ActionArgument, entity *entities.Entity) string {
|
||||||
|
defaultValue := cfgArg.Default
|
||||||
|
|
||||||
|
if defaultValue != "" {
|
||||||
|
defaultValue = tpl.ParseTemplateOfActionBeforeExec(defaultValue, entity)
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
func buildAction(actionBinding *executor.ActionBinding, rr *DashboardRenderRequest) *apiv1.Action {
|
func buildAction(actionBinding *executor.ActionBinding, rr *DashboardRenderRequest) *apiv1.Action {
|
||||||
action := actionBinding.Action
|
action := actionBinding.Action
|
||||||
|
|
||||||
|
|
@ -120,8 +131,8 @@ func buildAction(actionBinding *executor.ActionBinding, rr *DashboardRenderReque
|
||||||
|
|
||||||
btn := apiv1.Action{
|
btn := apiv1.Action{
|
||||||
BindingId: actionBinding.ID,
|
BindingId: actionBinding.ID,
|
||||||
Title: entities.ParseTemplateWith(action.Title, actionBinding.Entity),
|
Title: tpl.ParseTemplateOfActionBeforeExec(action.Title, actionBinding.Entity),
|
||||||
Icon: entities.ParseTemplateWith(action.Icon, actionBinding.Entity),
|
Icon: tpl.ParseTemplateOfActionBeforeExec(action.Icon, actionBinding.Entity),
|
||||||
CanExec: aclCanExec && enabledExprCanExec,
|
CanExec: aclCanExec && enabledExprCanExec,
|
||||||
PopupOnStart: action.PopupOnStart,
|
PopupOnStart: action.PopupOnStart,
|
||||||
Order: int32(actionBinding.ConfigOrder),
|
Order: int32(actionBinding.ConfigOrder),
|
||||||
|
|
@ -135,7 +146,7 @@ func buildAction(actionBinding *executor.ActionBinding, rr *DashboardRenderReque
|
||||||
Title: cfgArg.Title,
|
Title: cfgArg.Title,
|
||||||
Type: cfgArg.Type,
|
Type: cfgArg.Type,
|
||||||
Description: cfgArg.Description,
|
Description: cfgArg.Description,
|
||||||
DefaultValue: cfgArg.Default,
|
DefaultValue: getDefaultArgumentValue(cfgArg, actionBinding.Entity),
|
||||||
Choices: buildChoices(cfgArg),
|
Choices: buildChoices(cfgArg),
|
||||||
Suggestions: cfgArg.Suggestions,
|
Suggestions: cfgArg.Suggestions,
|
||||||
SuggestionsBrowserKey: cfgArg.SuggestionsBrowserKey,
|
SuggestionsBrowserKey: cfgArg.SuggestionsBrowserKey,
|
||||||
|
|
@ -162,8 +173,8 @@ func buildChoicesEntity(firstChoice config.ActionArgumentChoice, entityTitle str
|
||||||
|
|
||||||
for _, ent := range entList {
|
for _, ent := range entList {
|
||||||
ret = append(ret, &apiv1.ActionArgumentChoice{
|
ret = append(ret, &apiv1.ActionArgumentChoice{
|
||||||
Value: entities.ParseTemplateWith(firstChoice.Value, ent),
|
Value: tpl.ParseTemplateOfActionBeforeExec(firstChoice.Value, ent),
|
||||||
Title: entities.ParseTemplateWith(firstChoice.Title, ent),
|
Title: tpl.ParseTemplateOfActionBeforeExec(firstChoice.Title, ent),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -119,9 +119,9 @@ func TestGetEntities(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func setupTestEntities() {
|
func setupTestEntities() {
|
||||||
entities.ClearEntities("server")
|
entities.ClearEntitiesOfType("server")
|
||||||
entities.ClearEntities("database")
|
entities.ClearEntitiesOfType("database")
|
||||||
entities.ClearEntities("application")
|
entities.ClearEntitiesOfType("application")
|
||||||
|
|
||||||
entities.AddEntity("server", "zebra", map[string]any{"title": "Server Zebra", "hostname": "zebra.example.com"})
|
entities.AddEntity("server", "zebra", map[string]any{"title": "Server Zebra", "hostname": "zebra.example.com"})
|
||||||
entities.AddEntity("server", "alpha", map[string]any{"title": "Server Alpha", "hostname": "alpha.example.com"})
|
entities.AddEntity("server", "alpha", map[string]any{"title": "Server Alpha", "hostname": "alpha.example.com"})
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import (
|
||||||
apiv1 "github.com/OliveTin/OliveTin/gen/olivetin/api/v1"
|
apiv1 "github.com/OliveTin/OliveTin/gen/olivetin/api/v1"
|
||||||
config "github.com/OliveTin/OliveTin/internal/config"
|
config "github.com/OliveTin/OliveTin/internal/config"
|
||||||
entities "github.com/OliveTin/OliveTin/internal/entities"
|
entities "github.com/OliveTin/OliveTin/internal/entities"
|
||||||
|
"github.com/OliveTin/OliveTin/internal/tpl"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -23,14 +24,14 @@ func buildEntityFieldsets(entityTitle string, tpl *config.DashboardComponent, rr
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildEntityFieldset(tpl *config.DashboardComponent, ent *entities.Entity, rr *DashboardRenderRequest) *apiv1.DashboardComponent {
|
func buildEntityFieldset(component *config.DashboardComponent, ent *entities.Entity, rr *DashboardRenderRequest) *apiv1.DashboardComponent {
|
||||||
return &apiv1.DashboardComponent{
|
return &apiv1.DashboardComponent{
|
||||||
Title: entities.ParseTemplateWith(tpl.Title, ent),
|
Title: tpl.ParseTemplateOfActionBeforeExec(component.Title, ent),
|
||||||
Type: "fieldset",
|
Type: "fieldset",
|
||||||
Contents: removeFieldsetIfHasNoLinks(buildEntityFieldsetContents(tpl.Contents, ent, tpl.Entity, rr)),
|
Contents: removeFieldsetIfHasNoLinks(buildEntityFieldsetContents(component.Contents, ent, component.Entity, rr)),
|
||||||
CssClass: entities.ParseTemplateWith(tpl.CssClass, ent),
|
CssClass: tpl.ParseTemplateOfActionBeforeExec(component.CssClass, ent),
|
||||||
Action: rr.findAction(tpl.Title),
|
Action: rr.findAction(component.Title),
|
||||||
EntityType: tpl.Entity,
|
EntityType: component.Entity,
|
||||||
EntityKey: ent.UniqueKey,
|
EntityKey: ent.UniqueKey,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -68,7 +69,7 @@ func buildEntityFieldsetContents(contents []*config.DashboardComponent, ent *ent
|
||||||
|
|
||||||
func cloneItem(subitem *config.DashboardComponent, ent *entities.Entity, entityType string, rr *DashboardRenderRequest) *apiv1.DashboardComponent {
|
func cloneItem(subitem *config.DashboardComponent, ent *entities.Entity, entityType string, rr *DashboardRenderRequest) *apiv1.DashboardComponent {
|
||||||
clone := &apiv1.DashboardComponent{}
|
clone := &apiv1.DashboardComponent{}
|
||||||
clone.CssClass = entities.ParseTemplateWith(subitem.CssClass, ent)
|
clone.CssClass = tpl.ParseTemplateOfActionBeforeExec(subitem.CssClass, ent)
|
||||||
|
|
||||||
if isLinkType(subitem.Type) {
|
if isLinkType(subitem.Type) {
|
||||||
return cloneLinkItem(subitem, ent, clone, rr)
|
return cloneLinkItem(subitem, ent, clone, rr)
|
||||||
|
|
@ -83,7 +84,7 @@ func isLinkType(itemType string) bool {
|
||||||
|
|
||||||
func cloneLinkItem(subitem *config.DashboardComponent, ent *entities.Entity, clone *apiv1.DashboardComponent, rr *DashboardRenderRequest) *apiv1.DashboardComponent {
|
func cloneLinkItem(subitem *config.DashboardComponent, ent *entities.Entity, clone *apiv1.DashboardComponent, rr *DashboardRenderRequest) *apiv1.DashboardComponent {
|
||||||
clone.Type = "link"
|
clone.Type = "link"
|
||||||
clone.Title = entities.ParseTemplateWith(subitem.Title, ent)
|
clone.Title = tpl.ParseTemplateOfActionBeforeExec(subitem.Title, ent)
|
||||||
// Prefer an entity-specific action when available, but fall back to a
|
// Prefer an entity-specific action when available, but fall back to a
|
||||||
// non-entity-scoped action with the same title. This allows inline actions
|
// non-entity-scoped action with the same title. This allows inline actions
|
||||||
// defined inside entity dashboards to work without requiring an explicit
|
// defined inside entity dashboards to work without requiring an explicit
|
||||||
|
|
@ -98,7 +99,7 @@ func cloneLinkItem(subitem *config.DashboardComponent, ent *entities.Entity, clo
|
||||||
}
|
}
|
||||||
|
|
||||||
func cloneNonLinkItem(subitem *config.DashboardComponent, ent *entities.Entity, entityType string, clone *apiv1.DashboardComponent, rr *DashboardRenderRequest) *apiv1.DashboardComponent {
|
func cloneNonLinkItem(subitem *config.DashboardComponent, ent *entities.Entity, entityType string, clone *apiv1.DashboardComponent, rr *DashboardRenderRequest) *apiv1.DashboardComponent {
|
||||||
clone.Title = entities.ParseTemplateWith(subitem.Title, ent)
|
clone.Title = tpl.ParseTemplateOfActionBeforeExec(subitem.Title, ent)
|
||||||
clone.Type = subitem.Type
|
clone.Type = subitem.Type
|
||||||
|
|
||||||
if isDirectoryWithEntity(clone.Type, ent, entityType) {
|
if isDirectoryWithEntity(clone.Type, ent, entityType) {
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import (
|
||||||
apiv1 "github.com/OliveTin/OliveTin/gen/olivetin/api/v1"
|
apiv1 "github.com/OliveTin/OliveTin/gen/olivetin/api/v1"
|
||||||
config "github.com/OliveTin/OliveTin/internal/config"
|
config "github.com/OliveTin/OliveTin/internal/config"
|
||||||
entities "github.com/OliveTin/OliveTin/internal/entities"
|
entities "github.com/OliveTin/OliveTin/internal/entities"
|
||||||
|
"github.com/OliveTin/OliveTin/internal/tpl"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"golang.org/x/exp/slices"
|
"golang.org/x/exp/slices"
|
||||||
)
|
)
|
||||||
|
|
@ -236,7 +237,7 @@ func buildDashboardComponentSimpleWithEntity(subitem *config.DashboardComponent,
|
||||||
|
|
||||||
title := subitem.Title
|
title := subitem.Title
|
||||||
if entity != nil {
|
if entity != nil {
|
||||||
title = entities.ParseTemplateWith(subitem.Title, entity)
|
title = tpl.ParseTemplateOfActionBeforeExec(subitem.Title, entity)
|
||||||
}
|
}
|
||||||
|
|
||||||
newitem := &apiv1.DashboardComponent{
|
newitem := &apiv1.DashboardComponent{
|
||||||
|
|
|
||||||
|
|
@ -128,6 +128,7 @@ type Config struct {
|
||||||
ShowFooter bool `koanf:"showFooter"`
|
ShowFooter bool `koanf:"showFooter"`
|
||||||
ShowNavigation bool `koanf:"showNavigation"`
|
ShowNavigation bool `koanf:"showNavigation"`
|
||||||
ShowNewVersions bool `koanf:"showNewVersions"`
|
ShowNewVersions bool `koanf:"showNewVersions"`
|
||||||
|
ShowNavigateOnStartIcons bool `koanf:"showNavigateOnStartIcons"`
|
||||||
EnableCustomJs bool `koanf:"enableCustomJs"`
|
EnableCustomJs bool `koanf:"enableCustomJs"`
|
||||||
AuthJwtCookieName string `koanf:"authJwtCookieName"`
|
AuthJwtCookieName string `koanf:"authJwtCookieName"`
|
||||||
AuthJwtHeader string `koanf:"authJwtHeader"`
|
AuthJwtHeader string `koanf:"authJwtHeader"`
|
||||||
|
|
@ -244,6 +245,7 @@ func DefaultConfigWithBasePort(basePort int) *Config {
|
||||||
config.ShowFooter = true
|
config.ShowFooter = true
|
||||||
config.ShowNavigation = true
|
config.ShowNavigation = true
|
||||||
config.ShowNewVersions = true
|
config.ShowNewVersions = true
|
||||||
|
config.ShowNavigateOnStartIcons = true
|
||||||
config.EnableCustomJs = false
|
config.EnableCustomJs = false
|
||||||
config.ExternalRestAddress = "."
|
config.ExternalRestAddress = "."
|
||||||
config.LogLevel = "INFO"
|
config.LogLevel = "INFO"
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,9 @@ package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
|
"text/template"
|
||||||
|
|
||||||
|
"github.com/OliveTin/OliveTin/internal/env"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
@ -13,6 +15,7 @@ func (cfg *Config) Sanitize() {
|
||||||
cfg.sanitizeLogLevel()
|
cfg.sanitizeLogLevel()
|
||||||
cfg.sanitizeAuthRequireGuestsToLogin()
|
cfg.sanitizeAuthRequireGuestsToLogin()
|
||||||
cfg.sanitizeLogHistoryPageSize()
|
cfg.sanitizeLogHistoryPageSize()
|
||||||
|
cfg.sanitizeLocalUserPasswords()
|
||||||
|
|
||||||
// log.Infof("cfg %p", cfg)
|
// log.Infof("cfg %p", cfg)
|
||||||
|
|
||||||
|
|
@ -172,6 +175,29 @@ func (cfg *Config) sanitizeLogHistoryPageSize() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (cfg *Config) sanitizeLocalUserPasswords() {
|
||||||
|
for _, user := range cfg.AuthLocalUsers.Users {
|
||||||
|
if user.Password != "" {
|
||||||
|
user.Password = parsePasswordTemplate(user.Password)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// parsePasswordTemplate expands {{ .Env.VAR }} in local user password fields using the process environment.
|
||||||
|
func parsePasswordTemplate(source string) string {
|
||||||
|
t, err := template.New("password").Option("missingkey=error").Parse(source)
|
||||||
|
if err != nil {
|
||||||
|
log.WithFields(log.Fields{"error": err}).Debug("Password template parse failed, using literal")
|
||||||
|
return source
|
||||||
|
}
|
||||||
|
var b strings.Builder
|
||||||
|
if err := t.Execute(&b, map[string]interface{}{"Env": env.BuildEnvMap()}); err != nil {
|
||||||
|
log.WithFields(log.Fields{"error": err}).Debug("Password template execute failed, using literal")
|
||||||
|
return source
|
||||||
|
}
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
func getActionID(action *Action) string {
|
func getActionID(action *Action) string {
|
||||||
if action.ID == "" {
|
if action.ID == "" {
|
||||||
return uuid.NewString()
|
return uuid.NewString()
|
||||||
|
|
|
||||||
|
|
@ -135,7 +135,7 @@ func loadEntityFileYaml(filename string, entityname string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateSvFromFile(entityname string, data []map[string]any) {
|
func updateSvFromFile(entityname string, data []map[string]any) {
|
||||||
ClearEntities(entityname)
|
ClearEntitiesOfType(entityname)
|
||||||
|
|
||||||
for i, mapp := range data {
|
for i, mapp := range data {
|
||||||
AddEntity(entityname, fmt.Sprintf("%d", i), mapp)
|
AddEntity(entityname, fmt.Sprintf("%d", i), mapp)
|
||||||
|
|
|
||||||
|
|
@ -10,72 +10,31 @@ package entities
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/OliveTin/OliveTin/internal/installationinfo"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type entityInstancesByKey map[string]*Entity
|
type entityInstancesByKey map[string]*Entity
|
||||||
|
|
||||||
type entitiesByClass map[string]entityInstancesByKey
|
type EntitiesByClass map[string]entityInstancesByKey
|
||||||
|
|
||||||
type variableBase struct {
|
|
||||||
OliveTin installationInfo
|
|
||||||
Entities entitiesByClass
|
|
||||||
|
|
||||||
CurrentEntity interface{}
|
|
||||||
Arguments map[string]string
|
|
||||||
Env map[string]string
|
|
||||||
}
|
|
||||||
|
|
||||||
type installationInfo struct {
|
|
||||||
Build *installationinfo.BuildInfo
|
|
||||||
Runtime *installationinfo.RuntimeInfo
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
contents *variableBase
|
|
||||||
rwmutex = sync.RWMutex{}
|
rwmutex = sync.RWMutex{}
|
||||||
|
entities EntitiesByClass
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
rwmutex.Lock()
|
rwmutex.Lock()
|
||||||
|
entities = make(EntitiesByClass, 0)
|
||||||
envMap := make(map[string]string)
|
|
||||||
for _, env := range os.Environ() {
|
|
||||||
parts := strings.SplitN(env, "=", 2)
|
|
||||||
if len(parts) == 2 {
|
|
||||||
envMap[parts[0]] = parts[1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
contents = &variableBase{
|
|
||||||
OliveTin: installationInfo{
|
|
||||||
Build: installationinfo.Build,
|
|
||||||
Runtime: installationinfo.Runtime,
|
|
||||||
},
|
|
||||||
Entities: make(entitiesByClass, 0),
|
|
||||||
Env: envMap,
|
|
||||||
}
|
|
||||||
|
|
||||||
rwmutex.Unlock()
|
rwmutex.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetAll() *variableBase {
|
func GetEntities() EntitiesByClass {
|
||||||
rwmutex.RLock()
|
|
||||||
defer rwmutex.RUnlock()
|
|
||||||
|
|
||||||
return contents
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetEntities() entitiesByClass {
|
|
||||||
rwmutex.RLock()
|
rwmutex.RLock()
|
||||||
|
|
||||||
copiedEntities := make(entitiesByClass, len(contents.Entities))
|
copiedEntities := make(EntitiesByClass, len(entities))
|
||||||
|
|
||||||
for entityName, entityInstances := range contents.Entities {
|
for entityName, entityInstances := range entities {
|
||||||
copiedInstances := make(entityInstancesByKey, len(entityInstances))
|
copiedInstances := make(entityInstancesByKey, len(entityInstances))
|
||||||
|
|
||||||
for key, entity := range entityInstances {
|
for key, entity := range entityInstances {
|
||||||
|
|
@ -93,7 +52,7 @@ func GetEntityInstances(entityName string) entityInstancesByKey {
|
||||||
rwmutex.RLock()
|
rwmutex.RLock()
|
||||||
defer rwmutex.RUnlock()
|
defer rwmutex.RUnlock()
|
||||||
|
|
||||||
if entities, ok := contents.Entities[entityName]; ok {
|
if entities, ok := entities[entityName]; ok {
|
||||||
copiedInstances := make(entityInstancesByKey, len(entities))
|
copiedInstances := make(entityInstancesByKey, len(entities))
|
||||||
|
|
||||||
for key, entity := range entities {
|
for key, entity := range entities {
|
||||||
|
|
@ -108,11 +67,11 @@ func GetEntityInstances(entityName string) entityInstancesByKey {
|
||||||
func AddEntity(entityName string, entityKey string, data any) {
|
func AddEntity(entityName string, entityKey string, data any) {
|
||||||
rwmutex.Lock()
|
rwmutex.Lock()
|
||||||
|
|
||||||
if _, ok := contents.Entities[entityName]; !ok {
|
if _, ok := entities[entityName]; !ok {
|
||||||
contents.Entities[entityName] = make(entityInstancesByKey, 0)
|
entities[entityName] = make(entityInstancesByKey, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
contents.Entities[entityName][entityKey] = &Entity{
|
entities[entityName][entityKey] = &Entity{
|
||||||
Data: data,
|
Data: data,
|
||||||
UniqueKey: entityKey,
|
UniqueKey: entityKey,
|
||||||
Title: findEntityTitle(data),
|
Title: findEntityTitle(data),
|
||||||
|
|
@ -144,3 +103,10 @@ func findEntityTitle(data any) string {
|
||||||
|
|
||||||
return "Untitled Entity"
|
return "Untitled Entity"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ClearEntitiesOfType(entityType string) {
|
||||||
|
rwmutex.Lock()
|
||||||
|
defer rwmutex.Unlock()
|
||||||
|
|
||||||
|
delete(entities, entityType)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,134 +0,0 @@
|
||||||
package entities
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
"text/template"
|
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
var tpl = template.New("tpl")
|
|
||||||
|
|
||||||
var legacyArgumentRegex = regexp.MustCompile(`{{ ([a-zA-Z0-9_]+) }}`)
|
|
||||||
var legacyEntityPropertiesRegex = regexp.MustCompile(`{{ ([a-zA-Z0-9_]+)\.([a-zA-Z0-9_\.]+) }}`)
|
|
||||||
|
|
||||||
func migrateLegacyEntityProperties(rawShellCommand string) string {
|
|
||||||
foundArgumentNames := legacyEntityPropertiesRegex.FindAllStringSubmatch(rawShellCommand, -1)
|
|
||||||
|
|
||||||
for _, match := range foundArgumentNames {
|
|
||||||
entityName := match[1]
|
|
||||||
argName := match[2]
|
|
||||||
fullMatch := match[0] // The entire matched string like "{{ server.hostname }}"
|
|
||||||
|
|
||||||
if strings.Contains(argName, ".") {
|
|
||||||
replacement := "{{ .CurrentEntity." + argName + " }}"
|
|
||||||
|
|
||||||
rawShellCommand = strings.ReplaceAll(rawShellCommand, fullMatch, replacement)
|
|
||||||
|
|
||||||
log.WithFields(log.Fields{
|
|
||||||
"old": entityName,
|
|
||||||
"new": ".CurrentEntity",
|
|
||||||
}).Debugf("Legacy entity variable name found, changing to CurrentEntity")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if !strings.HasPrefix(argName, ".Arguments.") {
|
|
||||||
replacement := "{{ .CurrentEntity." + argName + " }}"
|
|
||||||
|
|
||||||
rawShellCommand = strings.ReplaceAll(rawShellCommand, fullMatch, replacement)
|
|
||||||
|
|
||||||
log.WithFields(log.Fields{
|
|
||||||
"old": argName,
|
|
||||||
"new": ".CurrentEntity." + argName,
|
|
||||||
}).Debugf("Legacy variable name found, changing to CurrentEntity")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return rawShellCommand
|
|
||||||
}
|
|
||||||
|
|
||||||
func migrateLegacyArgumentNames(rawShellCommand string) string {
|
|
||||||
foundArgumentNames := legacyArgumentRegex.FindAllStringSubmatch(rawShellCommand, -1)
|
|
||||||
|
|
||||||
for _, match := range foundArgumentNames {
|
|
||||||
argName := match[1]
|
|
||||||
|
|
||||||
if !strings.HasPrefix(argName, ".Arguments.") {
|
|
||||||
log.WithFields(log.Fields{
|
|
||||||
"old": argName,
|
|
||||||
"new": ".Arguments." + argName,
|
|
||||||
}).Debugf("Legacy variable name found, changing to Argument")
|
|
||||||
|
|
||||||
rawShellCommand = strings.ReplaceAll(rawShellCommand, argName, ".Arguments."+argName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return rawShellCommand
|
|
||||||
}
|
|
||||||
|
|
||||||
func ParseTemplateWithArgs(source string, ent *Entity, args map[string]string) string {
|
|
||||||
source = migrateLegacyArgumentNames(source)
|
|
||||||
source = migrateLegacyEntityProperties(source)
|
|
||||||
|
|
||||||
ret := ""
|
|
||||||
|
|
||||||
t, err := tpl.Parse(source)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.WithFields(log.Fields{
|
|
||||||
"source": source,
|
|
||||||
"err": err,
|
|
||||||
}).Error("Error parsing template")
|
|
||||||
return fmt.Sprintf("tpl parse error: %v", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
var entdata any
|
|
||||||
|
|
||||||
if ent != nil {
|
|
||||||
entdata = ent.Data
|
|
||||||
}
|
|
||||||
|
|
||||||
templateVariables := &variableBase{
|
|
||||||
OliveTin: GetAll().OliveTin,
|
|
||||||
Arguments: args,
|
|
||||||
CurrentEntity: entdata,
|
|
||||||
Env: GetAll().Env,
|
|
||||||
}
|
|
||||||
|
|
||||||
var sb strings.Builder
|
|
||||||
err = t.Execute(&sb, &templateVariables)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.WithFields(log.Fields{
|
|
||||||
"source": source,
|
|
||||||
"err": err,
|
|
||||||
"currentEntity": ent,
|
|
||||||
}).Errorf("Error executing template")
|
|
||||||
ret = fmt.Sprintf("tpl exec error: %v", err.Error())
|
|
||||||
} else {
|
|
||||||
ret = sb.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
func ParseTemplateWith(source string, ent *Entity) string {
|
|
||||||
return ParseTemplateWithArgs(source, ent, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func ParseTemplateBoolWith(source string, ent *Entity) bool {
|
|
||||||
source = strings.TrimSpace(source)
|
|
||||||
|
|
||||||
tplBool := ParseTemplateWith(source, ent)
|
|
||||||
|
|
||||||
return tplBool == "true"
|
|
||||||
}
|
|
||||||
|
|
||||||
func ClearEntities(entityType string) {
|
|
||||||
rwmutex.Lock()
|
|
||||||
defer rwmutex.Unlock()
|
|
||||||
|
|
||||||
delete(contents.Entities, entityType)
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
package env
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func BuildEnvMap() map[string]string {
|
||||||
|
envMap := make(map[string]string)
|
||||||
|
for _, e := range os.Environ() {
|
||||||
|
parts := strings.SplitN(e, "=", 2)
|
||||||
|
if len(parts) == 2 {
|
||||||
|
envMap[parts[0]] = parts[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return envMap
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,7 @@ package executor
|
||||||
import (
|
import (
|
||||||
config "github.com/OliveTin/OliveTin/internal/config"
|
config "github.com/OliveTin/OliveTin/internal/config"
|
||||||
"github.com/OliveTin/OliveTin/internal/entities"
|
"github.com/OliveTin/OliveTin/internal/entities"
|
||||||
|
"github.com/OliveTin/OliveTin/internal/tpl"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
@ -24,29 +25,12 @@ var (
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
func parseCommandForReplacements(shellCommand string, values map[string]string, entity any) (string, error) {
|
|
||||||
r := regexp.MustCompile(`{{ *?([a-zA-Z0-9_]+?) *?}}`)
|
|
||||||
foundArgumentNames := r.FindAllStringSubmatch(shellCommand, -1)
|
|
||||||
|
|
||||||
for _, match := range foundArgumentNames {
|
|
||||||
argName := match[1]
|
|
||||||
argValue, argProvided := values[argName]
|
|
||||||
|
|
||||||
if !argProvided {
|
|
||||||
return "", fmt.Errorf("required arg not provided: %v", argName)
|
|
||||||
}
|
|
||||||
|
|
||||||
shellCommand = strings.ReplaceAll(shellCommand, match[0], argValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
return shellCommand, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseExecArray parses all exec arguments in the action.
|
// parseExecArray parses all exec arguments in the action.
|
||||||
func parseExecArray(action *config.Action, values map[string]string, entity *entities.Entity) ([]string, error) {
|
func parseExecArray(action *config.Action, values map[string]string, entity *entities.Entity) ([]string, error) {
|
||||||
parsed := make([]string, len(action.Exec))
|
parsed := make([]string, len(action.Exec))
|
||||||
for i, a := range action.Exec {
|
|
||||||
out, err := parseSingleExec(a, values, entity)
|
for i, segment := range action.Exec {
|
||||||
|
out, err := parseExecSegment(segment, values, entity)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -62,20 +46,19 @@ func parseActionExec(values map[string]string, action *config.Action, entity *en
|
||||||
if err := validateArguments(values, action); err != nil {
|
if err := validateArguments(values, action); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
parsed, err := parseExecArray(action, values, entity)
|
parsed, err := parseExecArray(action, values, entity)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
logParsedExec(action, parsed, values)
|
logParsedExec(action, parsed, values)
|
||||||
return parsed, nil
|
return parsed, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseSingleExec(a string, values map[string]string, entity *entities.Entity) (string, error) {
|
func parseExecSegment(arg string, values map[string]string, entity *entities.Entity) (string, error) {
|
||||||
arg, err := parseCommandForReplacements(a, values, entity)
|
return tpl.ParseTemplateWithActionContext(arg, entity, values)
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return entities.ParseTemplateWithArgs(arg, entity, values), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateArguments(values map[string]string, action *config.Action) error {
|
func validateArguments(values map[string]string, action *config.Action) error {
|
||||||
|
|
@ -93,19 +76,17 @@ func logParsedExec(action *config.Action, parsed []string, values map[string]str
|
||||||
log.WithFields(log.Fields{"actionTitle": action.Title, "cmd": redacted}).Infof("Action parse args - After (Exec)")
|
log.WithFields(log.Fields{"actionTitle": action.Title, "cmd": redacted}).Infof("Action parse args - After (Exec)")
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseActionArguments(values map[string]string, action *config.Action, entity *entities.Entity) (string, error) {
|
func parseActionArguments(req *ExecutionRequest) (string, error) {
|
||||||
log.WithFields(log.Fields{
|
log.WithFields(log.Fields{
|
||||||
"actionTitle": action.Title,
|
"actionTitle": req.Binding.Action.Title,
|
||||||
"cmd": action.Shell,
|
"cmd": req.Binding.Action.Shell,
|
||||||
}).Infof("Action parse args - Before")
|
}).Infof("Action parse args - Before")
|
||||||
|
|
||||||
rawShellCommand, err := parseCommandForReplacements(action.Shell, values, entity)
|
for _, arg := range req.Binding.Action.Arguments {
|
||||||
|
|
||||||
for _, arg := range action.Arguments {
|
|
||||||
argName := arg.Name
|
argName := arg.Name
|
||||||
argValue := values[argName]
|
argValue := req.Arguments[argName]
|
||||||
|
|
||||||
err := typecheckActionArgument(&arg, argValue, action)
|
err := typecheckActionArgument(&arg, argValue, req.Binding.Action)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
|
|
@ -117,15 +98,16 @@ func parseActionArguments(values map[string]string, action *config.Action, entit
|
||||||
}).Debugf("Arg assigned")
|
}).Debugf("Arg assigned")
|
||||||
}
|
}
|
||||||
|
|
||||||
parsedShellCommand := entities.ParseTemplateWithArgs(rawShellCommand, entity, values)
|
parsedShellCommand, err := tpl.ParseTemplateWithActionContext(req.Binding.Action.Shell, req.Binding.Entity, req.Arguments)
|
||||||
redactedShellCommand := redactShellCommand(parsedShellCommand, action.Arguments, values)
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
redactedShellCommand := redactShellCommand(parsedShellCommand, req.Binding.Action.Arguments, req.Arguments)
|
||||||
|
|
||||||
log.WithFields(log.Fields{
|
log.WithFields(log.Fields{
|
||||||
"actionTitle": action.Title,
|
"actionTitle": req.Binding.Action.Title,
|
||||||
"cmd": redactedShellCommand,
|
"cmd": redactedShellCommand,
|
||||||
}).Infof("Action parse args - After")
|
}).Infof("Action parse args - After")
|
||||||
|
|
||||||
|
|
@ -172,7 +154,7 @@ func typecheckActionArgument(arg *config.ActionArgument, value string, action *c
|
||||||
return fmt.Errorf("argument name cannot be empty")
|
return fmt.Errorf("argument name cannot be empty")
|
||||||
}
|
}
|
||||||
|
|
||||||
return typecheckActionArgumentFound(value, action, arg)
|
return typecheckActionArgumentFound(value, arg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidateArgument validates a single argument value using the same logic as the executor.
|
// ValidateArgument validates a single argument value using the same logic as the executor.
|
||||||
|
|
@ -194,7 +176,7 @@ func ValidateArgument(arg *config.ActionArgument, value string, action *config.A
|
||||||
return typecheckActionArgument(arg, mangledValue, action)
|
return typecheckActionArgument(arg, mangledValue, action)
|
||||||
}
|
}
|
||||||
|
|
||||||
func typecheckActionArgumentFound(value string, action *config.Action, arg *config.ActionArgument) error {
|
func typecheckActionArgumentFound(value string, arg *config.ActionArgument) error {
|
||||||
if value == "" {
|
if value == "" {
|
||||||
return typecheckNull(arg)
|
return typecheckNull(arg)
|
||||||
}
|
}
|
||||||
|
|
@ -256,7 +238,7 @@ func typecheckChoiceEntity(value string, arg *config.ActionArgument) error {
|
||||||
templateChoice := arg.Choices[0].Value
|
templateChoice := arg.Choices[0].Value
|
||||||
|
|
||||||
for _, ent := range entities.GetEntityInstances(arg.Entity) {
|
for _, ent := range entities.GetEntityInstances(arg.Entity) {
|
||||||
choice := entities.ParseTemplateWith(templateChoice, ent)
|
choice := tpl.ParseTemplateOfActionBeforeExec(templateChoice, ent)
|
||||||
|
|
||||||
if value == choice {
|
if value == choice {
|
||||||
return nil
|
return nil
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import (
|
||||||
|
|
||||||
config "github.com/OliveTin/OliveTin/internal/config"
|
config "github.com/OliveTin/OliveTin/internal/config"
|
||||||
"github.com/OliveTin/OliveTin/internal/entities"
|
"github.com/OliveTin/OliveTin/internal/entities"
|
||||||
|
"github.com/OliveTin/OliveTin/internal/tpl"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
@ -114,36 +115,47 @@ func TestValidateArgumentCheckboxWithChoices(t *testing.T) {
|
||||||
assert.NotNil(t, err, "Expected unknown checkbox title to be rejected against choices")
|
assert.NotNil(t, err, "Expected unknown checkbox title to be rejected against choices")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newExecRequest() *ExecutionRequest {
|
||||||
|
return &ExecutionRequest{
|
||||||
|
Arguments: make(map[string]string),
|
||||||
|
Binding: &ActionBinding{
|
||||||
|
Action: &config.Action{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestArgumentValueNullable(t *testing.T) {
|
func TestArgumentValueNullable(t *testing.T) {
|
||||||
a1 := config.Action{
|
req := newExecRequest()
|
||||||
|
req.Binding.Action = &config.Action{
|
||||||
Title: "Release the hounds",
|
Title: "Release the hounds",
|
||||||
Shell: "echo 'Releasing {{ count }} hounds'",
|
Shell: "echo 'Releasing {{ count }} hounds'",
|
||||||
Arguments: []config.ActionArgument{
|
Arguments: []config.ActionArgument{
|
||||||
{
|
{
|
||||||
Name: "count",
|
Name: "count",
|
||||||
Type: "int",
|
Type: "int",
|
||||||
|
RejectNull: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
req.Arguments = map[string]string{
|
||||||
values := map[string]string{
|
|
||||||
"count": "",
|
"count": "",
|
||||||
}
|
}
|
||||||
|
|
||||||
out, err := parseActionArguments(values, &a1, nil)
|
out, err := parseActionArguments(req)
|
||||||
|
|
||||||
assert.Equal(t, "echo 'Releasing hounds'", out)
|
assert.Equal(t, "echo 'Releasing hounds'", out)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
|
|
||||||
a1.Arguments[0].RejectNull = true
|
req.Binding.Action.Arguments[0].RejectNull = true
|
||||||
|
|
||||||
_, err = parseActionArguments(values, &a1, nil)
|
_, err = parseActionArguments(req)
|
||||||
|
|
||||||
assert.NotNil(t, err)
|
assert.NotNil(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestArgumentNameNumbers(t *testing.T) {
|
func TestArgumentNameNumbers(t *testing.T) {
|
||||||
a1 := config.Action{
|
req := newExecRequest()
|
||||||
|
req.Binding.Action = &config.Action{
|
||||||
Title: "Do some tickles",
|
Title: "Do some tickles",
|
||||||
Shell: "echo 'Tickling {{ person1name }}'",
|
Shell: "echo 'Tickling {{ person1name }}'",
|
||||||
Arguments: []config.ActionArgument{
|
Arguments: []config.ActionArgument{
|
||||||
|
|
@ -154,18 +166,19 @@ func TestArgumentNameNumbers(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
values := map[string]string{
|
req.Arguments = map[string]string{
|
||||||
"person1name": "Fred",
|
"person1name": "Fred",
|
||||||
}
|
}
|
||||||
|
|
||||||
out, err := parseActionArguments(values, &a1, nil)
|
out, err := parseActionArguments(req)
|
||||||
|
|
||||||
assert.Equal(t, "echo 'Tickling Fred'", out)
|
assert.Equal(t, "echo 'Tickling Fred'", out)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestArgumentNotProvided(t *testing.T) {
|
func TestArgumentNotProvided(t *testing.T) {
|
||||||
a1 := config.Action{
|
req := newExecRequest()
|
||||||
|
req.Binding.Action = &config.Action{
|
||||||
Title: "Do some tickles",
|
Title: "Do some tickles",
|
||||||
Shell: "echo 'Tickling {{ personName }}'",
|
Shell: "echo 'Tickling {{ personName }}'",
|
||||||
Arguments: []config.ActionArgument{
|
Arguments: []config.ActionArgument{
|
||||||
|
|
@ -176,24 +189,25 @@ func TestArgumentNotProvided(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
values := map[string]string{}
|
req.Arguments = map[string]string{}
|
||||||
|
|
||||||
out, err := parseActionArguments(values, &a1, nil)
|
out, err := parseActionArguments(req)
|
||||||
|
|
||||||
assert.Equal(t, "", out)
|
assert.Equal(t, "", out)
|
||||||
assert.Equal(t, err.Error(), "required arg not provided: personName")
|
assert.Equal(t, err.Error(), "required arg not provided: personName")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestExecArrayParsing(t *testing.T) {
|
func TestExecArrayParsing(t *testing.T) {
|
||||||
a1 := config.Action{
|
req := newExecRequest()
|
||||||
|
req.Binding.Action = &config.Action{
|
||||||
Title: "List files",
|
Title: "List files",
|
||||||
Exec: []string{"ls", "-alh"},
|
Exec: []string{"ls", "-alh"},
|
||||||
Arguments: []config.ActionArgument{},
|
Arguments: []config.ActionArgument{},
|
||||||
}
|
}
|
||||||
|
|
||||||
values := map[string]string{}
|
req.Arguments = map[string]string{}
|
||||||
|
|
||||||
out, err := parseActionExec(values, &a1, nil)
|
out, err := parseActionExec(req.Arguments, req.Binding.Action, req.Binding.Entity)
|
||||||
|
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
assert.Equal(t, []string{"ls", "-alh"}, out)
|
assert.Equal(t, []string{"ls", "-alh"}, out)
|
||||||
|
|
@ -636,7 +650,7 @@ func TestParseCommandForReplacements(t *testing.T) {
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
output, err := parseCommandForReplacements(tt.shellCommand, tt.values, nil)
|
output, err := tpl.ParseTemplateWithActionContext(tt.shellCommand, nil, tt.values)
|
||||||
|
|
||||||
if tt.expectError {
|
if tt.expectError {
|
||||||
assert.NotNil(t, err, "Expected error but got none")
|
assert.NotNil(t, err, "Expected error but got none")
|
||||||
|
|
@ -654,56 +668,87 @@ func TestParseCommandForReplacements(t *testing.T) {
|
||||||
func TestArgumentChoicesValidation(t *testing.T) {
|
func TestArgumentChoicesValidation(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
action config.Action
|
req *ExecutionRequest
|
||||||
values map[string]string
|
|
||||||
expectError bool
|
expectError bool
|
||||||
description string
|
description string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "Valid choice",
|
name: "Valid choice",
|
||||||
action: config.Action{
|
req: &ExecutionRequest{
|
||||||
Title: "Test choices",
|
Binding: &ActionBinding{
|
||||||
Shell: "echo {{ option }}",
|
Action: &config.Action{
|
||||||
Arguments: []config.ActionArgument{
|
Title: "Test choices",
|
||||||
{
|
Shell: "echo {{ option }}",
|
||||||
Name: "option",
|
Arguments: []config.ActionArgument{
|
||||||
Type: "ascii",
|
{
|
||||||
Choices: []config.ActionArgumentChoice{
|
Name: "option",
|
||||||
{Value: "option1", Title: "Option 1"},
|
Type: "ascii",
|
||||||
{Value: "option2", Title: "Option 2"},
|
Choices: []config.ActionArgumentChoice{
|
||||||
|
{Value: "option1", Title: "Option 1"},
|
||||||
|
{Value: "option2", Title: "Option 2"},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Arguments: map[string]string{"option": "option1"},
|
||||||
},
|
},
|
||||||
values: map[string]string{"option": "option1"},
|
|
||||||
expectError: false,
|
expectError: false,
|
||||||
description: "Should accept valid choice",
|
description: "Should accept valid choice",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Invalid choice",
|
name: "Invalid choice",
|
||||||
action: config.Action{
|
req: &ExecutionRequest{
|
||||||
Title: "Test choices",
|
Binding: &ActionBinding{
|
||||||
Shell: "echo {{ option }}",
|
Action: &config.Action{
|
||||||
Arguments: []config.ActionArgument{
|
Title: "Test choices",
|
||||||
{
|
Shell: "echo {{ option }}",
|
||||||
Name: "option",
|
Arguments: []config.ActionArgument{
|
||||||
Type: "ascii",
|
{
|
||||||
Choices: []config.ActionArgumentChoice{
|
Name: "option",
|
||||||
{Value: "option1", Title: "Option 1"},
|
Type: "ascii",
|
||||||
{Value: "option2", Title: "Option 2"},
|
Choices: []config.ActionArgumentChoice{
|
||||||
|
{Value: "option1", Title: "Option 1"},
|
||||||
|
{Value: "option2", Title: "Option 2"},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Arguments: map[string]string{"option": "invalid_option"},
|
||||||
},
|
},
|
||||||
values: map[string]string{"option": "invalid_option"},
|
|
||||||
expectError: true,
|
expectError: true,
|
||||||
description: "Should reject invalid choice",
|
description: "Should reject invalid choice",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid choice",
|
||||||
|
req: &ExecutionRequest{
|
||||||
|
Binding: &ActionBinding{
|
||||||
|
Action: &config.Action{
|
||||||
|
Title: "Test choices",
|
||||||
|
Shell: "echo {{ option }}",
|
||||||
|
Arguments: []config.ActionArgument{
|
||||||
|
{
|
||||||
|
Name: "option",
|
||||||
|
Type: "ascii",
|
||||||
|
Choices: []config.ActionArgumentChoice{
|
||||||
|
{Value: "option1", Title: "Option 1"},
|
||||||
|
{Value: "option2", Title: "Option 2"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Arguments: map[string]string{"option": "option1"},
|
||||||
|
},
|
||||||
|
expectError: false,
|
||||||
|
description: "Should accept valid choice",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
_, err := parseActionArguments(tt.values, &tt.action, nil)
|
_, err := parseActionArguments(tt.req)
|
||||||
|
|
||||||
if tt.expectError {
|
if tt.expectError {
|
||||||
assert.NotNil(t, err, tt.description)
|
assert.NotNil(t, err, tt.description)
|
||||||
|
|
@ -737,7 +782,8 @@ func TestTypeSafetyCheckVeryDangerousRawString(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseActionArgumentsWithEntityPrefix(t *testing.T) {
|
func TestParseActionArgumentsWithEntityPrefix(t *testing.T) {
|
||||||
action := config.Action{
|
req := newExecRequest()
|
||||||
|
req.Binding.Action = &config.Action{
|
||||||
Title: "Test entity prefix",
|
Title: "Test entity prefix",
|
||||||
Shell: "echo 'Processing {{ name }} for entity'",
|
Shell: "echo 'Processing {{ name }} for entity'",
|
||||||
Arguments: []config.ActionArgument{
|
Arguments: []config.ActionArgument{
|
||||||
|
|
@ -745,16 +791,16 @@ func TestParseActionArgumentsWithEntityPrefix(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
values := map[string]string{
|
req.Arguments = map[string]string{
|
||||||
"name": "testuser",
|
"name": "testuser",
|
||||||
}
|
}
|
||||||
|
|
||||||
ent := &entities.Entity{
|
req.Binding.Entity = &entities.Entity{
|
||||||
Title: "entity_123",
|
Title: "entity_123",
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test with entity prefix
|
// Test with entity prefix
|
||||||
output, err := parseActionArguments(values, &action, ent)
|
output, err := parseActionArguments(req)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
assert.Contains(t, output, "testuser")
|
assert.Contains(t, output, "testuser")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import (
|
||||||
authpublic "github.com/OliveTin/OliveTin/internal/auth/authpublic"
|
authpublic "github.com/OliveTin/OliveTin/internal/auth/authpublic"
|
||||||
config "github.com/OliveTin/OliveTin/internal/config"
|
config "github.com/OliveTin/OliveTin/internal/config"
|
||||||
"github.com/OliveTin/OliveTin/internal/entities"
|
"github.com/OliveTin/OliveTin/internal/entities"
|
||||||
|
"github.com/OliveTin/OliveTin/internal/tpl"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
|
@ -111,6 +112,15 @@ type InternalLogEntry struct {
|
||||||
ActionIcon string
|
ActionIcon string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// .Binding can be nil, so we need to handle that.
|
||||||
|
func (e *InternalLogEntry) GetBindingId() string {
|
||||||
|
if e.Binding == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.Binding.ID
|
||||||
|
}
|
||||||
|
|
||||||
type executorStepFunc func(*ExecutionRequest) bool
|
type executorStepFunc func(*ExecutionRequest) bool
|
||||||
|
|
||||||
// DefaultExecutor returns an Executor, with a sensible "chain of command" for
|
// DefaultExecutor returns an Executor, with a sensible "chain of command" for
|
||||||
|
|
@ -680,7 +690,7 @@ func handleShellBranch(req *ExecutionRequest) bool {
|
||||||
return fail(req, err)
|
return fail(req, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd, err := parseActionArguments(req.Arguments, req.Binding.Action, req.Binding.Entity)
|
cmd, err := parseActionArguments(req)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fail(req, err)
|
return fail(req, err)
|
||||||
|
|
@ -728,7 +738,7 @@ func stepRequestAction(req *ExecutionRequest) bool {
|
||||||
|
|
||||||
req.logEntry.Binding = req.Binding
|
req.logEntry.Binding = req.Binding
|
||||||
req.logEntry.ActionConfigTitle = req.Binding.Action.Title
|
req.logEntry.ActionConfigTitle = req.Binding.Action.Title
|
||||||
req.logEntry.ActionTitle = entities.ParseTemplateWith(req.Binding.Action.Title, req.Binding.Entity)
|
req.logEntry.ActionTitle = tpl.ParseTemplateOfActionBeforeExec(req.Binding.Action.Title, req.Binding.Entity)
|
||||||
req.logEntry.ActionIcon = req.Binding.Action.Icon
|
req.logEntry.ActionIcon = req.Binding.Action.Icon
|
||||||
req.logEntry.Tags = req.Tags
|
req.logEntry.Tags = req.Tags
|
||||||
|
|
||||||
|
|
@ -893,7 +903,7 @@ func stepExecAfter(req *ExecutionRequest) bool {
|
||||||
"ot_username": req.AuthenticatedUser.Username,
|
"ot_username": req.AuthenticatedUser.Username,
|
||||||
}
|
}
|
||||||
|
|
||||||
finalParsedCommand, err := parseCommandForReplacements(req.Binding.Action.ShellAfterCompleted, args, req.Binding.Entity)
|
finalParsedCommand, err := tpl.ParseTemplateWithActionContext(req.Binding.Action.ShellAfterCompleted, req.Binding.Entity, args)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
msg := "Could not prepare shellAfterCompleted command: " + err.Error() + "\n"
|
msg := "Could not prepare shellAfterCompleted command: " + err.Error() + "\n"
|
||||||
|
|
|
||||||
|
|
@ -74,7 +74,8 @@ func TestExecNonExistant(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestArgumentNameCamelCase(t *testing.T) {
|
func TestArgumentNameCamelCase(t *testing.T) {
|
||||||
a1 := &config.Action{
|
req := newExecRequest()
|
||||||
|
req.Binding.Action = &config.Action{
|
||||||
Title: "Do some tickles",
|
Title: "Do some tickles",
|
||||||
Shell: "echo 'Tickling {{ personName }}'",
|
Shell: "echo 'Tickling {{ personName }}'",
|
||||||
Arguments: []config.ActionArgument{
|
Arguments: []config.ActionArgument{
|
||||||
|
|
@ -85,18 +86,19 @@ func TestArgumentNameCamelCase(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
values := map[string]string{
|
req.Arguments = map[string]string{
|
||||||
"personName": "Fred",
|
"personName": "Fred",
|
||||||
}
|
}
|
||||||
|
|
||||||
out, err := parseActionArguments(values, a1, nil)
|
out, err := parseActionArguments(req)
|
||||||
|
|
||||||
assert.Equal(t, "echo 'Tickling Fred'", out)
|
assert.Equal(t, "echo 'Tickling Fred'", out)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestArgumentNameSnakeCase(t *testing.T) {
|
func TestArgumentNameSnakeCase(t *testing.T) {
|
||||||
a1 := &config.Action{
|
req := newExecRequest()
|
||||||
|
req.Binding.Action = &config.Action{
|
||||||
Title: "Do some tickles",
|
Title: "Do some tickles",
|
||||||
Shell: "echo 'Tickling {{ person_name }}'",
|
Shell: "echo 'Tickling {{ person_name }}'",
|
||||||
Arguments: []config.ActionArgument{
|
Arguments: []config.ActionArgument{
|
||||||
|
|
@ -107,11 +109,11 @@ func TestArgumentNameSnakeCase(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
values := map[string]string{
|
req.Arguments = map[string]string{
|
||||||
"person_name": "Fred",
|
"person_name": "Fred",
|
||||||
}
|
}
|
||||||
|
|
||||||
out, err := parseActionArguments(values, a1, nil)
|
out, err := parseActionArguments(req)
|
||||||
|
|
||||||
assert.Equal(t, "echo 'Tickling Fred'", out)
|
assert.Equal(t, "echo 'Tickling Fred'", out)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
|
|
@ -205,7 +207,8 @@ func TestGetPagingIndexes(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUnsetRequiredArgument(t *testing.T) {
|
func TestUnsetRequiredArgument(t *testing.T) {
|
||||||
a1 := &config.Action{
|
req := newExecRequest()
|
||||||
|
req.Binding.Action = &config.Action{
|
||||||
Title: "Print your name",
|
Title: "Print your name",
|
||||||
Shell: "echo 'Your name is: {{ name }}'",
|
Shell: "echo 'Your name is: {{ name }}'",
|
||||||
Arguments: []config.ActionArgument{
|
Arguments: []config.ActionArgument{
|
||||||
|
|
@ -216,16 +219,17 @@ func TestUnsetRequiredArgument(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
values := map[string]string{}
|
req.Arguments = map[string]string{}
|
||||||
|
|
||||||
out, err := parseActionArguments(values, a1, nil)
|
out, err := parseActionArguments(req)
|
||||||
|
|
||||||
assert.Equal(t, "", out)
|
assert.Equal(t, "", out)
|
||||||
assert.NotNil(t, err)
|
assert.NotNil(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUnusedArgumentStillPassesTypeSafetyCheck(t *testing.T) {
|
func TestUnusedArgumentStillPassesTypeSafetyCheck(t *testing.T) {
|
||||||
a1 := &config.Action{
|
req := newExecRequest()
|
||||||
|
req.Binding.Action = &config.Action{
|
||||||
Title: "Print your name",
|
Title: "Print your name",
|
||||||
Shell: "echo 'Your name is: {{ name }}'",
|
Shell: "echo 'Your name is: {{ name }}'",
|
||||||
Arguments: []config.ActionArgument{
|
Arguments: []config.ActionArgument{
|
||||||
|
|
@ -240,12 +244,12 @@ func TestUnusedArgumentStillPassesTypeSafetyCheck(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
values := map[string]string{
|
req.Arguments = map[string]string{
|
||||||
"name": "Fred",
|
"name": "Fred",
|
||||||
"age": "Not an integer",
|
"age": "Not an integer",
|
||||||
}
|
}
|
||||||
|
|
||||||
out, err := parseActionArguments(values, a1, nil)
|
out, err := parseActionArguments(req)
|
||||||
|
|
||||||
assert.Equal(t, "", out)
|
assert.Equal(t, "", out)
|
||||||
assert.NotNil(t, err)
|
assert.NotNil(t, err)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,205 @@
|
||||||
|
package executor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LoadLogsFromDisk loads persisted logs from YAML files on disk and restores them to the executor.
|
||||||
|
// This should be called during startup if saveLogs is configured.
|
||||||
|
func (e *Executor) LoadLogsFromDisk() {
|
||||||
|
resultsDir := e.Cfg.SaveLogs.ResultsDirectory
|
||||||
|
if resultsDir == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
entries, skippedCount := e.readLogDirectory(resultsDir)
|
||||||
|
if entries == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
loadedLogs, skippedCount := e.parseLogFiles(resultsDir, entries, skippedCount)
|
||||||
|
|
||||||
|
sort.Slice(loadedLogs, func(i, j int) bool {
|
||||||
|
return loadedLogs[i].DatetimeStarted.Before(loadedLogs[j].DatetimeStarted)
|
||||||
|
})
|
||||||
|
|
||||||
|
skippedCount = e.restoreLogsToExecutor(loadedLogs, skippedCount)
|
||||||
|
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"loaded": len(loadedLogs),
|
||||||
|
"skipped": skippedCount,
|
||||||
|
}).Info("Finished loading persisted logs from disk")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Executor) readLogDirectory(resultsDir string) ([]os.DirEntry, int) {
|
||||||
|
if _, err := os.Stat(resultsDir); os.IsNotExist(err) {
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"directory": resultsDir,
|
||||||
|
}).Debug("Logs directory does not exist, skipping log loading")
|
||||||
|
return nil, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"directory": resultsDir,
|
||||||
|
}).Info("Loading persisted logs from disk")
|
||||||
|
|
||||||
|
entries, err := os.ReadDir(resultsDir)
|
||||||
|
if err != nil {
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"directory": resultsDir,
|
||||||
|
"error": err,
|
||||||
|
}).Warnf("Failed to read logs directory")
|
||||||
|
return nil, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Executor) parseLogFiles(resultsDir string, entries []os.DirEntry, skippedCount int) ([]*InternalLogEntry, int) {
|
||||||
|
loadedLogs := make([]*InternalLogEntry, 0)
|
||||||
|
|
||||||
|
for _, entry := range entries {
|
||||||
|
if !e.shouldProcessLogEntry(entry) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
logEntry, newSkippedCount := e.processLogFileEntry(resultsDir, entry.Name())
|
||||||
|
skippedCount += newSkippedCount
|
||||||
|
if logEntry != nil {
|
||||||
|
loadedLogs = append(loadedLogs, logEntry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return loadedLogs, skippedCount
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Executor) shouldProcessLogEntry(entry os.DirEntry) bool {
|
||||||
|
return !entry.IsDir() && strings.HasSuffix(entry.Name(), ".yaml")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Executor) processLogFileEntry(resultsDir, filename string) (*InternalLogEntry, int) {
|
||||||
|
logEntry, ok := e.loadLogFileFromPath(resultsDir, filename)
|
||||||
|
if !ok {
|
||||||
|
return nil, 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if logEntry.ExecutionTrackingID == "" {
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"file": filepath.Join(resultsDir, filename),
|
||||||
|
}).Warnf("Log file missing execution tracking ID, skipping")
|
||||||
|
return nil, 1
|
||||||
|
}
|
||||||
|
|
||||||
|
e.restoreBindingForLogEntry(logEntry, filepath.Join(resultsDir, filename))
|
||||||
|
return logEntry, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Executor) loadLogFileFromPath(resultsDir, filename string) (*InternalLogEntry, bool) {
|
||||||
|
filepath := filepath.Join(resultsDir, filename)
|
||||||
|
data, err := os.ReadFile(filepath)
|
||||||
|
if err != nil {
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"file": filepath,
|
||||||
|
"error": err,
|
||||||
|
}).Warnf("Failed to read log file")
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
var logEntry InternalLogEntry
|
||||||
|
if err := yaml.Unmarshal(data, &logEntry); err != nil {
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"file": filepath,
|
||||||
|
"error": err,
|
||||||
|
}).Warnf("Failed to unmarshal log file")
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
return &logEntry, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skipped when the entry already has a valid binding or has no ActionConfigTitle (e.g. action/entity removed from config).
|
||||||
|
func (e *Executor) restoreBindingForLogEntry(logEntry *InternalLogEntry, filepath string) {
|
||||||
|
if e.hasValidBinding(logEntry) || logEntry.ActionConfigTitle == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
binding := e.findBindingByActionTitle(logEntry.ActionConfigTitle, logEntry.EntityPrefix)
|
||||||
|
if binding != nil {
|
||||||
|
logEntry.Binding = binding
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
e.logBindingNotFound(logEntry, filepath)
|
||||||
|
logEntry.Binding = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Executor) hasValidBinding(logEntry *InternalLogEntry) bool {
|
||||||
|
return logEntry.Binding != nil && logEntry.Binding.Action != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Executor) logBindingNotFound(logEntry *InternalLogEntry, filepath string) {
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"file": filepath,
|
||||||
|
"actionTitle": logEntry.ActionConfigTitle,
|
||||||
|
"entityPrefix": logEntry.EntityPrefix,
|
||||||
|
"trackingId": logEntry.ExecutionTrackingID,
|
||||||
|
}).Debug("Could not find binding for log entry, loading without binding")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Executor) restoreLogsToExecutor(loadedLogs []*InternalLogEntry, skippedCount int) int {
|
||||||
|
e.logmutex.Lock()
|
||||||
|
defer e.logmutex.Unlock()
|
||||||
|
|
||||||
|
for _, logEntry := range loadedLogs {
|
||||||
|
if _, exists := e.logs[logEntry.ExecutionTrackingID]; exists {
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"trackingId": logEntry.ExecutionTrackingID,
|
||||||
|
}).Debug("Log entry already exists, skipping")
|
||||||
|
skippedCount++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
logEntry.Index = int64(len(e.logsTrackingIdsByDate))
|
||||||
|
e.logs[logEntry.ExecutionTrackingID] = logEntry
|
||||||
|
e.logsTrackingIdsByDate = append(e.logsTrackingIdsByDate, logEntry.ExecutionTrackingID)
|
||||||
|
|
||||||
|
if logEntry.Binding != nil {
|
||||||
|
e.addLogToBindingMap(logEntry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return skippedCount
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Executor) addLogToBindingMap(logEntry *InternalLogEntry) {
|
||||||
|
if _, containsKey := e.LogsByBindingId[logEntry.Binding.ID]; !containsKey {
|
||||||
|
e.LogsByBindingId[logEntry.Binding.ID] = make([]*InternalLogEntry, 0)
|
||||||
|
}
|
||||||
|
e.LogsByBindingId[logEntry.Binding.ID] = append(e.LogsByBindingId[logEntry.Binding.ID], logEntry)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Executor) findBindingByActionTitle(actionConfigTitle string, entityPrefix string) *ActionBinding {
|
||||||
|
e.MapActionBindingsLock.RLock()
|
||||||
|
defer e.MapActionBindingsLock.RUnlock()
|
||||||
|
|
||||||
|
for _, binding := range e.MapActionBindings {
|
||||||
|
if binding.Action.Title == actionConfigTitle && e.matchesEntityPrefix(binding, entityPrefix) {
|
||||||
|
return binding
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Executor) matchesEntityPrefix(binding *ActionBinding, entityPrefix string) bool {
|
||||||
|
if entityPrefix == "" {
|
||||||
|
return binding.Entity == nil
|
||||||
|
}
|
||||||
|
return binding.Entity != nil && binding.Entity.UniqueKey == entityPrefix
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,210 @@
|
||||||
|
package tpl
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"text/template"
|
||||||
|
|
||||||
|
"github.com/OliveTin/OliveTin/internal/entities"
|
||||||
|
"github.com/OliveTin/OliveTin/internal/env"
|
||||||
|
"github.com/OliveTin/OliveTin/internal/installationinfo"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
var tpl = template.New("tpl").
|
||||||
|
Option("missingkey=error")
|
||||||
|
|
||||||
|
type olivetinInfo struct {
|
||||||
|
Build *installationinfo.BuildInfo
|
||||||
|
Runtime *installationinfo.RuntimeInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
var legacyArgumentRegex = regexp.MustCompile(`{{\s*([a-zA-Z0-9_]+)\s*}}`)
|
||||||
|
var legacyEntityPropertiesRegex = regexp.MustCompile(`{{\s*([a-zA-Z0-9_]+)\.([a-zA-Z0-9_\.]+)\s*}}`)
|
||||||
|
|
||||||
|
type generalTemplateContext struct {
|
||||||
|
OliveTin olivetinInfo
|
||||||
|
Env map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
type actionTemplateContext struct {
|
||||||
|
CurrentEntity interface{}
|
||||||
|
Arguments map[string]string
|
||||||
|
|
||||||
|
// These are deliberately repeated because embedding structs
|
||||||
|
// won't work in text/template.
|
||||||
|
OliveTin olivetinInfo
|
||||||
|
Env map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
cachedOliveTinInfo olivetinInfo
|
||||||
|
cachedEnvMap map[string]string
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
cachedOliveTinInfo = olivetinInfo{
|
||||||
|
Build: installationinfo.Build,
|
||||||
|
Runtime: installationinfo.Runtime,
|
||||||
|
}
|
||||||
|
|
||||||
|
cachedEnvMap = env.BuildEnvMap()
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetNewGeneralTemplateContext() *generalTemplateContext {
|
||||||
|
return &generalTemplateContext{
|
||||||
|
OliveTin: cachedOliveTinInfo,
|
||||||
|
Env: cachedEnvMap,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func migrateLegacyEntityProperties(rawShellCommand string) string {
|
||||||
|
foundArgumentNames := legacyEntityPropertiesRegex.FindAllStringSubmatch(rawShellCommand, -1)
|
||||||
|
|
||||||
|
for _, match := range foundArgumentNames {
|
||||||
|
entityName := match[1]
|
||||||
|
argName := match[2]
|
||||||
|
fullMatch := match[0] // The entire matched string like "{{ server.hostname }}"
|
||||||
|
|
||||||
|
if strings.Contains(argName, ".") {
|
||||||
|
replacement := "{{ .CurrentEntity." + argName + " }}"
|
||||||
|
|
||||||
|
rawShellCommand = strings.ReplaceAll(rawShellCommand, fullMatch, replacement)
|
||||||
|
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"old": entityName,
|
||||||
|
"new": ".CurrentEntity",
|
||||||
|
}).Debugf("Legacy entity variable name found, changing to CurrentEntity")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.HasPrefix(argName, ".Arguments.") {
|
||||||
|
replacement := "{{ .CurrentEntity." + argName + " }}"
|
||||||
|
|
||||||
|
rawShellCommand = strings.ReplaceAll(rawShellCommand, fullMatch, replacement)
|
||||||
|
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"old": argName,
|
||||||
|
"new": ".CurrentEntity." + argName,
|
||||||
|
}).Debugf("Legacy variable name found, changing to CurrentEntity")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return rawShellCommand
|
||||||
|
}
|
||||||
|
|
||||||
|
func migrateLegacyArgumentNames(rawShellCommand string) string {
|
||||||
|
matches := legacyArgumentRegex.FindAllStringSubmatchIndex(rawShellCommand, -1)
|
||||||
|
|
||||||
|
for i := len(matches) - 1; i >= 0; i-- {
|
||||||
|
match := matches[i]
|
||||||
|
fullMatchStart := match[0]
|
||||||
|
fullMatchEnd := match[1]
|
||||||
|
argNameStart := match[2]
|
||||||
|
argNameEnd := match[3]
|
||||||
|
|
||||||
|
argName := rawShellCommand[argNameStart:argNameEnd]
|
||||||
|
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"old": argName,
|
||||||
|
"new": ".Arguments." + argName,
|
||||||
|
}).Debugf("Legacy variable name found, changing to Argument")
|
||||||
|
|
||||||
|
replacement := "{{ .Arguments." + argName + " }}"
|
||||||
|
rawShellCommand = rawShellCommand[:fullMatchStart] + replacement + rawShellCommand[fullMatchEnd:]
|
||||||
|
}
|
||||||
|
|
||||||
|
return rawShellCommand
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseTemplateWithActionContext(source string, ent *entities.Entity, args map[string]string) (string, error) {
|
||||||
|
source = migrateLegacyArgumentNames(source)
|
||||||
|
source = migrateLegacyEntityProperties(source)
|
||||||
|
|
||||||
|
var entdata any
|
||||||
|
|
||||||
|
if ent != nil {
|
||||||
|
entdata = ent.Data
|
||||||
|
}
|
||||||
|
|
||||||
|
templateVariables := &actionTemplateContext{
|
||||||
|
OliveTin: cachedOliveTinInfo,
|
||||||
|
Env: cachedEnvMap,
|
||||||
|
|
||||||
|
Arguments: args,
|
||||||
|
CurrentEntity: entdata,
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := parseTemplate(source, templateVariables)
|
||||||
|
|
||||||
|
if isMissingArgumentError, argName := checkMissingArgumentError(err); isMissingArgumentError {
|
||||||
|
return "", fmt.Errorf("required arg not provided: %s", argName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkMissingArgumentError(err error) (bool, string) {
|
||||||
|
if err == nil {
|
||||||
|
return false, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(err.Error(), "map has no entry for key") {
|
||||||
|
re := regexp.MustCompile(`\.Arguments\.(\w+)`)
|
||||||
|
match := re.FindStringSubmatch(err.Error())
|
||||||
|
if len(match) > 1 {
|
||||||
|
return true, match[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseTemplate(source string, data any) (string, error) {
|
||||||
|
t, err := tpl.Parse(source)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
var sb strings.Builder
|
||||||
|
err = t.Execute(&sb, data)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"source": source,
|
||||||
|
"err": err,
|
||||||
|
}).Errorf("Error executing template")
|
||||||
|
|
||||||
|
return "", err
|
||||||
|
} else {
|
||||||
|
return sb.String(), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseTemplateOfActionBeforeExec(source string, ent *entities.Entity) string {
|
||||||
|
result, err := ParseTemplateWithActionContext(source, ent, nil)
|
||||||
|
if err != nil {
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"source": source,
|
||||||
|
"err": err,
|
||||||
|
}).Errorf("Error parsing template of action before exec")
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
func ParseTemplateBoolWith(source string, ent *entities.Entity) bool {
|
||||||
|
source = strings.TrimSpace(source)
|
||||||
|
|
||||||
|
tplBool := ParseTemplateOfActionBeforeExec(source, ent)
|
||||||
|
|
||||||
|
return tplBool == "true"
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
@ -46,7 +46,12 @@ func (m *JSONMatcher) ExtractValue(pathExpr string) (string, error) {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Marshal to JSON for consistent string representation
|
// For string values, return directly without marshaling to avoid adding JSON quotes
|
||||||
|
if strValue, ok := value.(string); ok {
|
||||||
|
return strValue, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// For non-string values, marshal to JSON for consistent string representation
|
||||||
jsonBytes, err := json.Marshal(value)
|
jsonBytes, err := json.Marshal(value)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("failed to marshal extracted value: %w", err)
|
return "", fmt.Errorf("failed to marshal extracted value: %w", err)
|
||||||
|
|
|
||||||
|
|
@ -257,6 +257,8 @@ func main() {
|
||||||
executor.RebuildActionMap()
|
executor.RebuildActionMap()
|
||||||
config.AddListener(executor.RebuildActionMap)
|
config.AddListener(executor.RebuildActionMap)
|
||||||
|
|
||||||
|
executor.LoadLogsFromDisk()
|
||||||
|
|
||||||
go onstartup.Execute(cfg, executor)
|
go onstartup.Execute(cfg, executor)
|
||||||
go oncron.Schedule(cfg, executor)
|
go oncron.Schedule(cfg, executor)
|
||||||
go onfileindir.WatchFilesInDirectory(cfg, executor)
|
go onfileindir.WatchFilesInDirectory(cfg, executor)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue