lesavka: host no-upload receiver observer at dedicated HTTPS path
Some checks failed
Tests / Declarative: Post Actions failed: 42, skipped: 89, passed: 4273
Some checks failed
Tests / Declarative: Post Actions failed: 42, skipped: 89, passed: 4273
This commit is contained in:
parent
d114ce71e8
commit
e035cadb4d
28
services/bstein-dev-home/av-observer/activity-worklet.js
Normal file
28
services/bstein-dev-home/av-observer/activity-worklet.js
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
// A cumulative, one-credit metadata reporter: never copies or sends audio.
|
||||||
|
class Activity extends AudioWorkletProcessor {
|
||||||
|
constructor() {
|
||||||
|
super(); this.frames=0; this.inputFrames=0; this.zeroFrames=0;
|
||||||
|
this.energy=0; this.peak=0; this.credit=true; this.next=0; this.stopped=false;
|
||||||
|
this.port.onmessage=({data})=>{ if(data==='ack')this.credit=true; if(data==='stop')this.stopped=true; };
|
||||||
|
}
|
||||||
|
process(inputs,outputs) {
|
||||||
|
if(this.stopped || this.frames >= sampleRate*125) return false;
|
||||||
|
const block=outputs[0]?.[0]?.length ?? 128;
|
||||||
|
const samples=inputs[0]?.[0];
|
||||||
|
this.frames+=block;
|
||||||
|
if(samples) for(const value of samples) {
|
||||||
|
this.inputFrames++; this.energy+=value*value;
|
||||||
|
this.peak=Math.max(this.peak,Math.abs(value));
|
||||||
|
if(value===0)this.zeroFrames++;
|
||||||
|
}
|
||||||
|
// Output is left at zero: no monitoring/playback or acoustic feedback.
|
||||||
|
if(this.credit && this.frames>=this.next) {
|
||||||
|
this.port.postMessage({inputFrames:this.inputFrames,processedFrames:this.frames,
|
||||||
|
exactZeroFrames:this.zeroFrames,rms:this.inputFrames ? Math.sqrt(this.energy/this.inputFrames) : 0,
|
||||||
|
peak:this.peak,sampleRate});
|
||||||
|
this.credit=false; this.next=this.frames+sampleRate;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
registerProcessor('av-activity',Activity);
|
||||||
53
services/bstein-dev-home/av-observer/deployment.yaml
Normal file
53
services/bstein-dev-home/av-observer/deployment.yaml
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
# Static assets only; no API, uploads, credentials or outbound connections.
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: lesavka-av-observer
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
revisionHistoryLimit: 2
|
||||||
|
selector:
|
||||||
|
matchLabels: {app: lesavka-av-observer}
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels: {app: lesavka-av-observer}
|
||||||
|
spec:
|
||||||
|
automountServiceAccountToken: false
|
||||||
|
nodeSelector:
|
||||||
|
kubernetes.io/arch: arm64
|
||||||
|
node-role.kubernetes.io/worker: "true"
|
||||||
|
securityContext:
|
||||||
|
runAsNonRoot: true
|
||||||
|
runAsUser: 101
|
||||||
|
runAsGroup: 101
|
||||||
|
fsGroup: 101
|
||||||
|
seccompProfile: {type: RuntimeDefault}
|
||||||
|
containers:
|
||||||
|
- name: static
|
||||||
|
image: nginx:1.27.5-alpine@sha256:65645c7bb6a0661892a8b03b89d0743208a18dd2f3f17a54ef4b76fb8e2f2a10
|
||||||
|
command: [/usr/sbin/nginx]
|
||||||
|
args: [-c, /etc/nginx/observer/nginx.conf, -g, "daemon off;"]
|
||||||
|
ports: [{name: http, containerPort: 8080}]
|
||||||
|
securityContext:
|
||||||
|
allowPrivilegeEscalation: false
|
||||||
|
readOnlyRootFilesystem: true
|
||||||
|
capabilities: {drop: [ALL]}
|
||||||
|
resources:
|
||||||
|
requests: {cpu: 10m, memory: 16Mi}
|
||||||
|
limits: {cpu: 100m, memory: 64Mi}
|
||||||
|
readinessProbe:
|
||||||
|
httpGet: {path: /lesavka-av-test/, port: http}
|
||||||
|
livenessProbe:
|
||||||
|
httpGet: {path: /lesavka-av-test/, port: http}
|
||||||
|
initialDelaySeconds: 10
|
||||||
|
volumeMounts:
|
||||||
|
- {name: config, mountPath: /etc/nginx/observer, readOnly: true}
|
||||||
|
- {name: assets, mountPath: /usr/share/nginx/html/lesavka-av-test, readOnly: true}
|
||||||
|
- {name: tmp, mountPath: /tmp}
|
||||||
|
volumes:
|
||||||
|
- name: config
|
||||||
|
configMap: {name: lesavka-av-observer-nginx}
|
||||||
|
- name: assets
|
||||||
|
configMap: {name: lesavka-av-observer-assets}
|
||||||
|
- name: tmp
|
||||||
|
emptyDir: {medium: Memory, sizeLimit: 8Mi}
|
||||||
18
services/bstein-dev-home/av-observer/index.html
Normal file
18
services/bstein-dev-home/av-observer/index.html
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width">
|
||||||
|
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; connect-src 'none'; media-src 'self' blob:; style-src 'self'; object-src 'none'; base-uri 'none'">
|
||||||
|
<title>Lesavka — bundled A/V observer</title>
|
||||||
|
<link rel="stylesheet" href="style.css">
|
||||||
|
<main><h1>Bundled A/V observer</h1>
|
||||||
|
<p>No recording, uploads or saved camera/microphone content. Select the RCT's Lesavka video and microphone devices.</p>
|
||||||
|
<div><button id="devices">Choose devices…</button>
|
||||||
|
<label>Video <select id="video-device"></select></label>
|
||||||
|
<label>Microphone <select id="audio-device"></select></label></div>
|
||||||
|
<div><button id="start" disabled>Start 2-minute check</button><button id="stop" disabled>Stop</button><button id="export" disabled>Save metrics</button></div>
|
||||||
|
<p id="status" role="status">Stopped</p>
|
||||||
|
<video id="preview" autoplay muted playsinline></video>
|
||||||
|
<pre id="metrics">No observations.</pre>
|
||||||
|
<p>Sync and capture-to-RCT freshness: <strong>not measured by this observer</strong>.
|
||||||
|
Video callback gaps can include browser scheduling. Silence is not proof of audio loss.
|
||||||
|
Use the qualified paired-marker/clock-bounded probe for release acceptance.</p>
|
||||||
|
</main><script type="module" src="observer.js"></script></html>
|
||||||
23
services/bstein-dev-home/av-observer/ingress.yaml
Normal file
23
services/bstein-dev-home/av-observer/ingress.yaml
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
# Reuse the existing domain/certificate, without changing its homepage route.
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: lesavka-av-observer
|
||||||
|
annotations:
|
||||||
|
traefik.ingress.kubernetes.io/router.entrypoints: websecure
|
||||||
|
traefik.ingress.kubernetes.io/router.tls: "true"
|
||||||
|
spec:
|
||||||
|
ingressClassName: traefik
|
||||||
|
tls:
|
||||||
|
- hosts: [bstein.dev]
|
||||||
|
secretName: bstein-dev-home-tls
|
||||||
|
rules:
|
||||||
|
- host: bstein.dev
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: /lesavka-av-test
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: lesavka-av-observer
|
||||||
|
port: {number: 80}
|
||||||
17
services/bstein-dev-home/av-observer/kustomization.yaml
Normal file
17
services/bstein-dev-home/av-observer/kustomization.yaml
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
resources:
|
||||||
|
- deployment.yaml
|
||||||
|
- service.yaml
|
||||||
|
- ingress.yaml
|
||||||
|
- network-policy.yaml
|
||||||
|
configMapGenerator:
|
||||||
|
- name: lesavka-av-observer-nginx
|
||||||
|
files: [nginx.conf]
|
||||||
|
- name: lesavka-av-observer-assets
|
||||||
|
files:
|
||||||
|
- index.html
|
||||||
|
- observer.js
|
||||||
|
- metrics.js
|
||||||
|
- activity-worklet.js
|
||||||
|
- style.css
|
||||||
30
services/bstein-dev-home/av-observer/metrics.js
Normal file
30
services/bstein-dev-home/av-observer/metrics.js
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
// Metadata only. No frames or PCM are retained. These are OBSERVER metrics,
|
||||||
|
// never an automatically passing sync/freshness/artifact report.
|
||||||
|
export class VideoMetrics {
|
||||||
|
constructor() { this.last = null; this.intervals = []; this.callbacks = 0; this.unobserved = 0; this.resets = 0; }
|
||||||
|
observe(timeMs, presentedFrames) {
|
||||||
|
if (!Number.isFinite(timeMs) || !Number.isSafeInteger(presentedFrames) || presentedFrames < 0) return;
|
||||||
|
if (this.last) {
|
||||||
|
if (timeMs <= this.last.timeMs || presentedFrames <= this.last.presentedFrames) this.resets++;
|
||||||
|
else {
|
||||||
|
if (this.intervals.length < 15000) this.intervals.push(timeMs-this.last.timeMs);
|
||||||
|
this.unobserved += Math.max(0,presentedFrames-this.last.presentedFrames-1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.last = {timeMs,presentedFrames}; this.callbacks++;
|
||||||
|
}
|
||||||
|
summary() {
|
||||||
|
const sorted = [...this.intervals].sort((a,b)=>a-b);
|
||||||
|
return {callbacks:this.callbacks,unobservedPresentedFrames:this.unobserved,clockOrCounterResets:this.resets,
|
||||||
|
callbackIntervalP95Ms:sorted.length ? sorted[Math.ceil(sorted.length*.95)-1] : null,
|
||||||
|
callbackGapMaxMs:sorted.at(-1) ?? null,
|
||||||
|
note:"Browser presentation callbacks; gaps are not proof of transport loss or corrupted video."};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function receipt(video,audio,settings,elapsedMs,reason) {
|
||||||
|
return {schema:"lesavka.rct-av-observer.v1",kind:"diagnostic-only",reason,elapsedMs,
|
||||||
|
settings,video:video.summary(),audio,
|
||||||
|
syncMeasured:false,freshnessMeasured:false,artifactsMeasured:false,
|
||||||
|
releaseEligible:false,recordedMedia:false};
|
||||||
|
}
|
||||||
12
services/bstein-dev-home/av-observer/network-policy.yaml
Normal file
12
services/bstein-dev-home/av-observer/network-policy.yaml
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
# The observer only serves static files; it has no reason to initiate traffic.
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: NetworkPolicy
|
||||||
|
metadata:
|
||||||
|
name: lesavka-av-observer
|
||||||
|
spec:
|
||||||
|
podSelector:
|
||||||
|
matchLabels: {app: lesavka-av-observer}
|
||||||
|
policyTypes: [Ingress, Egress]
|
||||||
|
ingress:
|
||||||
|
- ports: [{protocol: TCP, port: 8080}]
|
||||||
|
egress: []
|
||||||
35
services/bstein-dev-home/av-observer/nginx.conf
Normal file
35
services/bstein-dev-home/av-observer/nginx.conf
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
worker_processes 1;
|
||||||
|
pid /tmp/nginx.pid;
|
||||||
|
error_log /dev/stderr warn;
|
||||||
|
events { worker_connections 128; }
|
||||||
|
http {
|
||||||
|
include /etc/nginx/mime.types;
|
||||||
|
default_type application/octet-stream;
|
||||||
|
access_log off;
|
||||||
|
server_tokens off;
|
||||||
|
client_max_body_size 1k;
|
||||||
|
client_body_temp_path /tmp/client;
|
||||||
|
proxy_temp_path /tmp/proxy;
|
||||||
|
fastcgi_temp_path /tmp/fastcgi;
|
||||||
|
uwsgi_temp_path /tmp/uwsgi;
|
||||||
|
scgi_temp_path /tmp/scgi;
|
||||||
|
server {
|
||||||
|
listen 8080;
|
||||||
|
absolute_redirect off;
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
if ($request_method !~ ^(GET|HEAD)$) { return 405; }
|
||||||
|
add_header Cache-Control "no-store" always;
|
||||||
|
add_header X-Content-Type-Options "nosniff" always;
|
||||||
|
add_header X-Frame-Options "DENY" always;
|
||||||
|
add_header Referrer-Policy "no-referrer" always;
|
||||||
|
add_header Permissions-Policy "camera=(self), microphone=(self), geolocation=(), display-capture=()" always;
|
||||||
|
add_header Cross-Origin-Opener-Policy "same-origin" always;
|
||||||
|
add_header Content-Security-Policy "default-src 'none'; script-src 'self'; style-src 'self'; media-src 'self' blob:; connect-src 'none'; worker-src 'self'; object-src 'none'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'" always;
|
||||||
|
location = /lesavka-av-test { return 308 /lesavka-av-test/; }
|
||||||
|
location = /lesavka-av-test/ { try_files /lesavka-av-test/index.html =404; }
|
||||||
|
location ~ ^/lesavka-av-test/(index\.html|observer\.js|metrics\.js|activity-worklet\.js|style\.css)$ {
|
||||||
|
try_files $uri =404;
|
||||||
|
}
|
||||||
|
location / { return 404; }
|
||||||
|
}
|
||||||
|
}
|
||||||
117
services/bstein-dev-home/av-observer/observer.js
Normal file
117
services/bstein-dev-home/av-observer/observer.js
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
import {VideoMetrics,receipt} from './metrics.js';
|
||||||
|
const el=id=>document.getElementById(id);
|
||||||
|
let generation=0, current=null, last=null;
|
||||||
|
const stopTracks=stream=>stream?.getTracks().forEach(track=>track.stop());
|
||||||
|
function release(run) {
|
||||||
|
if(run.released)return;
|
||||||
|
run.released=true;
|
||||||
|
clearTimeout(run.timer); clearTimeout(run.setupTimer);
|
||||||
|
if(run.callback)el('preview').cancelVideoFrameCallback(run.callback);
|
||||||
|
run.node?.port.postMessage('stop'); run.node?.disconnect(); run.source?.disconnect();
|
||||||
|
stopTracks(run.stream);
|
||||||
|
// Do not wait on a browser AudioContext close before fencing a stopped run.
|
||||||
|
run.context?.close().catch(()=>{});
|
||||||
|
}
|
||||||
|
function stop(reason='operator') {
|
||||||
|
generation++;
|
||||||
|
const run=current; current=null;
|
||||||
|
if(run) {
|
||||||
|
release(run);
|
||||||
|
last=receipt(run.video,run.audio,run.settings,performance.now()-run.started,reason);
|
||||||
|
el('metrics').textContent=JSON.stringify(last,null,2);
|
||||||
|
el('export').disabled=false;
|
||||||
|
}
|
||||||
|
el('preview').srcObject=null;
|
||||||
|
el('start').disabled=!el('audio-device').value || !el('video-device').value;
|
||||||
|
el('stop').disabled=true; el('devices').disabled=false;
|
||||||
|
el('status').textContent=`Stopped · ${reason}`;
|
||||||
|
}
|
||||||
|
el('devices').onclick=async()=>{
|
||||||
|
stop('selecting devices');
|
||||||
|
const ticket=++generation;
|
||||||
|
el('devices').disabled=true; el('start').disabled=true; el('stop').disabled=false;
|
||||||
|
el('status').textContent='Requesting device permission…';
|
||||||
|
const timer=setTimeout(()=>{if(generation===ticket)stop('permission timeout');},20000);
|
||||||
|
try {
|
||||||
|
const stream=await navigator.mediaDevices.getUserMedia({audio:true,video:true});
|
||||||
|
stopTracks(stream);
|
||||||
|
if(generation!==ticket)return;
|
||||||
|
const devices=await navigator.mediaDevices.enumerateDevices();
|
||||||
|
if(generation!==ticket)return;
|
||||||
|
for(const [id,kind] of [['video-device','videoinput'],['audio-device','audioinput']]) {
|
||||||
|
const select=el(id), saved=select.value;
|
||||||
|
select.replaceChildren(new Option('Select Lesavka device',''));
|
||||||
|
for(const d of devices.filter(d=>d.kind===kind)) select.add(new Option(d.label||'Unnamed device',d.deviceId));
|
||||||
|
select.value=saved;
|
||||||
|
}
|
||||||
|
el('status').textContent='Select both Lesavka devices, then Start.';
|
||||||
|
el('devices').disabled=false; el('stop').disabled=true;
|
||||||
|
} catch { if(generation===ticket)stop('device permission unavailable'); }
|
||||||
|
finally {clearTimeout(timer);}
|
||||||
|
};
|
||||||
|
for(const id of ['audio-device','video-device']) el(id).onchange=()=>{
|
||||||
|
if(current)stop('device selection changed');
|
||||||
|
el('start').disabled=!el('audio-device').value||!el('video-device').value;
|
||||||
|
};
|
||||||
|
el('start').onclick=async()=>{
|
||||||
|
if(current || !el('audio-device').value || !el('video-device').value)return;
|
||||||
|
if(!el('preview').requestVideoFrameCallback || !window.AudioWorkletNode) {
|
||||||
|
el('status').textContent='This browser lacks the required observation APIs.';return;
|
||||||
|
}
|
||||||
|
const ticket=++generation;
|
||||||
|
const run={started:performance.now(),video:new VideoMetrics(),audio:null,settings:{}};
|
||||||
|
current=run; last=null;
|
||||||
|
el('start').disabled=true; el('devices').disabled=true; el('stop').disabled=false; el('export').disabled=true;
|
||||||
|
el('status').textContent='Starting…';
|
||||||
|
run.setupTimer=setTimeout(()=>{if(current===run)stop('setup timeout');},20000);
|
||||||
|
try {
|
||||||
|
const stream=await navigator.mediaDevices.getUserMedia({
|
||||||
|
video:{deviceId:{exact:el('video-device').value}},
|
||||||
|
audio:{deviceId:{exact:el('audio-device').value},echoCancellation:false,noiseSuppression:false,autoGainControl:false},
|
||||||
|
});
|
||||||
|
if(generation!==ticket){stopTracks(stream);return;}
|
||||||
|
run.stream=stream;
|
||||||
|
for(const track of stream.getTracks())track.onended=()=>{if(current===run)stop('device ended');};
|
||||||
|
const vs=stream.getVideoTracks()[0].getSettings(),as=stream.getAudioTracks()[0].getSettings();
|
||||||
|
run.settings={video:{width:vs.width,height:vs.height,frameRate:vs.frameRate},
|
||||||
|
audio:{sampleRate:as.sampleRate,channelCount:as.channelCount,echoCancellation:as.echoCancellation,
|
||||||
|
noiseSuppression:as.noiseSuppression,autoGainControl:as.autoGainControl}};
|
||||||
|
run.context=new AudioContext();
|
||||||
|
await run.context.audioWorklet.addModule('activity-worklet.js');
|
||||||
|
if(generation!==ticket)return;
|
||||||
|
run.node=new AudioWorkletNode(run.context,'av-activity');
|
||||||
|
run.node.onprocessorerror=()=>{if(current===run)stop('audio observer error');};
|
||||||
|
run.node.port.onmessage=({data})=>{
|
||||||
|
if(current!==run)return;
|
||||||
|
run.audio=data; run.node.port.postMessage('ack');
|
||||||
|
el('metrics').textContent=JSON.stringify(receipt(run.video,run.audio,run.settings,performance.now()-run.started,'observing'),null,2);
|
||||||
|
};
|
||||||
|
run.source=run.context.createMediaStreamSource(new MediaStream(stream.getAudioTracks()));
|
||||||
|
run.source.connect(run.node);run.node.connect(run.context.destination);
|
||||||
|
await run.context.resume();
|
||||||
|
if(generation!==ticket)return;
|
||||||
|
el('preview').srcObject=stream;
|
||||||
|
await el('preview').play();
|
||||||
|
if(generation!==ticket)return;
|
||||||
|
clearTimeout(run.setupTimer);
|
||||||
|
run.started=performance.now();
|
||||||
|
function frame(now,metadata) {
|
||||||
|
if(current!==run)return;
|
||||||
|
run.video.observe(metadata.expectedDisplayTime,metadata.presentedFrames);
|
||||||
|
run.callback=el('preview').requestVideoFrameCallback(frame);
|
||||||
|
}
|
||||||
|
run.callback=el('preview').requestVideoFrameCallback(frame);
|
||||||
|
run.timer=setTimeout(()=>{if(current===run)stop('120 seconds complete');},120000);
|
||||||
|
el('status').textContent='Observing both devices · no recording';
|
||||||
|
} catch { if(current===run)stop('capture/setup failed'); }
|
||||||
|
finally { if(current!==run)release(run); }
|
||||||
|
};
|
||||||
|
el('stop').onclick=()=>stop();
|
||||||
|
el('export').onclick=()=>{
|
||||||
|
if(!last)return;
|
||||||
|
const url=URL.createObjectURL(new Blob([JSON.stringify(last,null,2)],{type:'application/json'}));
|
||||||
|
const link=document.createElement('a');link.href=url;link.download='lesavka-av-observer.json';link.click();
|
||||||
|
setTimeout(()=>URL.revokeObjectURL(url),1000);
|
||||||
|
};
|
||||||
|
window.addEventListener('pagehide',()=>stop('page closed'));
|
||||||
|
document.addEventListener('visibilitychange',()=>{if(document.hidden&¤t)stop('page hidden; timing invalid');});
|
||||||
7
services/bstein-dev-home/av-observer/service.yaml
Normal file
7
services/bstein-dev-home/av-observer/service.yaml
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: lesavka-av-observer
|
||||||
|
spec:
|
||||||
|
selector: {app: lesavka-av-observer}
|
||||||
|
ports: [{name: http, port: 80, targetPort: http}]
|
||||||
7
services/bstein-dev-home/av-observer/style.css
Normal file
7
services/bstein-dev-home/av-observer/style.css
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
:root { color-scheme: dark; font: 15px system-ui,sans-serif; }
|
||||||
|
body { margin: 24px; background: #171b22; color: #e7edf6; }
|
||||||
|
main { max-width: 1000px; margin: auto; }
|
||||||
|
button,select { padding: 8px; margin: 4px; max-width: 100%; }
|
||||||
|
label { display: inline-block; }
|
||||||
|
video { width: 100%; max-height: 55vh; background: #000; }
|
||||||
|
pre { white-space: pre-wrap; overflow-wrap: anywhere; }
|
||||||
@ -18,6 +18,7 @@ resources:
|
|||||||
- vaultwarden-cred-sync-cronjob.yaml
|
- vaultwarden-cred-sync-cronjob.yaml
|
||||||
- validation-jobs/portal-onboarding-e2e-test-job.yaml
|
- validation-jobs/portal-onboarding-e2e-test-job.yaml
|
||||||
- ingress.yaml
|
- ingress.yaml
|
||||||
|
- av-observer
|
||||||
images:
|
images:
|
||||||
- name: registry.bstein.dev/bstein/bstein-dev-home-frontend
|
- name: registry.bstein.dev/bstein/bstein-dev-home-frontend
|
||||||
newTag: 0.1.1-541 # {"$imagepolicy": "bstein-dev-home:bstein-dev-home-frontend:tag"}
|
newTag: 0.1.1-541 # {"$imagepolicy": "bstein-dev-home:bstein-dev-home-frontend:tag"}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user