Remove the delivery test bench
The delivery test bench and its notes were scaffolding for validating the software-delivery path during development. That work is complete and covered by tests, so the bench is removed along with its route registration rather than left in the shipped UI where it would read as a supported feature.
This commit is contained in:
@@ -23,16 +23,6 @@ export default [
|
||||
name: 'endpoints-monitors-settings',
|
||||
component: () => import('./views/MonitorsSettings.vue'),
|
||||
meta: { requiresAuth: true, requiresAdmin: true, plugin: 'endpoints' }
|
||||
},
|
||||
// TEMPORARY / DEV ONLY. A manual bench to exercise the update_app and
|
||||
// run_script delivery path end to end while Cameron builds the real UI.
|
||||
// Gated on endpoints.deploy inside the view; remove once the production
|
||||
// delivery UI lands.
|
||||
{
|
||||
path: 'endpoints/delivery-test',
|
||||
name: 'endpoints-delivery-test',
|
||||
component: () => import('./views/DeliveryTestBench.vue'),
|
||||
meta: { requiresAuth: true, plugin: 'endpoints' }
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@@ -1,327 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="page-header">
|
||||
<h2>Delivery Test Bench</h2>
|
||||
</div>
|
||||
|
||||
<!--
|
||||
TEMPORARY / DEV ONLY. This view exists to exercise the update_app and
|
||||
run_script delivery path end to end and to take notes for the real UI
|
||||
(owned by Cameron). It is gated on endpoints.deploy. Remove it once the
|
||||
production delivery UI lands. Nothing here is a supported operator surface.
|
||||
-->
|
||||
<div class="card dev-note">
|
||||
<strong>Development test vehicle.</strong>
|
||||
This bench pushes an allowlisted delivery command to one endpoint and reads
|
||||
back the result. The agent only runs a script name it already holds in its
|
||||
local allowlist, and only installs a ShopDB-hosted package pinned by digest.
|
||||
Both actions require the <code>endpoints.deploy</code> permission.
|
||||
</div>
|
||||
|
||||
<div v-if="!candeploy" class="card load-error">
|
||||
Your account lacks the <code>endpoints.deploy</code> permission, so no
|
||||
delivery command can be queued. Ask an administrator to grant it.
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<!-- 1. Pick the target endpoint -->
|
||||
<div class="card bench-section">
|
||||
<h3>1. Target endpoint</h3>
|
||||
<div class="bench-row">
|
||||
<input
|
||||
v-model="endpointSearch"
|
||||
type="text"
|
||||
class="form-control"
|
||||
placeholder="Search hostname or asset..."
|
||||
@keyup.enter="loadEndpoints"
|
||||
/>
|
||||
<button class="btn btn-secondary btn-sm" :disabled="busy" @click="loadEndpoints">Search</button>
|
||||
</div>
|
||||
<select v-model="selectedAssetId" class="form-control" :disabled="busy">
|
||||
<option value="">Select an endpoint...</option>
|
||||
<option v-for="row in endpoints" :key="row.assetid" :value="row.assetid">
|
||||
{{ row.hostname }} (asset {{ row.assetid }})
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- 2. Run a ShopDB-hosted script from the library -->
|
||||
<div class="card bench-section">
|
||||
<h3>2. Run script</h3>
|
||||
<p class="bench-hint">
|
||||
Pick a script from the ShopDB script library. The agent fetches the
|
||||
script body by digest with its collector credential and runs it in the
|
||||
script's configured run context (system or the interactive user).
|
||||
</p>
|
||||
<div class="bench-row">
|
||||
<select v-model="selectedScriptId" class="form-control" :disabled="busy">
|
||||
<option value="">Select script...</option>
|
||||
<option v-for="script in scripts" :key="script.id" :value="script.id">
|
||||
{{ script.scriptname }} ({{ script.runcontext }})
|
||||
<template v-if="!script.sha256"> (no body - upload first)</template>
|
||||
</option>
|
||||
</select>
|
||||
<button
|
||||
class="btn btn-warning btn-sm"
|
||||
:disabled="busy || !selectedAssetId || !selectedScriptId"
|
||||
@click="pushScript"
|
||||
>
|
||||
Push run_script
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 3. Install/update an application -->
|
||||
<div class="card bench-section">
|
||||
<h3>3. Install/update application</h3>
|
||||
<p class="bench-hint">
|
||||
Pick an application, then the app-level install source (the installer
|
||||
uploaded on the application itself). The agent fetches the pinned
|
||||
package by digest with its collector credential and runs it silently.
|
||||
</p>
|
||||
<div class="bench-row">
|
||||
<select v-model="selectedAppId" class="form-control" :disabled="busy" @change="loadSources">
|
||||
<option value="">Select application...</option>
|
||||
<option v-for="app in applications" :key="app.appid" :value="app.appid">
|
||||
{{ app.appname }}
|
||||
</option>
|
||||
</select>
|
||||
<select v-model="selectedSource" class="form-control" :disabled="busy || !selectedAppId">
|
||||
<option value="">Select install source...</option>
|
||||
<option v-for="source in installSources" :key="source.value" :value="source.value">
|
||||
{{ source.label }}
|
||||
</option>
|
||||
</select>
|
||||
<button
|
||||
class="btn btn-warning btn-sm"
|
||||
:disabled="busy || !selectedAssetId || !selectedSource"
|
||||
@click="pushUpdateApp"
|
||||
>
|
||||
Push update_app
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 4. Command results (the Web UI log pane) -->
|
||||
<div class="card bench-section">
|
||||
<div class="bench-row bench-row-between">
|
||||
<h3>4. Command log (Web UI side)</h3>
|
||||
<button class="btn btn-secondary btn-sm" :disabled="busy || !selectedAssetId" @click="loadCommands">
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
<p v-if="!selectedAssetId" class="bench-hint">Select an endpoint to see its recent commands.</p>
|
||||
<table v-else-if="commands.length" class="table bench-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>When</th>
|
||||
<th>Action</th>
|
||||
<th>Target</th>
|
||||
<th>Status</th>
|
||||
<th>Result</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="command in commands" :key="command.commandid">
|
||||
<td>{{ formatWhen(command.createdat) }}</td>
|
||||
<td>{{ command.action }}</td>
|
||||
<td>{{ command.target || '-' }}</td>
|
||||
<td><span class="status-badge" :class="statusClass(command.status)">{{ command.status }}</span></td>
|
||||
<td class="result-cell">{{ command.resultmessage || '-' }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p v-else class="bench-hint">No commands recorded for this endpoint yet.</p>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { endpointsApi, applicationsApi, scriptsApi } from '@/api'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useToast } from '@/composables/toast'
|
||||
import { apiError } from '@/utils/apiError'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const toast = useToast()
|
||||
|
||||
// Gated in the UI for convenience only; every enqueue is re-checked server-side.
|
||||
const candeploy = computed(() => authStore.hasPermission('endpoints.deploy'))
|
||||
|
||||
const busy = ref(false)
|
||||
|
||||
const endpointSearch = ref('')
|
||||
const endpoints = ref([])
|
||||
const selectedAssetId = ref('')
|
||||
|
||||
const scripts = ref([])
|
||||
const selectedScriptId = ref('')
|
||||
|
||||
const applications = ref([])
|
||||
const selectedAppId = ref('')
|
||||
const installSources = ref([])
|
||||
const selectedSource = ref('')
|
||||
|
||||
const commands = ref([])
|
||||
|
||||
onMounted(() => {
|
||||
if (!candeploy.value) return
|
||||
loadEndpoints()
|
||||
loadApplications()
|
||||
loadScripts()
|
||||
})
|
||||
|
||||
async function loadEndpoints() {
|
||||
busy.value = true
|
||||
try {
|
||||
const params = { perpage: 100 }
|
||||
if (endpointSearch.value) params.search = endpointSearch.value
|
||||
const response = await endpointsApi.list(params)
|
||||
endpoints.value = response.data.data || []
|
||||
} catch (error) {
|
||||
toast.error(apiError(error, 'Could not load endpoints'))
|
||||
endpoints.value = []
|
||||
} finally {
|
||||
busy.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadApplications() {
|
||||
try {
|
||||
applications.value = await applicationsApi.listAll()
|
||||
} catch (error) {
|
||||
applications.value = []
|
||||
}
|
||||
}
|
||||
|
||||
async function loadScripts() {
|
||||
try {
|
||||
scripts.value = await scriptsApi.listAll()
|
||||
} catch (error) {
|
||||
scripts.value = []
|
||||
}
|
||||
}
|
||||
|
||||
// Build the install-source options for the chosen application: the app-level
|
||||
// source (the installer uploaded on the application itself). The wire target is
|
||||
// 'app:<id>', which the server resolves into a pinned delivery descriptor.
|
||||
async function loadSources() {
|
||||
selectedSource.value = ''
|
||||
installSources.value = []
|
||||
if (!selectedAppId.value) return
|
||||
|
||||
const sources = []
|
||||
const app = applications.value.find(row => String(row.appid) === String(selectedAppId.value))
|
||||
if (app && app.installpath) {
|
||||
sources.push({
|
||||
value: `app:${app.appid}`,
|
||||
label: app.installersha256
|
||||
? 'App-level installer'
|
||||
: 'App-level installer (no digest - upload package first)',
|
||||
})
|
||||
}
|
||||
|
||||
installSources.value = sources
|
||||
}
|
||||
|
||||
async function pushScript() {
|
||||
await enqueue('run_script', `script:${selectedScriptId.value}`, 'run_script')
|
||||
}
|
||||
|
||||
async function pushUpdateApp() {
|
||||
await enqueue('update_app', selectedSource.value, 'update_app')
|
||||
}
|
||||
|
||||
async function enqueue(action, target, label) {
|
||||
if (!selectedAssetId.value) return
|
||||
busy.value = true
|
||||
try {
|
||||
await endpointsApi.enqueueCommand(selectedAssetId.value, action, target)
|
||||
toast.success(`${label} queued`)
|
||||
await loadCommands()
|
||||
} catch (error) {
|
||||
// 409 = a command is already in flight for this endpoint (at-most-one queue).
|
||||
const status = error.response?.status
|
||||
if (status === 409) {
|
||||
toast.info('A command is already in flight for this endpoint')
|
||||
} else {
|
||||
toast.error(apiError(error, `${label} failed`))
|
||||
}
|
||||
} finally {
|
||||
busy.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadCommands() {
|
||||
if (!selectedAssetId.value) return
|
||||
try {
|
||||
const response = await endpointsApi.listCommands(selectedAssetId.value)
|
||||
commands.value = response.data.data || []
|
||||
} catch (error) {
|
||||
commands.value = []
|
||||
}
|
||||
}
|
||||
|
||||
function formatWhen(iso) {
|
||||
if (!iso) return '-'
|
||||
const date = new Date(iso)
|
||||
return Number.isNaN(date.getTime()) ? iso : date.toLocaleString()
|
||||
}
|
||||
|
||||
function statusClass(status) {
|
||||
if (status === 'succeeded') return 'ok'
|
||||
if (status === 'failed') return 'bad'
|
||||
if (status === 'expired') return 'bad'
|
||||
if (status === 'running') return 'warn'
|
||||
return ''
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dev-note {
|
||||
border-left: 3px solid var(--warning, #d6a012);
|
||||
}
|
||||
.bench-section h3 {
|
||||
margin-top: 0;
|
||||
}
|
||||
.bench-row {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
align-items: center;
|
||||
}
|
||||
.bench-row-between {
|
||||
justify-content: space-between;
|
||||
}
|
||||
.bench-hint {
|
||||
color: var(--text-light);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.bench-table {
|
||||
width: 100%;
|
||||
}
|
||||
.result-cell {
|
||||
max-width: 32rem;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
.status-badge {
|
||||
padding: 0.1rem 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.status-badge.ok {
|
||||
color: var(--success, #2e7d32);
|
||||
}
|
||||
.status-badge.bad {
|
||||
color: var(--danger, #c62828);
|
||||
}
|
||||
.status-badge.warn {
|
||||
color: var(--warning, #d6a012);
|
||||
}
|
||||
.load-error {
|
||||
color: var(--danger, #c62828);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user