@cat-factory/app 0.53.0 → 0.54.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/components/settings/InfrastructureBackendPicker.vue +304 -0
- package/app/components/settings/InfrastructureWindow.vue +6 -10
- package/app/components/settings/KubernetesRunnerForm.vue +30 -0
- package/app/components/settings/ProviderConnectionTab.vue +46 -85
- package/app/components/settings/ProviderManifestEditor.vue +19 -3
- package/i18n/locales/en.json +16 -13
- package/i18n/locales/es.json +3 -11
- package/i18n/locales/fr.json +3 -11
- package/i18n/locales/pl.json +3 -11
- package/i18n/locales/uk.json +3 -11
- package/package.json +2 -2
- package/app/components/settings/ExecutionBackendSelector.vue +0 -171
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// ONE flat radio list per infrastructure axis — the single picker that replaces the old
|
|
3
|
+
// two-step "where it runs" radio + "runner/environment backend" dropdown. Each item is a
|
|
4
|
+
// concrete destination with a one-line description, so there is no more hidden second list:
|
|
5
|
+
// - execution axis → where agent containers run: the facade built-in (local Docker /
|
|
6
|
+
// Cloudflare Containers), the registered backend kinds (Kubernetes, a custom HTTP runner
|
|
7
|
+
// pool, plus any deployment-registered CUSTOM kind), and — in local mode — a low-config
|
|
8
|
+
// "Local Kubernetes (k3s)" preset.
|
|
9
|
+
// - testEnv axis → where the Tester's ephemeral environments run: the built-in
|
|
10
|
+
// (in-container docker-compose) + the registered backend kinds.
|
|
11
|
+
//
|
|
12
|
+
// The backend kinds come from the workspace snapshot (`providerConnections.backendKindsFor`),
|
|
13
|
+
// so a custom kind a deployment registered shows up as a first-class radio item. Selecting a
|
|
14
|
+
// pool/cluster item reveals its connect form inline (driven through ProviderConnectionTab). In
|
|
15
|
+
// LOCAL MODE the choice is a real per-workspace toggle (the `delegate*` settings) — but we
|
|
16
|
+
// DEFER writing `delegate=true` until a connection is actually registered, so a half-configured
|
|
17
|
+
// pick never routes runs to a non-existent pool. Off-local (Worker/Node) the active backend is
|
|
18
|
+
// deployment/registration-driven, so the radio doesn't write the toggle — it only reveals the
|
|
19
|
+
// connect forms — and a read-only "Active: …" line states what's effectively routing.
|
|
20
|
+
import { computed, ref, watch } from 'vue'
|
|
21
|
+
import type { ExecutionBackendKind, TestEnvBackendKind } from '@cat-factory/contracts'
|
|
22
|
+
import ProviderConnectionTab from '~/components/settings/ProviderConnectionTab.vue'
|
|
23
|
+
|
|
24
|
+
const props = defineProps<{ axis: 'execution' | 'testEnv' }>()
|
|
25
|
+
|
|
26
|
+
const { t } = useI18n()
|
|
27
|
+
const auth = useAuthStore()
|
|
28
|
+
const settings = useWorkspaceSettingsStore()
|
|
29
|
+
const providerConnections = useProviderConnectionsStore()
|
|
30
|
+
const toast = useToast()
|
|
31
|
+
|
|
32
|
+
type BackendKind = ExecutionBackendKind | TestEnvBackendKind
|
|
33
|
+
// A radio item: the built-in facade runtime, one per registered backend kind (built-in +
|
|
34
|
+
// custom), or the synthetic local-k3s preset. `backendKind` is the slug passed to the connect
|
|
35
|
+
// tab (absent on the built-in, which means "don't delegate"); `preset` prefills the k8s form.
|
|
36
|
+
interface PickerItem {
|
|
37
|
+
id: string
|
|
38
|
+
backendKind?: string
|
|
39
|
+
preset?: 'k3s'
|
|
40
|
+
label: string
|
|
41
|
+
desc: string
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// The built-in facade runtimes carry localized labels + descriptions. i18n leaf keys are
|
|
45
|
+
// spelled as literals so the typed-message-keys check stays live.
|
|
46
|
+
const BUILTIN_KEYS: Record<BackendKind, { label: string; desc: string }> = {
|
|
47
|
+
'local-docker': {
|
|
48
|
+
label: 'settings.infrastructure.executionBackend.local-docker',
|
|
49
|
+
desc: 'settings.infrastructure.executionBackend.local-dockerDesc',
|
|
50
|
+
},
|
|
51
|
+
'cloudflare-containers': {
|
|
52
|
+
label: 'settings.infrastructure.executionBackend.cloudflare-containers',
|
|
53
|
+
desc: 'settings.infrastructure.executionBackend.cloudflare-containersDesc',
|
|
54
|
+
},
|
|
55
|
+
'runner-pool': {
|
|
56
|
+
label: 'settings.infrastructure.executionBackend.runner-pool',
|
|
57
|
+
desc: 'settings.infrastructure.executionBackend.runner-poolDesc',
|
|
58
|
+
},
|
|
59
|
+
'local-compose': {
|
|
60
|
+
label: 'settings.infrastructure.testEnvBackend.local-compose',
|
|
61
|
+
desc: 'settings.infrastructure.testEnvBackend.local-composeDesc',
|
|
62
|
+
},
|
|
63
|
+
'environment-provider': {
|
|
64
|
+
label: 'settings.infrastructure.testEnvBackend.environment-provider',
|
|
65
|
+
desc: 'settings.infrastructure.testEnvBackend.environment-providerDesc',
|
|
66
|
+
},
|
|
67
|
+
}
|
|
68
|
+
// The built-in `kubernetes` backend kind's localized label/desc, per axis.
|
|
69
|
+
const KUBERNETES_KEYS: Record<'execution' | 'testEnv', { label: string; desc: string }> = {
|
|
70
|
+
execution: {
|
|
71
|
+
label: 'settings.infrastructure.executionBackend.kubernetes',
|
|
72
|
+
desc: 'settings.infrastructure.executionBackend.kubernetesDesc',
|
|
73
|
+
},
|
|
74
|
+
testEnv: {
|
|
75
|
+
label: 'settings.infrastructure.testEnvBackend.kubernetes',
|
|
76
|
+
desc: 'settings.infrastructure.testEnvBackend.kubernetesDesc',
|
|
77
|
+
},
|
|
78
|
+
}
|
|
79
|
+
// k3s is an execution-only preset (the env k8s config can't be reduced to low-config), so a
|
|
80
|
+
// single catalog key serves it — no dead test-env keys.
|
|
81
|
+
const K3S_KEYS = {
|
|
82
|
+
label: 'settings.infrastructure.executionBackend.k3s',
|
|
83
|
+
desc: 'settings.infrastructure.executionBackend.k3sDesc',
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const cap = computed<{ available: BackendKind[]; active: BackendKind } | null>(() => {
|
|
87
|
+
const c = auth.infrastructure?.[props.axis]
|
|
88
|
+
return c ? { available: c.available as BackendKind[], active: c.active as BackendKind } : null
|
|
89
|
+
})
|
|
90
|
+
const isLocal = computed(() => auth.localMode?.enabled === true)
|
|
91
|
+
const suggestedImage = computed(() => auth.infrastructure?.execution.suggestedExecutorImage)
|
|
92
|
+
|
|
93
|
+
// The kind reached by "delegating" away from the on-machine default + its connection kind.
|
|
94
|
+
const delegatedKind = computed<BackendKind>(() =>
|
|
95
|
+
props.axis === 'execution' ? 'runner-pool' : 'environment-provider',
|
|
96
|
+
)
|
|
97
|
+
const connectionKind = computed<'runner-pool' | 'environment'>(() =>
|
|
98
|
+
props.axis === 'execution' ? 'runner-pool' : 'environment',
|
|
99
|
+
)
|
|
100
|
+
// The built-in (the available option that isn't the delegated one), if this facade has one.
|
|
101
|
+
const builtinKind = computed<BackendKind | null>(
|
|
102
|
+
() => cap.value?.available.find((k) => k !== delegatedKind.value) ?? null,
|
|
103
|
+
)
|
|
104
|
+
const connection = computed(() => providerConnections.connectionFor(connectionKind.value))
|
|
105
|
+
const connectionRegistered = computed(() => !!connection.value)
|
|
106
|
+
// Pool/cluster items can be configured only when the deployment supports delegation AND the
|
|
107
|
+
// connect integration is enabled (not 503).
|
|
108
|
+
const poolConfigurable = computed(
|
|
109
|
+
() =>
|
|
110
|
+
(cap.value?.available.includes(delegatedKind.value) ?? false) &&
|
|
111
|
+
providerConnections.isAvailable(connectionKind.value),
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
// The delegation flag is a genuine per-workspace toggle ONLY in local mode.
|
|
115
|
+
const writable = computed(() => isLocal.value && (cap.value?.available.length ?? 0) > 1)
|
|
116
|
+
const delegated = computed(() =>
|
|
117
|
+
props.axis === 'execution'
|
|
118
|
+
? settings.settings.delegateAgentsToRunnerPool
|
|
119
|
+
: settings.settings.delegateTestEnvToProvider,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
// The effective active backend (matches the prior ExecutionBackendSelector logic): local
|
|
123
|
+
// mode follows the toggle; off-local the delegated backend is active when its pool is
|
|
124
|
+
// registered, else the deployment default.
|
|
125
|
+
const effectiveActive = computed<BackendKind | null>(() => {
|
|
126
|
+
if (!cap.value) return builtinKind.value
|
|
127
|
+
if (writable.value) return delegated.value ? delegatedKind.value : builtinKind.value
|
|
128
|
+
if (cap.value.available.includes(delegatedKind.value) && connectionRegistered.value) {
|
|
129
|
+
return delegatedKind.value
|
|
130
|
+
}
|
|
131
|
+
return cap.value.active
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
// Localized label/desc for a built-in backend kind; null for a CUSTOM kind (which uses its
|
|
135
|
+
// snapshot label and has no description).
|
|
136
|
+
function backendKindKeys(kind: string): { label: string; desc: string } | null {
|
|
137
|
+
if (kind === 'kubernetes') return KUBERNETES_KEYS[props.axis]
|
|
138
|
+
if (kind === 'manifest') return BUILTIN_KEYS[delegatedKind.value]
|
|
139
|
+
return null
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// The radio item the current state maps to (an item id). A stored connection reads back as its
|
|
143
|
+
// own backend-kind slug; k3s is a one-shot prefill, never re-derived.
|
|
144
|
+
const derivedItem = computed<string>(() => {
|
|
145
|
+
if (effectiveActive.value === delegatedKind.value) return connection.value?.kind ?? 'kubernetes'
|
|
146
|
+
return 'builtin'
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
const selected = ref<string>(derivedItem.value)
|
|
150
|
+
// Re-sync when the derived state changes (e.g. after a save/remove or a settings flip
|
|
151
|
+
// elsewhere). A pending pool/k3s pick before save doesn't move `derivedItem`, so the user's
|
|
152
|
+
// selection survives until a connection is registered.
|
|
153
|
+
watch(derivedItem, (v) => {
|
|
154
|
+
selected.value = v
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
const items = computed<PickerItem[]>(() => {
|
|
158
|
+
const out: PickerItem[] = []
|
|
159
|
+
if (builtinKind.value) {
|
|
160
|
+
const k = BUILTIN_KEYS[builtinKind.value]
|
|
161
|
+
out.push({ id: 'builtin', label: t(k.label), desc: t(k.desc) })
|
|
162
|
+
}
|
|
163
|
+
if (poolConfigurable.value) {
|
|
164
|
+
for (const opt of providerConnections.backendKindsFor(connectionKind.value)) {
|
|
165
|
+
const keys = backendKindKeys(opt.kind)
|
|
166
|
+
out.push({
|
|
167
|
+
id: opt.kind,
|
|
168
|
+
backendKind: opt.kind,
|
|
169
|
+
label: keys ? t(keys.label) : opt.label,
|
|
170
|
+
desc: keys ? t(keys.desc) : '',
|
|
171
|
+
})
|
|
172
|
+
}
|
|
173
|
+
// The k3s low-config preset prefills the RUNNER k8s form; the env k8s config (manifest
|
|
174
|
+
// source + URL derivation) can't be reduced to low-config, so it's execution-only.
|
|
175
|
+
if (isLocal.value && props.axis === 'execution') {
|
|
176
|
+
out.push({
|
|
177
|
+
id: 'k3s',
|
|
178
|
+
backendKind: 'kubernetes',
|
|
179
|
+
preset: 'k3s',
|
|
180
|
+
label: t(K3S_KEYS.label),
|
|
181
|
+
desc: t(K3S_KEYS.desc),
|
|
182
|
+
})
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return out
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
// The active-line label: prefer the registered connection's concrete kind over the generic
|
|
189
|
+
// "delegated" label so "Active: Kubernetes cluster" reads truthfully.
|
|
190
|
+
const activeLabel = computed(() => {
|
|
191
|
+
const c = connection.value
|
|
192
|
+
if (effectiveActive.value === delegatedKind.value && c?.kind) {
|
|
193
|
+
const keys = backendKindKeys(c.kind)
|
|
194
|
+
if (keys) return t(keys.label)
|
|
195
|
+
const opt = providerConnections
|
|
196
|
+
.backendKindsFor(connectionKind.value)
|
|
197
|
+
.find((o) => o.kind === c.kind)
|
|
198
|
+
return opt?.label ?? c.kind
|
|
199
|
+
}
|
|
200
|
+
const kind = effectiveActive.value
|
|
201
|
+
return kind ? t(BUILTIN_KEYS[kind].label) : ''
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
const saving = ref(false)
|
|
205
|
+
|
|
206
|
+
async function setDelegate(value: boolean) {
|
|
207
|
+
saving.value = true
|
|
208
|
+
try {
|
|
209
|
+
await settings.update(
|
|
210
|
+
props.axis === 'execution'
|
|
211
|
+
? { delegateAgentsToRunnerPool: value }
|
|
212
|
+
: { delegateTestEnvToProvider: value },
|
|
213
|
+
)
|
|
214
|
+
} catch (e) {
|
|
215
|
+
toast.add({
|
|
216
|
+
title: t('settings.infrastructure.updateFailed'),
|
|
217
|
+
description: e instanceof Error ? e.message : String(e),
|
|
218
|
+
icon: 'i-lucide-triangle-alert',
|
|
219
|
+
color: 'error',
|
|
220
|
+
})
|
|
221
|
+
} finally {
|
|
222
|
+
saving.value = false
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function select(id: string) {
|
|
227
|
+
selected.value = id
|
|
228
|
+
if (!writable.value) return // off-local: reveal the form, but don't flip a toggle.
|
|
229
|
+
if (id === 'builtin') {
|
|
230
|
+
await setDelegate(false)
|
|
231
|
+
return
|
|
232
|
+
}
|
|
233
|
+
// Pool/cluster pick: only commit delegation now if a pool is already registered; otherwise
|
|
234
|
+
// defer to onConnected so we never route to a non-existent pool (the amber hint nags).
|
|
235
|
+
if (connectionRegistered.value) await setDelegate(true)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Emitted by ProviderConnectionTab after a connection is successfully registered: now it's
|
|
239
|
+
// safe to activate delegation for the pending pick.
|
|
240
|
+
async function onConnected() {
|
|
241
|
+
if (writable.value && selected.value !== 'builtin' && !delegated.value) await setDelegate(true)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Which connect form ProviderConnectionTab should show, and the k3s prefill signal.
|
|
245
|
+
const selectedItem = computed<PickerItem | undefined>(() =>
|
|
246
|
+
items.value.find((i) => i.id === selected.value),
|
|
247
|
+
)
|
|
248
|
+
const showConnectForm = computed(() => !!selectedItem.value?.backendKind && poolConfigurable.value)
|
|
249
|
+
const selectedBackendKind = computed(() => selectedItem.value?.backendKind ?? 'manifest')
|
|
250
|
+
const selectedPreset = computed(() => selectedItem.value?.preset)
|
|
251
|
+
// Nag when a pool item is picked in local mode but no pool is registered to back it.
|
|
252
|
+
const showRegisterHint = computed(
|
|
253
|
+
() => writable.value && selected.value !== 'builtin' && !connectionRegistered.value,
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
const labelKey = computed(() =>
|
|
257
|
+
props.axis === 'execution'
|
|
258
|
+
? 'settings.infrastructure.executionBackend.label'
|
|
259
|
+
: 'settings.infrastructure.testEnvBackend.label',
|
|
260
|
+
)
|
|
261
|
+
</script>
|
|
262
|
+
|
|
263
|
+
<template>
|
|
264
|
+
<section v-if="cap" class="space-y-2 rounded-lg border border-slate-700 bg-slate-900/40 p-3">
|
|
265
|
+
<h3 class="text-sm font-semibold text-slate-200">{{ t(labelKey) }}</h3>
|
|
266
|
+
|
|
267
|
+
<!-- Off-local: the active backend is deployment/registration-driven; state it plainly. -->
|
|
268
|
+
<p v-if="!writable" class="text-sm text-slate-300" :data-testid="`${axis}-backend-active`">
|
|
269
|
+
{{ t('settings.infrastructure.active', { backend: activeLabel }) }}
|
|
270
|
+
</p>
|
|
271
|
+
|
|
272
|
+
<div class="space-y-1.5" :data-testid="`${axis}-backend-options`">
|
|
273
|
+
<label v-for="item in items" :key="item.id" class="flex cursor-pointer items-start gap-2">
|
|
274
|
+
<input
|
|
275
|
+
type="radio"
|
|
276
|
+
class="mt-1"
|
|
277
|
+
:value="item.id"
|
|
278
|
+
:checked="item.id === selected"
|
|
279
|
+
:disabled="saving"
|
|
280
|
+
:data-testid="`${axis}-backend-${item.id}`"
|
|
281
|
+
@change="select(item.id)"
|
|
282
|
+
/>
|
|
283
|
+
<span class="min-w-0">
|
|
284
|
+
<span class="text-sm text-slate-200">{{ item.label }}</span>
|
|
285
|
+
<span v-if="item.desc" class="block text-[11px] text-slate-400">{{ item.desc }}</span>
|
|
286
|
+
</span>
|
|
287
|
+
</label>
|
|
288
|
+
</div>
|
|
289
|
+
|
|
290
|
+
<p v-if="showRegisterHint" class="text-[11px] text-amber-300/80">
|
|
291
|
+
{{ t('settings.infrastructure.registerHint') }}
|
|
292
|
+
</p>
|
|
293
|
+
|
|
294
|
+
<!-- The connect form for the selected pool/cluster, driven through the shared tab. -->
|
|
295
|
+
<ProviderConnectionTab
|
|
296
|
+
v-if="showConnectForm"
|
|
297
|
+
:kind="connectionKind"
|
|
298
|
+
:backend-kind="selectedBackendKind"
|
|
299
|
+
:preset="selectedPreset"
|
|
300
|
+
:suggested-image="suggestedImage"
|
|
301
|
+
@connected="onConnected"
|
|
302
|
+
/>
|
|
303
|
+
</section>
|
|
304
|
+
</template>
|
|
@@ -11,8 +11,7 @@
|
|
|
11
11
|
// backend integration is disabled (503) simply doesn't render.
|
|
12
12
|
import { computed, ref, watch } from 'vue'
|
|
13
13
|
import type { ProviderConnectionKind } from '~/types/providerConnections'
|
|
14
|
-
import
|
|
15
|
-
import ExecutionBackendSelector from '~/components/settings/ExecutionBackendSelector.vue'
|
|
14
|
+
import InfrastructureBackendPicker from '~/components/settings/InfrastructureBackendPicker.vue'
|
|
16
15
|
import LocalContainerPoolSettings from '~/components/settings/LocalContainerPoolSettings.vue'
|
|
17
16
|
|
|
18
17
|
const { t } = useI18n()
|
|
@@ -103,10 +102,9 @@ watch([tabs, () => store.loaded], () => {
|
|
|
103
102
|
>
|
|
104
103
|
<template #runner-pool>
|
|
105
104
|
<div class="space-y-4">
|
|
106
|
-
<!--
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
<ProviderConnectionTab v-if="store.isAvailable('runner-pool')" kind="runner-pool" />
|
|
105
|
+
<!-- One unified list of where agent containers run; the selected pool/cluster
|
|
106
|
+
reveals its connect form inline. -->
|
|
107
|
+
<InfrastructureBackendPicker axis="execution" />
|
|
110
108
|
<!-- Local mode: the warm-pool + checkout reuse ARE the host agent-container
|
|
111
109
|
runtime, so they live here rather than in a separate menu. -->
|
|
112
110
|
<section v-if="isLocal" class="border-t border-slate-800 pt-4">
|
|
@@ -119,10 +117,8 @@ watch([tabs, () => store.loaded], () => {
|
|
|
119
117
|
</template>
|
|
120
118
|
<template #environment>
|
|
121
119
|
<div class="space-y-4">
|
|
122
|
-
<!--
|
|
123
|
-
<
|
|
124
|
-
<!-- The environment-provider connect form only when that integration is enabled. -->
|
|
125
|
-
<ProviderConnectionTab v-if="store.isAvailable('environment')" kind="environment" />
|
|
120
|
+
<!-- One unified list of where the Tester's ephemeral environments run. -->
|
|
121
|
+
<InfrastructureBackendPicker axis="testEnv" />
|
|
126
122
|
</div>
|
|
127
123
|
</template>
|
|
128
124
|
</UTabs>
|
|
@@ -9,6 +9,10 @@ import type { ProviderConnection } from '~/types/providerConnections'
|
|
|
9
9
|
|
|
10
10
|
const props = defineProps<{
|
|
11
11
|
connection: ProviderConnection | null
|
|
12
|
+
/** A low-config preset to seed the form with (today: local k3s). */
|
|
13
|
+
preset?: 'k3s'
|
|
14
|
+
/** The deployment's executor image, used to prefill the k3s preset's image field. */
|
|
15
|
+
suggestedImage?: string
|
|
12
16
|
supportsTest: boolean
|
|
13
17
|
testing: boolean
|
|
14
18
|
busy: boolean
|
|
@@ -30,6 +34,7 @@ const form = reactive({
|
|
|
30
34
|
imageUi: '',
|
|
31
35
|
caCertPem: '',
|
|
32
36
|
harnessPort: '',
|
|
37
|
+
insecureSkipTlsVerify: false,
|
|
33
38
|
})
|
|
34
39
|
const apiToken = ref('')
|
|
35
40
|
|
|
@@ -52,11 +57,28 @@ watch(
|
|
|
52
57
|
form.imageUi = typeof k.imageUi === 'string' ? k.imageUi : ''
|
|
53
58
|
form.caCertPem = typeof k.caCertPem === 'string' ? k.caCertPem : ''
|
|
54
59
|
form.harnessPort = typeof k.harnessPort === 'number' ? String(k.harnessPort) : ''
|
|
60
|
+
form.insecureSkipTlsVerify = k.insecureSkipTlsVerify === true
|
|
55
61
|
}
|
|
56
62
|
},
|
|
57
63
|
{ immediate: true },
|
|
58
64
|
)
|
|
59
65
|
|
|
66
|
+
// Low-config k3s preset: seed the local-cluster defaults so the operator only pastes a
|
|
67
|
+
// ServiceAccount token (and an image, unless the deployment surfaced one). Only seeds a
|
|
68
|
+
// fresh form — never clobbers an existing connection's config on edit.
|
|
69
|
+
watch(
|
|
70
|
+
() => props.preset,
|
|
71
|
+
(preset) => {
|
|
72
|
+
if (preset !== 'k3s' || props.connection) return
|
|
73
|
+
form.label = 'Local k3s'
|
|
74
|
+
form.apiServerUrl = 'https://127.0.0.1:6443'
|
|
75
|
+
form.namespace = 'cat-factory'
|
|
76
|
+
form.insecureSkipTlsVerify = true
|
|
77
|
+
if (props.suggestedImage) form.image = props.suggestedImage
|
|
78
|
+
},
|
|
79
|
+
{ immediate: true },
|
|
80
|
+
)
|
|
81
|
+
|
|
60
82
|
const canSave = computed(
|
|
61
83
|
() =>
|
|
62
84
|
!!form.label.trim() &&
|
|
@@ -75,6 +97,7 @@ function buildPayload(): { config: Record<string, unknown>; secrets: Record<stri
|
|
|
75
97
|
}
|
|
76
98
|
if (form.imageUi.trim()) kubernetes.imageUi = form.imageUi.trim()
|
|
77
99
|
if (form.caCertPem.trim()) kubernetes.caCertPem = form.caCertPem.trim()
|
|
100
|
+
if (form.insecureSkipTlsVerify) kubernetes.insecureSkipTlsVerify = true
|
|
78
101
|
const port = Number(form.harnessPort)
|
|
79
102
|
if (form.harnessPort.trim() && Number.isFinite(port)) kubernetes.harnessPort = port
|
|
80
103
|
return {
|
|
@@ -156,6 +179,13 @@ function buildPayload(): { config: Record<string, unknown>; secrets: Record<stri
|
|
|
156
179
|
/>
|
|
157
180
|
</UFormField>
|
|
158
181
|
|
|
182
|
+
<UFormField :help="t('settings.providerConnection.kubernetes.insecureSkipTlsVerifyHelp')">
|
|
183
|
+
<UCheckbox
|
|
184
|
+
v-model="form.insecureSkipTlsVerify"
|
|
185
|
+
:label="t('settings.providerConnection.kubernetes.insecureSkipTlsVerify')"
|
|
186
|
+
/>
|
|
187
|
+
</UFormField>
|
|
188
|
+
|
|
159
189
|
<UFormField
|
|
160
190
|
:label="
|
|
161
191
|
t('settings.providerConnection.form.optionalLabel', {
|
|
@@ -13,7 +13,18 @@ import ProviderManifestEditor from '~/components/settings/ProviderManifestEditor
|
|
|
13
13
|
import KubernetesRunnerForm from '~/components/settings/KubernetesRunnerForm.vue'
|
|
14
14
|
import KubernetesEnvironmentForm from '~/components/settings/KubernetesEnvironmentForm.vue'
|
|
15
15
|
|
|
16
|
-
const props = defineProps<{
|
|
16
|
+
const props = defineProps<{
|
|
17
|
+
kind: ProviderConnectionKind
|
|
18
|
+
/** The selected backend-kind slug — chosen by the parent picker's radio, not a local
|
|
19
|
+
* dropdown. Built-in (`manifest`/`kubernetes`) or a deployment-registered custom kind. */
|
|
20
|
+
backendKind: string
|
|
21
|
+
/** A low-config preset to prefill the Kubernetes form (today: local k3s). */
|
|
22
|
+
preset?: 'k3s'
|
|
23
|
+
/** The deployment's executor image, used to prefill the k3s preset's image field. */
|
|
24
|
+
suggestedImage?: string
|
|
25
|
+
}>()
|
|
26
|
+
|
|
27
|
+
const emit = defineEmits<{ connected: [] }>()
|
|
17
28
|
|
|
18
29
|
const { t } = useI18n()
|
|
19
30
|
const store = useProviderConnectionsStore()
|
|
@@ -31,7 +42,7 @@ const title = computed(() => t(`settings.providerConnection.kind.${props.kind}.t
|
|
|
31
42
|
|
|
32
43
|
watch(
|
|
33
44
|
() => props.kind,
|
|
34
|
-
(k) => void store.loadKind(k).then(resetDraft),
|
|
45
|
+
(k) => void store.loadKind(k, props.backendKind).then(resetDraft),
|
|
35
46
|
{ immediate: true },
|
|
36
47
|
)
|
|
37
48
|
|
|
@@ -109,7 +120,7 @@ function buildManifestPayload(): {
|
|
|
109
120
|
if (Object.keys(providerConfig).length) manifest.providerConfig = providerConfig
|
|
110
121
|
// Carry the selected kind so a CUSTOM backend's flat-form save is tagged with its slug
|
|
111
122
|
// (not silently wrapped into the built-in `manifest` backend).
|
|
112
|
-
return { manifest, secrets, backendKind: backendKind
|
|
123
|
+
return { manifest, secrets, backendKind: props.backendKind }
|
|
113
124
|
}
|
|
114
125
|
|
|
115
126
|
function notifyError(title: string, e: unknown) {
|
|
@@ -149,6 +160,7 @@ async function saveNative() {
|
|
|
149
160
|
try {
|
|
150
161
|
const payload = buildManifestPayload()
|
|
151
162
|
if (payload) await store.register(props.kind, payload)
|
|
163
|
+
emit('connected')
|
|
152
164
|
resetDraft()
|
|
153
165
|
toastSaved()
|
|
154
166
|
} catch (e) {
|
|
@@ -168,7 +180,7 @@ async function testManifest(payload: {
|
|
|
168
180
|
testing.value = true
|
|
169
181
|
testResult.value = null
|
|
170
182
|
try {
|
|
171
|
-
testResult.value = await store.test(props.kind, { ...payload, backendKind: backendKind
|
|
183
|
+
testResult.value = await store.test(props.kind, { ...payload, backendKind: props.backendKind })
|
|
172
184
|
} catch (e) {
|
|
173
185
|
testResult.value = { ok: false, message: e instanceof Error ? e.message : String(e) }
|
|
174
186
|
} finally {
|
|
@@ -182,7 +194,8 @@ async function saveManifest(payload: {
|
|
|
182
194
|
}) {
|
|
183
195
|
busy.value = true
|
|
184
196
|
try {
|
|
185
|
-
await store.register(props.kind, { ...payload, backendKind: backendKind
|
|
197
|
+
await store.register(props.kind, { ...payload, backendKind: props.backendKind })
|
|
198
|
+
emit('connected')
|
|
186
199
|
toastSaved()
|
|
187
200
|
} catch (e) {
|
|
188
201
|
notifyError(t('settings.providerConnection.toast.saveFailed'), e)
|
|
@@ -191,54 +204,20 @@ async function saveManifest(payload: {
|
|
|
191
204
|
}
|
|
192
205
|
}
|
|
193
206
|
|
|
194
|
-
// --- Backend
|
|
195
|
-
//
|
|
196
|
-
//
|
|
197
|
-
//
|
|
198
|
-
//
|
|
199
|
-
//
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
props.kind === 'environment'
|
|
204
|
-
? 'settings.providerConnection.backend.environmentSelectorLabel'
|
|
205
|
-
: 'settings.providerConnection.backend.selectorLabel',
|
|
206
|
-
),
|
|
207
|
-
)
|
|
208
|
-
// Built-in kinds keep their localized labels; a custom kind shows its snapshot displayLabel.
|
|
209
|
-
function backendKindLabel(option: { kind: string; label: string }): string {
|
|
210
|
-
if (option.kind === 'kubernetes') return t('settings.providerConnection.backend.kubernetes')
|
|
211
|
-
if (option.kind === 'manifest') {
|
|
212
|
-
return t(
|
|
213
|
-
props.kind === 'environment'
|
|
214
|
-
? 'settings.providerConnection.backend.environmentManifest'
|
|
215
|
-
: 'settings.providerConnection.backend.manifest',
|
|
216
|
-
)
|
|
217
|
-
}
|
|
218
|
-
return option.label
|
|
219
|
-
}
|
|
220
|
-
const backendKindItems = computed(() =>
|
|
221
|
-
store.backendKindsFor(props.kind).map((o) => ({ label: backendKindLabel(o), value: o.kind })),
|
|
222
|
-
)
|
|
207
|
+
// --- Backend connect forms ------------------------------------------------------------
|
|
208
|
+
// Which backend kind to configure (the built-in `manifest`/`kubernetes` backends or any
|
|
209
|
+
// CUSTOM kind a deployment registered) is decided by the parent picker's unified radio and
|
|
210
|
+
// passed in as `backendKind` — there is no longer a local dropdown here. The two K8s
|
|
211
|
+
// backends have bespoke forms; every other kind (manifest + custom) uses the descriptor-
|
|
212
|
+
// driven flat form / raw manifest editor. Switching the kind re-probes ONLY that kind's
|
|
213
|
+
// descriptor (so a not-yet-connected custom kind's connect form renders) WITHOUT re-fetching
|
|
214
|
+
// the stored connection — using `loadDescriptor` (not `loadKind`) avoids bouncing the
|
|
215
|
+
// picker's selection back to the stored kind via a connection re-read.
|
|
223
216
|
watch(
|
|
224
|
-
() =>
|
|
225
|
-
(
|
|
226
|
-
if (c?.kind) backendKind.value = c.kind
|
|
227
|
-
},
|
|
228
|
-
{ immediate: true },
|
|
217
|
+
() => props.backendKind,
|
|
218
|
+
(k) => void store.loadDescriptor(props.kind, k).then(resetDraft),
|
|
229
219
|
)
|
|
230
220
|
|
|
231
|
-
// Switching the backend kind re-probes ONLY that kind's descriptor (so a not-yet-connected
|
|
232
|
-
// custom kind's connect form renders). Always pass the explicit kind — including `manifest`,
|
|
233
|
-
// so picking it describes the manifest backend rather than falling back to the stored kind —
|
|
234
|
-
// and use `loadDescriptor` (not `loadKind`) so the stored connection isn't re-fetched and the
|
|
235
|
-
// selector isn't bounced back to the stored kind by the `connection` watch.
|
|
236
|
-
async function onBackendKindChange(k: string) {
|
|
237
|
-
backendKind.value = k
|
|
238
|
-
await store.loadDescriptor(props.kind, k)
|
|
239
|
-
resetDraft()
|
|
240
|
-
}
|
|
241
|
-
|
|
242
221
|
async function testConfig(payload: {
|
|
243
222
|
config: Record<string, unknown>
|
|
244
223
|
secrets: Record<string, string>
|
|
@@ -261,6 +240,7 @@ async function saveConfig(payload: {
|
|
|
261
240
|
busy.value = true
|
|
262
241
|
try {
|
|
263
242
|
await store.register(props.kind, payload)
|
|
243
|
+
emit('connected')
|
|
264
244
|
toastSaved()
|
|
265
245
|
} catch (e) {
|
|
266
246
|
notifyError(t('settings.providerConnection.toast.saveFailed'), e)
|
|
@@ -350,20 +330,12 @@ function fieldHelp(key: string): string | undefined {
|
|
|
350
330
|
}}
|
|
351
331
|
</div>
|
|
352
332
|
|
|
353
|
-
<!-- Backend selector: the BYO manifest backend, a native Kubernetes backend, or a
|
|
354
|
-
programmatically-registered custom kind. -->
|
|
355
|
-
<UFormField :label="backendSelectorLabel">
|
|
356
|
-
<USelect
|
|
357
|
-
v-model="backendKind"
|
|
358
|
-
:items="backendKindItems"
|
|
359
|
-
@update:model-value="onBackendKindChange(String($event))"
|
|
360
|
-
/>
|
|
361
|
-
</UFormField>
|
|
362
|
-
|
|
363
333
|
<!-- Native Kubernetes runner backend (runner-pool). -->
|
|
364
334
|
<KubernetesRunnerForm
|
|
365
335
|
v-if="kind === 'runner-pool' && backendKind === 'kubernetes'"
|
|
366
336
|
:connection="connection"
|
|
337
|
+
:preset="preset"
|
|
338
|
+
:suggested-image="suggestedImage"
|
|
367
339
|
:supports-test="descriptor.supportsTest"
|
|
368
340
|
:testing="testing"
|
|
369
341
|
:busy="busy"
|
|
@@ -463,31 +435,20 @@ function fieldHelp(key: string): string | undefined {
|
|
|
463
435
|
</div>
|
|
464
436
|
</div>
|
|
465
437
|
|
|
466
|
-
<!-- MANIFEST-driven provider: the raw JSON manifest editor.
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
<details
|
|
438
|
+
<!-- MANIFEST-driven provider: the raw JSON manifest editor. The radio already selected
|
|
439
|
+
"custom HTTP" so it's shown expanded — no extra disclosure. -->
|
|
440
|
+
<ProviderManifestEditor
|
|
470
441
|
v-else
|
|
471
|
-
|
|
472
|
-
:
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
:saved-manifest="descriptor.savedManifest"
|
|
483
|
-
:connected="!!connection"
|
|
484
|
-
:supports-test="descriptor.supportsTest"
|
|
485
|
-
:testing="testing"
|
|
486
|
-
:busy="busy"
|
|
487
|
-
:test-result="testResult"
|
|
488
|
-
@test="testManifest"
|
|
489
|
-
@save="saveManifest"
|
|
490
|
-
/>
|
|
491
|
-
</details>
|
|
442
|
+
:kind="kind"
|
|
443
|
+
:saved-manifest="descriptor.savedManifest"
|
|
444
|
+
:connected="!!connection"
|
|
445
|
+
:stored-secret-keys="connection?.secretKeys ?? []"
|
|
446
|
+
:supports-test="descriptor.supportsTest"
|
|
447
|
+
:testing="testing"
|
|
448
|
+
:busy="busy"
|
|
449
|
+
:test-result="testResult"
|
|
450
|
+
@test="testManifest"
|
|
451
|
+
@save="saveManifest"
|
|
452
|
+
/>
|
|
492
453
|
</div>
|
|
493
454
|
</template>
|
|
@@ -25,6 +25,9 @@ const props = defineProps<{
|
|
|
25
25
|
savedManifest?: Record<string, unknown>
|
|
26
26
|
/** Whether a connection already exists (drives the re-enter-secrets hint + button label). */
|
|
27
27
|
connected: boolean
|
|
28
|
+
/** The secret keys already stored for this connection (names only) — shown next to the
|
|
29
|
+
* write-only inputs so it's obvious what exists without scrolling to the summary. */
|
|
30
|
+
storedSecretKeys?: string[]
|
|
28
31
|
/** Whether the provider exposes a connection test the UI can call. */
|
|
29
32
|
supportsTest: boolean
|
|
30
33
|
/** Bubbled-up busy state from the tab's store calls (so the editor shows loading). */
|
|
@@ -235,9 +238,22 @@ function onSave() {
|
|
|
235
238
|
<p v-if="!secretKeys.length" class="text-[11px] text-slate-500">
|
|
236
239
|
{{ t('settings.providerConnection.manifestEditor.noSecrets') }}
|
|
237
240
|
</p>
|
|
238
|
-
<
|
|
239
|
-
|
|
240
|
-
|
|
241
|
+
<template v-else-if="connected">
|
|
242
|
+
<p
|
|
243
|
+
v-if="storedSecretKeys && storedSecretKeys.length"
|
|
244
|
+
class="text-[11px] text-slate-400"
|
|
245
|
+
data-testid="manifest-editor-stored"
|
|
246
|
+
>
|
|
247
|
+
{{
|
|
248
|
+
t('settings.providerConnection.manifestEditor.stored', {
|
|
249
|
+
keys: storedSecretKeys.join(', '),
|
|
250
|
+
})
|
|
251
|
+
}}
|
|
252
|
+
</p>
|
|
253
|
+
<p class="text-[11px] text-amber-300/80">
|
|
254
|
+
{{ t('settings.providerConnection.manifestEditor.reenterSecrets') }}
|
|
255
|
+
</p>
|
|
256
|
+
</template>
|
|
241
257
|
<UFormField v-for="key in secretKeys" :key="key" :label="key">
|
|
242
258
|
<UInput
|
|
243
259
|
v-model="secrets[key]"
|
package/i18n/locales/en.json
CHANGED
|
@@ -1275,13 +1275,24 @@
|
|
|
1275
1275
|
"executionBackend": {
|
|
1276
1276
|
"label": "Where agents run",
|
|
1277
1277
|
"local-docker": "Local Docker (host)",
|
|
1278
|
+
"local-dockerDesc": "Run each agent in a container on this machine's Docker daemon.",
|
|
1278
1279
|
"cloudflare-containers": "Cloudflare Containers (built-in)",
|
|
1279
|
-
"
|
|
1280
|
+
"cloudflare-containersDesc": "Run agents on Cloudflare's built-in per-run container platform.",
|
|
1281
|
+
"kubernetes": "Kubernetes cluster",
|
|
1282
|
+
"kubernetesDesc": "Run each agent as a pod in a Kubernetes cluster you operate.",
|
|
1283
|
+
"runner-pool": "Custom runner pool (HTTP)",
|
|
1284
|
+
"runner-poolDesc": "Dispatch agents to your own scheduler behind an HTTP manifest API.",
|
|
1285
|
+
"k3s": "Local Kubernetes (k3s)",
|
|
1286
|
+
"k3sDesc": "Prefilled for a local k3s/k3d cluster on this machine — just paste a token."
|
|
1280
1287
|
},
|
|
1281
1288
|
"testEnvBackend": {
|
|
1282
1289
|
"label": "Where test environments run",
|
|
1283
1290
|
"local-compose": "In-container docker-compose",
|
|
1284
|
-
"
|
|
1291
|
+
"local-composeDesc": "Stand the Tester's dependencies up with docker-compose inside the run's container.",
|
|
1292
|
+
"kubernetes": "Kubernetes cluster",
|
|
1293
|
+
"kubernetesDesc": "Provision a per-PR namespace in a Kubernetes cluster you operate.",
|
|
1294
|
+
"environment-provider": "Custom HTTP provider",
|
|
1295
|
+
"environment-providerDesc": "Provision ephemeral environments through your own HTTP management API."
|
|
1285
1296
|
}
|
|
1286
1297
|
},
|
|
1287
1298
|
"providerConnection": {
|
|
@@ -1302,13 +1313,6 @@
|
|
|
1302
1313
|
"blurb": "Where the coding agents run when not using Cloudflare Containers. Choose a self-hosted runner pool (your own scheduler) or a Kubernetes cluster, then configure its endpoint and credentials."
|
|
1303
1314
|
}
|
|
1304
1315
|
},
|
|
1305
|
-
"backend": {
|
|
1306
|
-
"selectorLabel": "Runner backend",
|
|
1307
|
-
"manifest": "Self-hosted pool (manifest)",
|
|
1308
|
-
"kubernetes": "Kubernetes",
|
|
1309
|
-
"environmentSelectorLabel": "Environment backend",
|
|
1310
|
-
"environmentManifest": "Custom HTTP API (manifest)"
|
|
1311
|
-
},
|
|
1312
1316
|
"kubernetesEnv": {
|
|
1313
1317
|
"label": "Name",
|
|
1314
1318
|
"labelPlaceholder": "Preview cluster",
|
|
@@ -1359,6 +1363,8 @@
|
|
|
1359
1363
|
"apiTokenHelp": "A bearer token with RBAC to create, get and delete pods and pods/proxy in the namespace. Stored encrypted; never shown again.",
|
|
1360
1364
|
"caCertPem": "Cluster CA certificate (PEM)",
|
|
1361
1365
|
"caCertPemHelp": "Paste the cluster CA bundle so the apiserver's TLS certificate verifies. Omit only for a publicly-trusted CA.",
|
|
1366
|
+
"insecureSkipTlsVerify": "Skip TLS verification",
|
|
1367
|
+
"insecureSkipTlsVerifyHelp": "Strongly discouraged. Disables apiserver TLS verification; use only for local k3s/kind/dev clusters.",
|
|
1362
1368
|
"harnessPort": "Harness port"
|
|
1363
1369
|
},
|
|
1364
1370
|
"manifestEditor": {
|
|
@@ -1370,13 +1376,10 @@
|
|
|
1370
1376
|
"schemaError": "Manifest problem: {message}",
|
|
1371
1377
|
"secretsLabel": "Secrets",
|
|
1372
1378
|
"noSecrets": "This manifest references no secrets.",
|
|
1379
|
+
"stored": "Currently stored: {keys}",
|
|
1373
1380
|
"reenterSecrets": "Re-enter every secret to save. Stored secrets are write-only and aren't shown.",
|
|
1374
1381
|
"starterHint": "This is a starter example. Edit it to match your provider's API."
|
|
1375
1382
|
},
|
|
1376
|
-
"advancedManifest": {
|
|
1377
|
-
"summary": "Advanced: custom API-based scheduler",
|
|
1378
|
-
"intro": "Only needed to integrate a custom API-based scheduler. The common backends (local Docker, Cloudflare Containers and Kubernetes) don't need this; describe your own scheduler's HTTP API here only if you run one."
|
|
1379
|
-
},
|
|
1380
1383
|
"viewLogs": "View logs",
|
|
1381
1384
|
"hideLogs": "Hide logs",
|
|
1382
1385
|
"connectedAt": "Connected · {baseUrl}",
|
package/i18n/locales/es.json
CHANGED
|
@@ -1256,6 +1256,7 @@
|
|
|
1256
1256
|
"schemaError": "Problema con el manifiesto — {message}",
|
|
1257
1257
|
"secretsLabel": "Secretos",
|
|
1258
1258
|
"noSecrets": "Este manifiesto no hace referencia a ningún secreto.",
|
|
1259
|
+
"stored": "Almacenados actualmente: {keys}",
|
|
1259
1260
|
"reenterSecrets": "Vuelve a introducir cada secreto para guardar: los secretos almacenados son de solo escritura y no se muestran.",
|
|
1260
1261
|
"starterHint": "Este es un ejemplo inicial. Edítalo para que coincida con la API de tu proveedor."
|
|
1261
1262
|
},
|
|
@@ -1283,13 +1284,6 @@
|
|
|
1283
1284
|
"removed": "Conexión eliminada",
|
|
1284
1285
|
"removeFailed": "No se pudo eliminar la conexión"
|
|
1285
1286
|
},
|
|
1286
|
-
"backend": {
|
|
1287
|
-
"selectorLabel": "Backend de ejecución",
|
|
1288
|
-
"manifest": "Pool autohospedado (manifiesto)",
|
|
1289
|
-
"kubernetes": "Kubernetes",
|
|
1290
|
-
"environmentSelectorLabel": "Backend de entorno",
|
|
1291
|
-
"environmentManifest": "API HTTP personalizada (manifiesto)"
|
|
1292
|
-
},
|
|
1293
1287
|
"kubernetesEnv": {
|
|
1294
1288
|
"label": "Nombre",
|
|
1295
1289
|
"labelPlaceholder": "Clúster de vista previa",
|
|
@@ -1340,11 +1334,9 @@
|
|
|
1340
1334
|
"apiTokenHelp": "Un token bearer con permisos RBAC para crear, obtener y eliminar pods y pods/proxy en el namespace. Se almacena cifrado; no se vuelve a mostrar.",
|
|
1341
1335
|
"caCertPem": "Certificado CA del clúster (PEM)",
|
|
1342
1336
|
"caCertPemHelp": "Pega el bundle CA del clúster para que el certificado TLS del apiserver se verifique. Omítelo solo para una CA de confianza pública.",
|
|
1337
|
+
"insecureSkipTlsVerify": "Omitir verificación TLS",
|
|
1338
|
+
"insecureSkipTlsVerifyHelp": "Muy desaconsejado. Desactiva la verificación TLS del apiserver; úsalo solo para clústeres locales k3s/kind/dev.",
|
|
1343
1339
|
"harnessPort": "Puerto del harness"
|
|
1344
|
-
},
|
|
1345
|
-
"advancedManifest": {
|
|
1346
|
-
"summary": "Avanzado: planificador personalizado basado en API",
|
|
1347
|
-
"intro": "Solo es necesario para integrar un planificador personalizado basado en API. Los backends habituales (Docker local, Cloudflare Containers y Kubernetes) no lo necesitan; describe aquí la API HTTP de tu propio planificador solo si usas uno."
|
|
1348
1340
|
}
|
|
1349
1341
|
},
|
|
1350
1342
|
"serviceFragmentDefaults": {
|
package/i18n/locales/fr.json
CHANGED
|
@@ -1256,6 +1256,7 @@
|
|
|
1256
1256
|
"schemaError": "Problème de manifeste — {message}",
|
|
1257
1257
|
"secretsLabel": "Secrets",
|
|
1258
1258
|
"noSecrets": "Ce manifeste ne référence aucun secret.",
|
|
1259
|
+
"stored": "Actuellement stockés : {keys}",
|
|
1259
1260
|
"reenterSecrets": "Saisissez à nouveau chaque secret pour enregistrer : les secrets stockés sont en écriture seule et ne sont pas affichés.",
|
|
1260
1261
|
"starterHint": "Ceci est un exemple de départ. Modifiez-le pour qu'il corresponde à l'API de votre fournisseur."
|
|
1261
1262
|
},
|
|
@@ -1283,13 +1284,6 @@
|
|
|
1283
1284
|
"removed": "Connexion supprimée",
|
|
1284
1285
|
"removeFailed": "Impossible de supprimer la connexion"
|
|
1285
1286
|
},
|
|
1286
|
-
"backend": {
|
|
1287
|
-
"selectorLabel": "Backend d'exécution",
|
|
1288
|
-
"manifest": "Pool auto-hébergé (manifeste)",
|
|
1289
|
-
"kubernetes": "Kubernetes",
|
|
1290
|
-
"environmentSelectorLabel": "Backend d'environnement",
|
|
1291
|
-
"environmentManifest": "API HTTP personnalisée (manifeste)"
|
|
1292
|
-
},
|
|
1293
1287
|
"kubernetesEnv": {
|
|
1294
1288
|
"label": "Nom",
|
|
1295
1289
|
"labelPlaceholder": "Cluster de prévisualisation",
|
|
@@ -1340,11 +1334,9 @@
|
|
|
1340
1334
|
"apiTokenHelp": "Un jeton bearer avec les droits RBAC pour créer, lire et supprimer les pods et pods/proxy dans le namespace. Stocké chiffré ; jamais réaffiché.",
|
|
1341
1335
|
"caCertPem": "Certificat CA du cluster (PEM)",
|
|
1342
1336
|
"caCertPemHelp": "Collez le bundle CA du cluster pour que le certificat TLS de l'apiserver soit vérifié. À omettre uniquement pour une CA publiquement approuvée.",
|
|
1337
|
+
"insecureSkipTlsVerify": "Ignorer la vérification TLS",
|
|
1338
|
+
"insecureSkipTlsVerifyHelp": "Fortement déconseillé. Désactive la vérification TLS de l'apiserver ; à utiliser uniquement pour des clusters locaux k3s/kind/dev.",
|
|
1343
1339
|
"harnessPort": "Port du harness"
|
|
1344
|
-
},
|
|
1345
|
-
"advancedManifest": {
|
|
1346
|
-
"summary": "Avancé : planificateur personnalisé basé sur une API",
|
|
1347
|
-
"intro": "Nécessaire uniquement pour intégrer un planificateur personnalisé basé sur une API. Les backends courants (Docker local, Cloudflare Containers et Kubernetes) n'en ont pas besoin ; décrivez ici l'API HTTP de votre propre planificateur uniquement si vous en utilisez un."
|
|
1348
1340
|
}
|
|
1349
1341
|
},
|
|
1350
1342
|
"serviceFragmentDefaults": {
|
package/i18n/locales/pl.json
CHANGED
|
@@ -1256,6 +1256,7 @@
|
|
|
1256
1256
|
"schemaError": "Problem z manifestem — {message}",
|
|
1257
1257
|
"secretsLabel": "Sekrety",
|
|
1258
1258
|
"noSecrets": "Ten manifest nie odwołuje się do żadnych sekretów.",
|
|
1259
|
+
"stored": "Obecnie przechowywane: {keys}",
|
|
1259
1260
|
"reenterSecrets": "Wprowadź ponownie każdy sekret, aby zapisać — przechowywane sekrety są tylko do zapisu i nie są wyświetlane.",
|
|
1260
1261
|
"starterHint": "To jest przykład startowy. Zmodyfikuj go, aby pasował do API Twojego dostawcy."
|
|
1261
1262
|
},
|
|
@@ -1283,13 +1284,6 @@
|
|
|
1283
1284
|
"removed": "Połączenie usunięte",
|
|
1284
1285
|
"removeFailed": "Nie udało się usunąć połączenia"
|
|
1285
1286
|
},
|
|
1286
|
-
"backend": {
|
|
1287
|
-
"selectorLabel": "Backend wykonawczy",
|
|
1288
|
-
"manifest": "Własny pool (manifest)",
|
|
1289
|
-
"kubernetes": "Kubernetes",
|
|
1290
|
-
"environmentSelectorLabel": "Backend środowiska",
|
|
1291
|
-
"environmentManifest": "Własne API HTTP (manifest)"
|
|
1292
|
-
},
|
|
1293
1287
|
"kubernetesEnv": {
|
|
1294
1288
|
"label": "Nazwa",
|
|
1295
1289
|
"labelPlaceholder": "Klaster podglądu",
|
|
@@ -1340,11 +1334,9 @@
|
|
|
1340
1334
|
"apiTokenHelp": "Token bearer z uprawnieniami RBAC do tworzenia, odczytu i usuwania podów oraz pods/proxy w namespace. Przechowywany w postaci zaszyfrowanej; nie jest ponownie pokazywany.",
|
|
1341
1335
|
"caCertPem": "Certyfikat CA klastra (PEM)",
|
|
1342
1336
|
"caCertPemHelp": "Wklej pakiet CA klastra, aby certyfikat TLS apiservera został zweryfikowany. Pomiń tylko dla publicznie zaufanego CA.",
|
|
1337
|
+
"insecureSkipTlsVerify": "Pomiń weryfikację TLS",
|
|
1338
|
+
"insecureSkipTlsVerifyHelp": "Zdecydowanie odradzane. Wyłącza weryfikację TLS apiservera; używaj tylko dla lokalnych klastrów k3s/kind/dev.",
|
|
1343
1339
|
"harnessPort": "Port harnessa"
|
|
1344
|
-
},
|
|
1345
|
-
"advancedManifest": {
|
|
1346
|
-
"summary": "Zaawansowane: niestandardowy harmonogram oparty na API",
|
|
1347
|
-
"intro": "Potrzebne tylko do zintegrowania niestandardowego harmonogramu opartego na API. Typowe backendy (lokalny Docker, Cloudflare Containers i Kubernetes) tego nie wymagają; opisz tutaj API HTTP własnego harmonogramu tylko, jeśli go używasz."
|
|
1348
1340
|
}
|
|
1349
1341
|
},
|
|
1350
1342
|
"serviceFragmentDefaults": {
|
package/i18n/locales/uk.json
CHANGED
|
@@ -1256,6 +1256,7 @@
|
|
|
1256
1256
|
"schemaError": "Проблема з маніфестом — {message}",
|
|
1257
1257
|
"secretsLabel": "Секрети",
|
|
1258
1258
|
"noSecrets": "Цей маніфест не посилається на жодні секрети.",
|
|
1259
|
+
"stored": "Зараз збережено: {keys}",
|
|
1259
1260
|
"reenterSecrets": "Введіть кожен секрет повторно, щоб зберегти — збережені секрети доступні лише для запису й не показуються.",
|
|
1260
1261
|
"starterHint": "Це початковий приклад. Відредагуйте його відповідно до API вашого постачальника."
|
|
1261
1262
|
},
|
|
@@ -1283,13 +1284,6 @@
|
|
|
1283
1284
|
"removed": "Підключення видалено",
|
|
1284
1285
|
"removeFailed": "Не вдалося видалити підключення"
|
|
1285
1286
|
},
|
|
1286
|
-
"backend": {
|
|
1287
|
-
"selectorLabel": "Бекенд виконавця",
|
|
1288
|
-
"manifest": "Власний пул (маніфест)",
|
|
1289
|
-
"kubernetes": "Kubernetes",
|
|
1290
|
-
"environmentSelectorLabel": "Бекенд середовища",
|
|
1291
|
-
"environmentManifest": "Власний HTTP API (маніфест)"
|
|
1292
|
-
},
|
|
1293
1287
|
"kubernetesEnv": {
|
|
1294
1288
|
"label": "Назва",
|
|
1295
1289
|
"labelPlaceholder": "Кластер попереднього перегляду",
|
|
@@ -1340,11 +1334,9 @@
|
|
|
1340
1334
|
"apiTokenHelp": "Bearer-токен з правами RBAC на створення, читання та видалення подів і pods/proxy у просторі імен. Зберігається зашифрованим; більше не показується.",
|
|
1341
1335
|
"caCertPem": "Сертифікат CA кластера (PEM)",
|
|
1342
1336
|
"caCertPemHelp": "Вставте CA-набір кластера, щоб TLS-сертифікат apiserver проходив перевірку. Пропустіть лише для публічно довіреного CA.",
|
|
1337
|
+
"insecureSkipTlsVerify": "Пропустити перевірку TLS",
|
|
1338
|
+
"insecureSkipTlsVerifyHelp": "Наполегливо не рекомендується. Вимикає перевірку TLS apiserver; використовуйте лише для локальних кластерів k3s/kind/dev.",
|
|
1343
1339
|
"harnessPort": "Порт harness"
|
|
1344
|
-
},
|
|
1345
|
-
"advancedManifest": {
|
|
1346
|
-
"summary": "Розширено: власний планувальник на основі API",
|
|
1347
|
-
"intro": "Потрібно лише для інтеграції власного планувальника на основі API. Звичайні бекенди (локальний Docker, Cloudflare Containers і Kubernetes) цього не потребують; опишіть тут HTTP API власного планувальника, лише якщо ви його використовуєте."
|
|
1348
1340
|
}
|
|
1349
1341
|
},
|
|
1350
1342
|
"serviceFragmentDefaults": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cat-factory/app",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.54.0",
|
|
4
4
|
"description": "Reusable Nuxt layer for the Agent Architecture Board SPA (components, stores, composables, pages). Consume it from a thin deployment app via `extends: ['@cat-factory/app']` and point it at your backend with NUXT_PUBLIC_API_BASE. See deploy/frontend for an example.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"valibot": "^1.4.2",
|
|
35
35
|
"vue": "^3.5.39",
|
|
36
36
|
"wretch": "^3.0.9",
|
|
37
|
-
"@cat-factory/contracts": "0.
|
|
37
|
+
"@cat-factory/contracts": "0.56.0"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@toad-contracts/testing": "0.3.2",
|
|
@@ -1,171 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
// A clear picker for WHERE work runs, replacing the old bare "delegate to runner pool"
|
|
3
|
-
// yes/no switch. It reads the deployment's available backends from the capability descriptor
|
|
4
|
-
// (`auth.infrastructure`) and presents the actual options for THIS deployment:
|
|
5
|
-
// - execution axis → where agent containers run (local Docker host / Cloudflare Containers /
|
|
6
|
-
// self-hosted runner pool);
|
|
7
|
-
// - testEnv axis → where the Tester's ephemeral environments run (in-container
|
|
8
|
-
// docker-compose / an environment provider).
|
|
9
|
-
//
|
|
10
|
-
// The control is WRITABLE only in local mode, where the choice is a real per-workspace toggle
|
|
11
|
-
// (the `delegateAgentsToRunnerPool` / `delegateTestEnvToProvider` settings — this selector is a
|
|
12
|
-
// nicer face over those existing booleans, no new persistence). On the Worker/Node facades the
|
|
13
|
-
// active backend is determined by the deployment + whether a pool is registered (the Worker
|
|
14
|
-
// routes to a registered pool automatically, else Cloudflare Containers), so there it renders a
|
|
15
|
-
// read-only "Active: …" line instead of a control that wouldn't actually switch anything.
|
|
16
|
-
import { computed, ref } from 'vue'
|
|
17
|
-
import type { ExecutionBackendKind, TestEnvBackendKind } from '@cat-factory/contracts'
|
|
18
|
-
|
|
19
|
-
const props = defineProps<{ axis: 'execution' | 'testEnv' }>()
|
|
20
|
-
|
|
21
|
-
const { t } = useI18n()
|
|
22
|
-
const auth = useAuthStore()
|
|
23
|
-
const settings = useWorkspaceSettingsStore()
|
|
24
|
-
const providerConnections = useProviderConnectionsStore()
|
|
25
|
-
const toast = useToast()
|
|
26
|
-
|
|
27
|
-
type BackendKind = ExecutionBackendKind | TestEnvBackendKind
|
|
28
|
-
|
|
29
|
-
// Per-axis labels — static literal keys whose leaves mirror the contract enum values verbatim
|
|
30
|
-
// (so the typed-message-keys check stays live and a dynamic lookup is total).
|
|
31
|
-
const EXECUTION_LABELS: Record<ExecutionBackendKind, string> = {
|
|
32
|
-
'local-docker': 'settings.infrastructure.executionBackend.local-docker',
|
|
33
|
-
'cloudflare-containers': 'settings.infrastructure.executionBackend.cloudflare-containers',
|
|
34
|
-
'runner-pool': 'settings.infrastructure.executionBackend.runner-pool',
|
|
35
|
-
}
|
|
36
|
-
const TEST_ENV_LABELS: Record<TestEnvBackendKind, string> = {
|
|
37
|
-
'local-compose': 'settings.infrastructure.testEnvBackend.local-compose',
|
|
38
|
-
'environment-provider': 'settings.infrastructure.testEnvBackend.environment-provider',
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function labelFor(kind: BackendKind): string {
|
|
42
|
-
const key =
|
|
43
|
-
props.axis === 'execution'
|
|
44
|
-
? EXECUTION_LABELS[kind as ExecutionBackendKind]
|
|
45
|
-
: TEST_ENV_LABELS[kind as TestEnvBackendKind]
|
|
46
|
-
return t(key)
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// Normalise to the union element type so array ops (`includes`/`find`) don't collapse to
|
|
50
|
-
// `never` across the execution/testEnv discriminated union.
|
|
51
|
-
const cap = computed<{ available: BackendKind[]; active: BackendKind } | null>(() => {
|
|
52
|
-
const c = auth.infrastructure?.[props.axis]
|
|
53
|
-
return c ? { available: c.available as BackendKind[], active: c.active as BackendKind } : null
|
|
54
|
-
})
|
|
55
|
-
const isLocal = computed(() => auth.localMode?.enabled === true)
|
|
56
|
-
|
|
57
|
-
// The backend reached by "delegating" away from the on-machine default, plus the provider
|
|
58
|
-
// connection that must exist for it to work.
|
|
59
|
-
const delegatedKind = computed<BackendKind>(() =>
|
|
60
|
-
props.axis === 'execution' ? 'runner-pool' : 'environment-provider',
|
|
61
|
-
)
|
|
62
|
-
const connectionKind = computed<'runner-pool' | 'environment'>(() =>
|
|
63
|
-
props.axis === 'execution' ? 'runner-pool' : 'environment',
|
|
64
|
-
)
|
|
65
|
-
const delegatedRegistered = computed(
|
|
66
|
-
() => !!providerConnections.connectionFor(connectionKind.value),
|
|
67
|
-
)
|
|
68
|
-
|
|
69
|
-
// The on-machine / built-in default (the available option that isn't the delegated one).
|
|
70
|
-
const localKind = computed<BackendKind>(
|
|
71
|
-
() =>
|
|
72
|
-
cap.value?.available.find((k) => k !== delegatedKind.value) ??
|
|
73
|
-
cap.value?.active ??
|
|
74
|
-
'local-docker',
|
|
75
|
-
)
|
|
76
|
-
|
|
77
|
-
// The delegation flag is a genuine per-workspace toggle ONLY in local mode; elsewhere the
|
|
78
|
-
// active backend is registration/deployment-driven, so the control is read-only there.
|
|
79
|
-
const writable = computed(() => isLocal.value && (cap.value?.available.length ?? 0) > 1)
|
|
80
|
-
|
|
81
|
-
const delegated = computed(() =>
|
|
82
|
-
props.axis === 'execution'
|
|
83
|
-
? settings.settings.delegateAgentsToRunnerPool
|
|
84
|
-
: settings.settings.delegateTestEnvToProvider,
|
|
85
|
-
)
|
|
86
|
-
|
|
87
|
-
// The effective active backend. In local mode it follows the delegation flag; otherwise the
|
|
88
|
-
// delegated backend is active when its provider is registered (the Worker auto-routes to a
|
|
89
|
-
// registered pool), else the deployment default from the descriptor.
|
|
90
|
-
const activeKind = computed<BackendKind>(() => {
|
|
91
|
-
if (!cap.value) return localKind.value
|
|
92
|
-
if (writable.value) return delegated.value ? delegatedKind.value : localKind.value
|
|
93
|
-
if (cap.value.available.includes(delegatedKind.value) && delegatedRegistered.value) {
|
|
94
|
-
return delegatedKind.value
|
|
95
|
-
}
|
|
96
|
-
return cap.value.active
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
const saving = ref(false)
|
|
100
|
-
|
|
101
|
-
async function select(kind: BackendKind) {
|
|
102
|
-
if (kind === activeKind.value) return
|
|
103
|
-
const toRunnerPool = kind === delegatedKind.value
|
|
104
|
-
saving.value = true
|
|
105
|
-
try {
|
|
106
|
-
await settings.update(
|
|
107
|
-
props.axis === 'execution'
|
|
108
|
-
? { delegateAgentsToRunnerPool: toRunnerPool }
|
|
109
|
-
: { delegateTestEnvToProvider: toRunnerPool },
|
|
110
|
-
)
|
|
111
|
-
} catch (e) {
|
|
112
|
-
toast.add({
|
|
113
|
-
title: t('settings.infrastructure.updateFailed'),
|
|
114
|
-
description: e instanceof Error ? e.message : String(e),
|
|
115
|
-
icon: 'i-lucide-triangle-alert',
|
|
116
|
-
color: 'error',
|
|
117
|
-
})
|
|
118
|
-
} finally {
|
|
119
|
-
saving.value = false
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
const labelKey = computed(() =>
|
|
124
|
-
props.axis === 'execution'
|
|
125
|
-
? 'settings.infrastructure.executionBackend.label'
|
|
126
|
-
: 'settings.infrastructure.testEnvBackend.label',
|
|
127
|
-
)
|
|
128
|
-
</script>
|
|
129
|
-
|
|
130
|
-
<template>
|
|
131
|
-
<section v-if="cap" class="space-y-2 rounded-lg border border-slate-700 bg-slate-900/40 p-3">
|
|
132
|
-
<h3 class="text-sm font-semibold text-slate-200">{{ t(labelKey) }}</h3>
|
|
133
|
-
|
|
134
|
-
<!-- Local mode: a real choice the user can flip (writes the delegation setting). -->
|
|
135
|
-
<div v-if="writable" class="space-y-1.5" :data-testid="`${axis}-backend-options`">
|
|
136
|
-
<label
|
|
137
|
-
v-for="kind in cap.available"
|
|
138
|
-
:key="kind"
|
|
139
|
-
class="flex items-start gap-2"
|
|
140
|
-
:class="
|
|
141
|
-
kind === delegatedKind && !delegatedRegistered
|
|
142
|
-
? 'cursor-not-allowed opacity-50'
|
|
143
|
-
: 'cursor-pointer'
|
|
144
|
-
"
|
|
145
|
-
>
|
|
146
|
-
<input
|
|
147
|
-
type="radio"
|
|
148
|
-
class="mt-1"
|
|
149
|
-
:value="kind"
|
|
150
|
-
:checked="kind === activeKind"
|
|
151
|
-
:disabled="saving || (kind === delegatedKind && !delegatedRegistered)"
|
|
152
|
-
@change="select(kind)"
|
|
153
|
-
/>
|
|
154
|
-
<span class="min-w-0">
|
|
155
|
-
<span class="text-sm text-slate-200">{{ labelFor(kind) }}</span>
|
|
156
|
-
<span
|
|
157
|
-
v-if="kind === delegatedKind && !delegatedRegistered"
|
|
158
|
-
class="block text-[11px] text-amber-300/80"
|
|
159
|
-
>
|
|
160
|
-
{{ t('settings.infrastructure.registerHint') }}
|
|
161
|
-
</span>
|
|
162
|
-
</span>
|
|
163
|
-
</label>
|
|
164
|
-
</div>
|
|
165
|
-
|
|
166
|
-
<!-- Worker/Node: the active backend is deployment/registration-driven, not a toggle. -->
|
|
167
|
-
<p v-else class="text-sm text-slate-300" :data-testid="`${axis}-backend-active`">
|
|
168
|
-
{{ t('settings.infrastructure.active', { backend: labelFor(activeKind) }) }}
|
|
169
|
-
</p>
|
|
170
|
-
</section>
|
|
171
|
-
</template>
|