docs: preserve reviewed NetKingdom session artifacts
This commit is contained in:
parent
cb60cd3bbe
commit
109a90a424
50 changed files with 8970 additions and 0 deletions
|
|
@ -0,0 +1,229 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Build-only public Forgejo owner recipe; no deployment or publication code."""
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import tarfile
|
||||
import time
|
||||
|
||||
OWNER=Path(__file__).resolve().parents[1]
|
||||
INPUTS=OWNER/'build/forgejo'
|
||||
LOCK=json.loads((INPUTS/'inputs.lock.json').read_text())
|
||||
|
||||
def digest(path):
|
||||
h=hashlib.sha256()
|
||||
with Path(path).open('rb') as f:
|
||||
for block in iter(lambda:f.read(1024*1024),b''):h.update(block)
|
||||
return h.hexdigest()
|
||||
|
||||
def save(path,data):
|
||||
Path(path).write_text(json.dumps(data,indent=2,sort_keys=True)+'\n')
|
||||
|
||||
def check(path,expected):
|
||||
if digest(path)!=expected:raise ValueError('frozen public input digest differs: '+str(path))
|
||||
|
||||
def base_env(work):
|
||||
return {'PATH':str(work/'node-v22.15.0-linux-x64/bin')+':'+str(work/'go/bin')+':/usr/bin:/bin',
|
||||
'HOME':str(work/'home'),'GOTOOLCHAIN':'local','GOPROXY':'off','GOSUMDB':'off','GOVCS':'*:off',
|
||||
'GOPATH':str(work/'gopath'),'GOMODCACHE':json.loads((work/'prepared.json').read_text())['module_cache'],
|
||||
'GOCACHE':str(work/'gocache'),'CGO_ENABLED':'1','GOMAXPROCS':'4',
|
||||
'GOFLAGS':'-trimpath -buildvcs=false -p=4','SOURCE_DATE_EPOCH':str(LOCK['source_date_epoch']),
|
||||
'TZ':'UTC','LC_ALL':'C','NODE_OPTIONS':'--max-old-space-size=4096',
|
||||
'GIT_CONFIG_NOSYSTEM':'1','GIT_CONFIG_GLOBAL':'/dev/null','GIT_TERMINAL_PROMPT':'0',
|
||||
'NPM_CONFIG_USERCONFIG':str(work/'empty.npmrc'),'NPM_CONFIG_GLOBALCONFIG':str(work/'empty-global.npmrc'),
|
||||
'NPM_CONFIG_CACHE':str(work/'npm-cache'),'NPM_CONFIG_REGISTRY':'https://registry.npmjs.org',
|
||||
'NPM_CONFIG_OFFLINE':'true','NPM_CONFIG_AUDIT':'false','NPM_CONFIG_FUND':'false',
|
||||
'NPM_CONFIG_UPDATE_NOTIFIER':'false','PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD':'1'}
|
||||
|
||||
def sandbox(work,env):
|
||||
cache=Path(env['GOMODCACHE'])
|
||||
return ['bwrap','--unshare-net','--ro-bind','/usr','/usr','--ro-bind','/lib','/lib',
|
||||
'--ro-bind','/lib64','/lib64','--symlink','usr/bin','/bin','--proc','/proc','--dev','/dev',
|
||||
'--tmpfs','/tmp','--bind',str(work),str(work),'--ro-bind',str(cache),str(cache),
|
||||
'--chdir',str(work/'source'),'--','env','-i']+[k+'='+v for k,v in env.items()]
|
||||
|
||||
def run(work,name,args,env=None,offline=False,cwd=None):
|
||||
actual=sandbox(work,env)+args if offline else args
|
||||
start=time.time()
|
||||
with (work/(name+'.log')).open('w') as out:
|
||||
proc=subprocess.Popen(actual,cwd=cwd or work,env={"PATH":"/usr/bin:/bin","HOME":str(work/"home")} if offline else env,stdout=out,stderr=subprocess.STDOUT)
|
||||
save(work/'process.json',{'phase':name,'pid':proc.pid,'driver_pid':os.getpid(),'argv':args,'state':'running'})
|
||||
code=proc.wait()
|
||||
result={'phase':name,'argv':args,'returncode':code,'seconds':round(time.time()-start,2)}
|
||||
save(work/(name+'-result.json'),result);save(work/'process.json',dict(result,state='complete',pid=None,driver_pid=None))
|
||||
print(json.dumps(result),flush=True)
|
||||
if code:raise RuntimeError('phase failed; retained log: '+str(work/(name+'.log')))
|
||||
return result
|
||||
|
||||
def checked_inputs(work):
|
||||
for n,h in LOCK['host_tools'].items():check(n,h)
|
||||
for n,h in LOCK['lockfiles'].items():check(work/'source'/n,h)
|
||||
for x in LOCK['reviewed_outputs']:check(work/'source'/x['path'],x['sha256'])
|
||||
|
||||
def prepare(a,work):
|
||||
if work.exists():raise ValueError('workspace must be new; existing trial is never restarted or overwritten')
|
||||
for n,key in [('source_archive','source_archive_sha256'),('go_archive','go_archive_sha256'),('node_archive','node_archive_sha256')]:
|
||||
if not getattr(a,n):raise ValueError('explicit '+n+' required')
|
||||
check(getattr(a,n),LOCK[key])
|
||||
if not a.module_cache:raise ValueError('explicit public module cache required')
|
||||
cache=Path(a.module_cache).resolve();assert cache.is_dir()
|
||||
check(INPUTS/'required-claim-values.patch',LOCK['patch_sha256'])
|
||||
for n,h in LOCK['host_tools'].items():check(n,h)
|
||||
work.mkdir(parents=True);(work/'home').mkdir()
|
||||
with tarfile.open(a.source_archive) as t:
|
||||
# The official archive is digest-bound before any member is extracted.
|
||||
parent=work/'unpacked';parent.mkdir();t.extractall(parent,filter='data')
|
||||
folders=list(parent.iterdir());assert len(folders)==1 and folders[0].is_dir();folders[0].rename(work/'source');parent.rmdir()
|
||||
for n in ['go_archive','node_archive']:
|
||||
with tarfile.open(getattr(a,n)) as t:t.extractall(work,filter='data')
|
||||
for n in ['empty.npmrc','empty-global.npmrc']:(work/n).write_text('')
|
||||
if a.go_cache_seed:shutil.copytree(Path(a.go_cache_seed).resolve(),work/'gocache')
|
||||
if a.npm_cache_seed:shutil.copytree(Path(a.npm_cache_seed).resolve(),work/'npm-cache')
|
||||
subprocess.run(['/usr/bin/git','apply','--check',str(INPUTS/'required-claim-values.patch')],cwd=work/'source',check=True,env={'PATH':'/usr/bin:/bin','HOME':str(work/'home'),'GIT_CONFIG_NOSYSTEM':'1','GIT_CONFIG_GLOBAL':'/dev/null'})
|
||||
subprocess.run(['/usr/bin/git','apply',str(INPUTS/'required-claim-values.patch')],cwd=work/'source',check=True,env={'PATH':'/usr/bin:/bin','HOME':str(work/'home'),'GIT_CONFIG_NOSYSTEM':'1','GIT_CONFIG_GLOBAL':'/dev/null'})
|
||||
save(work/'prepared.json',{'module_cache':str(cache),'lock_sha256':digest(INPUTS/'inputs.lock.json'),'public_archives_verified':True})
|
||||
checked_inputs(work)
|
||||
packages=json.loads((work/'source/package-lock.json').read_text())['packages']
|
||||
for name,package in packages.items():
|
||||
if not name:continue
|
||||
if not package.get('resolved','').startswith('https://registry.npmjs.org/') or not package.get('integrity','').startswith('sha512-'):
|
||||
raise ValueError('non-public/non-integrity-bound npm package: '+name)
|
||||
print(json.dumps({'prepared':True,'lockfile_npm_packages':len(packages)-1}))
|
||||
|
||||
def normalize(folder):
|
||||
epoch=LOCK['source_date_epoch']
|
||||
for root,dirs,files in os.walk(folder):
|
||||
dirs[:]=[x for x in dirs if x not in ('node_modules','.git')]
|
||||
for n in files:
|
||||
p=Path(root)/n
|
||||
if not p.is_symlink():os.utime(p,(epoch,epoch))
|
||||
os.utime(root,(epoch,epoch))
|
||||
|
||||
def build(work):
|
||||
checked_inputs(work);env=base_env(work)
|
||||
run(work,'verify-go-modules',['go','mod','verify'],env,True)
|
||||
run(work,'npm-offline-ci',['npm','ci','--offline','--ignore-scripts','--no-audit','--no-fund'],env,True)
|
||||
normalize(work/'source')
|
||||
version=['GOFLAGS=-trimpath -buildvcs=false -p=4','RELEASE_VERSION='+LOCK['release_version'],'FORGEJO_VERSION='+LOCK['release_version'],'FORGEJO_VERSION_API='+LOCK['release_version']]
|
||||
run(work,'frontend',['make']+version+['frontend'],env,True)
|
||||
for n in ['public','options','templates','modules/migration/schemas']:normalize(work/'source'/n)
|
||||
run(work,'embedded-and-static-backend',['make']+version+['TAGS='+LOCK['tags'],'go-check','generate-backend','static-executable'],env,True)
|
||||
checked_inputs(work)
|
||||
save(work/'build.json',{'binary_sha256':digest(work/'source/gitea'),'source_locks_preserved':True,'frontend_built':True,'tags':LOCK['tags'],'image_deployment_selected':False})
|
||||
|
||||
def verify(work):
|
||||
checked_inputs(work);binary=work/'source/gitea'
|
||||
target=work/'embedded-extracted'
|
||||
if target.exists():raise ValueError('retain prior verification; extraction target already exists')
|
||||
target.mkdir()
|
||||
# No source, asset tree, host /usr, libraries, configuration or HOME is
|
||||
# exposed to the executable. A filesystem fallback cannot pass this proof.
|
||||
bare=['bwrap','--unshare-net','--unshare-pid','--proc','/proc','--dev','/dev',
|
||||
'--tmpfs','/tmp','--dir','/app','--ro-bind',str(binary),'/app/forgejo',
|
||||
'--bind',str(target),'/results','--chdir','/tmp','--clearenv']
|
||||
for k,v in {'PATH':'/nonexistent','HOME':'/tmp/home','USER':'builder','USERNAME':'builder',
|
||||
'GITEA_WORK_DIR':'/tmp/work','GITEA_CUSTOM':'/tmp/custom',
|
||||
'GITEA_APP_INI':'/tmp/absent-app.ini','TZ':'UTC','LC_ALL':'C'}.items():
|
||||
bare+=['--setenv',k,v]
|
||||
bare+=['--','/app/forgejo']
|
||||
outer={'PATH':'/usr/bin:/bin','HOME':str(work/'home')}
|
||||
run(work,'binary-version',bare+['--version'],outer)
|
||||
run(work,'new-cli-help',bare+['admin','auth','add-oauth','--help'],outer)
|
||||
assert '--required-claim-values' in (work/'new-cli-help.log').read_text()
|
||||
run(work,'embedded-extract',bare+['embedded','extract','--include-vendored','--destination','/results','**'],outer)
|
||||
records={}
|
||||
for folder in ['public','options','templates']:
|
||||
for f in sorted((work/'source'/folder).rglob('*')):
|
||||
if not f.is_file():continue
|
||||
n=str(f.relative_to(work/'source'));q=target/n
|
||||
if not q.is_file() or digest(f)!=digest(q):raise ValueError('embedded asset missing/different: '+n)
|
||||
records[n]=digest(f)
|
||||
assert 'public/assets/js/index.js' in records and 'public/assets/css/index.css' in records
|
||||
assert 'public/assets/licenses.txt' in records
|
||||
save(work/'embedded-files.json',records)
|
||||
readelf=subprocess.run(['/usr/bin/readelf','-l',str(binary)],capture_output=True,text=True,check=True,env={'PATH':'/usr/bin:/bin'})
|
||||
assert 'INTERP' not in readelf.stdout
|
||||
save(work/'verification.json',{'binary_sha256':digest(binary),'embedded_files':len(records),
|
||||
'embedded_file_manifest_sha256':digest(work/'embedded-files.json'),'all_public_options_templates_exact':True,
|
||||
'no_source_assets_libraries_or_host_config_mounted':True,
|
||||
'no_dynamic_interpreter':True,'new_cli_flag_present':True,'network':'bwrap --unshare-net',
|
||||
'source_locks_preserved':True,'architecture_approval':False,'native_admission':False})
|
||||
print((work/'verification.json').read_text())
|
||||
|
||||
def container(work):
|
||||
verified=json.loads((work/'verification.json').read_text());check(work/'source/gitea',verified['binary_sha256'])
|
||||
context=work/'container-context'
|
||||
if context.exists():raise ValueError('container trial already exists; retain evidence')
|
||||
context.mkdir();shutil.copyfile(work/'source/gitea',context/'gitea')
|
||||
os.utime(context/'gitea',(LOCK['source_date_epoch'],)*2)
|
||||
(context/'Dockerfile').write_text('FROM '+LOCK['runtime_base']+'\nARG SOURCE_DATE_EPOCH\nLABEL org.opencontainers.image.version="'+LOCK['release_version']+'" org.opencontainers.image.revision="'+LOCK['source_commit']+'" org.railiance.source.patch.sha256="'+LOCK['patch_sha256']+'"\nCOPY --chown=0:0 --chmod=755 gitea /app/gitea/gitea\n')
|
||||
os.utime(context/'Dockerfile',(LOCK['source_date_epoch'],)*2)
|
||||
config=work/'docker-config';config.mkdir(exist_ok=True)
|
||||
env={'PATH':'/usr/bin:/bin','HOME':str(work/'home'),'DOCKER_CONFIG':str(config),'DOCKER_HOST':'unix:///var/run/docker.sock'}
|
||||
run(work,'container-build',['docker','buildx','build','--load','--platform',LOCK['platform'],'--pull=false',
|
||||
'--network=none','--provenance=false','--sbom=false','--iidfile',str(work/'image-id'),
|
||||
'--metadata-file',str(work/'image-build-metadata.json'),'--build-arg','SOURCE_DATE_EPOCH='+str(LOCK['source_date_epoch']),str(context)],env)
|
||||
export_container(work)
|
||||
|
||||
def export_container(work):
|
||||
verified=json.loads((work/'verification.json').read_text());check(work/'source/gitea',verified['binary_sha256'])
|
||||
check(work/'container-context/gitea',verified['binary_sha256'])
|
||||
assert json.loads((work/'container-build-result.json').read_text())['returncode']==0
|
||||
metadata=json.loads((work/'image-build-metadata.json').read_text())
|
||||
config_digest=(work/'image-id').read_text().strip()
|
||||
assert config_digest==metadata['containerimage.config.digest']
|
||||
image=metadata['containerimage.digest'];assert image.startswith('sha256:')
|
||||
env={'PATH':'/usr/bin:/bin','HOME':str(work/'home'),'DOCKER_CONFIG':str(work/'docker-config'),'DOCKER_HOST':'unix:///var/run/docker.sock'}
|
||||
# Docker's containerd store addresses an untagged image by manifest digest;
|
||||
# classic stores address it by config digest. Keep both identities explicit.
|
||||
reference=None
|
||||
for candidate in [image,config_digest]:
|
||||
r=subprocess.run(['docker','image','inspect','--format','{{.Id}}',candidate],env=env,capture_output=True,text=True)
|
||||
if r.returncode==0 and r.stdout.strip() in (image,config_digest):reference=candidate;break
|
||||
if reference is None:raise RuntimeError('exact built local image cannot be resolved; never rebuild blindly')
|
||||
archive_path=work/'image.tar'
|
||||
if archive_path.exists():raise ValueError('retain existing export before explicit retry')
|
||||
run(work,'container-save',['docker','image','save','--output',str(archive_path),reference],env)
|
||||
with tarfile.open(work/'image.tar') as archive:
|
||||
manifest=json.load(archive.extractfile('manifest.json'));assert len(manifest)==1
|
||||
item=manifest[0];config_bytes=archive.extractfile(item['Config']).read()
|
||||
assert 'sha256:'+hashlib.sha256(config_bytes).hexdigest()==config_digest
|
||||
config=json.loads(config_bytes)
|
||||
base_bytes=(INPUTS/'runtime-base-config.json').read_bytes();assert hashlib.sha256(base_bytes).hexdigest()==LOCK['runtime_base_config_sha256']
|
||||
original=json.loads(base_bytes)
|
||||
assert config['rootfs']['diff_ids'][:-1]==original['rootfs']['diff_ids']
|
||||
for key in ['User','Entrypoint','Cmd','Env','WorkingDir','Volumes']:
|
||||
assert config['config'].get(key)==original['config'].get(key),key
|
||||
assert config['config']['User']=='1000:1000'
|
||||
found=False
|
||||
with tarfile.open(fileobj=archive.extractfile(item['Layers'][-1]),mode='r|*') as layer:
|
||||
for member in layer:
|
||||
if member.name.lstrip('./')=='app/gitea/gitea':
|
||||
h=hashlib.sha256();stream=layer.extractfile(member)
|
||||
for block in iter(lambda:stream.read(1024*1024),b''):h.update(block)
|
||||
assert h.hexdigest()==verified['binary_sha256'];found=True
|
||||
assert found
|
||||
save(work/'container-result.json',{'local_image_id':image,'config_digest':config_digest,'local_export_reference':reference,'archive_sha256':digest(work/'image.tar'),
|
||||
'all_base_layers_and_rootless_runtime_config_preserved':True,'final_layer_binary_exact':True,
|
||||
'registry_publication':False,'deployment_tag':None,'host_or_secret_mounts':False,'privileged_containers':False})
|
||||
print((work/'container-result.json').read_text())
|
||||
|
||||
def main():
|
||||
parser=argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('phase',choices=['prepare','acquire-npm','build','verify','container','container-export'])
|
||||
parser.add_argument('--workspace',required=True)
|
||||
for n in ['source-archive','go-archive','node-archive','module-cache','go-cache-seed','npm-cache-seed']:parser.add_argument('--'+n)
|
||||
a=parser.parse_args();work=Path(a.workspace).resolve()
|
||||
if not str(work).startswith('/tmp/'):raise ValueError('isolated build workspace must be under /tmp')
|
||||
if a.phase=='prepare':return prepare(a,work)
|
||||
assert json.loads((work/'prepared.json').read_text())['lock_sha256']==digest(INPUTS/'inputs.lock.json')
|
||||
if a.phase=='acquire-npm':
|
||||
checked_inputs(work);env=base_env(work);env['NPM_CONFIG_OFFLINE']='false'
|
||||
return run(work,'npm-public-acquire',['npm','ci','--ignore-scripts','--no-audit','--no-fund'],env,cwd=work/'source')
|
||||
{'build':build,'verify':verify,'container':container,'container-export':export_container}[a.phase](work)
|
||||
|
||||
if __name__=='__main__':main()
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
import importlib.util,pathlib,json,hashlib,shutil,sys
|
||||
packet=pathlib.Path('/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-17/apps-forgejo-release-build-r1')
|
||||
spec=importlib.util.spec_from_file_location('release',packet/'owner-source/tools/forgejo-release-build.py');m=importlib.util.module_from_spec(spec);spec.loader.exec_module(m)
|
||||
which=sys.argv[1]
|
||||
work=pathlib.Path('/tmp/apps-forgejo-release-repro-r1' if which=='second' else '/tmp/apps-forgejo-release-build-r1')
|
||||
predecessor=m.digest(work/'source/gitea');shutil.copyfile(work/'source/gitea',work/'forgejo-before-directory-normalization')
|
||||
m.checked_inputs(work)
|
||||
for n in ['public','options','templates','modules/migration/schemas']:m.normalize(work/'source'/n)
|
||||
args=['make','GOFLAGS=-trimpath -buildvcs=false -p=4','RELEASE_VERSION='+m.LOCK['release_version'],'FORGEJO_VERSION='+m.LOCK['release_version'],'FORGEJO_VERSION_API='+m.LOCK['release_version'],'TAGS='+m.LOCK['tags'],'go-check','generate-backend','static-executable']
|
||||
if which=='second':
|
||||
env=m.base_env(work);m.run(work,'normalized-embedded-and-static-backend',args,env,True)
|
||||
else:
|
||||
env=json.loads((work/'build-env.json').read_text());env['GOCACHE']='/tmp/apps-forgejo-release-repro-r1/gocache'
|
||||
reviewer=pathlib.Path('/tmp/forgejo-independent-r1')
|
||||
cmd=['bwrap','--unshare-net','--ro-bind','/usr','/usr','--ro-bind','/lib','/lib','--ro-bind','/lib64','/lib64','--symlink','usr/bin','/bin','--proc','/proc','--dev','/dev','--tmpfs','/tmp','--bind',str(work),str(work),'--bind',env['GOCACHE'],env['GOCACHE'],'--ro-bind',str(reviewer/'go'),str(reviewer/'go'),'--ro-bind',str(reviewer/'gomodcache'),str(reviewer/'gomodcache'),'--chdir',str(work/'source'),'--','env','-i']+[k+'='+v for k,v in env.items()]+args
|
||||
m.run(work,'normalized-embedded-and-static-backend',cmd,{'PATH':'/usr/bin:/bin','HOME':str(work/'home')})
|
||||
m.checked_inputs(work)
|
||||
m.save(work/'normalized-build.json',{'binary_sha256':m.digest(work/'source/gitea'),'predecessor_sha256':predecessor,'file_and_directory_times_normalized':True,'explicit_make_GOFLAGS':True,'source_locks_preserved':True,'refinement_same_trial':True,'owner_approval':False})
|
||||
print((work/'normalized-build.json').read_text())
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
{
|
||||
"schemaVersion": 2,
|
||||
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||
"config": {
|
||||
"mediaType": "application/vnd.oci.image.config.v1+json",
|
||||
"digest": "sha256:bc5a3df111729582faadebd01d126ba769e2b65d0c62f306f6bc79f4e562452b",
|
||||
"size": 7360
|
||||
},
|
||||
"layers": [
|
||||
{
|
||||
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
|
||||
"digest": "sha256:f18232174bc91741fdf3da96d85011092101a032a93a388b79e99e69c2d5c870",
|
||||
"size": 3642247
|
||||
},
|
||||
{
|
||||
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
|
||||
"digest": "sha256:ee6a63471a2d8c6f987ad8699af167dc045c893d6060441f6f7aa216edf48b5b",
|
||||
"size": 16935733
|
||||
},
|
||||
{
|
||||
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
|
||||
"digest": "sha256:6dea1fa90bf9ce85ca9119fea80966f08328f12d1a7f78e01f1ec22b7bdf71a7",
|
||||
"size": 907
|
||||
},
|
||||
{
|
||||
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
|
||||
"digest": "sha256:9308091ee87a5af192b263f4e48a2b74532fc6173959cf815880960595f35f6d",
|
||||
"size": 162
|
||||
},
|
||||
{
|
||||
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
|
||||
"digest": "sha256:b2912c13f9ccbba1aa1dde241cf7db994fb5622a110d875cf20f8b33246aebc8",
|
||||
"size": 170
|
||||
},
|
||||
{
|
||||
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
|
||||
"digest": "sha256:3448a9fe7124e7ce78e2ba1c320f6149ecec4e1daa7a043b88e9dd3b7681d5e3",
|
||||
"size": 2349
|
||||
},
|
||||
{
|
||||
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
|
||||
"digest": "sha256:07f673aaf066a1cec01ab6768992ee260e4794f693cb22585d2e7d7386387faf",
|
||||
"size": 169
|
||||
},
|
||||
{
|
||||
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
|
||||
"digest": "sha256:a3ee7de65d57741a38da7c9852b81ed9a57b848c68bc5b03d07b6c07a4802c20",
|
||||
"size": 45351336
|
||||
},
|
||||
{
|
||||
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
|
||||
"digest": "sha256:047a4c09ff1e32c905762257e4c9b58eef86fc7d126e44e45ced4d266612505e",
|
||||
"size": 154
|
||||
},
|
||||
{
|
||||
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
|
||||
"digest": "sha256:ac2d856a9d2af551ff928db4d466771d9de670c7affd3f22e20e2b4b9db5db6b",
|
||||
"size": 7036678
|
||||
},
|
||||
{
|
||||
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
|
||||
"digest": "sha256:b9fa35057fd93015c8ec114eb39686e071fdccce318be5eff55b2cae165a2ff0",
|
||||
"size": 550
|
||||
},
|
||||
{
|
||||
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
|
||||
"digest": "sha256:4f4fb700ef54461cfa02571ae0db9a0dc1e0cdb5577484a6d75e68dc38e8acc1",
|
||||
"size": 32
|
||||
}
|
||||
]
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,73 @@
|
|||
{
|
||||
"schemaVersion": 2,
|
||||
"mediaType": "application/vnd.oci.image.index.v1+json",
|
||||
"manifests": [
|
||||
{
|
||||
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||
"digest": "sha256:41649dff5311664097d29b4d80620403f0b27e7c0eee0b8af79b132a37f3807b",
|
||||
"size": 2563,
|
||||
"platform": {
|
||||
"architecture": "amd64",
|
||||
"os": "linux"
|
||||
}
|
||||
},
|
||||
{
|
||||
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||
"digest": "sha256:5b8afd5c19392bf83b0bf2761eb03c214b98851e6460d99597bf133f81408522",
|
||||
"size": 2563,
|
||||
"platform": {
|
||||
"architecture": "arm64",
|
||||
"os": "linux"
|
||||
}
|
||||
},
|
||||
{
|
||||
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||
"digest": "sha256:476c51f625f2e42b604132cc1dc5756fd2391754ffacc93a4a330ff5da8ca213",
|
||||
"size": 2563,
|
||||
"platform": {
|
||||
"architecture": "arm",
|
||||
"os": "linux",
|
||||
"variant": "v6"
|
||||
}
|
||||
},
|
||||
{
|
||||
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||
"digest": "sha256:b7c7a4727334c8a16db692e17c4d9f1344dd08ad745d3047ce1e29e975b0dbb5",
|
||||
"size": 566,
|
||||
"annotations": {
|
||||
"vnd.docker.reference.digest": "sha256:41649dff5311664097d29b4d80620403f0b27e7c0eee0b8af79b132a37f3807b",
|
||||
"vnd.docker.reference.type": "attestation-manifest"
|
||||
},
|
||||
"platform": {
|
||||
"architecture": "unknown",
|
||||
"os": "unknown"
|
||||
}
|
||||
},
|
||||
{
|
||||
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||
"digest": "sha256:e1709efc46fe4720bb26c37eea9ba21ebb02db826d0192a3c8f2dc717c116057",
|
||||
"size": 566,
|
||||
"annotations": {
|
||||
"vnd.docker.reference.digest": "sha256:5b8afd5c19392bf83b0bf2761eb03c214b98851e6460d99597bf133f81408522",
|
||||
"vnd.docker.reference.type": "attestation-manifest"
|
||||
},
|
||||
"platform": {
|
||||
"architecture": "unknown",
|
||||
"os": "unknown"
|
||||
}
|
||||
},
|
||||
{
|
||||
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||
"digest": "sha256:7d5556ea38cca9d296fba95aa43ccafe5d74c23252dad69369ae6f9d88fbd9e4",
|
||||
"size": 566,
|
||||
"annotations": {
|
||||
"vnd.docker.reference.digest": "sha256:476c51f625f2e42b604132cc1dc5756fd2391754ffacc93a4a330ff5da8ca213",
|
||||
"vnd.docker.reference.type": "attestation-manifest"
|
||||
},
|
||||
"platform": {
|
||||
"architecture": "unknown",
|
||||
"os": "unknown"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"go": "go version go1.24.3 linux/amd64",
|
||||
"node": "v22.15.0",
|
||||
"gcc": "gcc (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0",
|
||||
"make": "GNU Make 4.3",
|
||||
"npm": "10.9.2",
|
||||
"platform": "linux/amd64",
|
||||
"empty_HOME_and_closed_environment": true
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"failed_phase": "embedded extraction harness",
|
||||
"reason": "upstream CLI requires explicit file pattern; omitted glob",
|
||||
"source_binary_unchanged": true,
|
||||
"correction": "supply literal ** argv, with no shell expansion",
|
||||
"failed_log_and_result_retained": true,
|
||||
"same_trial": true
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"all_public_options_templates_exact": true,
|
||||
"architecture_approval": false,
|
||||
"binary_sha256": "4df429454b0289c6df76d7fd95d41394ebc286191809349b2c1e655b6be494e4",
|
||||
"embedded_file_manifest_sha256": "74a0ec278ab99ac86ee434d7620229dc4a70146efd51876a1e4738d2c9f4ffdb",
|
||||
"embedded_files": 2234,
|
||||
"native_admission": false,
|
||||
"network": "bwrap --unshare-net",
|
||||
"new_cli_flag_present": true,
|
||||
"no_dynamic_interpreter": true,
|
||||
"no_source_assets_libraries_or_host_config_mounted": true,
|
||||
"source_locks_preserved": true
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"argv": [
|
||||
"go",
|
||||
"mod",
|
||||
"verify"
|
||||
],
|
||||
"phase": "verify-go-modules",
|
||||
"returncode": 0,
|
||||
"seconds": 11.88
|
||||
}
|
||||
|
|
@ -0,0 +1,684 @@
|
|||
SHELL := /usr/bin/env bash
|
||||
.DEFAULT_GOAL := help
|
||||
|
||||
VERGABE_RELEASE ?= vergabe-teilnahme
|
||||
VERGABE_NAMESPACE ?= vergabe-teilnahme
|
||||
VERGABE_CHART ?= charts/vergabe-teilnahme
|
||||
VERGABE_VALUES ?= helm/vergabe-teilnahme-values.yaml
|
||||
VERGABE_IMAGE_REPOSITORY ?= forgejo.coulomb.social/coulomb/vergabe-teilnahme
|
||||
VERGABE_IMAGE_TAG ?=
|
||||
VERGABE_INGRESS ?= manifests/vergabe-teilnahme-ingress.yaml
|
||||
|
||||
VERGABE_DB_SECRET ?= vergabe-app-credentials
|
||||
VERGABE_ENV_SECRET ?= vergabe-teilnahme-env
|
||||
VERGABE_DB_USER ?= vergabe
|
||||
VERGABE_DB_HOST ?= apps-pg-rw.databases
|
||||
VERGABE_DB_PORT ?= 5432
|
||||
VERGABE_DB_NAME ?= vergabe_db
|
||||
|
||||
COULOMB_SOCIAL_RELEASE ?= coulomb-social
|
||||
COULOMB_SOCIAL_NAMESPACE ?= coulomb-social
|
||||
COULOMB_SOCIAL_CHART ?= charts/coulomb-social
|
||||
COULOMB_SOCIAL_VALUES ?= helm/coulomb-social-values.yaml
|
||||
COULOMB_SOCIAL_IMAGE_REPOSITORY ?= forgejo.coulomb.social/coulomb/coulomb-social
|
||||
COULOMB_SOCIAL_INGRESS ?= manifests/coulomb-social-ingress.yaml
|
||||
COULOMB_SOCIAL_ENV_SECRET ?= coulomb-social-env
|
||||
|
||||
INTER_HUB_RELEASE ?= inter-hub
|
||||
INTER_HUB_NAMESPACE ?= inter-hub
|
||||
INTER_HUB_CHART ?= charts/inter-hub
|
||||
INTER_HUB_VALUES ?= helm/inter-hub-values.yaml
|
||||
INTER_HUB_IMAGE_REPOSITORY ?= forgejo.coulomb.social/coulomb/inter-hub
|
||||
INTER_HUB_IMAGE_TAG ?=
|
||||
INTER_HUB_BASE_URL ?= https://hub.coulomb.social
|
||||
|
||||
REUSE_RELEASE ?= reuse
|
||||
REUSE_NAMESPACE ?= reuse
|
||||
REUSE_CHART ?= charts/reuse-surface
|
||||
REUSE_VALUES ?= helm/reuse-surface-values.yaml
|
||||
REUSE_URL ?= https://reuse.coulomb.social
|
||||
REUSE_CERTIFICATE ?= reuse-surface-tls
|
||||
|
||||
POLICY_NEXUS_RAPP ?= ../rapp-policy-nexus
|
||||
POLICY_NEXUS_BINDING ?= bindings/policy-nexus-production.json
|
||||
POLICY_NEXUS_TARGET ?= railiance01
|
||||
POLICY_NEXUS_IMAGE_REPOSITORY ?= $(shell python3 -c 'import json; print(json.load(open("$(POLICY_NEXUS_BINDING)"))["image_repository"])')
|
||||
POLICY_NEXUS_IMAGE_DIGEST ?= $(shell python3 -c 'import json; print(json.load(open("$(POLICY_NEXUS_BINDING)")).get("image_digest") or "")')
|
||||
POLICY_NEXUS_PUBLICATION_DIGEST ?= $(shell python3 -c 'import json; print(json.load(open("$(POLICY_NEXUS_BINDING)")).get("publication_manifest_digest") or "")')
|
||||
POLICY_NEXUS_SOURCE_INVENTORY_DIGEST ?= $(shell python3 -c 'import json; print(json.load(open("$(POLICY_NEXUS_BINDING)")).get("source_inventory_digest") or "")')
|
||||
POLICY_NEXUS_SOURCE_SET_DIGEST ?= $(shell python3 -c 'import json; print(json.load(open("$(POLICY_NEXUS_BINDING)")).get("source_set_digest") or "")')
|
||||
POLICY_NEXUS_REVISION ?=
|
||||
|
||||
COULOMBCORE_KUBECONFIG ?= $(HOME)/.kube/config
|
||||
RAILIANCE01_KUBECONFIG ?= $(HOME)/.kube/config-railiance01
|
||||
PRODUCTION_KUBECONFIG ?= $(COULOMBCORE_KUBECONFIG)
|
||||
INTER_HUB_KUBECONFIG ?= $(RAILIANCE01_KUBECONFIG)
|
||||
REUSE_KUBECONFIG ?= $(RAILIANCE01_KUBECONFIG)
|
||||
FORGEJO_KUBECONFIG ?= $(RAILIANCE01_KUBECONFIG)
|
||||
FORGEJO_RELEASE ?= forgejo
|
||||
FORGEJO_NAMESPACE ?= forgejo
|
||||
FORGEJO_CHART ?= gitea-charts/gitea
|
||||
# Forgejo 11.x (Gitea 1.22) lacks `gitea config edit-ini`; chart 12.6+ requires Gitea 1.26+.
|
||||
FORGEJO_CHART_VERSION ?= 12.5.0
|
||||
FORGEJO_VALUES ?= helm/forgejo-values.yaml
|
||||
FORGEJO_REGISTRY_VALUES ?= helm/forgejo-registry-values.yaml
|
||||
FORGEJO_SECRETS ?= helm/forgejo-secrets.sops.yaml
|
||||
FORGEJO_SECRET_SCAN_TEMPLATE ?= manifests/forgejo-secret-scan-template.yaml
|
||||
FORGEJO_INGRESS ?= manifests/forgejo-ingress.yaml
|
||||
FORGEJO_SSH_NODEPORT ?= manifests/forgejo-ssh-nodeport.yaml
|
||||
FORGEJO_BASE_URL ?= https://forgejo.coulomb.social
|
||||
FORGEJO_DB_CLUSTER ?= forgejo-db
|
||||
FORGEJO_DB_NAMESPACE ?= databases
|
||||
FORGEJO_RUNNER_MANIFEST ?= manifests/forgejo-runner.yaml
|
||||
FORGEJO_RUNNER_REGISTRATION_SECRET ?= helm/forgejo-runner-registration.sops.yaml
|
||||
FORGEJO_MAILER_EXTERNAL_SECRET ?= manifests/forgejo-mailer-externalsecret.yaml
|
||||
REUSE_RUNTIME_EXTERNAL_SECRET ?= manifests/reuse-surface-runtime-externalsecret.yaml
|
||||
PLATFORM_REPO ?= $(HOME)/railiance-platform
|
||||
ESO_CHART_VERSION ?= 0.16.1
|
||||
INTER_HUB_IMAGE_REF = $(INTER_HUB_IMAGE_REPOSITORY):$(INTER_HUB_IMAGE_TAG)
|
||||
INTER_HUB_IMAGE_SET_ARG = $(if $(strip $(INTER_HUB_IMAGE_TAG)),--set image.tag=$(INTER_HUB_IMAGE_TAG),)
|
||||
|
||||
CORE_HUB_RELEASE ?= core-hub
|
||||
CORE_HUB_NAMESPACE ?= core-hub
|
||||
CORE_HUB_CHART ?= charts/core-hub
|
||||
CORE_HUB_VALUES ?= helm/core-hub-values.yaml
|
||||
CORE_HUB_STAGING_NAMESPACE ?= core-hub-staging
|
||||
CORE_HUB_STAGING_VALUES ?= helm/core-hub-staging-values.yaml
|
||||
CORE_HUB_IMAGE_REPOSITORY ?= forgejo.coulomb.social/coulomb/core-hub
|
||||
CORE_HUB_IMAGE_TAG ?=
|
||||
CORE_HUB_BASE_URL ?= https://hub.coulomb.social
|
||||
CORE_HUB_KUBECONFIG ?= $(PRODUCTION_KUBECONFIG)
|
||||
VERGABE_KUBECONFIG ?= $(PRODUCTION_KUBECONFIG)
|
||||
VERGABE_IMAGE_REF = $(VERGABE_IMAGE_REPOSITORY):$(VERGABE_IMAGE_TAG)
|
||||
VERGABE_IMAGE_SET_ARG = $(if $(strip $(VERGABE_IMAGE_TAG)),--set image.repository=$(VERGABE_IMAGE_REPOSITORY) --set image.tag=$(VERGABE_IMAGE_TAG),)
|
||||
CORE_HUB_IMAGE_REF = $(CORE_HUB_IMAGE_REPOSITORY):$(CORE_HUB_IMAGE_TAG)
|
||||
CORE_HUB_IMAGE_SET_ARG = $(if $(strip $(CORE_HUB_IMAGE_TAG)),--set image.tag=$(CORE_HUB_IMAGE_TAG),)
|
||||
|
||||
SOPS_SENTINEL ?= $(FORGEJO_SECRETS)
|
||||
DRY_RUN_CREATE_NAMESPACES ?= false
|
||||
|
||||
##@ Operator checks
|
||||
|
||||
check-tools: ## Check required operator tools and warn about optional diagnostics
|
||||
tools/check-tools.sh
|
||||
|
||||
check-sops: ## Verify the local SOPS age key can decrypt SOPS_SENTINEL
|
||||
SOPS_SENTINEL="$(SOPS_SENTINEL)" tools/check-sops.sh
|
||||
|
||||
k8s-server-dry-run: ## Server-side dry-run rendered Helm and committed manifests
|
||||
DRY_RUN_CREATE_NAMESPACES=$(DRY_RUN_CREATE_NAMESPACES) tools/k8s-server-dry-run.sh
|
||||
|
||||
apps-pg-status: ## Check the shared apps-pg cnpg cluster
|
||||
@if kubectl cnpg status apps-pg -n databases >/dev/null 2>&1; then \
|
||||
kubectl cnpg status apps-pg -n databases; \
|
||||
else \
|
||||
echo "kubectl cnpg plugin not available; falling back to cnpg resources"; \
|
||||
kubectl get cluster apps-pg -n databases; \
|
||||
kubectl get pods -n databases -l cnpg.io/cluster=apps-pg; \
|
||||
fi
|
||||
|
||||
check-railiance01-kubeconfig: ## Verify Railiance01 production kubeconfig exists
|
||||
@test -r "$(RAILIANCE01_KUBECONFIG)" || { \
|
||||
echo "Missing Railiance01 kubeconfig: $(RAILIANCE01_KUBECONFIG)" >&2; \
|
||||
echo "Restore it with:" >&2; \
|
||||
echo " ssh tegwick@92.205.62.239 'sudo cat /etc/rancher/k3s/k3s.yaml' | sed 's|127.0.0.1|92.205.62.239|' > $(RAILIANCE01_KUBECONFIG)" >&2; \
|
||||
exit 1; \
|
||||
}
|
||||
|
||||
check-coulombcore-kubeconfig: ## Verify CoulombCore kubeconfig and k3s API tunnel
|
||||
@test -r "$(COULOMBCORE_KUBECONFIG)" || { \
|
||||
echo "Missing CoulombCore kubeconfig: $(COULOMBCORE_KUBECONFIG)" >&2; \
|
||||
echo "Ensure ops-bridge tunnel is up: bridge up k3s-api-coulombcore" >&2; \
|
||||
exit 1; \
|
||||
}
|
||||
@KUBECONFIG="$(COULOMBCORE_KUBECONFIG)" kubectl get --raw=/readyz >/dev/null 2>&1 || { \
|
||||
echo "CoulombCore API unreachable via $(COULOMBCORE_KUBECONFIG)." >&2; \
|
||||
echo "Run: bridge up k3s-api-coulombcore" >&2; \
|
||||
exit 1; \
|
||||
}
|
||||
|
||||
check-production-kubeconfig: check-coulombcore-kubeconfig ## Verify production app cluster API (CoulombCore)
|
||||
|
||||
check-inter-hub-image-tag: ## Require an explicit inter-hub image tag for production deploys
|
||||
@test -n "$(INTER_HUB_IMAGE_TAG)" || { \
|
||||
echo "Set INTER_HUB_IMAGE_TAG=<sha> for inter-hub production deploys." >&2; \
|
||||
echo "Example: INTER_HUB_IMAGE_TAG=91037a4 make inter-hub-deploy" >&2; \
|
||||
exit 1; \
|
||||
}
|
||||
|
||||
check-inter-hub-image: check-inter-hub-image-tag ## Verify the inter-hub OCI image tag exists before deploy
|
||||
tools/check-oci-image.sh "$(INTER_HUB_IMAGE_REF)"
|
||||
|
||||
check-core-hub-image-tag: ## Require an explicit core-hub image tag for production deploys
|
||||
@test -n "$(CORE_HUB_IMAGE_TAG)" || { \
|
||||
echo "Set CORE_HUB_IMAGE_TAG=<sha> for core-hub production deploys." >&2; \
|
||||
echo "Example: CORE_HUB_IMAGE_TAG=59bf1e4 make core-hub-deploy" >&2; \
|
||||
exit 1; \
|
||||
}
|
||||
|
||||
check-core-hub-image: check-core-hub-image-tag ## Verify the core-hub OCI image tag exists before deploy
|
||||
tools/check-oci-image.sh "$(CORE_HUB_IMAGE_REF)"
|
||||
|
||||
cnpg-backup-status: check-production-kubeconfig ## Report Option A / ScheduledBackup coverage
|
||||
KUBECONFIG="$(PRODUCTION_KUBECONFIG)" tools/cnpg-backup-status.sh
|
||||
|
||||
cnpg-backup-offsite-secret-apply: check-production-kubeconfig ## Materialize databases/cnpg-backup-offsite from OpenBao (T02)
|
||||
chmod +x tools/cnpg-backup-offsite-secret-apply.sh
|
||||
KUBECONFIG="$(PRODUCTION_KUBECONFIG)" tools/cnpg-backup-offsite-secret-apply.sh
|
||||
|
||||
cnpg-backup-offsite-secret-dry-run: check-production-kubeconfig ## Dry-run Secret materialization (no apply)
|
||||
chmod +x tools/cnpg-backup-offsite-secret-apply.sh
|
||||
KUBECONFIG="$(PRODUCTION_KUBECONFIG)" DRY_RUN=1 tools/cnpg-backup-offsite-secret-apply.sh
|
||||
|
||||
cnpg-option-a-apply: check-production-kubeconfig ## Apply Option A CronJobs + RBAC (production-touching)
|
||||
KUBECONFIG="$(PRODUCTION_KUBECONFIG)" kubectl apply -f manifests/cnpg-option-a-backup.yaml
|
||||
|
||||
cnpg-option-a-dry-run: check-production-kubeconfig ## Server-side dry-run of Option A manifests
|
||||
KUBECONFIG="$(PRODUCTION_KUBECONFIG)" kubectl apply --dry-run=server -f manifests/cnpg-option-a-backup.yaml
|
||||
|
||||
cnpg-logical-backup-dry-run: check-production-kubeconfig ## Logical dump + age for all four clusters (no upload)
|
||||
chmod +x tools/cnpg-logical-backup.sh
|
||||
KUBECONFIG="$(PRODUCTION_KUBECONFIG)" PLATFORM_REPO="$(PLATFORM_REPO)" RAILIANCE_BACKUP_DRY_RUN=1 \
|
||||
tools/cnpg-logical-backup.sh
|
||||
|
||||
cnpg-logical-backup: check-production-kubeconfig ## Logical dump + age + Nextcloud for all four clusters
|
||||
chmod +x tools/cnpg-logical-backup.sh
|
||||
@KUBECONFIG="$(PRODUCTION_KUBECONFIG)" PLATFORM_REPO="$(PLATFORM_REPO)" bao kv metadata get \
|
||||
platform/workloads/railiance/backup/offsite-lane >/dev/null 2>&1 || { \
|
||||
echo "ERROR: OpenBao backup-lane auth missing." >&2; \
|
||||
echo "Run: bao login -method=oidc -path=netkingdom role=railiance-backup-workload-kv-read" >&2; \
|
||||
exit 1; \
|
||||
}
|
||||
KUBECONFIG="$(PRODUCTION_KUBECONFIG)" PLATFORM_REPO="$(PLATFORM_REPO)" RAILIANCE_BACKUP_DRY_RUN=0 \
|
||||
tools/cnpg-logical-backup.sh
|
||||
|
||||
##@ Option A multi-host (platform CLI / activity-core)
|
||||
|
||||
cnpg-option-a-backup-dry-run: ## Platform multi-host Option A dump+age (JSON, no upload)
|
||||
chmod +x "$(PLATFORM_REPO)/tools/cmd/cnpg-option-a-backup"
|
||||
KUBECONFIG_CORE="$(COULOMBCORE_KUBECONFIG)" KUBECONFIG_R01="$(RAILIANCE01_KUBECONFIG)" \
|
||||
RAILIANCE_BACKUP_DRY_RUN=1 "$(PLATFORM_REPO)/tools/cmd/cnpg-option-a-backup"
|
||||
|
||||
cnpg-option-a-backup: ## Platform multi-host Option A dump+age+upload (JSON)
|
||||
chmod +x "$(PLATFORM_REPO)/tools/cmd/cnpg-option-a-backup"
|
||||
@bao kv metadata get platform/workloads/railiance/backup/offsite-lane >/dev/null 2>&1 || { \
|
||||
echo "ERROR: OpenBao backup-lane auth missing or set RAILIANCE_BACKUP_NC_TOKEN." >&2; \
|
||||
exit 1; \
|
||||
}
|
||||
KUBECONFIG_CORE="$(COULOMBCORE_KUBECONFIG)" KUBECONFIG_R01="$(RAILIANCE01_KUBECONFIG)" \
|
||||
RAILIANCE_BACKUP_DRY_RUN=0 "$(PLATFORM_REPO)/tools/cmd/cnpg-option-a-backup"
|
||||
|
||||
apps-pg-backup-dry-run: check-production-kubeconfig ## Logical pg_dump + age encrypt for vergabe_db (no upload)
|
||||
chmod +x tools/apps-pg-backup-dry-run.sh
|
||||
KUBECONFIG="$(PRODUCTION_KUBECONFIG)" PLATFORM_REPO="$(PLATFORM_REPO)" RAILIANCE_BACKUP_DRY_RUN=1 \
|
||||
tools/apps-pg-backup-dry-run.sh
|
||||
|
||||
apps-pg-restore-drill: check-production-kubeconfig ## Isolated vergabe_db restore drill with row-count gate
|
||||
chmod +x tools/apps-pg-restore-drill.sh
|
||||
KUBECONFIG="$(PRODUCTION_KUBECONFIG)" tools/apps-pg-restore-drill.sh
|
||||
|
||||
apps-pg-backup: check-production-kubeconfig ## Logical pg_dump + age encrypt + Nextcloud upload for vergabe_db
|
||||
chmod +x tools/apps-pg-backup-dry-run.sh
|
||||
@KUBECONFIG="$(PRODUCTION_KUBECONFIG)" PLATFORM_REPO="$(PLATFORM_REPO)" bao kv metadata get \
|
||||
platform/workloads/railiance/backup/offsite-lane >/dev/null 2>&1 || { \
|
||||
echo "ERROR: OpenBao backup-lane auth missing." >&2; \
|
||||
echo "Run: bao login -method=oidc -path=netkingdom role=railiance-backup-workload-kv-read" >&2; \
|
||||
exit 1; \
|
||||
}
|
||||
KUBECONFIG="$(PRODUCTION_KUBECONFIG)" PLATFORM_REPO="$(PLATFORM_REPO)" RAILIANCE_BACKUP_DRY_RUN=0 \
|
||||
tools/apps-pg-backup-dry-run.sh
|
||||
|
||||
##@ Vergabe Teilnahme
|
||||
|
||||
check-vergabe-image-tag: ## Require an explicit vergabe-teilnahme image tag for production deploys
|
||||
@test -n "$(VERGABE_IMAGE_TAG)" || { \
|
||||
echo "Set VERGABE_IMAGE_TAG=<sha> for vergabe production deploys." >&2; \
|
||||
echo "Example: VERGABE_IMAGE_TAG=1061f3e make vergabe-deploy" >&2; \
|
||||
exit 1; \
|
||||
}
|
||||
|
||||
check-vergabe-image: check-vergabe-image-tag ## Verify the vergabe OCI image exists on Forgejo before deploy
|
||||
tools/check-oci-image.sh "$(VERGABE_IMAGE_REF)"
|
||||
|
||||
vergabe-dry-run: check-vergabe-image-tag ## helm template render (no apply) for inspection
|
||||
helm template $(VERGABE_RELEASE) $(VERGABE_CHART) \
|
||||
--namespace $(VERGABE_NAMESPACE) \
|
||||
-f $(VERGABE_VALUES) $(VERGABE_IMAGE_SET_ARG)
|
||||
|
||||
vergabe-deploy: check-production-kubeconfig check-vergabe-image ## Deploy / upgrade vergabe-teilnahme Helm release
|
||||
KUBECONFIG="$(VERGABE_KUBECONFIG)" helm upgrade --install $(VERGABE_RELEASE) $(VERGABE_CHART) \
|
||||
--namespace $(VERGABE_NAMESPACE) --create-namespace \
|
||||
-f $(VERGABE_VALUES) $(VERGABE_IMAGE_SET_ARG) --wait --timeout 3m
|
||||
|
||||
vergabe-ingress-deploy: ## Apply the vergabe-teilnahme ingress (whywhynot.de)
|
||||
kubectl apply -f $(VERGABE_INGRESS)
|
||||
|
||||
vergabe-status: check-production-kubeconfig ## Show vergabe-teilnahme pod / svc / ingress / cert state
|
||||
KUBECONFIG="$(VERGABE_KUBECONFIG)" kubectl get pods,svc,ingress,certificate -n $(VERGABE_NAMESPACE) -l app.kubernetes.io/instance=$(VERGABE_RELEASE) --ignore-not-found
|
||||
|
||||
vergabe-migrate: ## Run Django migrations against the live deployment
|
||||
kubectl exec -n $(VERGABE_NAMESPACE) deploy/$(VERGABE_RELEASE) -- python manage.py migrate --noinput
|
||||
|
||||
vergabe-seed: ## Run the idempotent seed command
|
||||
kubectl exec -n $(VERGABE_NAMESPACE) deploy/$(VERGABE_RELEASE) -- python manage.py seed_dev
|
||||
|
||||
vergabe-superuser: ## Open an interactive shell for createsuperuser
|
||||
kubectl exec -it -n $(VERGABE_NAMESPACE) deploy/$(VERGABE_RELEASE) -- python manage.py createsuperuser
|
||||
|
||||
vergabe-logs: ## Tail vergabe-teilnahme app logs
|
||||
kubectl logs -n $(VERGABE_NAMESPACE) -l app.kubernetes.io/instance=$(VERGABE_RELEASE) -f --tail=50
|
||||
|
||||
vergabe-db-url-secret: ## Rebuild DATABASE_URL with a URL-encoded cnpg password
|
||||
APP_NAMESPACE=$(VERGABE_NAMESPACE) \
|
||||
APP_ENV_SECRET=$(VERGABE_ENV_SECRET) \
|
||||
APP_DB_SECRET=$(VERGABE_DB_SECRET) \
|
||||
APP_DB_USER=$(VERGABE_DB_USER) \
|
||||
APP_DB_HOST=$(VERGABE_DB_HOST) \
|
||||
APP_DB_PORT=$(VERGABE_DB_PORT) \
|
||||
APP_DB_NAME=$(VERGABE_DB_NAME) \
|
||||
tools/build-database-url-secret.sh
|
||||
|
||||
|
||||
|
||||
check-coulomb-social-image-tag: ## Require an explicit coulomb-social image tag
|
||||
@test -n "$(COULOMB_SOCIAL_IMAGE_TAG)" || ( \
|
||||
echo "Set COULOMB_SOCIAL_IMAGE_TAG=<sha> for coulomb-social deploys." >&2; \
|
||||
echo "Example: COULOMB_SOCIAL_IMAGE_TAG=deadbeef make coulomb-social-deploy" >&2; \
|
||||
exit 1)
|
||||
|
||||
coulomb-social-dry-run: check-coulomb-social-image-tag ## helm template render for coulomb-social
|
||||
helm template $(COULOMB_SOCIAL_RELEASE) $(COULOMB_SOCIAL_CHART) \
|
||||
-f $(COULOMB_SOCIAL_VALUES) \
|
||||
--set-string image.tag=$(COULOMB_SOCIAL_IMAGE_TAG) \
|
||||
--namespace $(COULOMB_SOCIAL_NAMESPACE)
|
||||
|
||||
coulomb-social-deploy: check-production-kubeconfig check-coulomb-social-image-tag ## Deploy / upgrade coulomb-social
|
||||
kubectl create namespace $(COULOMB_SOCIAL_NAMESPACE) --dry-run=client -o yaml | kubectl apply -f -
|
||||
helm upgrade --install $(COULOMB_SOCIAL_RELEASE) $(COULOMB_SOCIAL_CHART) \
|
||||
-f $(COULOMB_SOCIAL_VALUES) \
|
||||
--set-string image.tag=$(COULOMB_SOCIAL_IMAGE_TAG) \
|
||||
--namespace $(COULOMB_SOCIAL_NAMESPACE) \
|
||||
--wait --timeout 5m
|
||||
|
||||
coulomb-social-ingress-deploy: ## Apply coulomb-social ingress
|
||||
kubectl apply -f $(COULOMB_SOCIAL_INGRESS)
|
||||
|
||||
coulomb-social-status: check-production-kubeconfig ## Show coulomb-social pod/svc/ingress
|
||||
kubectl -n $(COULOMB_SOCIAL_NAMESPACE) get deploy,svc,ingress,pods
|
||||
|
||||
coulomb-social-logs: ## Tail coulomb-social app logs
|
||||
kubectl -n $(COULOMB_SOCIAL_NAMESPACE) logs -l app.kubernetes.io/name=coulomb-social -f --tail=200
|
||||
|
||||
|
||||
coulomb-social-env-secret: ## Create/patch coulomb-social-env (SECRET_KEY, DATABASE_URL, USER_ENGINE_PROXY_SECRET)
|
||||
@bash tools/create-coulomb-social-env-secret.sh $(COULOMB_SOCIAL_ENV_SECRET_ARGS)
|
||||
|
||||
coulomb-social-env-secret-dry-run: ## Plan coulomb-social-env keys without applying
|
||||
@bash tools/create-coulomb-social-env-secret.sh --dry-run $(COULOMB_SOCIAL_ENV_SECRET_ARGS)
|
||||
##@ Inter-Hub
|
||||
|
||||
inter-hub-render-baseline: ## Render checked-in inter-hub values for chart validation only
|
||||
helm template $(INTER_HUB_RELEASE) $(INTER_HUB_CHART) \
|
||||
--namespace $(INTER_HUB_NAMESPACE) \
|
||||
-f $(INTER_HUB_VALUES)
|
||||
|
||||
inter-hub-dry-run: check-railiance01-kubeconfig check-inter-hub-image-tag ## helm template render with an explicit production image tag
|
||||
KUBECONFIG="$(INTER_HUB_KUBECONFIG)" helm template $(INTER_HUB_RELEASE) $(INTER_HUB_CHART) \
|
||||
--namespace $(INTER_HUB_NAMESPACE) \
|
||||
-f $(INTER_HUB_VALUES) $(INTER_HUB_IMAGE_SET_ARG)
|
||||
|
||||
inter-hub-server-dry-run: check-railiance01-kubeconfig check-inter-hub-image ## Helm server dry-run inter-hub upgrade on Railiance01
|
||||
KUBECONFIG="$(INTER_HUB_KUBECONFIG)" helm upgrade --install $(INTER_HUB_RELEASE) $(INTER_HUB_CHART) \
|
||||
--namespace $(INTER_HUB_NAMESPACE) --create-namespace \
|
||||
-f $(INTER_HUB_VALUES) $(INTER_HUB_IMAGE_SET_ARG) --dry-run=server --timeout 5m
|
||||
|
||||
inter-hub-deploy: check-railiance01-kubeconfig check-inter-hub-image ## Deploy / upgrade inter-hub Helm release on Railiance01
|
||||
KUBECONFIG="$(INTER_HUB_KUBECONFIG)" helm upgrade --install $(INTER_HUB_RELEASE) $(INTER_HUB_CHART) \
|
||||
--namespace $(INTER_HUB_NAMESPACE) --create-namespace \
|
||||
-f $(INTER_HUB_VALUES) $(INTER_HUB_IMAGE_SET_ARG) --wait --timeout 5m
|
||||
|
||||
inter-hub-status: check-railiance01-kubeconfig ## Show inter-hub pod / svc / ingress / cert state on Railiance01
|
||||
KUBECONFIG="$(INTER_HUB_KUBECONFIG)" kubectl get pods,svc,ingress,certificate -n $(INTER_HUB_NAMESPACE) -l app=$(INTER_HUB_RELEASE) --ignore-not-found
|
||||
|
||||
inter-hub-release-info: check-railiance01-kubeconfig ## Show inter-hub Helm history, values, deployment, and pods
|
||||
KUBECONFIG="$(INTER_HUB_KUBECONFIG)" helm history $(INTER_HUB_RELEASE) -n $(INTER_HUB_NAMESPACE)
|
||||
KUBECONFIG="$(INTER_HUB_KUBECONFIG)" helm get values $(INTER_HUB_RELEASE) -n $(INTER_HUB_NAMESPACE)
|
||||
KUBECONFIG="$(INTER_HUB_KUBECONFIG)" kubectl describe deploy/$(INTER_HUB_RELEASE) -n $(INTER_HUB_NAMESPACE)
|
||||
KUBECONFIG="$(INTER_HUB_KUBECONFIG)" kubectl get pods -n $(INTER_HUB_NAMESPACE) -o wide
|
||||
|
||||
inter-hub-smoke: ## Verify public inter-hub v2 route and OpenAPI surface after rollout
|
||||
INTER_HUB_BASE_URL="$(INTER_HUB_BASE_URL)" tools/inter-hub-smoke.sh
|
||||
|
||||
inter-hub-logs: check-railiance01-kubeconfig ## Tail inter-hub app logs from Railiance01
|
||||
KUBECONFIG="$(INTER_HUB_KUBECONFIG)" kubectl logs -n $(INTER_HUB_NAMESPACE) -l app=$(INTER_HUB_RELEASE) -f --tail=50
|
||||
|
||||
##@ Core Hub
|
||||
|
||||
core-hub-helm-adopt: check-production-kubeconfig ## Annotate live Core Hub resources for Helm ownership
|
||||
chmod +x tools/core-hub-helm-adopt.sh
|
||||
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" CORE_HUB_NAMESPACE="$(CORE_HUB_NAMESPACE)" CORE_HUB_RELEASE="$(CORE_HUB_RELEASE)" \
|
||||
tools/core-hub-helm-adopt.sh
|
||||
|
||||
core-hub-render-baseline: ## Render checked-in core-hub values for chart validation only
|
||||
helm template $(CORE_HUB_RELEASE) $(CORE_HUB_CHART) \
|
||||
--namespace $(CORE_HUB_NAMESPACE) \
|
||||
-f $(CORE_HUB_VALUES)
|
||||
|
||||
core-hub-dry-run: check-production-kubeconfig check-core-hub-image-tag ## helm template render with an explicit production image tag
|
||||
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" helm template $(CORE_HUB_RELEASE) $(CORE_HUB_CHART) \
|
||||
--namespace $(CORE_HUB_NAMESPACE) \
|
||||
-f $(CORE_HUB_VALUES) $(CORE_HUB_IMAGE_SET_ARG)
|
||||
|
||||
core-hub-server-dry-run: check-production-kubeconfig check-core-hub-image ## Helm server dry-run core-hub upgrade on CoulombCore
|
||||
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" helm upgrade --install $(CORE_HUB_RELEASE) $(CORE_HUB_CHART) \
|
||||
--namespace $(CORE_HUB_NAMESPACE) --create-namespace \
|
||||
-f $(CORE_HUB_VALUES) $(CORE_HUB_IMAGE_SET_ARG) --dry-run=server --timeout 5m
|
||||
|
||||
core-hub-deploy: check-production-kubeconfig check-core-hub-image ## Deploy / upgrade core-hub Helm release on CoulombCore
|
||||
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" helm upgrade --install $(CORE_HUB_RELEASE) $(CORE_HUB_CHART) \
|
||||
--namespace $(CORE_HUB_NAMESPACE) --create-namespace \
|
||||
-f $(CORE_HUB_VALUES) $(CORE_HUB_IMAGE_SET_ARG) --wait --timeout 5m
|
||||
|
||||
core-hub-migrate: check-production-kubeconfig check-core-hub-image ## Run Alembic migrations via a one-off Job
|
||||
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" kubectl -n $(CORE_HUB_NAMESPACE) delete job $(CORE_HUB_RELEASE)-prod-migrate --ignore-not-found
|
||||
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" helm template $(CORE_HUB_RELEASE) $(CORE_HUB_CHART) \
|
||||
--namespace $(CORE_HUB_NAMESPACE) \
|
||||
-f $(CORE_HUB_VALUES) $(CORE_HUB_IMAGE_SET_ARG) \
|
||||
--set migration.enabled=true | KUBECONFIG="$(CORE_HUB_KUBECONFIG)" kubectl apply -f -
|
||||
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" kubectl -n $(CORE_HUB_NAMESPACE) wait --for=condition=complete \
|
||||
job/$(CORE_HUB_RELEASE)-prod-migrate --timeout=180s
|
||||
|
||||
core-hub-status: check-production-kubeconfig ## Show core-hub pod / svc / ingress / cert state on CoulombCore
|
||||
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" kubectl get pods,svc,ingress,certificate -n $(CORE_HUB_NAMESPACE) \
|
||||
-l app.kubernetes.io/name=core-hub --ignore-not-found
|
||||
|
||||
core-hub-release-info: check-production-kubeconfig ## Show core-hub Helm history, values, deployment, and pods
|
||||
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" helm history $(CORE_HUB_RELEASE) -n $(CORE_HUB_NAMESPACE)
|
||||
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" helm get values $(CORE_HUB_RELEASE) -n $(CORE_HUB_NAMESPACE)
|
||||
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" kubectl describe deploy/$(CORE_HUB_RELEASE)-api -n $(CORE_HUB_NAMESPACE)
|
||||
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" kubectl get pods -n $(CORE_HUB_NAMESPACE) -o wide
|
||||
|
||||
core-hub-smoke: ## Verify public Core Hub health and v2 API surface after rollout
|
||||
CORE_HUB_BASE_URL="$(CORE_HUB_BASE_URL)" tools/core-hub-smoke.sh
|
||||
|
||||
core-hub-logs: check-production-kubeconfig ## Tail core-hub app logs from CoulombCore
|
||||
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" kubectl logs -n $(CORE_HUB_NAMESPACE) \
|
||||
-l app.kubernetes.io/name=core-hub,app.kubernetes.io/component=api -f --tail=50
|
||||
|
||||
core-hub-staging-dry-run: check-production-kubeconfig check-core-hub-image-tag ## helm template render for core-hub staging
|
||||
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" helm template $(CORE_HUB_RELEASE) $(CORE_HUB_CHART) \
|
||||
--namespace $(CORE_HUB_STAGING_NAMESPACE) \
|
||||
-f $(CORE_HUB_STAGING_VALUES) $(CORE_HUB_IMAGE_SET_ARG)
|
||||
|
||||
core-hub-staging-deploy: check-production-kubeconfig check-core-hub-image ## Deploy / upgrade core-hub staging release
|
||||
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" helm upgrade --install $(CORE_HUB_RELEASE) $(CORE_HUB_CHART) \
|
||||
--namespace $(CORE_HUB_STAGING_NAMESPACE) --create-namespace \
|
||||
-f $(CORE_HUB_STAGING_VALUES) $(CORE_HUB_IMAGE_SET_ARG) --wait --timeout 5m
|
||||
|
||||
core-hub-staging-migrate: check-production-kubeconfig check-core-hub-image ## Run Alembic migrations for core-hub staging
|
||||
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" kubectl -n $(CORE_HUB_STAGING_NAMESPACE) delete job $(CORE_HUB_RELEASE)-staging-migrate --ignore-not-found
|
||||
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" helm template $(CORE_HUB_RELEASE) $(CORE_HUB_CHART) \
|
||||
--namespace $(CORE_HUB_STAGING_NAMESPACE) \
|
||||
-f $(CORE_HUB_STAGING_VALUES) $(CORE_HUB_IMAGE_SET_ARG) \
|
||||
--set migration.enabled=true | KUBECONFIG="$(CORE_HUB_KUBECONFIG)" kubectl apply -f -
|
||||
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" kubectl -n $(CORE_HUB_STAGING_NAMESPACE) wait --for=condition=complete \
|
||||
job/$(CORE_HUB_RELEASE)-staging-migrate --timeout=180s
|
||||
|
||||
core-hub-staging-status: check-production-kubeconfig ## Show core-hub staging pod / svc state
|
||||
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" kubectl get pods,svc -n $(CORE_HUB_STAGING_NAMESPACE) \
|
||||
-l app.kubernetes.io/name=core-hub --ignore-not-found
|
||||
|
||||
##@ Forgejo (forgejo.coulomb.social)
|
||||
|
||||
eso-deploy: check-railiance01-kubeconfig ## Install External Secrets Operator on railiance01
|
||||
KUBECONFIG="$(FORGEJO_KUBECONFIG)" helm repo add external-secrets https://charts.external-secrets.io --force-update
|
||||
KUBECONFIG="$(FORGEJO_KUBECONFIG)" helm upgrade --install external-secrets external-secrets/external-secrets \
|
||||
--version $(ESO_CHART_VERSION) \
|
||||
--namespace external-secrets --create-namespace \
|
||||
--set installCRDs=true \
|
||||
--set serviceAccount.create=true \
|
||||
--set serviceAccount.name=external-secrets \
|
||||
--wait --timeout 5m
|
||||
|
||||
forgejo-openbao-eso-token-apply: ## Mint coulombcore OpenBao ESO token + Secret on railiance01 (interactive)
|
||||
tools/forgejo-openbao-eso-token-apply.sh
|
||||
|
||||
forgejo-openbao-store-deploy: check-railiance01-kubeconfig eso-deploy ## Apply openbao-forgejo ClusterSecretStore
|
||||
test -d "$(PLATFORM_REPO)/argocd/platform-addons/openbao-secretstore"
|
||||
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl apply -f "$(PLATFORM_REPO)/argocd/platform-addons/openbao-secretstore/openbao-forgejo.clustersecretstore.yaml"
|
||||
|
||||
forgejo-mailer-es-deploy: check-railiance01-kubeconfig forgejo-openbao-store-deploy ## Apply Forgejo mailer ExternalSecret
|
||||
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl apply -f $(FORGEJO_MAILER_EXTERNAL_SECRET)
|
||||
|
||||
forgejo-mailer-es-status: check-railiance01-kubeconfig ## Show Forgejo mailer ExternalSecret sync status
|
||||
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl get externalsecret,secret -n $(FORGEJO_NAMESPACE) forgejo-mailer
|
||||
|
||||
forgejo-dry-run: check-railiance01-kubeconfig ## helm template render for Forgejo
|
||||
KUBECONFIG="$(FORGEJO_KUBECONFIG)" helm template $(FORGEJO_RELEASE) $(FORGEJO_CHART) \
|
||||
--version $(FORGEJO_CHART_VERSION) \
|
||||
--namespace $(FORGEJO_NAMESPACE) \
|
||||
-f $(FORGEJO_VALUES) \
|
||||
-f $(FORGEJO_REGISTRY_VALUES) \
|
||||
-f <(KUBECONFIG="$(FORGEJO_KUBECONFIG)" tools/forgejo-helm-secrets.sh)
|
||||
|
||||
forgejo-server-dry-run: check-railiance01-kubeconfig ## Helm server dry-run Forgejo upgrade
|
||||
KUBECONFIG="$(FORGEJO_KUBECONFIG)" helm upgrade --install $(FORGEJO_RELEASE) $(FORGEJO_CHART) \
|
||||
--version $(FORGEJO_CHART_VERSION) \
|
||||
--namespace $(FORGEJO_NAMESPACE) --create-namespace \
|
||||
-f $(FORGEJO_VALUES) \
|
||||
-f $(FORGEJO_REGISTRY_VALUES) \
|
||||
-f <(KUBECONFIG="$(FORGEJO_KUBECONFIG)" tools/forgejo-helm-secrets.sh) \
|
||||
--dry-run=server --timeout 5m
|
||||
|
||||
forgejo-secret-scan-template-deploy: check-railiance01-kubeconfig ## Install the immutable, non-secret receive-hook template
|
||||
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl apply --server-side -f $(FORGEJO_SECRET_SCAN_TEMPLATE)
|
||||
|
||||
forgejo-deploy: check-railiance01-kubeconfig forgejo-mailer-es-deploy forgejo-secret-scan-template-deploy ## Deploy / upgrade Forgejo on railiance01
|
||||
@test -n "$$(KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl get secret forgejo-mailer -n $(FORGEJO_NAMESPACE) -o name 2>/dev/null)" || { \
|
||||
echo "Missing secret forgejo-mailer in $(FORGEJO_NAMESPACE)." >&2; \
|
||||
echo "Ensure ExternalSecret is Ready and OpenBao role external-secrets-forgejo is configured." >&2; \
|
||||
exit 1; \
|
||||
}
|
||||
KUBECONFIG="$(FORGEJO_KUBECONFIG)" helm repo add gitea-charts https://dl.gitea.com/charts/ --force-update
|
||||
KUBECONFIG="$(FORGEJO_KUBECONFIG)" helm upgrade --install $(FORGEJO_RELEASE) $(FORGEJO_CHART) \
|
||||
--version $(FORGEJO_CHART_VERSION) \
|
||||
--namespace $(FORGEJO_NAMESPACE) --create-namespace \
|
||||
-f $(FORGEJO_VALUES) \
|
||||
-f $(FORGEJO_REGISTRY_VALUES) \
|
||||
-f <(KUBECONFIG="$(FORGEJO_KUBECONFIG)" tools/forgejo-helm-secrets.sh) \
|
||||
--wait --timeout 10m
|
||||
|
||||
forgejo-ingress-deploy: check-railiance01-kubeconfig ## Apply Forgejo HTTPS ingress
|
||||
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl apply -f $(FORGEJO_INGRESS)
|
||||
|
||||
forgejo-ssh-nodeport-deploy: check-railiance01-kubeconfig ## Apply Forgejo SSH NodePort (30022)
|
||||
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl apply -f $(FORGEJO_SSH_NODEPORT)
|
||||
|
||||
forgejo-status: check-railiance01-kubeconfig ## Show Forgejo pods, svc, ingress, cert, database
|
||||
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl get pods,svc,ingress,certificate,pvc -n $(FORGEJO_NAMESPACE) --ignore-not-found
|
||||
@if KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl cnpg status $(FORGEJO_DB_CLUSTER) -n $(FORGEJO_DB_NAMESPACE) >/dev/null 2>&1; then \
|
||||
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl cnpg status $(FORGEJO_DB_CLUSTER) -n $(FORGEJO_DB_NAMESPACE); \
|
||||
else \
|
||||
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl get cluster $(FORGEJO_DB_CLUSTER) -n $(FORGEJO_DB_NAMESPACE) --ignore-not-found; \
|
||||
fi
|
||||
|
||||
forgejo-smoke: ## Verify Forgejo web and OCI registry challenge
|
||||
FORGEJO_BASE_URL="$(FORGEJO_BASE_URL)" tools/forgejo-smoke.sh
|
||||
|
||||
forgejo-npm-smoke: ## T07 npm install smoke (@coulomb/forgejo-npm-probe)
|
||||
tools/forgejo-npm-smoke.sh
|
||||
|
||||
forgejo-verify: check-railiance01-kubeconfig ## T05 acceptance: smoke, TLS, DB, mailer ESO, SSH operator, SOPS sentinels
|
||||
FORGEJO_BASE_URL="$(FORGEJO_BASE_URL)" KUBECONFIG="$(FORGEJO_KUBECONFIG)" tools/forgejo-verify.sh
|
||||
|
||||
forgejo-operator-bootstrap: ## Idempotent operator user + SSH key bootstrap (no secrets in Git)
|
||||
FORGEJO_BASE_URL="$(FORGEJO_BASE_URL)" tools/forgejo-operator-bootstrap.sh
|
||||
|
||||
forgejo-breakglass-seal: check-railiance01-kubeconfig ## Founder: rotate break-glass admin password into OpenBao (interactive, production-touching)
|
||||
FORGEJO_KUBECONFIG="$(FORGEJO_KUBECONFIG)" FORGEJO_NAMESPACE="$(FORGEJO_NAMESPACE)" tools/forgejo-breakglass-seal.sh
|
||||
|
||||
forgejo-breakglass-verify: ## Founder: verify one break-glass sign-in and write value-free evidence
|
||||
FORGEJO_BASE_URL="$(FORGEJO_BASE_URL)" tools/forgejo-breakglass-verify.sh
|
||||
|
||||
forgejo-runner-registration-sops-bootstrap: check-railiance01-kubeconfig ## Capture cluster runner token into SOPS (operator age key)
|
||||
KUBECONFIG="$(FORGEJO_KUBECONFIG)" tools/forgejo-runner-registration-sops-bootstrap.sh
|
||||
|
||||
forgejo-secrets-check: ## Verify Forgejo SOPS sentinels exist and are encrypted
|
||||
@test -f $(FORGEJO_SECRETS) && grep -q '^sops:' $(FORGEJO_SECRETS) || { echo "Missing encrypted $(FORGEJO_SECRETS)" >&2; exit 1; }
|
||||
@if [ -f $(FORGEJO_RUNNER_REGISTRATION_SECRET) ]; then \
|
||||
grep -q '^sops:' $(FORGEJO_RUNNER_REGISTRATION_SECRET) || { echo "Runner registration not SOPS-encrypted" >&2; exit 1; }; \
|
||||
else \
|
||||
echo "WARN: $(FORGEJO_RUNNER_REGISTRATION_SECRET) missing — run forgejo-runner-registration-sops-bootstrap on operator host" >&2; \
|
||||
fi
|
||||
@echo "ok: Forgejo SOPS sentinels"
|
||||
|
||||
forgejo-logs: check-railiance01-kubeconfig ## Tail Forgejo application logs
|
||||
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl logs -n $(FORGEJO_NAMESPACE) -l app.kubernetes.io/instance=$(FORGEJO_RELEASE) -f --tail=50
|
||||
|
||||
forgejo-runner-registration-deploy: check-railiance01-kubeconfig check-sops ## Apply SOPS runner registration secret
|
||||
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl apply -f <(sops -d $(FORGEJO_RUNNER_REGISTRATION_SECRET))
|
||||
|
||||
forgejo-runner-deploy: check-railiance01-kubeconfig ## Deploy in-cluster Forgejo Actions runner (ADR-004)
|
||||
@test -n "$$(KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl get secret forgejo-runner-registration -n $(FORGEJO_NAMESPACE) -o name 2>/dev/null)" || { \
|
||||
echo "Missing secret forgejo-runner-registration in $(FORGEJO_NAMESPACE)." >&2; \
|
||||
echo "Apply helm/forgejo-runner-registration.sops.yaml or bootstrap once:" >&2; \
|
||||
echo " kubectl create secret generic forgejo-runner-registration -n forgejo --from-literal=token=<token>" >&2; \
|
||||
exit 1; \
|
||||
}
|
||||
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl apply -f $(FORGEJO_RUNNER_MANIFEST)
|
||||
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl rollout status deployment/forgejo-runner -n $(FORGEJO_NAMESPACE) --timeout=5m
|
||||
|
||||
forgejo-runner-status: check-railiance01-kubeconfig ## Show in-cluster runner pod and PVC state
|
||||
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl get deploy,pods,pvc -n $(FORGEJO_NAMESPACE) -l app.kubernetes.io/name=forgejo-runner --ignore-not-found
|
||||
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl logs -n $(FORGEJO_NAMESPACE) deploy/forgejo-runner -c runner --tail=20 2>/dev/null || true
|
||||
|
||||
forgejo-runner-logs: check-railiance01-kubeconfig ## Tail in-cluster runner logs
|
||||
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl logs -n $(FORGEJO_NAMESPACE) deploy/forgejo-runner -c runner -f --tail=50
|
||||
|
||||
##@ reuse-surface (reuse.coulomb.social)
|
||||
|
||||
reuse-dry-run: check-railiance01-kubeconfig ## helm template render (no apply) for reuse-surface
|
||||
KUBECONFIG="$(REUSE_KUBECONFIG)" helm template $(REUSE_RELEASE) $(REUSE_CHART) \
|
||||
--namespace $(REUSE_NAMESPACE) \
|
||||
-f $(REUSE_VALUES)
|
||||
|
||||
reuse-deploy: check-railiance01-kubeconfig ## Deploy / upgrade reuse-surface Helm release on Railiance01
|
||||
KUBECONFIG="$(REUSE_KUBECONFIG)" helm upgrade --install $(REUSE_RELEASE) $(REUSE_CHART) \
|
||||
--namespace $(REUSE_NAMESPACE) --create-namespace \
|
||||
-f $(REUSE_VALUES) --wait --timeout 5m
|
||||
|
||||
reuse-status: check-railiance01-kubeconfig ## Show reuse-surface pod / svc / ingress / cert state on Railiance01
|
||||
KUBECONFIG="$(REUSE_KUBECONFIG)" kubectl get pods,svc,ingress,pvc,certificate -n $(REUSE_NAMESPACE) -l app.kubernetes.io/instance=$(REUSE_RELEASE) --ignore-not-found
|
||||
|
||||
reuse-smoke: check-railiance01-kubeconfig ## Verify reuse public health, repo list, and TLS certificate readiness
|
||||
curl -fsS "$(REUSE_URL)/health"
|
||||
curl -fsS "$(REUSE_URL)/v1/repos"
|
||||
KUBECONFIG="$(REUSE_KUBECONFIG)" kubectl wait -n $(REUSE_NAMESPACE) --for=condition=Ready certificate/$(REUSE_CERTIFICATE) --timeout=30s
|
||||
|
||||
reuse-logs: check-railiance01-kubeconfig ## Tail reuse-surface service logs from Railiance01
|
||||
KUBECONFIG="$(REUSE_KUBECONFIG)" kubectl logs -n $(REUSE_NAMESPACE) -l app.kubernetes.io/instance=$(REUSE_RELEASE) -f --tail=50
|
||||
|
||||
reuse-forgejo-webhook: check-railiance01-kubeconfig ## Idempotent org webhook on Forgejo -> reuse hub (REUSE-WP-0019-T03)
|
||||
bash tools/reuse-forgejo-webhook.sh
|
||||
|
||||
reuse-openbao-eso-token-apply: ## Mint coulombcore OpenBao ESO token + Secret on railiance01 (CCR-2026-0005)
|
||||
OPENBAO_TOKEN_FILE="$(HOME)/.local/openbao/platform-admin.token" bash tools/reuse-openbao-eso-token-apply.sh
|
||||
|
||||
reuse-openbao-store-deploy: check-railiance01-kubeconfig eso-deploy ## Apply openbao-reuse ClusterSecretStore
|
||||
test -d "$(PLATFORM_REPO)/argocd/platform-addons/openbao-secretstore"
|
||||
KUBECONFIG="$(REUSE_KUBECONFIG)" kubectl apply -f "$(PLATFORM_REPO)/argocd/platform-addons/openbao-secretstore/openbao-reuse.clustersecretstore.yaml"
|
||||
|
||||
reuse-runtime-es-deploy: check-railiance01-kubeconfig reuse-openbao-store-deploy ## Apply reuse-surface runtime ExternalSecret
|
||||
KUBECONFIG="$(REUSE_KUBECONFIG)" kubectl apply -f $(REUSE_RUNTIME_EXTERNAL_SECRET)
|
||||
|
||||
reuse-runtime-es-status: check-railiance01-kubeconfig ## Show reuse-surface runtime ExternalSecret sync status
|
||||
KUBECONFIG="$(REUSE_KUBECONFIG)" kubectl get externalsecret,secret -n $(REUSE_NAMESPACE) reuse-surface-runtime reuse-surface-env
|
||||
|
||||
reuse-webhook-smoke: check-railiance01-kubeconfig ## Webhook + ESO + federated smoke (RAILIANCE-WP-0011-T04)
|
||||
bash tools/reuse-webhook-smoke.sh
|
||||
|
||||
##@ policy-nexus (policy.coulomb.social)
|
||||
|
||||
policy-nexus-check-binding: ## Require a source-controlled, immutable production binding
|
||||
@test -d "$(POLICY_NEXUS_RAPP)" || { echo "Missing sibling rApp repo: $(POLICY_NEXUS_RAPP)" >&2; exit 2; }
|
||||
python3 tools/check-policy-nexus-binding.py "$(POLICY_NEXUS_BINDING)"
|
||||
|
||||
policy-nexus-check-image: policy-nexus-check-binding ## Verify the bound OCI digest exists in Forgejo
|
||||
tools/check-oci-image.sh "$(POLICY_NEXUS_IMAGE_REPOSITORY)@$(POLICY_NEXUS_IMAGE_DIGEST)"
|
||||
|
||||
policy-nexus-render: policy-nexus-check-binding ## Render the rApp chart with the approved production binding
|
||||
$(MAKE) -C "$(POLICY_NEXUS_RAPP)" render \
|
||||
IMAGE_REPOSITORY="$(POLICY_NEXUS_IMAGE_REPOSITORY)" \
|
||||
IMAGE_DIGEST="$(POLICY_NEXUS_IMAGE_DIGEST)" \
|
||||
PUBLICATION_MANIFEST_DIGEST="$(POLICY_NEXUS_PUBLICATION_DIGEST)" \
|
||||
SOURCE_INVENTORY_DIGEST="$(POLICY_NEXUS_SOURCE_INVENTORY_DIGEST)" \
|
||||
SOURCE_SET_DIGEST="$(POLICY_NEXUS_SOURCE_SET_DIGEST)"
|
||||
|
||||
policy-nexus-server-dry-run: policy-nexus-check-binding ## Validate the approved policy package against Railiance01
|
||||
$(MAKE) -C "$(POLICY_NEXUS_RAPP)" server-dry-run TARGET="$(POLICY_NEXUS_TARGET)" \
|
||||
IMAGE_REPOSITORY="$(POLICY_NEXUS_IMAGE_REPOSITORY)" \
|
||||
IMAGE_DIGEST="$(POLICY_NEXUS_IMAGE_DIGEST)" \
|
||||
PUBLICATION_MANIFEST_DIGEST="$(POLICY_NEXUS_PUBLICATION_DIGEST)" \
|
||||
SOURCE_INVENTORY_DIGEST="$(POLICY_NEXUS_SOURCE_INVENTORY_DIGEST)" \
|
||||
SOURCE_SET_DIGEST="$(POLICY_NEXUS_SOURCE_SET_DIGEST)"
|
||||
|
||||
policy-nexus-deploy: policy-nexus-check-binding ## Deploy the approved policy release through its rApp package
|
||||
$(MAKE) -C "$(POLICY_NEXUS_RAPP)" deploy TARGET="$(POLICY_NEXUS_TARGET)" \
|
||||
IMAGE_REPOSITORY="$(POLICY_NEXUS_IMAGE_REPOSITORY)" \
|
||||
IMAGE_DIGEST="$(POLICY_NEXUS_IMAGE_DIGEST)" \
|
||||
PUBLICATION_MANIFEST_DIGEST="$(POLICY_NEXUS_PUBLICATION_DIGEST)" \
|
||||
SOURCE_INVENTORY_DIGEST="$(POLICY_NEXUS_SOURCE_INVENTORY_DIGEST)" \
|
||||
SOURCE_SET_DIGEST="$(POLICY_NEXUS_SOURCE_SET_DIGEST)"
|
||||
|
||||
policy-nexus-status: ## Show the policy-nexus Helm release state
|
||||
$(MAKE) -C "$(POLICY_NEXUS_RAPP)" status TARGET="$(POLICY_NEXUS_TARGET)"
|
||||
|
||||
policy-nexus-smoke: policy-nexus-check-binding ## Verify public content, TLS, and both immutable release identities
|
||||
$(MAKE) -C "$(POLICY_NEXUS_RAPP)" verify-live TARGET="$(POLICY_NEXUS_TARGET)" \
|
||||
IMAGE_REPOSITORY="$(POLICY_NEXUS_IMAGE_REPOSITORY)" \
|
||||
IMAGE_DIGEST="$(POLICY_NEXUS_IMAGE_DIGEST)" \
|
||||
PUBLICATION_MANIFEST_DIGEST="$(POLICY_NEXUS_PUBLICATION_DIGEST)" \
|
||||
SOURCE_INVENTORY_DIGEST="$(POLICY_NEXUS_SOURCE_INVENTORY_DIGEST)" \
|
||||
SOURCE_SET_DIGEST="$(POLICY_NEXUS_SOURCE_SET_DIGEST)"
|
||||
|
||||
policy-nexus-rollback: ## Roll back to a named previous Helm revision
|
||||
@test -n "$(POLICY_NEXUS_REVISION)" || { echo "POLICY_NEXUS_REVISION is required" >&2; exit 2; }
|
||||
$(MAKE) -C "$(POLICY_NEXUS_RAPP)" rollback TARGET="$(POLICY_NEXUS_TARGET)" REVISION="$(POLICY_NEXUS_REVISION)"
|
||||
|
||||
##@ Help
|
||||
|
||||
help: ## Show this help
|
||||
@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m<target>\033[0m\n"} \
|
||||
/^[a-zA-Z0-9_-]+:.*?##/ { printf " \033[36m%-20s\033[0m %s\n", $$1, $$2 } \
|
||||
/^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) }' $(MAKEFILE_LIST)
|
||||
|
||||
.PHONY: check-tools check-sops k8s-server-dry-run apps-pg-status apps-pg-restore-drill check-railiance01-kubeconfig check-inter-hub-image-tag check-inter-hub-image check-vergabe-image-tag check-vergabe-image vergabe-dry-run vergabe-deploy vergabe-ingress-deploy vergabe-status vergabe-migrate vergabe-seed vergabe-superuser vergabe-logs vergabe-db-url-secret check-coulomb-social-image-tag coulomb-social-dry-run coulomb-social-deploy coulomb-social-ingress-deploy coulomb-social-status coulomb-social-logs coulomb-social-env-secret coulomb-social-env-secret-dry-run eso-deploy forgejo-openbao-eso-token-apply forgejo-openbao-store-deploy forgejo-mailer-es-deploy forgejo-mailer-es-status forgejo-dry-run forgejo-server-dry-run forgejo-deploy forgejo-ingress-deploy forgejo-ssh-nodeport-deploy forgejo-status forgejo-smoke forgejo-npm-smoke forgejo-verify forgejo-operator-bootstrap forgejo-runner-registration-sops-bootstrap forgejo-secrets-check forgejo-logs forgejo-runner-registration-deploy forgejo-runner-deploy forgejo-runner-status forgejo-runner-logs inter-hub-render-baseline inter-hub-dry-run inter-hub-server-dry-run inter-hub-deploy inter-hub-status inter-hub-release-info inter-hub-smoke inter-hub-logs reuse-dry-run reuse-deploy reuse-status reuse-smoke reuse-logs reuse-forgejo-webhook reuse-openbao-eso-token-apply reuse-openbao-store-deploy reuse-runtime-es-deploy reuse-runtime-es-status reuse-webhook-smoke policy-nexus-check-binding policy-nexus-check-image policy-nexus-render policy-nexus-server-dry-run policy-nexus-deploy policy-nexus-status policy-nexus-smoke policy-nexus-rollback help
|
||||
|
||||
.PHONY: forgejo-secret-scan-template-deploy
|
||||
|
||||
|
||||
##@ Isolated Forgejo source release preparation (no deployment/publication)
|
||||
FORGEJO_BUILD_WORKSPACE ?= /tmp/railiance-apps-forgejo-release
|
||||
FORGEJO_SOURCE_ARCHIVE ?=
|
||||
FORGEJO_GO_ARCHIVE ?=
|
||||
FORGEJO_NODE_ARCHIVE ?=
|
||||
FORGEJO_BUILD_GOMODCACHE ?=
|
||||
FORGEJO_BUILD_GOCACHE_SEED ?=
|
||||
FORGEJO_BUILD_NPM_CACHE_SEED ?=
|
||||
|
||||
.PHONY: forgejo-release-prepare forgejo-release-acquire-npm forgejo-release-build forgejo-release-verify forgejo-release-container
|
||||
forgejo-release-prepare: ## Verify archives/patch/locks and prepare a new isolated workspace
|
||||
/usr/bin/python3 -I tools/forgejo-release-build.py prepare --workspace "$(FORGEJO_BUILD_WORKSPACE)" --source-archive "$(FORGEJO_SOURCE_ARCHIVE)" --go-archive "$(FORGEJO_GO_ARCHIVE)" --node-archive "$(FORGEJO_NODE_ARCHIVE)" --module-cache "$(FORGEJO_BUILD_GOMODCACHE)" $(if $(FORGEJO_BUILD_GOCACHE_SEED),--go-cache-seed "$(FORGEJO_BUILD_GOCACHE_SEED)") $(if $(FORGEJO_BUILD_NPM_CACHE_SEED),--npm-cache-seed "$(FORGEJO_BUILD_NPM_CACHE_SEED)")
|
||||
|
||||
forgejo-release-acquire-npm: ## Explicit public registry dependency acquisition; ignores lifecycle scripts
|
||||
/usr/bin/python3 -I tools/forgejo-release-build.py acquire-npm --workspace "$(FORGEJO_BUILD_WORKSPACE)"
|
||||
|
||||
forgejo-release-build: ## Offline full frontend, bindata and static backend via upstream targets
|
||||
/usr/bin/python3 -I tools/forgejo-release-build.py build --workspace "$(FORGEJO_BUILD_WORKSPACE)"
|
||||
|
||||
forgejo-release-verify: ## Verify all embedded assets, static executable and new CLI shape offline
|
||||
/usr/bin/python3 -I tools/forgejo-release-build.py verify --workspace "$(FORGEJO_BUILD_WORKSPACE)"
|
||||
|
||||
forgejo-release-container: ## Build/verify a deterministic local rootless OCI archive; never publish or deploy
|
||||
/usr/bin/python3 -I tools/forgejo-release-build.py container --workspace "$(FORGEJO_BUILD_WORKSPACE)"
|
||||
|
||||
|
|
@ -0,0 +1,239 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Build-only public Forgejo owner recipe; no deployment or publication code."""
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import tarfile
|
||||
import time
|
||||
import math
|
||||
import datetime
|
||||
|
||||
OWNER=Path(__file__).resolve().parents[1]
|
||||
INPUTS=OWNER/'build/forgejo'
|
||||
LOCK=json.loads((INPUTS/'inputs.lock.json').read_text())
|
||||
|
||||
def digest(path):
|
||||
h=hashlib.sha256()
|
||||
with Path(path).open('rb') as f:
|
||||
for block in iter(lambda:f.read(1024*1024),b''):h.update(block)
|
||||
return h.hexdigest()
|
||||
|
||||
def save(path,data):
|
||||
Path(path).write_text(json.dumps(data,indent=2,sort_keys=True)+'\n')
|
||||
|
||||
def check(path,expected):
|
||||
if digest(path)!=expected:raise ValueError('frozen public input digest differs: '+str(path))
|
||||
|
||||
def base_env(work):
|
||||
return {'PATH':str(work/'node-v22.15.0-linux-x64/bin')+':'+str(work/'go/bin')+':/usr/bin:/bin',
|
||||
'HOME':str(work/'home'),'GOTOOLCHAIN':'local','GOPROXY':'off','GOSUMDB':'off','GOVCS':'*:off',
|
||||
'GOPATH':str(work/'gopath'),'GOMODCACHE':json.loads((work/'prepared.json').read_text())['module_cache'],
|
||||
'GOCACHE':str(work/'gocache'),'CGO_ENABLED':'1','GOMAXPROCS':'4',
|
||||
'GOFLAGS':'-trimpath -buildvcs=false -p=4','SOURCE_DATE_EPOCH':str(LOCK['source_date_epoch']),
|
||||
'TZ':'UTC','LC_ALL':'C','NODE_OPTIONS':'--max-old-space-size=4096',
|
||||
'GIT_CONFIG_NOSYSTEM':'1','GIT_CONFIG_GLOBAL':'/dev/null','GIT_TERMINAL_PROMPT':'0',
|
||||
'NPM_CONFIG_USERCONFIG':str(work/'empty.npmrc'),'NPM_CONFIG_GLOBALCONFIG':str(work/'empty-global.npmrc'),
|
||||
'NPM_CONFIG_CACHE':str(work/'npm-cache'),'NPM_CONFIG_REGISTRY':'https://registry.npmjs.org',
|
||||
'NPM_CONFIG_OFFLINE':'true','NPM_CONFIG_AUDIT':'false','NPM_CONFIG_FUND':'false',
|
||||
'NPM_CONFIG_UPDATE_NOTIFIER':'false','PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD':'1'}
|
||||
|
||||
def sandbox(work,env):
|
||||
cache=Path(env['GOMODCACHE'])
|
||||
return ['bwrap','--unshare-net','--ro-bind','/usr','/usr','--ro-bind','/lib','/lib',
|
||||
'--ro-bind','/lib64','/lib64','--symlink','usr/bin','/bin','--proc','/proc','--dev','/dev',
|
||||
'--tmpfs','/tmp','--bind',str(work),str(work),'--ro-bind',str(cache),str(cache),
|
||||
'--chdir',str(work/'source'),'--','env','-i']+[k+'='+v for k,v in env.items()]
|
||||
|
||||
def run(work,name,args,env=None,offline=False,cwd=None):
|
||||
actual=sandbox(work,env)+args if offline else args
|
||||
start=time.time()
|
||||
with (work/(name+'.log')).open('w') as out:
|
||||
proc=subprocess.Popen(actual,cwd=cwd or work,env={"PATH":"/usr/bin:/bin","HOME":str(work/"home")} if offline else env,stdout=out,stderr=subprocess.STDOUT)
|
||||
save(work/'process.json',{'phase':name,'pid':proc.pid,'driver_pid':os.getpid(),'argv':args,'state':'running'})
|
||||
code=proc.wait()
|
||||
result={'phase':name,'argv':args,'returncode':code,'seconds':round(time.time()-start,2)}
|
||||
save(work/(name+'-result.json'),result);save(work/'process.json',dict(result,state='complete',pid=None,driver_pid=None))
|
||||
print(json.dumps(result),flush=True)
|
||||
if code:raise RuntimeError('phase failed; retained log: '+str(work/(name+'.log')))
|
||||
return result
|
||||
|
||||
def checked_inputs(work):
|
||||
for n,h in LOCK['host_tools'].items():check(n,h)
|
||||
for n,h in LOCK['lockfiles'].items():check(work/'source'/n,h)
|
||||
for x in LOCK['reviewed_outputs']:check(work/'source'/x['path'],x['sha256'])
|
||||
|
||||
def prepare(a,work):
|
||||
if work.exists():raise ValueError('workspace must be new; existing trial is never restarted or overwritten')
|
||||
for n,key in [('source_archive','source_archive_sha256'),('go_archive','go_archive_sha256'),('node_archive','node_archive_sha256')]:
|
||||
if not getattr(a,n):raise ValueError('explicit '+n+' required')
|
||||
check(getattr(a,n),LOCK[key])
|
||||
if not a.module_cache:raise ValueError('explicit public module cache required')
|
||||
cache=Path(a.module_cache).resolve();assert cache.is_dir()
|
||||
check(INPUTS/'required-claim-values.patch',LOCK['patch_sha256'])
|
||||
for n,h in LOCK['host_tools'].items():check(n,h)
|
||||
work.mkdir(parents=True);(work/'home').mkdir()
|
||||
with tarfile.open(a.source_archive) as t:
|
||||
# The official archive is digest-bound before any member is extracted.
|
||||
parent=work/'unpacked';parent.mkdir();t.extractall(parent,filter='data')
|
||||
folders=list(parent.iterdir());assert len(folders)==1 and folders[0].is_dir();folders[0].rename(work/'source');parent.rmdir()
|
||||
for n in ['go_archive','node_archive']:
|
||||
with tarfile.open(getattr(a,n)) as t:t.extractall(work,filter='data')
|
||||
for n in ['empty.npmrc','empty-global.npmrc']:(work/n).write_text('')
|
||||
if a.go_cache_seed:shutil.copytree(Path(a.go_cache_seed).resolve(),work/'gocache')
|
||||
if a.npm_cache_seed:shutil.copytree(Path(a.npm_cache_seed).resolve(),work/'npm-cache')
|
||||
subprocess.run(['/usr/bin/git','apply','--check',str(INPUTS/'required-claim-values.patch')],cwd=work/'source',check=True,env={'PATH':'/usr/bin:/bin','HOME':str(work/'home'),'GIT_CONFIG_NOSYSTEM':'1','GIT_CONFIG_GLOBAL':'/dev/null'})
|
||||
subprocess.run(['/usr/bin/git','apply',str(INPUTS/'required-claim-values.patch')],cwd=work/'source',check=True,env={'PATH':'/usr/bin:/bin','HOME':str(work/'home'),'GIT_CONFIG_NOSYSTEM':'1','GIT_CONFIG_GLOBAL':'/dev/null'})
|
||||
save(work/'prepared.json',{'module_cache':str(cache),'lock_sha256':digest(INPUTS/'inputs.lock.json'),'public_archives_verified':True})
|
||||
checked_inputs(work)
|
||||
packages=json.loads((work/'source/package-lock.json').read_text())['packages']
|
||||
for name,package in packages.items():
|
||||
if not name:continue
|
||||
if not package.get('resolved','').startswith('https://registry.npmjs.org/') or not package.get('integrity','').startswith('sha512-'):
|
||||
raise ValueError('non-public/non-integrity-bound npm package: '+name)
|
||||
print(json.dumps({'prepared':True,'lockfile_npm_packages':len(packages)-1}))
|
||||
|
||||
def normalize(folder):
|
||||
epoch=LOCK['source_date_epoch']
|
||||
for root,dirs,files in os.walk(folder):
|
||||
dirs[:]=[x for x in dirs if x not in ('node_modules','.git')]
|
||||
for n in files:
|
||||
p=Path(root)/n
|
||||
if not p.is_symlink():os.utime(p,(epoch,epoch))
|
||||
os.utime(root,(epoch,epoch))
|
||||
|
||||
def build(work):
|
||||
checked_inputs(work);env=base_env(work)
|
||||
run(work,'verify-go-modules',['go','mod','verify'],env,True)
|
||||
run(work,'npm-offline-ci',['npm','ci','--offline','--ignore-scripts','--no-audit','--no-fund'],env,True)
|
||||
normalize(work/'source')
|
||||
version=['GOFLAGS=-trimpath -buildvcs=false -p=4','RELEASE_VERSION='+LOCK['release_version'],'FORGEJO_VERSION='+LOCK['release_version'],'FORGEJO_VERSION_API='+LOCK['release_version']]
|
||||
run(work,'frontend',['make']+version+['frontend'],env,True)
|
||||
for n in ['public','options','templates','modules/migration/schemas']:normalize(work/'source'/n)
|
||||
run(work,'embedded-and-static-backend',['make']+version+['TAGS='+LOCK['tags'],'go-check','generate-backend','static-executable'],env,True)
|
||||
checked_inputs(work)
|
||||
save(work/'build.json',{'binary_sha256':digest(work/'source/gitea'),'source_locks_preserved':True,'frontend_built':True,'tags':LOCK['tags'],'image_deployment_selected':False})
|
||||
|
||||
def verify(work):
|
||||
checked_inputs(work);binary=work/'source/gitea'
|
||||
target=work/'embedded-extracted'
|
||||
if target.exists():raise ValueError('retain prior verification; extraction target already exists')
|
||||
target.mkdir()
|
||||
# No source, asset tree, host /usr, libraries, configuration or HOME is
|
||||
# exposed to the executable. A filesystem fallback cannot pass this proof.
|
||||
bare=['bwrap','--unshare-net','--unshare-pid','--proc','/proc','--dev','/dev',
|
||||
'--tmpfs','/tmp','--dir','/app','--ro-bind',str(binary),'/app/forgejo',
|
||||
'--bind',str(target),'/results','--chdir','/tmp','--clearenv']
|
||||
for k,v in {'PATH':'/nonexistent','HOME':'/tmp/home','USER':'builder','USERNAME':'builder',
|
||||
'GITEA_WORK_DIR':'/tmp/work','GITEA_CUSTOM':'/tmp/custom',
|
||||
'GITEA_APP_INI':'/tmp/absent-app.ini','TZ':'UTC','LC_ALL':'C'}.items():
|
||||
bare+=['--setenv',k,v]
|
||||
bare+=['--','/app/forgejo']
|
||||
outer={'PATH':'/usr/bin:/bin','HOME':str(work/'home')}
|
||||
run(work,'binary-version',bare+['--version'],outer)
|
||||
run(work,'new-cli-help',bare+['admin','auth','add-oauth','--help'],outer)
|
||||
assert '--required-claim-values' in (work/'new-cli-help.log').read_text()
|
||||
run(work,'embedded-extract',bare+['embedded','extract','--include-vendored','--destination','/results','**'],outer)
|
||||
records={}
|
||||
for folder in ['public','options','templates']:
|
||||
for f in sorted((work/'source'/folder).rglob('*')):
|
||||
if not f.is_file():continue
|
||||
n=str(f.relative_to(work/'source'));q=target/n
|
||||
if not q.is_file() or digest(f)!=digest(q):raise ValueError('embedded asset missing/different: '+n)
|
||||
records[n]=digest(f)
|
||||
assert 'public/assets/js/index.js' in records and 'public/assets/css/index.css' in records
|
||||
assert 'public/assets/licenses.txt' in records
|
||||
save(work/'embedded-files.json',records)
|
||||
readelf=subprocess.run(['/usr/bin/readelf','-l',str(binary)],capture_output=True,text=True,check=True,env={'PATH':'/usr/bin:/bin'})
|
||||
assert 'INTERP' not in readelf.stdout
|
||||
save(work/'verification.json',{'binary_sha256':digest(binary),'embedded_files':len(records),
|
||||
'embedded_file_manifest_sha256':digest(work/'embedded-files.json'),'all_public_options_templates_exact':True,
|
||||
'no_source_assets_libraries_or_host_config_mounted':True,
|
||||
'no_dynamic_interpreter':True,'new_cli_flag_present':True,'network':'bwrap --unshare-net',
|
||||
'source_locks_preserved':True,'architecture_approval':False,'native_admission':False})
|
||||
print((work/'verification.json').read_text())
|
||||
|
||||
def container(work):
|
||||
verified=json.loads((work/'verification.json').read_text());check(work/'source/gitea',verified['binary_sha256'])
|
||||
context=work/'container-context'
|
||||
if context.exists():raise ValueError('container trial already exists; retain evidence before an explicit revised trial')
|
||||
archive_path=work/'image.oci.tar'
|
||||
if archive_path.exists():raise ValueError('retain existing OCI artifact; never overwrite it')
|
||||
base_bytes=(INPUTS/'runtime-base-config.json').read_bytes()
|
||||
assert hashlib.sha256(base_bytes).hexdigest()==LOCK['runtime_base_config_sha256']
|
||||
original=json.loads(base_bytes)
|
||||
epoch=max(LOCK['source_date_epoch'],math.ceil(datetime.datetime.fromisoformat(original['created'].replace('Z','+00:00')).timestamp()))
|
||||
context.mkdir();shutil.copyfile(work/'source/gitea',context/'gitea')
|
||||
os.utime(context/'gitea',(LOCK['source_date_epoch'],)*2)
|
||||
(context/'Dockerfile').write_text('FROM '+LOCK['runtime_base']+'\nARG SOURCE_DATE_EPOCH\nLABEL org.opencontainers.image.version="'+LOCK['release_version']+'" org.opencontainers.image.revision="'+LOCK['source_commit']+'" org.railiance.source.patch.sha256="'+LOCK['patch_sha256']+'"\nCOPY --chown=0:0 --chmod=755 gitea /app/gitea/gitea\n')
|
||||
os.utime(context/'Dockerfile',(LOCK['source_date_epoch'],)*2)
|
||||
config=work/'docker-config';config.mkdir(exist_ok=True)
|
||||
env={'PATH':'/usr/bin:/bin','HOME':str(work/'home'),'DOCKER_CONFIG':str(config),'DOCKER_HOST':'unix:///var/run/docker.sock'}
|
||||
# SOURCE_DATE_EPOCH alone does not normalise COPY parent-directory mtimes.
|
||||
# OCI output permits layer rewriting without the Docker-store unpack conflict.
|
||||
# The epoch never predates the immutable base, preserving its exact layer prefix.
|
||||
run(work,'container-build',['docker','buildx','build','--output','type=oci,dest='+str(archive_path)+',rewrite-timestamp=true',
|
||||
'--no-cache','--platform',LOCK['platform'],'--pull=false','--network=none','--provenance=false','--sbom=false',
|
||||
'--metadata-file',str(work/'image-build-metadata.json'),'--build-arg','SOURCE_DATE_EPOCH='+str(epoch),str(context)],env)
|
||||
metadata=json.loads((work/'image-build-metadata.json').read_text())
|
||||
with tarfile.open(archive_path) as archive:
|
||||
index_bytes=archive.extractfile('index.json').read();index=json.loads(index_bytes)
|
||||
assert len(index['manifests'])==1
|
||||
descriptor=index['manifests'][0]
|
||||
assert descriptor['platform']=={'architecture':'amd64','os':'linux'}
|
||||
assert descriptor['digest']==metadata['containerimage.digest']
|
||||
def blob(d):
|
||||
value=archive.extractfile('blobs/sha256/'+d.split(':')[1]).read()
|
||||
assert 'sha256:'+hashlib.sha256(value).hexdigest()==d
|
||||
return value
|
||||
manifest=json.loads(blob(descriptor['digest']))
|
||||
config_digest=manifest['config']['digest'];config=json.loads(blob(config_digest))
|
||||
assert config_digest==metadata['containerimage.config.digest']
|
||||
for layer in manifest['layers']:blob(layer['digest'])
|
||||
assert config['rootfs']['diff_ids'][:-1]==original['rootfs']['diff_ids']
|
||||
assert config['architecture']=='amd64' and config['os']=='linux'
|
||||
for key in ['User','Entrypoint','Cmd','Env','WorkingDir','Volumes']:
|
||||
assert config['config'].get(key)==original['config'].get(key),key
|
||||
labels=dict(original['config'].get('Labels',{}))
|
||||
labels.update({'org.opencontainers.image.version':LOCK['release_version'],
|
||||
'org.opencontainers.image.revision':LOCK['source_commit'],'org.railiance.source.patch.sha256':LOCK['patch_sha256']})
|
||||
assert config['config'].get('Labels')==labels
|
||||
assert config['config']['User']=='1000:1000'
|
||||
expected_created=datetime.datetime.fromtimestamp(epoch,datetime.timezone.utc).isoformat(timespec='seconds').replace('+00:00','Z')
|
||||
assert config['created']==expected_created
|
||||
found=False
|
||||
last='blobs/sha256/'+manifest['layers'][-1]['digest'].split(':')[1]
|
||||
with tarfile.open(fileobj=archive.extractfile(last),mode='r|*') as layer:
|
||||
for member in layer:
|
||||
name=member.name.lstrip('./');assert name in ('app','app/gitea','app/gitea/gitea')
|
||||
if name=='app/gitea/gitea':
|
||||
h=hashlib.sha256();stream=layer.extractfile(member)
|
||||
for block in iter(lambda:stream.read(1024*1024),b''):h.update(block)
|
||||
assert h.hexdigest()==verified['binary_sha256'];found=True
|
||||
assert found
|
||||
save(work/'container-result.json',{'image_manifest_digest':descriptor['digest'],'config_digest':config_digest,
|
||||
'oci_index_sha256':hashlib.sha256(index_bytes).hexdigest(),'archive_sha256':digest(archive_path),
|
||||
'artifact_path':str(archive_path),'source_epoch':LOCK['source_date_epoch'],'container_epoch':epoch,
|
||||
'all_base_layers_and_rootless_runtime_config_preserved':True,'all_oci_blob_digests_verified':True,
|
||||
'final_layer_binary_exact':True,'registry_publication':False,'deployment_tag':None,
|
||||
'host_or_secret_mounts':False,'privileged_containers':False,'local_runtime_image_loaded':False})
|
||||
print((work/'container-result.json').read_text())
|
||||
|
||||
def main():
|
||||
parser=argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('phase',choices=['prepare','acquire-npm','build','verify','container'])
|
||||
parser.add_argument('--workspace',required=True)
|
||||
for n in ['source-archive','go-archive','node-archive','module-cache','go-cache-seed','npm-cache-seed']:parser.add_argument('--'+n)
|
||||
a=parser.parse_args();work=Path(a.workspace).resolve()
|
||||
if not str(work).startswith('/tmp/'):raise ValueError('isolated build workspace must be under /tmp')
|
||||
if a.phase=='prepare':return prepare(a,work)
|
||||
assert json.loads((work/'prepared.json').read_text())['lock_sha256']==digest(INPUTS/'inputs.lock.json')
|
||||
if a.phase=='acquire-npm':
|
||||
checked_inputs(work);env=base_env(work);env['NPM_CONFIG_OFFLINE']='false'
|
||||
return run(work,'npm-public-acquire',['npm','ci','--ignore-scripts','--no-audit','--no-fund'],env,cwd=work/'source')
|
||||
{'build':build,'verify':verify,'container':container}[a.phase](work)
|
||||
|
||||
if __name__=='__main__':main()
|
||||
|
|
@ -0,0 +1,657 @@
|
|||
SHELL := /usr/bin/env bash
|
||||
.DEFAULT_GOAL := help
|
||||
|
||||
VERGABE_RELEASE ?= vergabe-teilnahme
|
||||
VERGABE_NAMESPACE ?= vergabe-teilnahme
|
||||
VERGABE_CHART ?= charts/vergabe-teilnahme
|
||||
VERGABE_VALUES ?= helm/vergabe-teilnahme-values.yaml
|
||||
VERGABE_IMAGE_REPOSITORY ?= forgejo.coulomb.social/coulomb/vergabe-teilnahme
|
||||
VERGABE_IMAGE_TAG ?=
|
||||
VERGABE_INGRESS ?= manifests/vergabe-teilnahme-ingress.yaml
|
||||
|
||||
VERGABE_DB_SECRET ?= vergabe-app-credentials
|
||||
VERGABE_ENV_SECRET ?= vergabe-teilnahme-env
|
||||
VERGABE_DB_USER ?= vergabe
|
||||
VERGABE_DB_HOST ?= apps-pg-rw.databases
|
||||
VERGABE_DB_PORT ?= 5432
|
||||
VERGABE_DB_NAME ?= vergabe_db
|
||||
|
||||
COULOMB_SOCIAL_RELEASE ?= coulomb-social
|
||||
COULOMB_SOCIAL_NAMESPACE ?= coulomb-social
|
||||
COULOMB_SOCIAL_CHART ?= charts/coulomb-social
|
||||
COULOMB_SOCIAL_VALUES ?= helm/coulomb-social-values.yaml
|
||||
COULOMB_SOCIAL_IMAGE_REPOSITORY ?= forgejo.coulomb.social/coulomb/coulomb-social
|
||||
COULOMB_SOCIAL_INGRESS ?= manifests/coulomb-social-ingress.yaml
|
||||
COULOMB_SOCIAL_ENV_SECRET ?= coulomb-social-env
|
||||
|
||||
INTER_HUB_RELEASE ?= inter-hub
|
||||
INTER_HUB_NAMESPACE ?= inter-hub
|
||||
INTER_HUB_CHART ?= charts/inter-hub
|
||||
INTER_HUB_VALUES ?= helm/inter-hub-values.yaml
|
||||
INTER_HUB_IMAGE_REPOSITORY ?= forgejo.coulomb.social/coulomb/inter-hub
|
||||
INTER_HUB_IMAGE_TAG ?=
|
||||
INTER_HUB_BASE_URL ?= https://hub.coulomb.social
|
||||
|
||||
REUSE_RELEASE ?= reuse
|
||||
REUSE_NAMESPACE ?= reuse
|
||||
REUSE_CHART ?= charts/reuse-surface
|
||||
REUSE_VALUES ?= helm/reuse-surface-values.yaml
|
||||
REUSE_URL ?= https://reuse.coulomb.social
|
||||
REUSE_CERTIFICATE ?= reuse-surface-tls
|
||||
|
||||
POLICY_NEXUS_RAPP ?= ../rapp-policy-nexus
|
||||
POLICY_NEXUS_BINDING ?= bindings/policy-nexus-production.json
|
||||
POLICY_NEXUS_TARGET ?= railiance01
|
||||
POLICY_NEXUS_IMAGE_REPOSITORY ?= $(shell python3 -c 'import json; print(json.load(open("$(POLICY_NEXUS_BINDING)"))["image_repository"])')
|
||||
POLICY_NEXUS_IMAGE_DIGEST ?= $(shell python3 -c 'import json; print(json.load(open("$(POLICY_NEXUS_BINDING)")).get("image_digest") or "")')
|
||||
POLICY_NEXUS_PUBLICATION_DIGEST ?= $(shell python3 -c 'import json; print(json.load(open("$(POLICY_NEXUS_BINDING)")).get("publication_manifest_digest") or "")')
|
||||
POLICY_NEXUS_SOURCE_INVENTORY_DIGEST ?= $(shell python3 -c 'import json; print(json.load(open("$(POLICY_NEXUS_BINDING)")).get("source_inventory_digest") or "")')
|
||||
POLICY_NEXUS_SOURCE_SET_DIGEST ?= $(shell python3 -c 'import json; print(json.load(open("$(POLICY_NEXUS_BINDING)")).get("source_set_digest") or "")')
|
||||
POLICY_NEXUS_REVISION ?=
|
||||
|
||||
COULOMBCORE_KUBECONFIG ?= $(HOME)/.kube/config
|
||||
RAILIANCE01_KUBECONFIG ?= $(HOME)/.kube/config-railiance01
|
||||
PRODUCTION_KUBECONFIG ?= $(COULOMBCORE_KUBECONFIG)
|
||||
INTER_HUB_KUBECONFIG ?= $(RAILIANCE01_KUBECONFIG)
|
||||
REUSE_KUBECONFIG ?= $(RAILIANCE01_KUBECONFIG)
|
||||
FORGEJO_KUBECONFIG ?= $(RAILIANCE01_KUBECONFIG)
|
||||
FORGEJO_RELEASE ?= forgejo
|
||||
FORGEJO_NAMESPACE ?= forgejo
|
||||
FORGEJO_CHART ?= gitea-charts/gitea
|
||||
# Forgejo 11.x (Gitea 1.22) lacks `gitea config edit-ini`; chart 12.6+ requires Gitea 1.26+.
|
||||
FORGEJO_CHART_VERSION ?= 12.5.0
|
||||
FORGEJO_VALUES ?= helm/forgejo-values.yaml
|
||||
FORGEJO_REGISTRY_VALUES ?= helm/forgejo-registry-values.yaml
|
||||
FORGEJO_SECRETS ?= helm/forgejo-secrets.sops.yaml
|
||||
FORGEJO_SECRET_SCAN_TEMPLATE ?= manifests/forgejo-secret-scan-template.yaml
|
||||
FORGEJO_INGRESS ?= manifests/forgejo-ingress.yaml
|
||||
FORGEJO_SSH_NODEPORT ?= manifests/forgejo-ssh-nodeport.yaml
|
||||
FORGEJO_BASE_URL ?= https://forgejo.coulomb.social
|
||||
FORGEJO_DB_CLUSTER ?= forgejo-db
|
||||
FORGEJO_DB_NAMESPACE ?= databases
|
||||
FORGEJO_RUNNER_MANIFEST ?= manifests/forgejo-runner.yaml
|
||||
FORGEJO_RUNNER_REGISTRATION_SECRET ?= helm/forgejo-runner-registration.sops.yaml
|
||||
FORGEJO_MAILER_EXTERNAL_SECRET ?= manifests/forgejo-mailer-externalsecret.yaml
|
||||
REUSE_RUNTIME_EXTERNAL_SECRET ?= manifests/reuse-surface-runtime-externalsecret.yaml
|
||||
PLATFORM_REPO ?= $(HOME)/railiance-platform
|
||||
ESO_CHART_VERSION ?= 0.16.1
|
||||
INTER_HUB_IMAGE_REF = $(INTER_HUB_IMAGE_REPOSITORY):$(INTER_HUB_IMAGE_TAG)
|
||||
INTER_HUB_IMAGE_SET_ARG = $(if $(strip $(INTER_HUB_IMAGE_TAG)),--set image.tag=$(INTER_HUB_IMAGE_TAG),)
|
||||
|
||||
CORE_HUB_RELEASE ?= core-hub
|
||||
CORE_HUB_NAMESPACE ?= core-hub
|
||||
CORE_HUB_CHART ?= charts/core-hub
|
||||
CORE_HUB_VALUES ?= helm/core-hub-values.yaml
|
||||
CORE_HUB_STAGING_NAMESPACE ?= core-hub-staging
|
||||
CORE_HUB_STAGING_VALUES ?= helm/core-hub-staging-values.yaml
|
||||
CORE_HUB_IMAGE_REPOSITORY ?= forgejo.coulomb.social/coulomb/core-hub
|
||||
CORE_HUB_IMAGE_TAG ?=
|
||||
CORE_HUB_BASE_URL ?= https://hub.coulomb.social
|
||||
CORE_HUB_KUBECONFIG ?= $(PRODUCTION_KUBECONFIG)
|
||||
VERGABE_KUBECONFIG ?= $(PRODUCTION_KUBECONFIG)
|
||||
VERGABE_IMAGE_REF = $(VERGABE_IMAGE_REPOSITORY):$(VERGABE_IMAGE_TAG)
|
||||
VERGABE_IMAGE_SET_ARG = $(if $(strip $(VERGABE_IMAGE_TAG)),--set image.repository=$(VERGABE_IMAGE_REPOSITORY) --set image.tag=$(VERGABE_IMAGE_TAG),)
|
||||
CORE_HUB_IMAGE_REF = $(CORE_HUB_IMAGE_REPOSITORY):$(CORE_HUB_IMAGE_TAG)
|
||||
CORE_HUB_IMAGE_SET_ARG = $(if $(strip $(CORE_HUB_IMAGE_TAG)),--set image.tag=$(CORE_HUB_IMAGE_TAG),)
|
||||
|
||||
SOPS_SENTINEL ?= $(FORGEJO_SECRETS)
|
||||
DRY_RUN_CREATE_NAMESPACES ?= false
|
||||
|
||||
##@ Operator checks
|
||||
|
||||
check-tools: ## Check required operator tools and warn about optional diagnostics
|
||||
tools/check-tools.sh
|
||||
|
||||
check-sops: ## Verify the local SOPS age key can decrypt SOPS_SENTINEL
|
||||
SOPS_SENTINEL="$(SOPS_SENTINEL)" tools/check-sops.sh
|
||||
|
||||
k8s-server-dry-run: ## Server-side dry-run rendered Helm and committed manifests
|
||||
DRY_RUN_CREATE_NAMESPACES=$(DRY_RUN_CREATE_NAMESPACES) tools/k8s-server-dry-run.sh
|
||||
|
||||
apps-pg-status: ## Check the shared apps-pg cnpg cluster
|
||||
@if kubectl cnpg status apps-pg -n databases >/dev/null 2>&1; then \
|
||||
kubectl cnpg status apps-pg -n databases; \
|
||||
else \
|
||||
echo "kubectl cnpg plugin not available; falling back to cnpg resources"; \
|
||||
kubectl get cluster apps-pg -n databases; \
|
||||
kubectl get pods -n databases -l cnpg.io/cluster=apps-pg; \
|
||||
fi
|
||||
|
||||
check-railiance01-kubeconfig: ## Verify Railiance01 production kubeconfig exists
|
||||
@test -r "$(RAILIANCE01_KUBECONFIG)" || { \
|
||||
echo "Missing Railiance01 kubeconfig: $(RAILIANCE01_KUBECONFIG)" >&2; \
|
||||
echo "Restore it with:" >&2; \
|
||||
echo " ssh tegwick@92.205.62.239 'sudo cat /etc/rancher/k3s/k3s.yaml' | sed 's|127.0.0.1|92.205.62.239|' > $(RAILIANCE01_KUBECONFIG)" >&2; \
|
||||
exit 1; \
|
||||
}
|
||||
|
||||
check-coulombcore-kubeconfig: ## Verify CoulombCore kubeconfig and k3s API tunnel
|
||||
@test -r "$(COULOMBCORE_KUBECONFIG)" || { \
|
||||
echo "Missing CoulombCore kubeconfig: $(COULOMBCORE_KUBECONFIG)" >&2; \
|
||||
echo "Ensure ops-bridge tunnel is up: bridge up k3s-api-coulombcore" >&2; \
|
||||
exit 1; \
|
||||
}
|
||||
@KUBECONFIG="$(COULOMBCORE_KUBECONFIG)" kubectl get --raw=/readyz >/dev/null 2>&1 || { \
|
||||
echo "CoulombCore API unreachable via $(COULOMBCORE_KUBECONFIG)." >&2; \
|
||||
echo "Run: bridge up k3s-api-coulombcore" >&2; \
|
||||
exit 1; \
|
||||
}
|
||||
|
||||
check-production-kubeconfig: check-coulombcore-kubeconfig ## Verify production app cluster API (CoulombCore)
|
||||
|
||||
check-inter-hub-image-tag: ## Require an explicit inter-hub image tag for production deploys
|
||||
@test -n "$(INTER_HUB_IMAGE_TAG)" || { \
|
||||
echo "Set INTER_HUB_IMAGE_TAG=<sha> for inter-hub production deploys." >&2; \
|
||||
echo "Example: INTER_HUB_IMAGE_TAG=91037a4 make inter-hub-deploy" >&2; \
|
||||
exit 1; \
|
||||
}
|
||||
|
||||
check-inter-hub-image: check-inter-hub-image-tag ## Verify the inter-hub OCI image tag exists before deploy
|
||||
tools/check-oci-image.sh "$(INTER_HUB_IMAGE_REF)"
|
||||
|
||||
check-core-hub-image-tag: ## Require an explicit core-hub image tag for production deploys
|
||||
@test -n "$(CORE_HUB_IMAGE_TAG)" || { \
|
||||
echo "Set CORE_HUB_IMAGE_TAG=<sha> for core-hub production deploys." >&2; \
|
||||
echo "Example: CORE_HUB_IMAGE_TAG=59bf1e4 make core-hub-deploy" >&2; \
|
||||
exit 1; \
|
||||
}
|
||||
|
||||
check-core-hub-image: check-core-hub-image-tag ## Verify the core-hub OCI image tag exists before deploy
|
||||
tools/check-oci-image.sh "$(CORE_HUB_IMAGE_REF)"
|
||||
|
||||
cnpg-backup-status: check-production-kubeconfig ## Report Option A / ScheduledBackup coverage
|
||||
KUBECONFIG="$(PRODUCTION_KUBECONFIG)" tools/cnpg-backup-status.sh
|
||||
|
||||
cnpg-backup-offsite-secret-apply: check-production-kubeconfig ## Materialize databases/cnpg-backup-offsite from OpenBao (T02)
|
||||
chmod +x tools/cnpg-backup-offsite-secret-apply.sh
|
||||
KUBECONFIG="$(PRODUCTION_KUBECONFIG)" tools/cnpg-backup-offsite-secret-apply.sh
|
||||
|
||||
cnpg-backup-offsite-secret-dry-run: check-production-kubeconfig ## Dry-run Secret materialization (no apply)
|
||||
chmod +x tools/cnpg-backup-offsite-secret-apply.sh
|
||||
KUBECONFIG="$(PRODUCTION_KUBECONFIG)" DRY_RUN=1 tools/cnpg-backup-offsite-secret-apply.sh
|
||||
|
||||
cnpg-option-a-apply: check-production-kubeconfig ## Apply Option A CronJobs + RBAC (production-touching)
|
||||
KUBECONFIG="$(PRODUCTION_KUBECONFIG)" kubectl apply -f manifests/cnpg-option-a-backup.yaml
|
||||
|
||||
cnpg-option-a-dry-run: check-production-kubeconfig ## Server-side dry-run of Option A manifests
|
||||
KUBECONFIG="$(PRODUCTION_KUBECONFIG)" kubectl apply --dry-run=server -f manifests/cnpg-option-a-backup.yaml
|
||||
|
||||
cnpg-logical-backup-dry-run: check-production-kubeconfig ## Logical dump + age for all four clusters (no upload)
|
||||
chmod +x tools/cnpg-logical-backup.sh
|
||||
KUBECONFIG="$(PRODUCTION_KUBECONFIG)" PLATFORM_REPO="$(PLATFORM_REPO)" RAILIANCE_BACKUP_DRY_RUN=1 \
|
||||
tools/cnpg-logical-backup.sh
|
||||
|
||||
cnpg-logical-backup: check-production-kubeconfig ## Logical dump + age + Nextcloud for all four clusters
|
||||
chmod +x tools/cnpg-logical-backup.sh
|
||||
@KUBECONFIG="$(PRODUCTION_KUBECONFIG)" PLATFORM_REPO="$(PLATFORM_REPO)" bao kv metadata get \
|
||||
platform/workloads/railiance/backup/offsite-lane >/dev/null 2>&1 || { \
|
||||
echo "ERROR: OpenBao backup-lane auth missing." >&2; \
|
||||
echo "Run: bao login -method=oidc -path=netkingdom role=railiance-backup-workload-kv-read" >&2; \
|
||||
exit 1; \
|
||||
}
|
||||
KUBECONFIG="$(PRODUCTION_KUBECONFIG)" PLATFORM_REPO="$(PLATFORM_REPO)" RAILIANCE_BACKUP_DRY_RUN=0 \
|
||||
tools/cnpg-logical-backup.sh
|
||||
|
||||
##@ Option A multi-host (platform CLI / activity-core)
|
||||
|
||||
cnpg-option-a-backup-dry-run: ## Platform multi-host Option A dump+age (JSON, no upload)
|
||||
chmod +x "$(PLATFORM_REPO)/tools/cmd/cnpg-option-a-backup"
|
||||
KUBECONFIG_CORE="$(COULOMBCORE_KUBECONFIG)" KUBECONFIG_R01="$(RAILIANCE01_KUBECONFIG)" \
|
||||
RAILIANCE_BACKUP_DRY_RUN=1 "$(PLATFORM_REPO)/tools/cmd/cnpg-option-a-backup"
|
||||
|
||||
cnpg-option-a-backup: ## Platform multi-host Option A dump+age+upload (JSON)
|
||||
chmod +x "$(PLATFORM_REPO)/tools/cmd/cnpg-option-a-backup"
|
||||
@bao kv metadata get platform/workloads/railiance/backup/offsite-lane >/dev/null 2>&1 || { \
|
||||
echo "ERROR: OpenBao backup-lane auth missing or set RAILIANCE_BACKUP_NC_TOKEN." >&2; \
|
||||
exit 1; \
|
||||
}
|
||||
KUBECONFIG_CORE="$(COULOMBCORE_KUBECONFIG)" KUBECONFIG_R01="$(RAILIANCE01_KUBECONFIG)" \
|
||||
RAILIANCE_BACKUP_DRY_RUN=0 "$(PLATFORM_REPO)/tools/cmd/cnpg-option-a-backup"
|
||||
|
||||
apps-pg-backup-dry-run: check-production-kubeconfig ## Logical pg_dump + age encrypt for vergabe_db (no upload)
|
||||
chmod +x tools/apps-pg-backup-dry-run.sh
|
||||
KUBECONFIG="$(PRODUCTION_KUBECONFIG)" PLATFORM_REPO="$(PLATFORM_REPO)" RAILIANCE_BACKUP_DRY_RUN=1 \
|
||||
tools/apps-pg-backup-dry-run.sh
|
||||
|
||||
apps-pg-restore-drill: check-production-kubeconfig ## Isolated vergabe_db restore drill with row-count gate
|
||||
chmod +x tools/apps-pg-restore-drill.sh
|
||||
KUBECONFIG="$(PRODUCTION_KUBECONFIG)" tools/apps-pg-restore-drill.sh
|
||||
|
||||
apps-pg-backup: check-production-kubeconfig ## Logical pg_dump + age encrypt + Nextcloud upload for vergabe_db
|
||||
chmod +x tools/apps-pg-backup-dry-run.sh
|
||||
@KUBECONFIG="$(PRODUCTION_KUBECONFIG)" PLATFORM_REPO="$(PLATFORM_REPO)" bao kv metadata get \
|
||||
platform/workloads/railiance/backup/offsite-lane >/dev/null 2>&1 || { \
|
||||
echo "ERROR: OpenBao backup-lane auth missing." >&2; \
|
||||
echo "Run: bao login -method=oidc -path=netkingdom role=railiance-backup-workload-kv-read" >&2; \
|
||||
exit 1; \
|
||||
}
|
||||
KUBECONFIG="$(PRODUCTION_KUBECONFIG)" PLATFORM_REPO="$(PLATFORM_REPO)" RAILIANCE_BACKUP_DRY_RUN=0 \
|
||||
tools/apps-pg-backup-dry-run.sh
|
||||
|
||||
##@ Vergabe Teilnahme
|
||||
|
||||
check-vergabe-image-tag: ## Require an explicit vergabe-teilnahme image tag for production deploys
|
||||
@test -n "$(VERGABE_IMAGE_TAG)" || { \
|
||||
echo "Set VERGABE_IMAGE_TAG=<sha> for vergabe production deploys." >&2; \
|
||||
echo "Example: VERGABE_IMAGE_TAG=1061f3e make vergabe-deploy" >&2; \
|
||||
exit 1; \
|
||||
}
|
||||
|
||||
check-vergabe-image: check-vergabe-image-tag ## Verify the vergabe OCI image exists on Forgejo before deploy
|
||||
tools/check-oci-image.sh "$(VERGABE_IMAGE_REF)"
|
||||
|
||||
vergabe-dry-run: check-vergabe-image-tag ## helm template render (no apply) for inspection
|
||||
helm template $(VERGABE_RELEASE) $(VERGABE_CHART) \
|
||||
--namespace $(VERGABE_NAMESPACE) \
|
||||
-f $(VERGABE_VALUES) $(VERGABE_IMAGE_SET_ARG)
|
||||
|
||||
vergabe-deploy: check-production-kubeconfig check-vergabe-image ## Deploy / upgrade vergabe-teilnahme Helm release
|
||||
KUBECONFIG="$(VERGABE_KUBECONFIG)" helm upgrade --install $(VERGABE_RELEASE) $(VERGABE_CHART) \
|
||||
--namespace $(VERGABE_NAMESPACE) --create-namespace \
|
||||
-f $(VERGABE_VALUES) $(VERGABE_IMAGE_SET_ARG) --wait --timeout 3m
|
||||
|
||||
vergabe-ingress-deploy: ## Apply the vergabe-teilnahme ingress (whywhynot.de)
|
||||
kubectl apply -f $(VERGABE_INGRESS)
|
||||
|
||||
vergabe-status: check-production-kubeconfig ## Show vergabe-teilnahme pod / svc / ingress / cert state
|
||||
KUBECONFIG="$(VERGABE_KUBECONFIG)" kubectl get pods,svc,ingress,certificate -n $(VERGABE_NAMESPACE) -l app.kubernetes.io/instance=$(VERGABE_RELEASE) --ignore-not-found
|
||||
|
||||
vergabe-migrate: ## Run Django migrations against the live deployment
|
||||
kubectl exec -n $(VERGABE_NAMESPACE) deploy/$(VERGABE_RELEASE) -- python manage.py migrate --noinput
|
||||
|
||||
vergabe-seed: ## Run the idempotent seed command
|
||||
kubectl exec -n $(VERGABE_NAMESPACE) deploy/$(VERGABE_RELEASE) -- python manage.py seed_dev
|
||||
|
||||
vergabe-superuser: ## Open an interactive shell for createsuperuser
|
||||
kubectl exec -it -n $(VERGABE_NAMESPACE) deploy/$(VERGABE_RELEASE) -- python manage.py createsuperuser
|
||||
|
||||
vergabe-logs: ## Tail vergabe-teilnahme app logs
|
||||
kubectl logs -n $(VERGABE_NAMESPACE) -l app.kubernetes.io/instance=$(VERGABE_RELEASE) -f --tail=50
|
||||
|
||||
vergabe-db-url-secret: ## Rebuild DATABASE_URL with a URL-encoded cnpg password
|
||||
APP_NAMESPACE=$(VERGABE_NAMESPACE) \
|
||||
APP_ENV_SECRET=$(VERGABE_ENV_SECRET) \
|
||||
APP_DB_SECRET=$(VERGABE_DB_SECRET) \
|
||||
APP_DB_USER=$(VERGABE_DB_USER) \
|
||||
APP_DB_HOST=$(VERGABE_DB_HOST) \
|
||||
APP_DB_PORT=$(VERGABE_DB_PORT) \
|
||||
APP_DB_NAME=$(VERGABE_DB_NAME) \
|
||||
tools/build-database-url-secret.sh
|
||||
|
||||
|
||||
|
||||
check-coulomb-social-image-tag: ## Require an explicit coulomb-social image tag
|
||||
@test -n "$(COULOMB_SOCIAL_IMAGE_TAG)" || ( \
|
||||
echo "Set COULOMB_SOCIAL_IMAGE_TAG=<sha> for coulomb-social deploys." >&2; \
|
||||
echo "Example: COULOMB_SOCIAL_IMAGE_TAG=deadbeef make coulomb-social-deploy" >&2; \
|
||||
exit 1)
|
||||
|
||||
coulomb-social-dry-run: check-coulomb-social-image-tag ## helm template render for coulomb-social
|
||||
helm template $(COULOMB_SOCIAL_RELEASE) $(COULOMB_SOCIAL_CHART) \
|
||||
-f $(COULOMB_SOCIAL_VALUES) \
|
||||
--set-string image.tag=$(COULOMB_SOCIAL_IMAGE_TAG) \
|
||||
--namespace $(COULOMB_SOCIAL_NAMESPACE)
|
||||
|
||||
coulomb-social-deploy: check-production-kubeconfig check-coulomb-social-image-tag ## Deploy / upgrade coulomb-social
|
||||
kubectl create namespace $(COULOMB_SOCIAL_NAMESPACE) --dry-run=client -o yaml | kubectl apply -f -
|
||||
helm upgrade --install $(COULOMB_SOCIAL_RELEASE) $(COULOMB_SOCIAL_CHART) \
|
||||
-f $(COULOMB_SOCIAL_VALUES) \
|
||||
--set-string image.tag=$(COULOMB_SOCIAL_IMAGE_TAG) \
|
||||
--namespace $(COULOMB_SOCIAL_NAMESPACE) \
|
||||
--wait --timeout 5m
|
||||
|
||||
coulomb-social-ingress-deploy: ## Apply coulomb-social ingress
|
||||
kubectl apply -f $(COULOMB_SOCIAL_INGRESS)
|
||||
|
||||
coulomb-social-status: check-production-kubeconfig ## Show coulomb-social pod/svc/ingress
|
||||
kubectl -n $(COULOMB_SOCIAL_NAMESPACE) get deploy,svc,ingress,pods
|
||||
|
||||
coulomb-social-logs: ## Tail coulomb-social app logs
|
||||
kubectl -n $(COULOMB_SOCIAL_NAMESPACE) logs -l app.kubernetes.io/name=coulomb-social -f --tail=200
|
||||
|
||||
|
||||
coulomb-social-env-secret: ## Create/patch coulomb-social-env (SECRET_KEY, DATABASE_URL, USER_ENGINE_PROXY_SECRET)
|
||||
@bash tools/create-coulomb-social-env-secret.sh $(COULOMB_SOCIAL_ENV_SECRET_ARGS)
|
||||
|
||||
coulomb-social-env-secret-dry-run: ## Plan coulomb-social-env keys without applying
|
||||
@bash tools/create-coulomb-social-env-secret.sh --dry-run $(COULOMB_SOCIAL_ENV_SECRET_ARGS)
|
||||
##@ Inter-Hub
|
||||
|
||||
inter-hub-render-baseline: ## Render checked-in inter-hub values for chart validation only
|
||||
helm template $(INTER_HUB_RELEASE) $(INTER_HUB_CHART) \
|
||||
--namespace $(INTER_HUB_NAMESPACE) \
|
||||
-f $(INTER_HUB_VALUES)
|
||||
|
||||
inter-hub-dry-run: check-railiance01-kubeconfig check-inter-hub-image-tag ## helm template render with an explicit production image tag
|
||||
KUBECONFIG="$(INTER_HUB_KUBECONFIG)" helm template $(INTER_HUB_RELEASE) $(INTER_HUB_CHART) \
|
||||
--namespace $(INTER_HUB_NAMESPACE) \
|
||||
-f $(INTER_HUB_VALUES) $(INTER_HUB_IMAGE_SET_ARG)
|
||||
|
||||
inter-hub-server-dry-run: check-railiance01-kubeconfig check-inter-hub-image ## Helm server dry-run inter-hub upgrade on Railiance01
|
||||
KUBECONFIG="$(INTER_HUB_KUBECONFIG)" helm upgrade --install $(INTER_HUB_RELEASE) $(INTER_HUB_CHART) \
|
||||
--namespace $(INTER_HUB_NAMESPACE) --create-namespace \
|
||||
-f $(INTER_HUB_VALUES) $(INTER_HUB_IMAGE_SET_ARG) --dry-run=server --timeout 5m
|
||||
|
||||
inter-hub-deploy: check-railiance01-kubeconfig check-inter-hub-image ## Deploy / upgrade inter-hub Helm release on Railiance01
|
||||
KUBECONFIG="$(INTER_HUB_KUBECONFIG)" helm upgrade --install $(INTER_HUB_RELEASE) $(INTER_HUB_CHART) \
|
||||
--namespace $(INTER_HUB_NAMESPACE) --create-namespace \
|
||||
-f $(INTER_HUB_VALUES) $(INTER_HUB_IMAGE_SET_ARG) --wait --timeout 5m
|
||||
|
||||
inter-hub-status: check-railiance01-kubeconfig ## Show inter-hub pod / svc / ingress / cert state on Railiance01
|
||||
KUBECONFIG="$(INTER_HUB_KUBECONFIG)" kubectl get pods,svc,ingress,certificate -n $(INTER_HUB_NAMESPACE) -l app=$(INTER_HUB_RELEASE) --ignore-not-found
|
||||
|
||||
inter-hub-release-info: check-railiance01-kubeconfig ## Show inter-hub Helm history, values, deployment, and pods
|
||||
KUBECONFIG="$(INTER_HUB_KUBECONFIG)" helm history $(INTER_HUB_RELEASE) -n $(INTER_HUB_NAMESPACE)
|
||||
KUBECONFIG="$(INTER_HUB_KUBECONFIG)" helm get values $(INTER_HUB_RELEASE) -n $(INTER_HUB_NAMESPACE)
|
||||
KUBECONFIG="$(INTER_HUB_KUBECONFIG)" kubectl describe deploy/$(INTER_HUB_RELEASE) -n $(INTER_HUB_NAMESPACE)
|
||||
KUBECONFIG="$(INTER_HUB_KUBECONFIG)" kubectl get pods -n $(INTER_HUB_NAMESPACE) -o wide
|
||||
|
||||
inter-hub-smoke: ## Verify public inter-hub v2 route and OpenAPI surface after rollout
|
||||
INTER_HUB_BASE_URL="$(INTER_HUB_BASE_URL)" tools/inter-hub-smoke.sh
|
||||
|
||||
inter-hub-logs: check-railiance01-kubeconfig ## Tail inter-hub app logs from Railiance01
|
||||
KUBECONFIG="$(INTER_HUB_KUBECONFIG)" kubectl logs -n $(INTER_HUB_NAMESPACE) -l app=$(INTER_HUB_RELEASE) -f --tail=50
|
||||
|
||||
##@ Core Hub
|
||||
|
||||
core-hub-helm-adopt: check-production-kubeconfig ## Annotate live Core Hub resources for Helm ownership
|
||||
chmod +x tools/core-hub-helm-adopt.sh
|
||||
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" CORE_HUB_NAMESPACE="$(CORE_HUB_NAMESPACE)" CORE_HUB_RELEASE="$(CORE_HUB_RELEASE)" \
|
||||
tools/core-hub-helm-adopt.sh
|
||||
|
||||
core-hub-render-baseline: ## Render checked-in core-hub values for chart validation only
|
||||
helm template $(CORE_HUB_RELEASE) $(CORE_HUB_CHART) \
|
||||
--namespace $(CORE_HUB_NAMESPACE) \
|
||||
-f $(CORE_HUB_VALUES)
|
||||
|
||||
core-hub-dry-run: check-production-kubeconfig check-core-hub-image-tag ## helm template render with an explicit production image tag
|
||||
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" helm template $(CORE_HUB_RELEASE) $(CORE_HUB_CHART) \
|
||||
--namespace $(CORE_HUB_NAMESPACE) \
|
||||
-f $(CORE_HUB_VALUES) $(CORE_HUB_IMAGE_SET_ARG)
|
||||
|
||||
core-hub-server-dry-run: check-production-kubeconfig check-core-hub-image ## Helm server dry-run core-hub upgrade on CoulombCore
|
||||
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" helm upgrade --install $(CORE_HUB_RELEASE) $(CORE_HUB_CHART) \
|
||||
--namespace $(CORE_HUB_NAMESPACE) --create-namespace \
|
||||
-f $(CORE_HUB_VALUES) $(CORE_HUB_IMAGE_SET_ARG) --dry-run=server --timeout 5m
|
||||
|
||||
core-hub-deploy: check-production-kubeconfig check-core-hub-image ## Deploy / upgrade core-hub Helm release on CoulombCore
|
||||
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" helm upgrade --install $(CORE_HUB_RELEASE) $(CORE_HUB_CHART) \
|
||||
--namespace $(CORE_HUB_NAMESPACE) --create-namespace \
|
||||
-f $(CORE_HUB_VALUES) $(CORE_HUB_IMAGE_SET_ARG) --wait --timeout 5m
|
||||
|
||||
core-hub-migrate: check-production-kubeconfig check-core-hub-image ## Run Alembic migrations via a one-off Job
|
||||
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" kubectl -n $(CORE_HUB_NAMESPACE) delete job $(CORE_HUB_RELEASE)-prod-migrate --ignore-not-found
|
||||
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" helm template $(CORE_HUB_RELEASE) $(CORE_HUB_CHART) \
|
||||
--namespace $(CORE_HUB_NAMESPACE) \
|
||||
-f $(CORE_HUB_VALUES) $(CORE_HUB_IMAGE_SET_ARG) \
|
||||
--set migration.enabled=true | KUBECONFIG="$(CORE_HUB_KUBECONFIG)" kubectl apply -f -
|
||||
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" kubectl -n $(CORE_HUB_NAMESPACE) wait --for=condition=complete \
|
||||
job/$(CORE_HUB_RELEASE)-prod-migrate --timeout=180s
|
||||
|
||||
core-hub-status: check-production-kubeconfig ## Show core-hub pod / svc / ingress / cert state on CoulombCore
|
||||
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" kubectl get pods,svc,ingress,certificate -n $(CORE_HUB_NAMESPACE) \
|
||||
-l app.kubernetes.io/name=core-hub --ignore-not-found
|
||||
|
||||
core-hub-release-info: check-production-kubeconfig ## Show core-hub Helm history, values, deployment, and pods
|
||||
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" helm history $(CORE_HUB_RELEASE) -n $(CORE_HUB_NAMESPACE)
|
||||
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" helm get values $(CORE_HUB_RELEASE) -n $(CORE_HUB_NAMESPACE)
|
||||
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" kubectl describe deploy/$(CORE_HUB_RELEASE)-api -n $(CORE_HUB_NAMESPACE)
|
||||
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" kubectl get pods -n $(CORE_HUB_NAMESPACE) -o wide
|
||||
|
||||
core-hub-smoke: ## Verify public Core Hub health and v2 API surface after rollout
|
||||
CORE_HUB_BASE_URL="$(CORE_HUB_BASE_URL)" tools/core-hub-smoke.sh
|
||||
|
||||
core-hub-logs: check-production-kubeconfig ## Tail core-hub app logs from CoulombCore
|
||||
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" kubectl logs -n $(CORE_HUB_NAMESPACE) \
|
||||
-l app.kubernetes.io/name=core-hub,app.kubernetes.io/component=api -f --tail=50
|
||||
|
||||
core-hub-staging-dry-run: check-production-kubeconfig check-core-hub-image-tag ## helm template render for core-hub staging
|
||||
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" helm template $(CORE_HUB_RELEASE) $(CORE_HUB_CHART) \
|
||||
--namespace $(CORE_HUB_STAGING_NAMESPACE) \
|
||||
-f $(CORE_HUB_STAGING_VALUES) $(CORE_HUB_IMAGE_SET_ARG)
|
||||
|
||||
core-hub-staging-deploy: check-production-kubeconfig check-core-hub-image ## Deploy / upgrade core-hub staging release
|
||||
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" helm upgrade --install $(CORE_HUB_RELEASE) $(CORE_HUB_CHART) \
|
||||
--namespace $(CORE_HUB_STAGING_NAMESPACE) --create-namespace \
|
||||
-f $(CORE_HUB_STAGING_VALUES) $(CORE_HUB_IMAGE_SET_ARG) --wait --timeout 5m
|
||||
|
||||
core-hub-staging-migrate: check-production-kubeconfig check-core-hub-image ## Run Alembic migrations for core-hub staging
|
||||
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" kubectl -n $(CORE_HUB_STAGING_NAMESPACE) delete job $(CORE_HUB_RELEASE)-staging-migrate --ignore-not-found
|
||||
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" helm template $(CORE_HUB_RELEASE) $(CORE_HUB_CHART) \
|
||||
--namespace $(CORE_HUB_STAGING_NAMESPACE) \
|
||||
-f $(CORE_HUB_STAGING_VALUES) $(CORE_HUB_IMAGE_SET_ARG) \
|
||||
--set migration.enabled=true | KUBECONFIG="$(CORE_HUB_KUBECONFIG)" kubectl apply -f -
|
||||
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" kubectl -n $(CORE_HUB_STAGING_NAMESPACE) wait --for=condition=complete \
|
||||
job/$(CORE_HUB_RELEASE)-staging-migrate --timeout=180s
|
||||
|
||||
core-hub-staging-status: check-production-kubeconfig ## Show core-hub staging pod / svc state
|
||||
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" kubectl get pods,svc -n $(CORE_HUB_STAGING_NAMESPACE) \
|
||||
-l app.kubernetes.io/name=core-hub --ignore-not-found
|
||||
|
||||
##@ Forgejo (forgejo.coulomb.social)
|
||||
|
||||
eso-deploy: check-railiance01-kubeconfig ## Install External Secrets Operator on railiance01
|
||||
KUBECONFIG="$(FORGEJO_KUBECONFIG)" helm repo add external-secrets https://charts.external-secrets.io --force-update
|
||||
KUBECONFIG="$(FORGEJO_KUBECONFIG)" helm upgrade --install external-secrets external-secrets/external-secrets \
|
||||
--version $(ESO_CHART_VERSION) \
|
||||
--namespace external-secrets --create-namespace \
|
||||
--set installCRDs=true \
|
||||
--set serviceAccount.create=true \
|
||||
--set serviceAccount.name=external-secrets \
|
||||
--wait --timeout 5m
|
||||
|
||||
forgejo-openbao-eso-token-apply: ## Mint coulombcore OpenBao ESO token + Secret on railiance01 (interactive)
|
||||
tools/forgejo-openbao-eso-token-apply.sh
|
||||
|
||||
forgejo-openbao-store-deploy: check-railiance01-kubeconfig eso-deploy ## Apply openbao-forgejo ClusterSecretStore
|
||||
test -d "$(PLATFORM_REPO)/argocd/platform-addons/openbao-secretstore"
|
||||
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl apply -f "$(PLATFORM_REPO)/argocd/platform-addons/openbao-secretstore/openbao-forgejo.clustersecretstore.yaml"
|
||||
|
||||
forgejo-mailer-es-deploy: check-railiance01-kubeconfig forgejo-openbao-store-deploy ## Apply Forgejo mailer ExternalSecret
|
||||
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl apply -f $(FORGEJO_MAILER_EXTERNAL_SECRET)
|
||||
|
||||
forgejo-mailer-es-status: check-railiance01-kubeconfig ## Show Forgejo mailer ExternalSecret sync status
|
||||
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl get externalsecret,secret -n $(FORGEJO_NAMESPACE) forgejo-mailer
|
||||
|
||||
forgejo-dry-run: check-railiance01-kubeconfig ## helm template render for Forgejo
|
||||
KUBECONFIG="$(FORGEJO_KUBECONFIG)" helm template $(FORGEJO_RELEASE) $(FORGEJO_CHART) \
|
||||
--version $(FORGEJO_CHART_VERSION) \
|
||||
--namespace $(FORGEJO_NAMESPACE) \
|
||||
-f $(FORGEJO_VALUES) \
|
||||
-f $(FORGEJO_REGISTRY_VALUES) \
|
||||
-f <(KUBECONFIG="$(FORGEJO_KUBECONFIG)" tools/forgejo-helm-secrets.sh)
|
||||
|
||||
forgejo-server-dry-run: check-railiance01-kubeconfig ## Helm server dry-run Forgejo upgrade
|
||||
KUBECONFIG="$(FORGEJO_KUBECONFIG)" helm upgrade --install $(FORGEJO_RELEASE) $(FORGEJO_CHART) \
|
||||
--version $(FORGEJO_CHART_VERSION) \
|
||||
--namespace $(FORGEJO_NAMESPACE) --create-namespace \
|
||||
-f $(FORGEJO_VALUES) \
|
||||
-f $(FORGEJO_REGISTRY_VALUES) \
|
||||
-f <(KUBECONFIG="$(FORGEJO_KUBECONFIG)" tools/forgejo-helm-secrets.sh) \
|
||||
--dry-run=server --timeout 5m
|
||||
|
||||
forgejo-secret-scan-template-deploy: check-railiance01-kubeconfig ## Install the immutable, non-secret receive-hook template
|
||||
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl apply --server-side -f $(FORGEJO_SECRET_SCAN_TEMPLATE)
|
||||
|
||||
forgejo-deploy: check-railiance01-kubeconfig forgejo-mailer-es-deploy forgejo-secret-scan-template-deploy ## Deploy / upgrade Forgejo on railiance01
|
||||
@test -n "$$(KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl get secret forgejo-mailer -n $(FORGEJO_NAMESPACE) -o name 2>/dev/null)" || { \
|
||||
echo "Missing secret forgejo-mailer in $(FORGEJO_NAMESPACE)." >&2; \
|
||||
echo "Ensure ExternalSecret is Ready and OpenBao role external-secrets-forgejo is configured." >&2; \
|
||||
exit 1; \
|
||||
}
|
||||
KUBECONFIG="$(FORGEJO_KUBECONFIG)" helm repo add gitea-charts https://dl.gitea.com/charts/ --force-update
|
||||
KUBECONFIG="$(FORGEJO_KUBECONFIG)" helm upgrade --install $(FORGEJO_RELEASE) $(FORGEJO_CHART) \
|
||||
--version $(FORGEJO_CHART_VERSION) \
|
||||
--namespace $(FORGEJO_NAMESPACE) --create-namespace \
|
||||
-f $(FORGEJO_VALUES) \
|
||||
-f $(FORGEJO_REGISTRY_VALUES) \
|
||||
-f <(KUBECONFIG="$(FORGEJO_KUBECONFIG)" tools/forgejo-helm-secrets.sh) \
|
||||
--wait --timeout 10m
|
||||
|
||||
forgejo-ingress-deploy: check-railiance01-kubeconfig ## Apply Forgejo HTTPS ingress
|
||||
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl apply -f $(FORGEJO_INGRESS)
|
||||
|
||||
forgejo-ssh-nodeport-deploy: check-railiance01-kubeconfig ## Apply Forgejo SSH NodePort (30022)
|
||||
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl apply -f $(FORGEJO_SSH_NODEPORT)
|
||||
|
||||
forgejo-status: check-railiance01-kubeconfig ## Show Forgejo pods, svc, ingress, cert, database
|
||||
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl get pods,svc,ingress,certificate,pvc -n $(FORGEJO_NAMESPACE) --ignore-not-found
|
||||
@if KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl cnpg status $(FORGEJO_DB_CLUSTER) -n $(FORGEJO_DB_NAMESPACE) >/dev/null 2>&1; then \
|
||||
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl cnpg status $(FORGEJO_DB_CLUSTER) -n $(FORGEJO_DB_NAMESPACE); \
|
||||
else \
|
||||
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl get cluster $(FORGEJO_DB_CLUSTER) -n $(FORGEJO_DB_NAMESPACE) --ignore-not-found; \
|
||||
fi
|
||||
|
||||
forgejo-smoke: ## Verify Forgejo web and OCI registry challenge
|
||||
FORGEJO_BASE_URL="$(FORGEJO_BASE_URL)" tools/forgejo-smoke.sh
|
||||
|
||||
forgejo-npm-smoke: ## T07 npm install smoke (@coulomb/forgejo-npm-probe)
|
||||
tools/forgejo-npm-smoke.sh
|
||||
|
||||
forgejo-verify: check-railiance01-kubeconfig ## T05 acceptance: smoke, TLS, DB, mailer ESO, SSH operator, SOPS sentinels
|
||||
FORGEJO_BASE_URL="$(FORGEJO_BASE_URL)" KUBECONFIG="$(FORGEJO_KUBECONFIG)" tools/forgejo-verify.sh
|
||||
|
||||
forgejo-operator-bootstrap: ## Idempotent operator user + SSH key bootstrap (no secrets in Git)
|
||||
FORGEJO_BASE_URL="$(FORGEJO_BASE_URL)" tools/forgejo-operator-bootstrap.sh
|
||||
|
||||
forgejo-breakglass-seal: check-railiance01-kubeconfig ## Founder: rotate break-glass admin password into OpenBao (interactive, production-touching)
|
||||
FORGEJO_KUBECONFIG="$(FORGEJO_KUBECONFIG)" FORGEJO_NAMESPACE="$(FORGEJO_NAMESPACE)" tools/forgejo-breakglass-seal.sh
|
||||
|
||||
forgejo-breakglass-verify: ## Founder: verify one break-glass sign-in and write value-free evidence
|
||||
FORGEJO_BASE_URL="$(FORGEJO_BASE_URL)" tools/forgejo-breakglass-verify.sh
|
||||
|
||||
forgejo-runner-registration-sops-bootstrap: check-railiance01-kubeconfig ## Capture cluster runner token into SOPS (operator age key)
|
||||
KUBECONFIG="$(FORGEJO_KUBECONFIG)" tools/forgejo-runner-registration-sops-bootstrap.sh
|
||||
|
||||
forgejo-secrets-check: ## Verify Forgejo SOPS sentinels exist and are encrypted
|
||||
@test -f $(FORGEJO_SECRETS) && grep -q '^sops:' $(FORGEJO_SECRETS) || { echo "Missing encrypted $(FORGEJO_SECRETS)" >&2; exit 1; }
|
||||
@if [ -f $(FORGEJO_RUNNER_REGISTRATION_SECRET) ]; then \
|
||||
grep -q '^sops:' $(FORGEJO_RUNNER_REGISTRATION_SECRET) || { echo "Runner registration not SOPS-encrypted" >&2; exit 1; }; \
|
||||
else \
|
||||
echo "WARN: $(FORGEJO_RUNNER_REGISTRATION_SECRET) missing — run forgejo-runner-registration-sops-bootstrap on operator host" >&2; \
|
||||
fi
|
||||
@echo "ok: Forgejo SOPS sentinels"
|
||||
|
||||
forgejo-logs: check-railiance01-kubeconfig ## Tail Forgejo application logs
|
||||
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl logs -n $(FORGEJO_NAMESPACE) -l app.kubernetes.io/instance=$(FORGEJO_RELEASE) -f --tail=50
|
||||
|
||||
forgejo-runner-registration-deploy: check-railiance01-kubeconfig check-sops ## Apply SOPS runner registration secret
|
||||
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl apply -f <(sops -d $(FORGEJO_RUNNER_REGISTRATION_SECRET))
|
||||
|
||||
forgejo-runner-deploy: check-railiance01-kubeconfig ## Deploy in-cluster Forgejo Actions runner (ADR-004)
|
||||
@test -n "$$(KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl get secret forgejo-runner-registration -n $(FORGEJO_NAMESPACE) -o name 2>/dev/null)" || { \
|
||||
echo "Missing secret forgejo-runner-registration in $(FORGEJO_NAMESPACE)." >&2; \
|
||||
echo "Apply helm/forgejo-runner-registration.sops.yaml or bootstrap once:" >&2; \
|
||||
echo " kubectl create secret generic forgejo-runner-registration -n forgejo --from-literal=token=<token>" >&2; \
|
||||
exit 1; \
|
||||
}
|
||||
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl apply -f $(FORGEJO_RUNNER_MANIFEST)
|
||||
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl rollout status deployment/forgejo-runner -n $(FORGEJO_NAMESPACE) --timeout=5m
|
||||
|
||||
forgejo-runner-status: check-railiance01-kubeconfig ## Show in-cluster runner pod and PVC state
|
||||
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl get deploy,pods,pvc -n $(FORGEJO_NAMESPACE) -l app.kubernetes.io/name=forgejo-runner --ignore-not-found
|
||||
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl logs -n $(FORGEJO_NAMESPACE) deploy/forgejo-runner -c runner --tail=20 2>/dev/null || true
|
||||
|
||||
forgejo-runner-logs: check-railiance01-kubeconfig ## Tail in-cluster runner logs
|
||||
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl logs -n $(FORGEJO_NAMESPACE) deploy/forgejo-runner -c runner -f --tail=50
|
||||
|
||||
##@ reuse-surface (reuse.coulomb.social)
|
||||
|
||||
reuse-dry-run: check-railiance01-kubeconfig ## helm template render (no apply) for reuse-surface
|
||||
KUBECONFIG="$(REUSE_KUBECONFIG)" helm template $(REUSE_RELEASE) $(REUSE_CHART) \
|
||||
--namespace $(REUSE_NAMESPACE) \
|
||||
-f $(REUSE_VALUES)
|
||||
|
||||
reuse-deploy: check-railiance01-kubeconfig ## Deploy / upgrade reuse-surface Helm release on Railiance01
|
||||
KUBECONFIG="$(REUSE_KUBECONFIG)" helm upgrade --install $(REUSE_RELEASE) $(REUSE_CHART) \
|
||||
--namespace $(REUSE_NAMESPACE) --create-namespace \
|
||||
-f $(REUSE_VALUES) --wait --timeout 5m
|
||||
|
||||
reuse-status: check-railiance01-kubeconfig ## Show reuse-surface pod / svc / ingress / cert state on Railiance01
|
||||
KUBECONFIG="$(REUSE_KUBECONFIG)" kubectl get pods,svc,ingress,pvc,certificate -n $(REUSE_NAMESPACE) -l app.kubernetes.io/instance=$(REUSE_RELEASE) --ignore-not-found
|
||||
|
||||
reuse-smoke: check-railiance01-kubeconfig ## Verify reuse public health, repo list, and TLS certificate readiness
|
||||
curl -fsS "$(REUSE_URL)/health"
|
||||
curl -fsS "$(REUSE_URL)/v1/repos"
|
||||
KUBECONFIG="$(REUSE_KUBECONFIG)" kubectl wait -n $(REUSE_NAMESPACE) --for=condition=Ready certificate/$(REUSE_CERTIFICATE) --timeout=30s
|
||||
|
||||
reuse-logs: check-railiance01-kubeconfig ## Tail reuse-surface service logs from Railiance01
|
||||
KUBECONFIG="$(REUSE_KUBECONFIG)" kubectl logs -n $(REUSE_NAMESPACE) -l app.kubernetes.io/instance=$(REUSE_RELEASE) -f --tail=50
|
||||
|
||||
reuse-forgejo-webhook: check-railiance01-kubeconfig ## Idempotent org webhook on Forgejo -> reuse hub (REUSE-WP-0019-T03)
|
||||
bash tools/reuse-forgejo-webhook.sh
|
||||
|
||||
reuse-openbao-eso-token-apply: ## Mint coulombcore OpenBao ESO token + Secret on railiance01 (CCR-2026-0005)
|
||||
OPENBAO_TOKEN_FILE="$(HOME)/.local/openbao/platform-admin.token" bash tools/reuse-openbao-eso-token-apply.sh
|
||||
|
||||
reuse-openbao-store-deploy: check-railiance01-kubeconfig eso-deploy ## Apply openbao-reuse ClusterSecretStore
|
||||
test -d "$(PLATFORM_REPO)/argocd/platform-addons/openbao-secretstore"
|
||||
KUBECONFIG="$(REUSE_KUBECONFIG)" kubectl apply -f "$(PLATFORM_REPO)/argocd/platform-addons/openbao-secretstore/openbao-reuse.clustersecretstore.yaml"
|
||||
|
||||
reuse-runtime-es-deploy: check-railiance01-kubeconfig reuse-openbao-store-deploy ## Apply reuse-surface runtime ExternalSecret
|
||||
KUBECONFIG="$(REUSE_KUBECONFIG)" kubectl apply -f $(REUSE_RUNTIME_EXTERNAL_SECRET)
|
||||
|
||||
reuse-runtime-es-status: check-railiance01-kubeconfig ## Show reuse-surface runtime ExternalSecret sync status
|
||||
KUBECONFIG="$(REUSE_KUBECONFIG)" kubectl get externalsecret,secret -n $(REUSE_NAMESPACE) reuse-surface-runtime reuse-surface-env
|
||||
|
||||
reuse-webhook-smoke: check-railiance01-kubeconfig ## Webhook + ESO + federated smoke (RAILIANCE-WP-0011-T04)
|
||||
bash tools/reuse-webhook-smoke.sh
|
||||
|
||||
##@ policy-nexus (policy.coulomb.social)
|
||||
|
||||
policy-nexus-check-binding: ## Require a source-controlled, immutable production binding
|
||||
@test -d "$(POLICY_NEXUS_RAPP)" || { echo "Missing sibling rApp repo: $(POLICY_NEXUS_RAPP)" >&2; exit 2; }
|
||||
python3 tools/check-policy-nexus-binding.py "$(POLICY_NEXUS_BINDING)"
|
||||
|
||||
policy-nexus-check-image: policy-nexus-check-binding ## Verify the bound OCI digest exists in Forgejo
|
||||
tools/check-oci-image.sh "$(POLICY_NEXUS_IMAGE_REPOSITORY)@$(POLICY_NEXUS_IMAGE_DIGEST)"
|
||||
|
||||
policy-nexus-render: policy-nexus-check-binding ## Render the rApp chart with the approved production binding
|
||||
$(MAKE) -C "$(POLICY_NEXUS_RAPP)" render \
|
||||
IMAGE_REPOSITORY="$(POLICY_NEXUS_IMAGE_REPOSITORY)" \
|
||||
IMAGE_DIGEST="$(POLICY_NEXUS_IMAGE_DIGEST)" \
|
||||
PUBLICATION_MANIFEST_DIGEST="$(POLICY_NEXUS_PUBLICATION_DIGEST)" \
|
||||
SOURCE_INVENTORY_DIGEST="$(POLICY_NEXUS_SOURCE_INVENTORY_DIGEST)" \
|
||||
SOURCE_SET_DIGEST="$(POLICY_NEXUS_SOURCE_SET_DIGEST)"
|
||||
|
||||
policy-nexus-server-dry-run: policy-nexus-check-binding ## Validate the approved policy package against Railiance01
|
||||
$(MAKE) -C "$(POLICY_NEXUS_RAPP)" server-dry-run TARGET="$(POLICY_NEXUS_TARGET)" \
|
||||
IMAGE_REPOSITORY="$(POLICY_NEXUS_IMAGE_REPOSITORY)" \
|
||||
IMAGE_DIGEST="$(POLICY_NEXUS_IMAGE_DIGEST)" \
|
||||
PUBLICATION_MANIFEST_DIGEST="$(POLICY_NEXUS_PUBLICATION_DIGEST)" \
|
||||
SOURCE_INVENTORY_DIGEST="$(POLICY_NEXUS_SOURCE_INVENTORY_DIGEST)" \
|
||||
SOURCE_SET_DIGEST="$(POLICY_NEXUS_SOURCE_SET_DIGEST)"
|
||||
|
||||
policy-nexus-deploy: policy-nexus-check-binding ## Deploy the approved policy release through its rApp package
|
||||
$(MAKE) -C "$(POLICY_NEXUS_RAPP)" deploy TARGET="$(POLICY_NEXUS_TARGET)" \
|
||||
IMAGE_REPOSITORY="$(POLICY_NEXUS_IMAGE_REPOSITORY)" \
|
||||
IMAGE_DIGEST="$(POLICY_NEXUS_IMAGE_DIGEST)" \
|
||||
PUBLICATION_MANIFEST_DIGEST="$(POLICY_NEXUS_PUBLICATION_DIGEST)" \
|
||||
SOURCE_INVENTORY_DIGEST="$(POLICY_NEXUS_SOURCE_INVENTORY_DIGEST)" \
|
||||
SOURCE_SET_DIGEST="$(POLICY_NEXUS_SOURCE_SET_DIGEST)"
|
||||
|
||||
policy-nexus-status: ## Show the policy-nexus Helm release state
|
||||
$(MAKE) -C "$(POLICY_NEXUS_RAPP)" status TARGET="$(POLICY_NEXUS_TARGET)"
|
||||
|
||||
policy-nexus-smoke: policy-nexus-check-binding ## Verify public content, TLS, and both immutable release identities
|
||||
$(MAKE) -C "$(POLICY_NEXUS_RAPP)" verify-live TARGET="$(POLICY_NEXUS_TARGET)" \
|
||||
IMAGE_REPOSITORY="$(POLICY_NEXUS_IMAGE_REPOSITORY)" \
|
||||
IMAGE_DIGEST="$(POLICY_NEXUS_IMAGE_DIGEST)" \
|
||||
PUBLICATION_MANIFEST_DIGEST="$(POLICY_NEXUS_PUBLICATION_DIGEST)" \
|
||||
SOURCE_INVENTORY_DIGEST="$(POLICY_NEXUS_SOURCE_INVENTORY_DIGEST)" \
|
||||
SOURCE_SET_DIGEST="$(POLICY_NEXUS_SOURCE_SET_DIGEST)"
|
||||
|
||||
policy-nexus-rollback: ## Roll back to a named previous Helm revision
|
||||
@test -n "$(POLICY_NEXUS_REVISION)" || { echo "POLICY_NEXUS_REVISION is required" >&2; exit 2; }
|
||||
$(MAKE) -C "$(POLICY_NEXUS_RAPP)" rollback TARGET="$(POLICY_NEXUS_TARGET)" REVISION="$(POLICY_NEXUS_REVISION)"
|
||||
|
||||
##@ Help
|
||||
|
||||
help: ## Show this help
|
||||
@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m<target>\033[0m\n"} \
|
||||
/^[a-zA-Z0-9_-]+:.*?##/ { printf " \033[36m%-20s\033[0m %s\n", $$1, $$2 } \
|
||||
/^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) }' $(MAKEFILE_LIST)
|
||||
|
||||
.PHONY: check-tools check-sops k8s-server-dry-run apps-pg-status apps-pg-restore-drill check-railiance01-kubeconfig check-inter-hub-image-tag check-inter-hub-image check-vergabe-image-tag check-vergabe-image vergabe-dry-run vergabe-deploy vergabe-ingress-deploy vergabe-status vergabe-migrate vergabe-seed vergabe-superuser vergabe-logs vergabe-db-url-secret check-coulomb-social-image-tag coulomb-social-dry-run coulomb-social-deploy coulomb-social-ingress-deploy coulomb-social-status coulomb-social-logs coulomb-social-env-secret coulomb-social-env-secret-dry-run eso-deploy forgejo-openbao-eso-token-apply forgejo-openbao-store-deploy forgejo-mailer-es-deploy forgejo-mailer-es-status forgejo-dry-run forgejo-server-dry-run forgejo-deploy forgejo-ingress-deploy forgejo-ssh-nodeport-deploy forgejo-status forgejo-smoke forgejo-npm-smoke forgejo-verify forgejo-operator-bootstrap forgejo-runner-registration-sops-bootstrap forgejo-secrets-check forgejo-logs forgejo-runner-registration-deploy forgejo-runner-deploy forgejo-runner-status forgejo-runner-logs inter-hub-render-baseline inter-hub-dry-run inter-hub-server-dry-run inter-hub-deploy inter-hub-status inter-hub-release-info inter-hub-smoke inter-hub-logs reuse-dry-run reuse-deploy reuse-status reuse-smoke reuse-logs reuse-forgejo-webhook reuse-openbao-eso-token-apply reuse-openbao-store-deploy reuse-runtime-es-deploy reuse-runtime-es-status reuse-webhook-smoke policy-nexus-check-binding policy-nexus-check-image policy-nexus-render policy-nexus-server-dry-run policy-nexus-deploy policy-nexus-status policy-nexus-smoke policy-nexus-rollback help
|
||||
|
||||
.PHONY: forgejo-secret-scan-template-deploy
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"all_owned_exec_handles_complete": true,
|
||||
"live_exec_handles": [],
|
||||
"live_wait_cells": [],
|
||||
"live_owned_build_processes": [],
|
||||
"completed_handles": [
|
||||
40476,
|
||||
7562,
|
||||
46393,
|
||||
15845,
|
||||
33305,
|
||||
32691,
|
||||
43522,
|
||||
52717,
|
||||
47031,
|
||||
86788,
|
||||
38632,
|
||||
80551,
|
||||
50174,
|
||||
83373,
|
||||
31050,
|
||||
33100
|
||||
],
|
||||
"application_container_started": false,
|
||||
"note": "Existing Docker daemon/build cache and retained prototype images are not live owned jobs. No restart on observation timeout."
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,684 @@
|
|||
SHELL := /usr/bin/env bash
|
||||
.DEFAULT_GOAL := help
|
||||
|
||||
VERGABE_RELEASE ?= vergabe-teilnahme
|
||||
VERGABE_NAMESPACE ?= vergabe-teilnahme
|
||||
VERGABE_CHART ?= charts/vergabe-teilnahme
|
||||
VERGABE_VALUES ?= helm/vergabe-teilnahme-values.yaml
|
||||
VERGABE_IMAGE_REPOSITORY ?= forgejo.coulomb.social/coulomb/vergabe-teilnahme
|
||||
VERGABE_IMAGE_TAG ?=
|
||||
VERGABE_INGRESS ?= manifests/vergabe-teilnahme-ingress.yaml
|
||||
|
||||
VERGABE_DB_SECRET ?= vergabe-app-credentials
|
||||
VERGABE_ENV_SECRET ?= vergabe-teilnahme-env
|
||||
VERGABE_DB_USER ?= vergabe
|
||||
VERGABE_DB_HOST ?= apps-pg-rw.databases
|
||||
VERGABE_DB_PORT ?= 5432
|
||||
VERGABE_DB_NAME ?= vergabe_db
|
||||
|
||||
COULOMB_SOCIAL_RELEASE ?= coulomb-social
|
||||
COULOMB_SOCIAL_NAMESPACE ?= coulomb-social
|
||||
COULOMB_SOCIAL_CHART ?= charts/coulomb-social
|
||||
COULOMB_SOCIAL_VALUES ?= helm/coulomb-social-values.yaml
|
||||
COULOMB_SOCIAL_IMAGE_REPOSITORY ?= forgejo.coulomb.social/coulomb/coulomb-social
|
||||
COULOMB_SOCIAL_INGRESS ?= manifests/coulomb-social-ingress.yaml
|
||||
COULOMB_SOCIAL_ENV_SECRET ?= coulomb-social-env
|
||||
|
||||
INTER_HUB_RELEASE ?= inter-hub
|
||||
INTER_HUB_NAMESPACE ?= inter-hub
|
||||
INTER_HUB_CHART ?= charts/inter-hub
|
||||
INTER_HUB_VALUES ?= helm/inter-hub-values.yaml
|
||||
INTER_HUB_IMAGE_REPOSITORY ?= forgejo.coulomb.social/coulomb/inter-hub
|
||||
INTER_HUB_IMAGE_TAG ?=
|
||||
INTER_HUB_BASE_URL ?= https://hub.coulomb.social
|
||||
|
||||
REUSE_RELEASE ?= reuse
|
||||
REUSE_NAMESPACE ?= reuse
|
||||
REUSE_CHART ?= charts/reuse-surface
|
||||
REUSE_VALUES ?= helm/reuse-surface-values.yaml
|
||||
REUSE_URL ?= https://reuse.coulomb.social
|
||||
REUSE_CERTIFICATE ?= reuse-surface-tls
|
||||
|
||||
POLICY_NEXUS_RAPP ?= ../rapp-policy-nexus
|
||||
POLICY_NEXUS_BINDING ?= bindings/policy-nexus-production.json
|
||||
POLICY_NEXUS_TARGET ?= railiance01
|
||||
POLICY_NEXUS_IMAGE_REPOSITORY ?= $(shell python3 -c 'import json; print(json.load(open("$(POLICY_NEXUS_BINDING)"))["image_repository"])')
|
||||
POLICY_NEXUS_IMAGE_DIGEST ?= $(shell python3 -c 'import json; print(json.load(open("$(POLICY_NEXUS_BINDING)")).get("image_digest") or "")')
|
||||
POLICY_NEXUS_PUBLICATION_DIGEST ?= $(shell python3 -c 'import json; print(json.load(open("$(POLICY_NEXUS_BINDING)")).get("publication_manifest_digest") or "")')
|
||||
POLICY_NEXUS_SOURCE_INVENTORY_DIGEST ?= $(shell python3 -c 'import json; print(json.load(open("$(POLICY_NEXUS_BINDING)")).get("source_inventory_digest") or "")')
|
||||
POLICY_NEXUS_SOURCE_SET_DIGEST ?= $(shell python3 -c 'import json; print(json.load(open("$(POLICY_NEXUS_BINDING)")).get("source_set_digest") or "")')
|
||||
POLICY_NEXUS_REVISION ?=
|
||||
|
||||
COULOMBCORE_KUBECONFIG ?= $(HOME)/.kube/config
|
||||
RAILIANCE01_KUBECONFIG ?= $(HOME)/.kube/config-railiance01
|
||||
PRODUCTION_KUBECONFIG ?= $(COULOMBCORE_KUBECONFIG)
|
||||
INTER_HUB_KUBECONFIG ?= $(RAILIANCE01_KUBECONFIG)
|
||||
REUSE_KUBECONFIG ?= $(RAILIANCE01_KUBECONFIG)
|
||||
FORGEJO_KUBECONFIG ?= $(RAILIANCE01_KUBECONFIG)
|
||||
FORGEJO_RELEASE ?= forgejo
|
||||
FORGEJO_NAMESPACE ?= forgejo
|
||||
FORGEJO_CHART ?= gitea-charts/gitea
|
||||
# Forgejo 11.x (Gitea 1.22) lacks `gitea config edit-ini`; chart 12.6+ requires Gitea 1.26+.
|
||||
FORGEJO_CHART_VERSION ?= 12.5.0
|
||||
FORGEJO_VALUES ?= helm/forgejo-values.yaml
|
||||
FORGEJO_REGISTRY_VALUES ?= helm/forgejo-registry-values.yaml
|
||||
FORGEJO_SECRETS ?= helm/forgejo-secrets.sops.yaml
|
||||
FORGEJO_SECRET_SCAN_TEMPLATE ?= manifests/forgejo-secret-scan-template.yaml
|
||||
FORGEJO_INGRESS ?= manifests/forgejo-ingress.yaml
|
||||
FORGEJO_SSH_NODEPORT ?= manifests/forgejo-ssh-nodeport.yaml
|
||||
FORGEJO_BASE_URL ?= https://forgejo.coulomb.social
|
||||
FORGEJO_DB_CLUSTER ?= forgejo-db
|
||||
FORGEJO_DB_NAMESPACE ?= databases
|
||||
FORGEJO_RUNNER_MANIFEST ?= manifests/forgejo-runner.yaml
|
||||
FORGEJO_RUNNER_REGISTRATION_SECRET ?= helm/forgejo-runner-registration.sops.yaml
|
||||
FORGEJO_MAILER_EXTERNAL_SECRET ?= manifests/forgejo-mailer-externalsecret.yaml
|
||||
REUSE_RUNTIME_EXTERNAL_SECRET ?= manifests/reuse-surface-runtime-externalsecret.yaml
|
||||
PLATFORM_REPO ?= $(HOME)/railiance-platform
|
||||
ESO_CHART_VERSION ?= 0.16.1
|
||||
INTER_HUB_IMAGE_REF = $(INTER_HUB_IMAGE_REPOSITORY):$(INTER_HUB_IMAGE_TAG)
|
||||
INTER_HUB_IMAGE_SET_ARG = $(if $(strip $(INTER_HUB_IMAGE_TAG)),--set image.tag=$(INTER_HUB_IMAGE_TAG),)
|
||||
|
||||
CORE_HUB_RELEASE ?= core-hub
|
||||
CORE_HUB_NAMESPACE ?= core-hub
|
||||
CORE_HUB_CHART ?= charts/core-hub
|
||||
CORE_HUB_VALUES ?= helm/core-hub-values.yaml
|
||||
CORE_HUB_STAGING_NAMESPACE ?= core-hub-staging
|
||||
CORE_HUB_STAGING_VALUES ?= helm/core-hub-staging-values.yaml
|
||||
CORE_HUB_IMAGE_REPOSITORY ?= forgejo.coulomb.social/coulomb/core-hub
|
||||
CORE_HUB_IMAGE_TAG ?=
|
||||
CORE_HUB_BASE_URL ?= https://hub.coulomb.social
|
||||
CORE_HUB_KUBECONFIG ?= $(PRODUCTION_KUBECONFIG)
|
||||
VERGABE_KUBECONFIG ?= $(PRODUCTION_KUBECONFIG)
|
||||
VERGABE_IMAGE_REF = $(VERGABE_IMAGE_REPOSITORY):$(VERGABE_IMAGE_TAG)
|
||||
VERGABE_IMAGE_SET_ARG = $(if $(strip $(VERGABE_IMAGE_TAG)),--set image.repository=$(VERGABE_IMAGE_REPOSITORY) --set image.tag=$(VERGABE_IMAGE_TAG),)
|
||||
CORE_HUB_IMAGE_REF = $(CORE_HUB_IMAGE_REPOSITORY):$(CORE_HUB_IMAGE_TAG)
|
||||
CORE_HUB_IMAGE_SET_ARG = $(if $(strip $(CORE_HUB_IMAGE_TAG)),--set image.tag=$(CORE_HUB_IMAGE_TAG),)
|
||||
|
||||
SOPS_SENTINEL ?= $(FORGEJO_SECRETS)
|
||||
DRY_RUN_CREATE_NAMESPACES ?= false
|
||||
|
||||
##@ Operator checks
|
||||
|
||||
check-tools: ## Check required operator tools and warn about optional diagnostics
|
||||
tools/check-tools.sh
|
||||
|
||||
check-sops: ## Verify the local SOPS age key can decrypt SOPS_SENTINEL
|
||||
SOPS_SENTINEL="$(SOPS_SENTINEL)" tools/check-sops.sh
|
||||
|
||||
k8s-server-dry-run: ## Server-side dry-run rendered Helm and committed manifests
|
||||
DRY_RUN_CREATE_NAMESPACES=$(DRY_RUN_CREATE_NAMESPACES) tools/k8s-server-dry-run.sh
|
||||
|
||||
apps-pg-status: ## Check the shared apps-pg cnpg cluster
|
||||
@if kubectl cnpg status apps-pg -n databases >/dev/null 2>&1; then \
|
||||
kubectl cnpg status apps-pg -n databases; \
|
||||
else \
|
||||
echo "kubectl cnpg plugin not available; falling back to cnpg resources"; \
|
||||
kubectl get cluster apps-pg -n databases; \
|
||||
kubectl get pods -n databases -l cnpg.io/cluster=apps-pg; \
|
||||
fi
|
||||
|
||||
check-railiance01-kubeconfig: ## Verify Railiance01 production kubeconfig exists
|
||||
@test -r "$(RAILIANCE01_KUBECONFIG)" || { \
|
||||
echo "Missing Railiance01 kubeconfig: $(RAILIANCE01_KUBECONFIG)" >&2; \
|
||||
echo "Restore it with:" >&2; \
|
||||
echo " ssh tegwick@92.205.62.239 'sudo cat /etc/rancher/k3s/k3s.yaml' | sed 's|127.0.0.1|92.205.62.239|' > $(RAILIANCE01_KUBECONFIG)" >&2; \
|
||||
exit 1; \
|
||||
}
|
||||
|
||||
check-coulombcore-kubeconfig: ## Verify CoulombCore kubeconfig and k3s API tunnel
|
||||
@test -r "$(COULOMBCORE_KUBECONFIG)" || { \
|
||||
echo "Missing CoulombCore kubeconfig: $(COULOMBCORE_KUBECONFIG)" >&2; \
|
||||
echo "Ensure ops-bridge tunnel is up: bridge up k3s-api-coulombcore" >&2; \
|
||||
exit 1; \
|
||||
}
|
||||
@KUBECONFIG="$(COULOMBCORE_KUBECONFIG)" kubectl get --raw=/readyz >/dev/null 2>&1 || { \
|
||||
echo "CoulombCore API unreachable via $(COULOMBCORE_KUBECONFIG)." >&2; \
|
||||
echo "Run: bridge up k3s-api-coulombcore" >&2; \
|
||||
exit 1; \
|
||||
}
|
||||
|
||||
check-production-kubeconfig: check-coulombcore-kubeconfig ## Verify production app cluster API (CoulombCore)
|
||||
|
||||
check-inter-hub-image-tag: ## Require an explicit inter-hub image tag for production deploys
|
||||
@test -n "$(INTER_HUB_IMAGE_TAG)" || { \
|
||||
echo "Set INTER_HUB_IMAGE_TAG=<sha> for inter-hub production deploys." >&2; \
|
||||
echo "Example: INTER_HUB_IMAGE_TAG=91037a4 make inter-hub-deploy" >&2; \
|
||||
exit 1; \
|
||||
}
|
||||
|
||||
check-inter-hub-image: check-inter-hub-image-tag ## Verify the inter-hub OCI image tag exists before deploy
|
||||
tools/check-oci-image.sh "$(INTER_HUB_IMAGE_REF)"
|
||||
|
||||
check-core-hub-image-tag: ## Require an explicit core-hub image tag for production deploys
|
||||
@test -n "$(CORE_HUB_IMAGE_TAG)" || { \
|
||||
echo "Set CORE_HUB_IMAGE_TAG=<sha> for core-hub production deploys." >&2; \
|
||||
echo "Example: CORE_HUB_IMAGE_TAG=59bf1e4 make core-hub-deploy" >&2; \
|
||||
exit 1; \
|
||||
}
|
||||
|
||||
check-core-hub-image: check-core-hub-image-tag ## Verify the core-hub OCI image tag exists before deploy
|
||||
tools/check-oci-image.sh "$(CORE_HUB_IMAGE_REF)"
|
||||
|
||||
cnpg-backup-status: check-production-kubeconfig ## Report Option A / ScheduledBackup coverage
|
||||
KUBECONFIG="$(PRODUCTION_KUBECONFIG)" tools/cnpg-backup-status.sh
|
||||
|
||||
cnpg-backup-offsite-secret-apply: check-production-kubeconfig ## Materialize databases/cnpg-backup-offsite from OpenBao (T02)
|
||||
chmod +x tools/cnpg-backup-offsite-secret-apply.sh
|
||||
KUBECONFIG="$(PRODUCTION_KUBECONFIG)" tools/cnpg-backup-offsite-secret-apply.sh
|
||||
|
||||
cnpg-backup-offsite-secret-dry-run: check-production-kubeconfig ## Dry-run Secret materialization (no apply)
|
||||
chmod +x tools/cnpg-backup-offsite-secret-apply.sh
|
||||
KUBECONFIG="$(PRODUCTION_KUBECONFIG)" DRY_RUN=1 tools/cnpg-backup-offsite-secret-apply.sh
|
||||
|
||||
cnpg-option-a-apply: check-production-kubeconfig ## Apply Option A CronJobs + RBAC (production-touching)
|
||||
KUBECONFIG="$(PRODUCTION_KUBECONFIG)" kubectl apply -f manifests/cnpg-option-a-backup.yaml
|
||||
|
||||
cnpg-option-a-dry-run: check-production-kubeconfig ## Server-side dry-run of Option A manifests
|
||||
KUBECONFIG="$(PRODUCTION_KUBECONFIG)" kubectl apply --dry-run=server -f manifests/cnpg-option-a-backup.yaml
|
||||
|
||||
cnpg-logical-backup-dry-run: check-production-kubeconfig ## Logical dump + age for all four clusters (no upload)
|
||||
chmod +x tools/cnpg-logical-backup.sh
|
||||
KUBECONFIG="$(PRODUCTION_KUBECONFIG)" PLATFORM_REPO="$(PLATFORM_REPO)" RAILIANCE_BACKUP_DRY_RUN=1 \
|
||||
tools/cnpg-logical-backup.sh
|
||||
|
||||
cnpg-logical-backup: check-production-kubeconfig ## Logical dump + age + Nextcloud for all four clusters
|
||||
chmod +x tools/cnpg-logical-backup.sh
|
||||
@KUBECONFIG="$(PRODUCTION_KUBECONFIG)" PLATFORM_REPO="$(PLATFORM_REPO)" bao kv metadata get \
|
||||
platform/workloads/railiance/backup/offsite-lane >/dev/null 2>&1 || { \
|
||||
echo "ERROR: OpenBao backup-lane auth missing." >&2; \
|
||||
echo "Run: bao login -method=oidc -path=netkingdom role=railiance-backup-workload-kv-read" >&2; \
|
||||
exit 1; \
|
||||
}
|
||||
KUBECONFIG="$(PRODUCTION_KUBECONFIG)" PLATFORM_REPO="$(PLATFORM_REPO)" RAILIANCE_BACKUP_DRY_RUN=0 \
|
||||
tools/cnpg-logical-backup.sh
|
||||
|
||||
##@ Option A multi-host (platform CLI / activity-core)
|
||||
|
||||
cnpg-option-a-backup-dry-run: ## Platform multi-host Option A dump+age (JSON, no upload)
|
||||
chmod +x "$(PLATFORM_REPO)/tools/cmd/cnpg-option-a-backup"
|
||||
KUBECONFIG_CORE="$(COULOMBCORE_KUBECONFIG)" KUBECONFIG_R01="$(RAILIANCE01_KUBECONFIG)" \
|
||||
RAILIANCE_BACKUP_DRY_RUN=1 "$(PLATFORM_REPO)/tools/cmd/cnpg-option-a-backup"
|
||||
|
||||
cnpg-option-a-backup: ## Platform multi-host Option A dump+age+upload (JSON)
|
||||
chmod +x "$(PLATFORM_REPO)/tools/cmd/cnpg-option-a-backup"
|
||||
@bao kv metadata get platform/workloads/railiance/backup/offsite-lane >/dev/null 2>&1 || { \
|
||||
echo "ERROR: OpenBao backup-lane auth missing or set RAILIANCE_BACKUP_NC_TOKEN." >&2; \
|
||||
exit 1; \
|
||||
}
|
||||
KUBECONFIG_CORE="$(COULOMBCORE_KUBECONFIG)" KUBECONFIG_R01="$(RAILIANCE01_KUBECONFIG)" \
|
||||
RAILIANCE_BACKUP_DRY_RUN=0 "$(PLATFORM_REPO)/tools/cmd/cnpg-option-a-backup"
|
||||
|
||||
apps-pg-backup-dry-run: check-production-kubeconfig ## Logical pg_dump + age encrypt for vergabe_db (no upload)
|
||||
chmod +x tools/apps-pg-backup-dry-run.sh
|
||||
KUBECONFIG="$(PRODUCTION_KUBECONFIG)" PLATFORM_REPO="$(PLATFORM_REPO)" RAILIANCE_BACKUP_DRY_RUN=1 \
|
||||
tools/apps-pg-backup-dry-run.sh
|
||||
|
||||
apps-pg-restore-drill: check-production-kubeconfig ## Isolated vergabe_db restore drill with row-count gate
|
||||
chmod +x tools/apps-pg-restore-drill.sh
|
||||
KUBECONFIG="$(PRODUCTION_KUBECONFIG)" tools/apps-pg-restore-drill.sh
|
||||
|
||||
apps-pg-backup: check-production-kubeconfig ## Logical pg_dump + age encrypt + Nextcloud upload for vergabe_db
|
||||
chmod +x tools/apps-pg-backup-dry-run.sh
|
||||
@KUBECONFIG="$(PRODUCTION_KUBECONFIG)" PLATFORM_REPO="$(PLATFORM_REPO)" bao kv metadata get \
|
||||
platform/workloads/railiance/backup/offsite-lane >/dev/null 2>&1 || { \
|
||||
echo "ERROR: OpenBao backup-lane auth missing." >&2; \
|
||||
echo "Run: bao login -method=oidc -path=netkingdom role=railiance-backup-workload-kv-read" >&2; \
|
||||
exit 1; \
|
||||
}
|
||||
KUBECONFIG="$(PRODUCTION_KUBECONFIG)" PLATFORM_REPO="$(PLATFORM_REPO)" RAILIANCE_BACKUP_DRY_RUN=0 \
|
||||
tools/apps-pg-backup-dry-run.sh
|
||||
|
||||
##@ Vergabe Teilnahme
|
||||
|
||||
check-vergabe-image-tag: ## Require an explicit vergabe-teilnahme image tag for production deploys
|
||||
@test -n "$(VERGABE_IMAGE_TAG)" || { \
|
||||
echo "Set VERGABE_IMAGE_TAG=<sha> for vergabe production deploys." >&2; \
|
||||
echo "Example: VERGABE_IMAGE_TAG=1061f3e make vergabe-deploy" >&2; \
|
||||
exit 1; \
|
||||
}
|
||||
|
||||
check-vergabe-image: check-vergabe-image-tag ## Verify the vergabe OCI image exists on Forgejo before deploy
|
||||
tools/check-oci-image.sh "$(VERGABE_IMAGE_REF)"
|
||||
|
||||
vergabe-dry-run: check-vergabe-image-tag ## helm template render (no apply) for inspection
|
||||
helm template $(VERGABE_RELEASE) $(VERGABE_CHART) \
|
||||
--namespace $(VERGABE_NAMESPACE) \
|
||||
-f $(VERGABE_VALUES) $(VERGABE_IMAGE_SET_ARG)
|
||||
|
||||
vergabe-deploy: check-production-kubeconfig check-vergabe-image ## Deploy / upgrade vergabe-teilnahme Helm release
|
||||
KUBECONFIG="$(VERGABE_KUBECONFIG)" helm upgrade --install $(VERGABE_RELEASE) $(VERGABE_CHART) \
|
||||
--namespace $(VERGABE_NAMESPACE) --create-namespace \
|
||||
-f $(VERGABE_VALUES) $(VERGABE_IMAGE_SET_ARG) --wait --timeout 3m
|
||||
|
||||
vergabe-ingress-deploy: ## Apply the vergabe-teilnahme ingress (whywhynot.de)
|
||||
kubectl apply -f $(VERGABE_INGRESS)
|
||||
|
||||
vergabe-status: check-production-kubeconfig ## Show vergabe-teilnahme pod / svc / ingress / cert state
|
||||
KUBECONFIG="$(VERGABE_KUBECONFIG)" kubectl get pods,svc,ingress,certificate -n $(VERGABE_NAMESPACE) -l app.kubernetes.io/instance=$(VERGABE_RELEASE) --ignore-not-found
|
||||
|
||||
vergabe-migrate: ## Run Django migrations against the live deployment
|
||||
kubectl exec -n $(VERGABE_NAMESPACE) deploy/$(VERGABE_RELEASE) -- python manage.py migrate --noinput
|
||||
|
||||
vergabe-seed: ## Run the idempotent seed command
|
||||
kubectl exec -n $(VERGABE_NAMESPACE) deploy/$(VERGABE_RELEASE) -- python manage.py seed_dev
|
||||
|
||||
vergabe-superuser: ## Open an interactive shell for createsuperuser
|
||||
kubectl exec -it -n $(VERGABE_NAMESPACE) deploy/$(VERGABE_RELEASE) -- python manage.py createsuperuser
|
||||
|
||||
vergabe-logs: ## Tail vergabe-teilnahme app logs
|
||||
kubectl logs -n $(VERGABE_NAMESPACE) -l app.kubernetes.io/instance=$(VERGABE_RELEASE) -f --tail=50
|
||||
|
||||
vergabe-db-url-secret: ## Rebuild DATABASE_URL with a URL-encoded cnpg password
|
||||
APP_NAMESPACE=$(VERGABE_NAMESPACE) \
|
||||
APP_ENV_SECRET=$(VERGABE_ENV_SECRET) \
|
||||
APP_DB_SECRET=$(VERGABE_DB_SECRET) \
|
||||
APP_DB_USER=$(VERGABE_DB_USER) \
|
||||
APP_DB_HOST=$(VERGABE_DB_HOST) \
|
||||
APP_DB_PORT=$(VERGABE_DB_PORT) \
|
||||
APP_DB_NAME=$(VERGABE_DB_NAME) \
|
||||
tools/build-database-url-secret.sh
|
||||
|
||||
|
||||
|
||||
check-coulomb-social-image-tag: ## Require an explicit coulomb-social image tag
|
||||
@test -n "$(COULOMB_SOCIAL_IMAGE_TAG)" || ( \
|
||||
echo "Set COULOMB_SOCIAL_IMAGE_TAG=<sha> for coulomb-social deploys." >&2; \
|
||||
echo "Example: COULOMB_SOCIAL_IMAGE_TAG=deadbeef make coulomb-social-deploy" >&2; \
|
||||
exit 1)
|
||||
|
||||
coulomb-social-dry-run: check-coulomb-social-image-tag ## helm template render for coulomb-social
|
||||
helm template $(COULOMB_SOCIAL_RELEASE) $(COULOMB_SOCIAL_CHART) \
|
||||
-f $(COULOMB_SOCIAL_VALUES) \
|
||||
--set-string image.tag=$(COULOMB_SOCIAL_IMAGE_TAG) \
|
||||
--namespace $(COULOMB_SOCIAL_NAMESPACE)
|
||||
|
||||
coulomb-social-deploy: check-production-kubeconfig check-coulomb-social-image-tag ## Deploy / upgrade coulomb-social
|
||||
kubectl create namespace $(COULOMB_SOCIAL_NAMESPACE) --dry-run=client -o yaml | kubectl apply -f -
|
||||
helm upgrade --install $(COULOMB_SOCIAL_RELEASE) $(COULOMB_SOCIAL_CHART) \
|
||||
-f $(COULOMB_SOCIAL_VALUES) \
|
||||
--set-string image.tag=$(COULOMB_SOCIAL_IMAGE_TAG) \
|
||||
--namespace $(COULOMB_SOCIAL_NAMESPACE) \
|
||||
--wait --timeout 5m
|
||||
|
||||
coulomb-social-ingress-deploy: ## Apply coulomb-social ingress
|
||||
kubectl apply -f $(COULOMB_SOCIAL_INGRESS)
|
||||
|
||||
coulomb-social-status: check-production-kubeconfig ## Show coulomb-social pod/svc/ingress
|
||||
kubectl -n $(COULOMB_SOCIAL_NAMESPACE) get deploy,svc,ingress,pods
|
||||
|
||||
coulomb-social-logs: ## Tail coulomb-social app logs
|
||||
kubectl -n $(COULOMB_SOCIAL_NAMESPACE) logs -l app.kubernetes.io/name=coulomb-social -f --tail=200
|
||||
|
||||
|
||||
coulomb-social-env-secret: ## Create/patch coulomb-social-env (SECRET_KEY, DATABASE_URL, USER_ENGINE_PROXY_SECRET)
|
||||
@bash tools/create-coulomb-social-env-secret.sh $(COULOMB_SOCIAL_ENV_SECRET_ARGS)
|
||||
|
||||
coulomb-social-env-secret-dry-run: ## Plan coulomb-social-env keys without applying
|
||||
@bash tools/create-coulomb-social-env-secret.sh --dry-run $(COULOMB_SOCIAL_ENV_SECRET_ARGS)
|
||||
##@ Inter-Hub
|
||||
|
||||
inter-hub-render-baseline: ## Render checked-in inter-hub values for chart validation only
|
||||
helm template $(INTER_HUB_RELEASE) $(INTER_HUB_CHART) \
|
||||
--namespace $(INTER_HUB_NAMESPACE) \
|
||||
-f $(INTER_HUB_VALUES)
|
||||
|
||||
inter-hub-dry-run: check-railiance01-kubeconfig check-inter-hub-image-tag ## helm template render with an explicit production image tag
|
||||
KUBECONFIG="$(INTER_HUB_KUBECONFIG)" helm template $(INTER_HUB_RELEASE) $(INTER_HUB_CHART) \
|
||||
--namespace $(INTER_HUB_NAMESPACE) \
|
||||
-f $(INTER_HUB_VALUES) $(INTER_HUB_IMAGE_SET_ARG)
|
||||
|
||||
inter-hub-server-dry-run: check-railiance01-kubeconfig check-inter-hub-image ## Helm server dry-run inter-hub upgrade on Railiance01
|
||||
KUBECONFIG="$(INTER_HUB_KUBECONFIG)" helm upgrade --install $(INTER_HUB_RELEASE) $(INTER_HUB_CHART) \
|
||||
--namespace $(INTER_HUB_NAMESPACE) --create-namespace \
|
||||
-f $(INTER_HUB_VALUES) $(INTER_HUB_IMAGE_SET_ARG) --dry-run=server --timeout 5m
|
||||
|
||||
inter-hub-deploy: check-railiance01-kubeconfig check-inter-hub-image ## Deploy / upgrade inter-hub Helm release on Railiance01
|
||||
KUBECONFIG="$(INTER_HUB_KUBECONFIG)" helm upgrade --install $(INTER_HUB_RELEASE) $(INTER_HUB_CHART) \
|
||||
--namespace $(INTER_HUB_NAMESPACE) --create-namespace \
|
||||
-f $(INTER_HUB_VALUES) $(INTER_HUB_IMAGE_SET_ARG) --wait --timeout 5m
|
||||
|
||||
inter-hub-status: check-railiance01-kubeconfig ## Show inter-hub pod / svc / ingress / cert state on Railiance01
|
||||
KUBECONFIG="$(INTER_HUB_KUBECONFIG)" kubectl get pods,svc,ingress,certificate -n $(INTER_HUB_NAMESPACE) -l app=$(INTER_HUB_RELEASE) --ignore-not-found
|
||||
|
||||
inter-hub-release-info: check-railiance01-kubeconfig ## Show inter-hub Helm history, values, deployment, and pods
|
||||
KUBECONFIG="$(INTER_HUB_KUBECONFIG)" helm history $(INTER_HUB_RELEASE) -n $(INTER_HUB_NAMESPACE)
|
||||
KUBECONFIG="$(INTER_HUB_KUBECONFIG)" helm get values $(INTER_HUB_RELEASE) -n $(INTER_HUB_NAMESPACE)
|
||||
KUBECONFIG="$(INTER_HUB_KUBECONFIG)" kubectl describe deploy/$(INTER_HUB_RELEASE) -n $(INTER_HUB_NAMESPACE)
|
||||
KUBECONFIG="$(INTER_HUB_KUBECONFIG)" kubectl get pods -n $(INTER_HUB_NAMESPACE) -o wide
|
||||
|
||||
inter-hub-smoke: ## Verify public inter-hub v2 route and OpenAPI surface after rollout
|
||||
INTER_HUB_BASE_URL="$(INTER_HUB_BASE_URL)" tools/inter-hub-smoke.sh
|
||||
|
||||
inter-hub-logs: check-railiance01-kubeconfig ## Tail inter-hub app logs from Railiance01
|
||||
KUBECONFIG="$(INTER_HUB_KUBECONFIG)" kubectl logs -n $(INTER_HUB_NAMESPACE) -l app=$(INTER_HUB_RELEASE) -f --tail=50
|
||||
|
||||
##@ Core Hub
|
||||
|
||||
core-hub-helm-adopt: check-production-kubeconfig ## Annotate live Core Hub resources for Helm ownership
|
||||
chmod +x tools/core-hub-helm-adopt.sh
|
||||
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" CORE_HUB_NAMESPACE="$(CORE_HUB_NAMESPACE)" CORE_HUB_RELEASE="$(CORE_HUB_RELEASE)" \
|
||||
tools/core-hub-helm-adopt.sh
|
||||
|
||||
core-hub-render-baseline: ## Render checked-in core-hub values for chart validation only
|
||||
helm template $(CORE_HUB_RELEASE) $(CORE_HUB_CHART) \
|
||||
--namespace $(CORE_HUB_NAMESPACE) \
|
||||
-f $(CORE_HUB_VALUES)
|
||||
|
||||
core-hub-dry-run: check-production-kubeconfig check-core-hub-image-tag ## helm template render with an explicit production image tag
|
||||
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" helm template $(CORE_HUB_RELEASE) $(CORE_HUB_CHART) \
|
||||
--namespace $(CORE_HUB_NAMESPACE) \
|
||||
-f $(CORE_HUB_VALUES) $(CORE_HUB_IMAGE_SET_ARG)
|
||||
|
||||
core-hub-server-dry-run: check-production-kubeconfig check-core-hub-image ## Helm server dry-run core-hub upgrade on CoulombCore
|
||||
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" helm upgrade --install $(CORE_HUB_RELEASE) $(CORE_HUB_CHART) \
|
||||
--namespace $(CORE_HUB_NAMESPACE) --create-namespace \
|
||||
-f $(CORE_HUB_VALUES) $(CORE_HUB_IMAGE_SET_ARG) --dry-run=server --timeout 5m
|
||||
|
||||
core-hub-deploy: check-production-kubeconfig check-core-hub-image ## Deploy / upgrade core-hub Helm release on CoulombCore
|
||||
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" helm upgrade --install $(CORE_HUB_RELEASE) $(CORE_HUB_CHART) \
|
||||
--namespace $(CORE_HUB_NAMESPACE) --create-namespace \
|
||||
-f $(CORE_HUB_VALUES) $(CORE_HUB_IMAGE_SET_ARG) --wait --timeout 5m
|
||||
|
||||
core-hub-migrate: check-production-kubeconfig check-core-hub-image ## Run Alembic migrations via a one-off Job
|
||||
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" kubectl -n $(CORE_HUB_NAMESPACE) delete job $(CORE_HUB_RELEASE)-prod-migrate --ignore-not-found
|
||||
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" helm template $(CORE_HUB_RELEASE) $(CORE_HUB_CHART) \
|
||||
--namespace $(CORE_HUB_NAMESPACE) \
|
||||
-f $(CORE_HUB_VALUES) $(CORE_HUB_IMAGE_SET_ARG) \
|
||||
--set migration.enabled=true | KUBECONFIG="$(CORE_HUB_KUBECONFIG)" kubectl apply -f -
|
||||
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" kubectl -n $(CORE_HUB_NAMESPACE) wait --for=condition=complete \
|
||||
job/$(CORE_HUB_RELEASE)-prod-migrate --timeout=180s
|
||||
|
||||
core-hub-status: check-production-kubeconfig ## Show core-hub pod / svc / ingress / cert state on CoulombCore
|
||||
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" kubectl get pods,svc,ingress,certificate -n $(CORE_HUB_NAMESPACE) \
|
||||
-l app.kubernetes.io/name=core-hub --ignore-not-found
|
||||
|
||||
core-hub-release-info: check-production-kubeconfig ## Show core-hub Helm history, values, deployment, and pods
|
||||
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" helm history $(CORE_HUB_RELEASE) -n $(CORE_HUB_NAMESPACE)
|
||||
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" helm get values $(CORE_HUB_RELEASE) -n $(CORE_HUB_NAMESPACE)
|
||||
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" kubectl describe deploy/$(CORE_HUB_RELEASE)-api -n $(CORE_HUB_NAMESPACE)
|
||||
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" kubectl get pods -n $(CORE_HUB_NAMESPACE) -o wide
|
||||
|
||||
core-hub-smoke: ## Verify public Core Hub health and v2 API surface after rollout
|
||||
CORE_HUB_BASE_URL="$(CORE_HUB_BASE_URL)" tools/core-hub-smoke.sh
|
||||
|
||||
core-hub-logs: check-production-kubeconfig ## Tail core-hub app logs from CoulombCore
|
||||
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" kubectl logs -n $(CORE_HUB_NAMESPACE) \
|
||||
-l app.kubernetes.io/name=core-hub,app.kubernetes.io/component=api -f --tail=50
|
||||
|
||||
core-hub-staging-dry-run: check-production-kubeconfig check-core-hub-image-tag ## helm template render for core-hub staging
|
||||
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" helm template $(CORE_HUB_RELEASE) $(CORE_HUB_CHART) \
|
||||
--namespace $(CORE_HUB_STAGING_NAMESPACE) \
|
||||
-f $(CORE_HUB_STAGING_VALUES) $(CORE_HUB_IMAGE_SET_ARG)
|
||||
|
||||
core-hub-staging-deploy: check-production-kubeconfig check-core-hub-image ## Deploy / upgrade core-hub staging release
|
||||
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" helm upgrade --install $(CORE_HUB_RELEASE) $(CORE_HUB_CHART) \
|
||||
--namespace $(CORE_HUB_STAGING_NAMESPACE) --create-namespace \
|
||||
-f $(CORE_HUB_STAGING_VALUES) $(CORE_HUB_IMAGE_SET_ARG) --wait --timeout 5m
|
||||
|
||||
core-hub-staging-migrate: check-production-kubeconfig check-core-hub-image ## Run Alembic migrations for core-hub staging
|
||||
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" kubectl -n $(CORE_HUB_STAGING_NAMESPACE) delete job $(CORE_HUB_RELEASE)-staging-migrate --ignore-not-found
|
||||
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" helm template $(CORE_HUB_RELEASE) $(CORE_HUB_CHART) \
|
||||
--namespace $(CORE_HUB_STAGING_NAMESPACE) \
|
||||
-f $(CORE_HUB_STAGING_VALUES) $(CORE_HUB_IMAGE_SET_ARG) \
|
||||
--set migration.enabled=true | KUBECONFIG="$(CORE_HUB_KUBECONFIG)" kubectl apply -f -
|
||||
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" kubectl -n $(CORE_HUB_STAGING_NAMESPACE) wait --for=condition=complete \
|
||||
job/$(CORE_HUB_RELEASE)-staging-migrate --timeout=180s
|
||||
|
||||
core-hub-staging-status: check-production-kubeconfig ## Show core-hub staging pod / svc state
|
||||
KUBECONFIG="$(CORE_HUB_KUBECONFIG)" kubectl get pods,svc -n $(CORE_HUB_STAGING_NAMESPACE) \
|
||||
-l app.kubernetes.io/name=core-hub --ignore-not-found
|
||||
|
||||
##@ Forgejo (forgejo.coulomb.social)
|
||||
|
||||
eso-deploy: check-railiance01-kubeconfig ## Install External Secrets Operator on railiance01
|
||||
KUBECONFIG="$(FORGEJO_KUBECONFIG)" helm repo add external-secrets https://charts.external-secrets.io --force-update
|
||||
KUBECONFIG="$(FORGEJO_KUBECONFIG)" helm upgrade --install external-secrets external-secrets/external-secrets \
|
||||
--version $(ESO_CHART_VERSION) \
|
||||
--namespace external-secrets --create-namespace \
|
||||
--set installCRDs=true \
|
||||
--set serviceAccount.create=true \
|
||||
--set serviceAccount.name=external-secrets \
|
||||
--wait --timeout 5m
|
||||
|
||||
forgejo-openbao-eso-token-apply: ## Mint coulombcore OpenBao ESO token + Secret on railiance01 (interactive)
|
||||
tools/forgejo-openbao-eso-token-apply.sh
|
||||
|
||||
forgejo-openbao-store-deploy: check-railiance01-kubeconfig eso-deploy ## Apply openbao-forgejo ClusterSecretStore
|
||||
test -d "$(PLATFORM_REPO)/argocd/platform-addons/openbao-secretstore"
|
||||
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl apply -f "$(PLATFORM_REPO)/argocd/platform-addons/openbao-secretstore/openbao-forgejo.clustersecretstore.yaml"
|
||||
|
||||
forgejo-mailer-es-deploy: check-railiance01-kubeconfig forgejo-openbao-store-deploy ## Apply Forgejo mailer ExternalSecret
|
||||
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl apply -f $(FORGEJO_MAILER_EXTERNAL_SECRET)
|
||||
|
||||
forgejo-mailer-es-status: check-railiance01-kubeconfig ## Show Forgejo mailer ExternalSecret sync status
|
||||
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl get externalsecret,secret -n $(FORGEJO_NAMESPACE) forgejo-mailer
|
||||
|
||||
forgejo-dry-run: check-railiance01-kubeconfig ## helm template render for Forgejo
|
||||
KUBECONFIG="$(FORGEJO_KUBECONFIG)" helm template $(FORGEJO_RELEASE) $(FORGEJO_CHART) \
|
||||
--version $(FORGEJO_CHART_VERSION) \
|
||||
--namespace $(FORGEJO_NAMESPACE) \
|
||||
-f $(FORGEJO_VALUES) \
|
||||
-f $(FORGEJO_REGISTRY_VALUES) \
|
||||
-f <(KUBECONFIG="$(FORGEJO_KUBECONFIG)" tools/forgejo-helm-secrets.sh)
|
||||
|
||||
forgejo-server-dry-run: check-railiance01-kubeconfig ## Helm server dry-run Forgejo upgrade
|
||||
KUBECONFIG="$(FORGEJO_KUBECONFIG)" helm upgrade --install $(FORGEJO_RELEASE) $(FORGEJO_CHART) \
|
||||
--version $(FORGEJO_CHART_VERSION) \
|
||||
--namespace $(FORGEJO_NAMESPACE) --create-namespace \
|
||||
-f $(FORGEJO_VALUES) \
|
||||
-f $(FORGEJO_REGISTRY_VALUES) \
|
||||
-f <(KUBECONFIG="$(FORGEJO_KUBECONFIG)" tools/forgejo-helm-secrets.sh) \
|
||||
--dry-run=server --timeout 5m
|
||||
|
||||
forgejo-secret-scan-template-deploy: check-railiance01-kubeconfig ## Install the immutable, non-secret receive-hook template
|
||||
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl apply --server-side -f $(FORGEJO_SECRET_SCAN_TEMPLATE)
|
||||
|
||||
forgejo-deploy: check-railiance01-kubeconfig forgejo-mailer-es-deploy forgejo-secret-scan-template-deploy ## Deploy / upgrade Forgejo on railiance01
|
||||
@test -n "$$(KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl get secret forgejo-mailer -n $(FORGEJO_NAMESPACE) -o name 2>/dev/null)" || { \
|
||||
echo "Missing secret forgejo-mailer in $(FORGEJO_NAMESPACE)." >&2; \
|
||||
echo "Ensure ExternalSecret is Ready and OpenBao role external-secrets-forgejo is configured." >&2; \
|
||||
exit 1; \
|
||||
}
|
||||
KUBECONFIG="$(FORGEJO_KUBECONFIG)" helm repo add gitea-charts https://dl.gitea.com/charts/ --force-update
|
||||
KUBECONFIG="$(FORGEJO_KUBECONFIG)" helm upgrade --install $(FORGEJO_RELEASE) $(FORGEJO_CHART) \
|
||||
--version $(FORGEJO_CHART_VERSION) \
|
||||
--namespace $(FORGEJO_NAMESPACE) --create-namespace \
|
||||
-f $(FORGEJO_VALUES) \
|
||||
-f $(FORGEJO_REGISTRY_VALUES) \
|
||||
-f <(KUBECONFIG="$(FORGEJO_KUBECONFIG)" tools/forgejo-helm-secrets.sh) \
|
||||
--wait --timeout 10m
|
||||
|
||||
forgejo-ingress-deploy: check-railiance01-kubeconfig ## Apply Forgejo HTTPS ingress
|
||||
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl apply -f $(FORGEJO_INGRESS)
|
||||
|
||||
forgejo-ssh-nodeport-deploy: check-railiance01-kubeconfig ## Apply Forgejo SSH NodePort (30022)
|
||||
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl apply -f $(FORGEJO_SSH_NODEPORT)
|
||||
|
||||
forgejo-status: check-railiance01-kubeconfig ## Show Forgejo pods, svc, ingress, cert, database
|
||||
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl get pods,svc,ingress,certificate,pvc -n $(FORGEJO_NAMESPACE) --ignore-not-found
|
||||
@if KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl cnpg status $(FORGEJO_DB_CLUSTER) -n $(FORGEJO_DB_NAMESPACE) >/dev/null 2>&1; then \
|
||||
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl cnpg status $(FORGEJO_DB_CLUSTER) -n $(FORGEJO_DB_NAMESPACE); \
|
||||
else \
|
||||
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl get cluster $(FORGEJO_DB_CLUSTER) -n $(FORGEJO_DB_NAMESPACE) --ignore-not-found; \
|
||||
fi
|
||||
|
||||
forgejo-smoke: ## Verify Forgejo web and OCI registry challenge
|
||||
FORGEJO_BASE_URL="$(FORGEJO_BASE_URL)" tools/forgejo-smoke.sh
|
||||
|
||||
forgejo-npm-smoke: ## T07 npm install smoke (@coulomb/forgejo-npm-probe)
|
||||
tools/forgejo-npm-smoke.sh
|
||||
|
||||
forgejo-verify: check-railiance01-kubeconfig ## T05 acceptance: smoke, TLS, DB, mailer ESO, SSH operator, SOPS sentinels
|
||||
FORGEJO_BASE_URL="$(FORGEJO_BASE_URL)" KUBECONFIG="$(FORGEJO_KUBECONFIG)" tools/forgejo-verify.sh
|
||||
|
||||
forgejo-operator-bootstrap: ## Idempotent operator user + SSH key bootstrap (no secrets in Git)
|
||||
FORGEJO_BASE_URL="$(FORGEJO_BASE_URL)" tools/forgejo-operator-bootstrap.sh
|
||||
|
||||
forgejo-breakglass-seal: check-railiance01-kubeconfig ## Founder: rotate break-glass admin password into OpenBao (interactive, production-touching)
|
||||
FORGEJO_KUBECONFIG="$(FORGEJO_KUBECONFIG)" FORGEJO_NAMESPACE="$(FORGEJO_NAMESPACE)" tools/forgejo-breakglass-seal.sh
|
||||
|
||||
forgejo-breakglass-verify: ## Founder: verify one break-glass sign-in and write value-free evidence
|
||||
FORGEJO_BASE_URL="$(FORGEJO_BASE_URL)" tools/forgejo-breakglass-verify.sh
|
||||
|
||||
forgejo-runner-registration-sops-bootstrap: check-railiance01-kubeconfig ## Capture cluster runner token into SOPS (operator age key)
|
||||
KUBECONFIG="$(FORGEJO_KUBECONFIG)" tools/forgejo-runner-registration-sops-bootstrap.sh
|
||||
|
||||
forgejo-secrets-check: ## Verify Forgejo SOPS sentinels exist and are encrypted
|
||||
@test -f $(FORGEJO_SECRETS) && grep -q '^sops:' $(FORGEJO_SECRETS) || { echo "Missing encrypted $(FORGEJO_SECRETS)" >&2; exit 1; }
|
||||
@if [ -f $(FORGEJO_RUNNER_REGISTRATION_SECRET) ]; then \
|
||||
grep -q '^sops:' $(FORGEJO_RUNNER_REGISTRATION_SECRET) || { echo "Runner registration not SOPS-encrypted" >&2; exit 1; }; \
|
||||
else \
|
||||
echo "WARN: $(FORGEJO_RUNNER_REGISTRATION_SECRET) missing — run forgejo-runner-registration-sops-bootstrap on operator host" >&2; \
|
||||
fi
|
||||
@echo "ok: Forgejo SOPS sentinels"
|
||||
|
||||
forgejo-logs: check-railiance01-kubeconfig ## Tail Forgejo application logs
|
||||
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl logs -n $(FORGEJO_NAMESPACE) -l app.kubernetes.io/instance=$(FORGEJO_RELEASE) -f --tail=50
|
||||
|
||||
forgejo-runner-registration-deploy: check-railiance01-kubeconfig check-sops ## Apply SOPS runner registration secret
|
||||
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl apply -f <(sops -d $(FORGEJO_RUNNER_REGISTRATION_SECRET))
|
||||
|
||||
forgejo-runner-deploy: check-railiance01-kubeconfig ## Deploy in-cluster Forgejo Actions runner (ADR-004)
|
||||
@test -n "$$(KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl get secret forgejo-runner-registration -n $(FORGEJO_NAMESPACE) -o name 2>/dev/null)" || { \
|
||||
echo "Missing secret forgejo-runner-registration in $(FORGEJO_NAMESPACE)." >&2; \
|
||||
echo "Apply helm/forgejo-runner-registration.sops.yaml or bootstrap once:" >&2; \
|
||||
echo " kubectl create secret generic forgejo-runner-registration -n forgejo --from-literal=token=<token>" >&2; \
|
||||
exit 1; \
|
||||
}
|
||||
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl apply -f $(FORGEJO_RUNNER_MANIFEST)
|
||||
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl rollout status deployment/forgejo-runner -n $(FORGEJO_NAMESPACE) --timeout=5m
|
||||
|
||||
forgejo-runner-status: check-railiance01-kubeconfig ## Show in-cluster runner pod and PVC state
|
||||
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl get deploy,pods,pvc -n $(FORGEJO_NAMESPACE) -l app.kubernetes.io/name=forgejo-runner --ignore-not-found
|
||||
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl logs -n $(FORGEJO_NAMESPACE) deploy/forgejo-runner -c runner --tail=20 2>/dev/null || true
|
||||
|
||||
forgejo-runner-logs: check-railiance01-kubeconfig ## Tail in-cluster runner logs
|
||||
KUBECONFIG="$(FORGEJO_KUBECONFIG)" kubectl logs -n $(FORGEJO_NAMESPACE) deploy/forgejo-runner -c runner -f --tail=50
|
||||
|
||||
##@ reuse-surface (reuse.coulomb.social)
|
||||
|
||||
reuse-dry-run: check-railiance01-kubeconfig ## helm template render (no apply) for reuse-surface
|
||||
KUBECONFIG="$(REUSE_KUBECONFIG)" helm template $(REUSE_RELEASE) $(REUSE_CHART) \
|
||||
--namespace $(REUSE_NAMESPACE) \
|
||||
-f $(REUSE_VALUES)
|
||||
|
||||
reuse-deploy: check-railiance01-kubeconfig ## Deploy / upgrade reuse-surface Helm release on Railiance01
|
||||
KUBECONFIG="$(REUSE_KUBECONFIG)" helm upgrade --install $(REUSE_RELEASE) $(REUSE_CHART) \
|
||||
--namespace $(REUSE_NAMESPACE) --create-namespace \
|
||||
-f $(REUSE_VALUES) --wait --timeout 5m
|
||||
|
||||
reuse-status: check-railiance01-kubeconfig ## Show reuse-surface pod / svc / ingress / cert state on Railiance01
|
||||
KUBECONFIG="$(REUSE_KUBECONFIG)" kubectl get pods,svc,ingress,pvc,certificate -n $(REUSE_NAMESPACE) -l app.kubernetes.io/instance=$(REUSE_RELEASE) --ignore-not-found
|
||||
|
||||
reuse-smoke: check-railiance01-kubeconfig ## Verify reuse public health, repo list, and TLS certificate readiness
|
||||
curl -fsS "$(REUSE_URL)/health"
|
||||
curl -fsS "$(REUSE_URL)/v1/repos"
|
||||
KUBECONFIG="$(REUSE_KUBECONFIG)" kubectl wait -n $(REUSE_NAMESPACE) --for=condition=Ready certificate/$(REUSE_CERTIFICATE) --timeout=30s
|
||||
|
||||
reuse-logs: check-railiance01-kubeconfig ## Tail reuse-surface service logs from Railiance01
|
||||
KUBECONFIG="$(REUSE_KUBECONFIG)" kubectl logs -n $(REUSE_NAMESPACE) -l app.kubernetes.io/instance=$(REUSE_RELEASE) -f --tail=50
|
||||
|
||||
reuse-forgejo-webhook: check-railiance01-kubeconfig ## Idempotent org webhook on Forgejo -> reuse hub (REUSE-WP-0019-T03)
|
||||
bash tools/reuse-forgejo-webhook.sh
|
||||
|
||||
reuse-openbao-eso-token-apply: ## Mint coulombcore OpenBao ESO token + Secret on railiance01 (CCR-2026-0005)
|
||||
OPENBAO_TOKEN_FILE="$(HOME)/.local/openbao/platform-admin.token" bash tools/reuse-openbao-eso-token-apply.sh
|
||||
|
||||
reuse-openbao-store-deploy: check-railiance01-kubeconfig eso-deploy ## Apply openbao-reuse ClusterSecretStore
|
||||
test -d "$(PLATFORM_REPO)/argocd/platform-addons/openbao-secretstore"
|
||||
KUBECONFIG="$(REUSE_KUBECONFIG)" kubectl apply -f "$(PLATFORM_REPO)/argocd/platform-addons/openbao-secretstore/openbao-reuse.clustersecretstore.yaml"
|
||||
|
||||
reuse-runtime-es-deploy: check-railiance01-kubeconfig reuse-openbao-store-deploy ## Apply reuse-surface runtime ExternalSecret
|
||||
KUBECONFIG="$(REUSE_KUBECONFIG)" kubectl apply -f $(REUSE_RUNTIME_EXTERNAL_SECRET)
|
||||
|
||||
reuse-runtime-es-status: check-railiance01-kubeconfig ## Show reuse-surface runtime ExternalSecret sync status
|
||||
KUBECONFIG="$(REUSE_KUBECONFIG)" kubectl get externalsecret,secret -n $(REUSE_NAMESPACE) reuse-surface-runtime reuse-surface-env
|
||||
|
||||
reuse-webhook-smoke: check-railiance01-kubeconfig ## Webhook + ESO + federated smoke (RAILIANCE-WP-0011-T04)
|
||||
bash tools/reuse-webhook-smoke.sh
|
||||
|
||||
##@ policy-nexus (policy.coulomb.social)
|
||||
|
||||
policy-nexus-check-binding: ## Require a source-controlled, immutable production binding
|
||||
@test -d "$(POLICY_NEXUS_RAPP)" || { echo "Missing sibling rApp repo: $(POLICY_NEXUS_RAPP)" >&2; exit 2; }
|
||||
python3 tools/check-policy-nexus-binding.py "$(POLICY_NEXUS_BINDING)"
|
||||
|
||||
policy-nexus-check-image: policy-nexus-check-binding ## Verify the bound OCI digest exists in Forgejo
|
||||
tools/check-oci-image.sh "$(POLICY_NEXUS_IMAGE_REPOSITORY)@$(POLICY_NEXUS_IMAGE_DIGEST)"
|
||||
|
||||
policy-nexus-render: policy-nexus-check-binding ## Render the rApp chart with the approved production binding
|
||||
$(MAKE) -C "$(POLICY_NEXUS_RAPP)" render \
|
||||
IMAGE_REPOSITORY="$(POLICY_NEXUS_IMAGE_REPOSITORY)" \
|
||||
IMAGE_DIGEST="$(POLICY_NEXUS_IMAGE_DIGEST)" \
|
||||
PUBLICATION_MANIFEST_DIGEST="$(POLICY_NEXUS_PUBLICATION_DIGEST)" \
|
||||
SOURCE_INVENTORY_DIGEST="$(POLICY_NEXUS_SOURCE_INVENTORY_DIGEST)" \
|
||||
SOURCE_SET_DIGEST="$(POLICY_NEXUS_SOURCE_SET_DIGEST)"
|
||||
|
||||
policy-nexus-server-dry-run: policy-nexus-check-binding ## Validate the approved policy package against Railiance01
|
||||
$(MAKE) -C "$(POLICY_NEXUS_RAPP)" server-dry-run TARGET="$(POLICY_NEXUS_TARGET)" \
|
||||
IMAGE_REPOSITORY="$(POLICY_NEXUS_IMAGE_REPOSITORY)" \
|
||||
IMAGE_DIGEST="$(POLICY_NEXUS_IMAGE_DIGEST)" \
|
||||
PUBLICATION_MANIFEST_DIGEST="$(POLICY_NEXUS_PUBLICATION_DIGEST)" \
|
||||
SOURCE_INVENTORY_DIGEST="$(POLICY_NEXUS_SOURCE_INVENTORY_DIGEST)" \
|
||||
SOURCE_SET_DIGEST="$(POLICY_NEXUS_SOURCE_SET_DIGEST)"
|
||||
|
||||
policy-nexus-deploy: policy-nexus-check-binding ## Deploy the approved policy release through its rApp package
|
||||
$(MAKE) -C "$(POLICY_NEXUS_RAPP)" deploy TARGET="$(POLICY_NEXUS_TARGET)" \
|
||||
IMAGE_REPOSITORY="$(POLICY_NEXUS_IMAGE_REPOSITORY)" \
|
||||
IMAGE_DIGEST="$(POLICY_NEXUS_IMAGE_DIGEST)" \
|
||||
PUBLICATION_MANIFEST_DIGEST="$(POLICY_NEXUS_PUBLICATION_DIGEST)" \
|
||||
SOURCE_INVENTORY_DIGEST="$(POLICY_NEXUS_SOURCE_INVENTORY_DIGEST)" \
|
||||
SOURCE_SET_DIGEST="$(POLICY_NEXUS_SOURCE_SET_DIGEST)"
|
||||
|
||||
policy-nexus-status: ## Show the policy-nexus Helm release state
|
||||
$(MAKE) -C "$(POLICY_NEXUS_RAPP)" status TARGET="$(POLICY_NEXUS_TARGET)"
|
||||
|
||||
policy-nexus-smoke: policy-nexus-check-binding ## Verify public content, TLS, and both immutable release identities
|
||||
$(MAKE) -C "$(POLICY_NEXUS_RAPP)" verify-live TARGET="$(POLICY_NEXUS_TARGET)" \
|
||||
IMAGE_REPOSITORY="$(POLICY_NEXUS_IMAGE_REPOSITORY)" \
|
||||
IMAGE_DIGEST="$(POLICY_NEXUS_IMAGE_DIGEST)" \
|
||||
PUBLICATION_MANIFEST_DIGEST="$(POLICY_NEXUS_PUBLICATION_DIGEST)" \
|
||||
SOURCE_INVENTORY_DIGEST="$(POLICY_NEXUS_SOURCE_INVENTORY_DIGEST)" \
|
||||
SOURCE_SET_DIGEST="$(POLICY_NEXUS_SOURCE_SET_DIGEST)"
|
||||
|
||||
policy-nexus-rollback: ## Roll back to a named previous Helm revision
|
||||
@test -n "$(POLICY_NEXUS_REVISION)" || { echo "POLICY_NEXUS_REVISION is required" >&2; exit 2; }
|
||||
$(MAKE) -C "$(POLICY_NEXUS_RAPP)" rollback TARGET="$(POLICY_NEXUS_TARGET)" REVISION="$(POLICY_NEXUS_REVISION)"
|
||||
|
||||
##@ Help
|
||||
|
||||
help: ## Show this help
|
||||
@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m<target>\033[0m\n"} \
|
||||
/^[a-zA-Z0-9_-]+:.*?##/ { printf " \033[36m%-20s\033[0m %s\n", $$1, $$2 } \
|
||||
/^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) }' $(MAKEFILE_LIST)
|
||||
|
||||
.PHONY: check-tools check-sops k8s-server-dry-run apps-pg-status apps-pg-restore-drill check-railiance01-kubeconfig check-inter-hub-image-tag check-inter-hub-image check-vergabe-image-tag check-vergabe-image vergabe-dry-run vergabe-deploy vergabe-ingress-deploy vergabe-status vergabe-migrate vergabe-seed vergabe-superuser vergabe-logs vergabe-db-url-secret check-coulomb-social-image-tag coulomb-social-dry-run coulomb-social-deploy coulomb-social-ingress-deploy coulomb-social-status coulomb-social-logs coulomb-social-env-secret coulomb-social-env-secret-dry-run eso-deploy forgejo-openbao-eso-token-apply forgejo-openbao-store-deploy forgejo-mailer-es-deploy forgejo-mailer-es-status forgejo-dry-run forgejo-server-dry-run forgejo-deploy forgejo-ingress-deploy forgejo-ssh-nodeport-deploy forgejo-status forgejo-smoke forgejo-npm-smoke forgejo-verify forgejo-operator-bootstrap forgejo-runner-registration-sops-bootstrap forgejo-secrets-check forgejo-logs forgejo-runner-registration-deploy forgejo-runner-deploy forgejo-runner-status forgejo-runner-logs inter-hub-render-baseline inter-hub-dry-run inter-hub-server-dry-run inter-hub-deploy inter-hub-status inter-hub-release-info inter-hub-smoke inter-hub-logs reuse-dry-run reuse-deploy reuse-status reuse-smoke reuse-logs reuse-forgejo-webhook reuse-openbao-eso-token-apply reuse-openbao-store-deploy reuse-runtime-es-deploy reuse-runtime-es-status reuse-webhook-smoke policy-nexus-check-binding policy-nexus-check-image policy-nexus-render policy-nexus-server-dry-run policy-nexus-deploy policy-nexus-status policy-nexus-smoke policy-nexus-rollback help
|
||||
|
||||
.PHONY: forgejo-secret-scan-template-deploy
|
||||
|
||||
|
||||
##@ Isolated Forgejo source release preparation (no deployment/publication)
|
||||
FORGEJO_BUILD_WORKSPACE ?= /tmp/railiance-apps-forgejo-release
|
||||
FORGEJO_SOURCE_ARCHIVE ?=
|
||||
FORGEJO_GO_ARCHIVE ?=
|
||||
FORGEJO_NODE_ARCHIVE ?=
|
||||
FORGEJO_BUILD_GOMODCACHE ?=
|
||||
FORGEJO_BUILD_GOCACHE_SEED ?=
|
||||
FORGEJO_BUILD_NPM_CACHE_SEED ?=
|
||||
|
||||
.PHONY: forgejo-release-prepare forgejo-release-acquire-npm forgejo-release-build forgejo-release-verify forgejo-release-container
|
||||
forgejo-release-prepare: ## Verify archives/patch/locks and prepare a new isolated workspace
|
||||
/usr/bin/python3 -I tools/forgejo-release-build.py prepare --workspace "$(FORGEJO_BUILD_WORKSPACE)" --source-archive "$(FORGEJO_SOURCE_ARCHIVE)" --go-archive "$(FORGEJO_GO_ARCHIVE)" --node-archive "$(FORGEJO_NODE_ARCHIVE)" --module-cache "$(FORGEJO_BUILD_GOMODCACHE)" $(if $(FORGEJO_BUILD_GOCACHE_SEED),--go-cache-seed "$(FORGEJO_BUILD_GOCACHE_SEED)") $(if $(FORGEJO_BUILD_NPM_CACHE_SEED),--npm-cache-seed "$(FORGEJO_BUILD_NPM_CACHE_SEED)")
|
||||
|
||||
forgejo-release-acquire-npm: ## Explicit public registry dependency acquisition; ignores lifecycle scripts
|
||||
/usr/bin/python3 -I tools/forgejo-release-build.py acquire-npm --workspace "$(FORGEJO_BUILD_WORKSPACE)"
|
||||
|
||||
forgejo-release-build: ## Offline full frontend, bindata and static backend via upstream targets
|
||||
/usr/bin/python3 -I tools/forgejo-release-build.py build --workspace "$(FORGEJO_BUILD_WORKSPACE)"
|
||||
|
||||
forgejo-release-verify: ## Verify all embedded assets, static executable and new CLI shape offline
|
||||
/usr/bin/python3 -I tools/forgejo-release-build.py verify --workspace "$(FORGEJO_BUILD_WORKSPACE)"
|
||||
|
||||
forgejo-release-container: ## Build/verify a deterministic local rootless OCI archive; never publish or deploy
|
||||
/usr/bin/python3 -I tools/forgejo-release-build.py container --workspace "$(FORGEJO_BUILD_WORKSPACE)"
|
||||
|
||||
|
|
@ -0,0 +1,239 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Build-only public Forgejo owner recipe; no deployment or publication code."""
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import tarfile
|
||||
import time
|
||||
import math
|
||||
import datetime
|
||||
|
||||
OWNER=Path(__file__).resolve().parents[1]
|
||||
INPUTS=OWNER/'build/forgejo'
|
||||
LOCK=json.loads((INPUTS/'inputs.lock.json').read_text())
|
||||
|
||||
def digest(path):
|
||||
h=hashlib.sha256()
|
||||
with Path(path).open('rb') as f:
|
||||
for block in iter(lambda:f.read(1024*1024),b''):h.update(block)
|
||||
return h.hexdigest()
|
||||
|
||||
def save(path,data):
|
||||
Path(path).write_text(json.dumps(data,indent=2,sort_keys=True)+'\n')
|
||||
|
||||
def check(path,expected):
|
||||
if digest(path)!=expected:raise ValueError('frozen public input digest differs: '+str(path))
|
||||
|
||||
def base_env(work):
|
||||
return {'PATH':str(work/'node-v22.15.0-linux-x64/bin')+':'+str(work/'go/bin')+':/usr/bin:/bin',
|
||||
'HOME':str(work/'home'),'GOTOOLCHAIN':'local','GOPROXY':'off','GOSUMDB':'off','GOVCS':'*:off',
|
||||
'GOPATH':str(work/'gopath'),'GOMODCACHE':json.loads((work/'prepared.json').read_text())['module_cache'],
|
||||
'GOCACHE':str(work/'gocache'),'CGO_ENABLED':'1','GOMAXPROCS':'4',
|
||||
'GOFLAGS':'-trimpath -buildvcs=false -p=4','SOURCE_DATE_EPOCH':str(LOCK['source_date_epoch']),
|
||||
'TZ':'UTC','LC_ALL':'C','NODE_OPTIONS':'--max-old-space-size=4096',
|
||||
'GIT_CONFIG_NOSYSTEM':'1','GIT_CONFIG_GLOBAL':'/dev/null','GIT_TERMINAL_PROMPT':'0',
|
||||
'NPM_CONFIG_USERCONFIG':str(work/'empty.npmrc'),'NPM_CONFIG_GLOBALCONFIG':str(work/'empty-global.npmrc'),
|
||||
'NPM_CONFIG_CACHE':str(work/'npm-cache'),'NPM_CONFIG_REGISTRY':'https://registry.npmjs.org',
|
||||
'NPM_CONFIG_OFFLINE':'true','NPM_CONFIG_AUDIT':'false','NPM_CONFIG_FUND':'false',
|
||||
'NPM_CONFIG_UPDATE_NOTIFIER':'false','PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD':'1'}
|
||||
|
||||
def sandbox(work,env):
|
||||
cache=Path(env['GOMODCACHE'])
|
||||
return ['bwrap','--unshare-net','--unshare-pid','--ro-bind','/usr','/usr','--ro-bind','/lib','/lib',
|
||||
'--ro-bind','/lib64','/lib64','--symlink','usr/bin','/bin','--proc','/proc','--dev','/dev',
|
||||
'--tmpfs','/tmp','--bind',str(work),str(work),'--ro-bind',str(cache),str(cache),
|
||||
'--chdir',str(work/'source'),'--','env','-i']+[k+'='+v for k,v in env.items()]
|
||||
|
||||
def run(work,name,args,env=None,offline=False,cwd=None):
|
||||
actual=sandbox(work,env)+args if offline else args
|
||||
start=time.time()
|
||||
with (work/(name+'.log')).open('w') as out:
|
||||
proc=subprocess.Popen(actual,cwd=cwd or work,env={"PATH":"/usr/bin:/bin","HOME":str(work/"home")} if offline else env,stdout=out,stderr=subprocess.STDOUT)
|
||||
save(work/'process.json',{'phase':name,'pid':proc.pid,'driver_pid':os.getpid(),'argv':args,'state':'running'})
|
||||
code=proc.wait()
|
||||
result={'phase':name,'argv':args,'returncode':code,'seconds':round(time.time()-start,2)}
|
||||
save(work/(name+'-result.json'),result);save(work/'process.json',dict(result,state='complete',pid=None,driver_pid=None))
|
||||
print(json.dumps(result),flush=True)
|
||||
if code:raise RuntimeError('phase failed; retained log: '+str(work/(name+'.log')))
|
||||
return result
|
||||
|
||||
def checked_inputs(work):
|
||||
for n,h in LOCK['host_tools'].items():check(n,h)
|
||||
for n,h in LOCK['lockfiles'].items():check(work/'source'/n,h)
|
||||
for x in LOCK['reviewed_outputs']:check(work/'source'/x['path'],x['sha256'])
|
||||
|
||||
def prepare(a,work):
|
||||
if work.exists():raise ValueError('workspace must be new; existing trial is never restarted or overwritten')
|
||||
for n,key in [('source_archive','source_archive_sha256'),('go_archive','go_archive_sha256'),('node_archive','node_archive_sha256')]:
|
||||
if not getattr(a,n):raise ValueError('explicit '+n+' required')
|
||||
check(getattr(a,n),LOCK[key])
|
||||
if not a.module_cache:raise ValueError('explicit public module cache required')
|
||||
cache=Path(a.module_cache).resolve();assert cache.is_dir()
|
||||
check(INPUTS/'required-claim-values.patch',LOCK['patch_sha256'])
|
||||
for n,h in LOCK['host_tools'].items():check(n,h)
|
||||
work.mkdir(parents=True);(work/'home').mkdir()
|
||||
with tarfile.open(a.source_archive) as t:
|
||||
# The official archive is digest-bound before any member is extracted.
|
||||
parent=work/'unpacked';parent.mkdir();t.extractall(parent,filter='data')
|
||||
folders=list(parent.iterdir());assert len(folders)==1 and folders[0].is_dir();folders[0].rename(work/'source');parent.rmdir()
|
||||
for n in ['go_archive','node_archive']:
|
||||
with tarfile.open(getattr(a,n)) as t:t.extractall(work,filter='data')
|
||||
for n in ['empty.npmrc','empty-global.npmrc']:(work/n).write_text('')
|
||||
if a.go_cache_seed:shutil.copytree(Path(a.go_cache_seed).resolve(),work/'gocache')
|
||||
if a.npm_cache_seed:shutil.copytree(Path(a.npm_cache_seed).resolve(),work/'npm-cache')
|
||||
subprocess.run(['/usr/bin/git','apply','--check',str(INPUTS/'required-claim-values.patch')],cwd=work/'source',check=True,env={'PATH':'/usr/bin:/bin','HOME':str(work/'home'),'GIT_CONFIG_NOSYSTEM':'1','GIT_CONFIG_GLOBAL':'/dev/null'})
|
||||
subprocess.run(['/usr/bin/git','apply',str(INPUTS/'required-claim-values.patch')],cwd=work/'source',check=True,env={'PATH':'/usr/bin:/bin','HOME':str(work/'home'),'GIT_CONFIG_NOSYSTEM':'1','GIT_CONFIG_GLOBAL':'/dev/null'})
|
||||
save(work/'prepared.json',{'module_cache':str(cache),'lock_sha256':digest(INPUTS/'inputs.lock.json'),'public_archives_verified':True})
|
||||
checked_inputs(work)
|
||||
packages=json.loads((work/'source/package-lock.json').read_text())['packages']
|
||||
for name,package in packages.items():
|
||||
if not name:continue
|
||||
if not package.get('resolved','').startswith('https://registry.npmjs.org/') or not package.get('integrity','').startswith('sha512-'):
|
||||
raise ValueError('non-public/non-integrity-bound npm package: '+name)
|
||||
print(json.dumps({'prepared':True,'lockfile_npm_packages':len(packages)-1}))
|
||||
|
||||
def normalize(folder):
|
||||
epoch=LOCK['source_date_epoch']
|
||||
for root,dirs,files in os.walk(folder):
|
||||
dirs[:]=[x for x in dirs if x not in ('node_modules','.git')]
|
||||
for n in files:
|
||||
p=Path(root)/n
|
||||
if not p.is_symlink():os.utime(p,(epoch,epoch))
|
||||
os.utime(root,(epoch,epoch))
|
||||
|
||||
def build(work):
|
||||
checked_inputs(work);env=base_env(work)
|
||||
run(work,'verify-go-modules',['go','mod','verify'],env,True)
|
||||
run(work,'npm-offline-ci',['npm','ci','--offline','--ignore-scripts','--no-audit','--no-fund'],env,True)
|
||||
normalize(work/'source')
|
||||
version=['GOFLAGS=-trimpath -buildvcs=false -p=4','RELEASE_VERSION='+LOCK['release_version'],'FORGEJO_VERSION='+LOCK['release_version'],'FORGEJO_VERSION_API='+LOCK['release_version']]
|
||||
run(work,'frontend',['make']+version+['frontend'],env,True)
|
||||
for n in ['public','options','templates','modules/migration/schemas']:normalize(work/'source'/n)
|
||||
run(work,'embedded-and-static-backend',['make']+version+['TAGS='+LOCK['tags'],'go-check','generate-backend','static-executable'],env,True)
|
||||
checked_inputs(work)
|
||||
save(work/'build.json',{'binary_sha256':digest(work/'source/gitea'),'source_locks_preserved':True,'frontend_built':True,'tags':LOCK['tags'],'image_deployment_selected':False})
|
||||
|
||||
def verify(work):
|
||||
checked_inputs(work);binary=work/'source/gitea'
|
||||
target=work/'embedded-extracted'
|
||||
if target.exists():raise ValueError('retain prior verification; extraction target already exists')
|
||||
target.mkdir()
|
||||
# No source, asset tree, host /usr, libraries, configuration or HOME is
|
||||
# exposed to the executable. A filesystem fallback cannot pass this proof.
|
||||
bare=['bwrap','--unshare-net','--unshare-pid','--proc','/proc','--dev','/dev',
|
||||
'--tmpfs','/tmp','--dir','/app','--ro-bind',str(binary),'/app/forgejo',
|
||||
'--bind',str(target),'/results','--chdir','/tmp','--clearenv']
|
||||
for k,v in {'PATH':'/nonexistent','HOME':'/tmp/home','USER':'builder','USERNAME':'builder',
|
||||
'GITEA_WORK_DIR':'/tmp/work','GITEA_CUSTOM':'/tmp/custom',
|
||||
'GITEA_APP_INI':'/tmp/absent-app.ini','TZ':'UTC','LC_ALL':'C'}.items():
|
||||
bare+=['--setenv',k,v]
|
||||
bare+=['--','/app/forgejo']
|
||||
outer={'PATH':'/usr/bin:/bin','HOME':str(work/'home')}
|
||||
run(work,'binary-version',bare+['--version'],outer)
|
||||
run(work,'new-cli-help',bare+['admin','auth','add-oauth','--help'],outer)
|
||||
assert '--required-claim-values' in (work/'new-cli-help.log').read_text()
|
||||
run(work,'embedded-extract',bare+['embedded','extract','--include-vendored','--destination','/results','**'],outer)
|
||||
records={}
|
||||
for folder in ['public','options','templates']:
|
||||
for f in sorted((work/'source'/folder).rglob('*')):
|
||||
if not f.is_file():continue
|
||||
n=str(f.relative_to(work/'source'));q=target/n
|
||||
if not q.is_file() or digest(f)!=digest(q):raise ValueError('embedded asset missing/different: '+n)
|
||||
records[n]=digest(f)
|
||||
assert 'public/assets/js/index.js' in records and 'public/assets/css/index.css' in records
|
||||
assert 'public/assets/licenses.txt' in records
|
||||
save(work/'embedded-files.json',records)
|
||||
readelf=subprocess.run(['/usr/bin/readelf','-l',str(binary)],capture_output=True,text=True,check=True,env={'PATH':'/usr/bin:/bin'})
|
||||
assert 'INTERP' not in readelf.stdout
|
||||
save(work/'verification.json',{'binary_sha256':digest(binary),'embedded_files':len(records),
|
||||
'embedded_file_manifest_sha256':digest(work/'embedded-files.json'),'all_public_options_templates_exact':True,
|
||||
'no_source_assets_libraries_or_host_config_mounted':True,
|
||||
'no_dynamic_interpreter':True,'new_cli_flag_present':True,'network':'bwrap --unshare-net',
|
||||
'source_locks_preserved':True,'architecture_approval':False,'native_admission':False})
|
||||
print((work/'verification.json').read_text())
|
||||
|
||||
def container(work):
|
||||
verified=json.loads((work/'verification.json').read_text());check(work/'source/gitea',verified['binary_sha256'])
|
||||
context=work/'container-context'
|
||||
if context.exists():raise ValueError('container trial already exists; retain evidence before an explicit revised trial')
|
||||
archive_path=work/'image.oci.tar'
|
||||
if archive_path.exists():raise ValueError('retain existing OCI artifact; never overwrite it')
|
||||
base_bytes=(INPUTS/'runtime-base-config.json').read_bytes()
|
||||
assert hashlib.sha256(base_bytes).hexdigest()==LOCK['runtime_base_config_sha256']
|
||||
original=json.loads(base_bytes)
|
||||
epoch=max(LOCK['source_date_epoch'],math.ceil(datetime.datetime.fromisoformat(original['created'].replace('Z','+00:00')).timestamp()))
|
||||
context.mkdir();shutil.copyfile(work/'source/gitea',context/'gitea')
|
||||
os.utime(context/'gitea',(LOCK['source_date_epoch'],)*2)
|
||||
(context/'Dockerfile').write_text('FROM '+LOCK['runtime_base']+'\nARG SOURCE_DATE_EPOCH\nLABEL org.opencontainers.image.version="'+LOCK['release_version']+'" org.opencontainers.image.revision="'+LOCK['source_commit']+'" org.railiance.source.patch.sha256="'+LOCK['patch_sha256']+'"\nCOPY --chown=0:0 --chmod=755 gitea /app/gitea/gitea\n')
|
||||
os.utime(context/'Dockerfile',(LOCK['source_date_epoch'],)*2)
|
||||
config=work/'docker-config';config.mkdir(exist_ok=True)
|
||||
env={'PATH':'/usr/bin:/bin','HOME':str(work/'home'),'DOCKER_CONFIG':str(config),'DOCKER_HOST':'unix:///var/run/docker.sock'}
|
||||
# SOURCE_DATE_EPOCH alone does not normalise COPY parent-directory mtimes.
|
||||
# OCI output permits layer rewriting without the Docker-store unpack conflict.
|
||||
# The epoch never predates the immutable base, preserving its exact layer prefix.
|
||||
run(work,'container-build',['docker','buildx','build','--output','type=oci,dest='+str(archive_path)+',rewrite-timestamp=true',
|
||||
'--no-cache','--platform',LOCK['platform'],'--pull=false','--network=none','--provenance=false','--sbom=false',
|
||||
'--metadata-file',str(work/'image-build-metadata.json'),'--build-arg','SOURCE_DATE_EPOCH='+str(epoch),str(context)],env)
|
||||
metadata=json.loads((work/'image-build-metadata.json').read_text())
|
||||
with tarfile.open(archive_path) as archive:
|
||||
index_bytes=archive.extractfile('index.json').read();index=json.loads(index_bytes)
|
||||
assert len(index['manifests'])==1
|
||||
descriptor=index['manifests'][0]
|
||||
assert descriptor['platform']=={'architecture':'amd64','os':'linux'}
|
||||
assert descriptor['digest']==metadata['containerimage.digest']
|
||||
def blob(d):
|
||||
value=archive.extractfile('blobs/sha256/'+d.split(':')[1]).read()
|
||||
assert 'sha256:'+hashlib.sha256(value).hexdigest()==d
|
||||
return value
|
||||
manifest=json.loads(blob(descriptor['digest']))
|
||||
config_digest=manifest['config']['digest'];config=json.loads(blob(config_digest))
|
||||
assert config_digest==metadata['containerimage.config.digest']
|
||||
for layer in manifest['layers']:blob(layer['digest'])
|
||||
assert config['rootfs']['diff_ids'][:-1]==original['rootfs']['diff_ids']
|
||||
assert config['architecture']=='amd64' and config['os']=='linux'
|
||||
for key in ['User','Entrypoint','Cmd','Env','WorkingDir','Volumes']:
|
||||
assert config['config'].get(key)==original['config'].get(key),key
|
||||
labels=dict(original['config'].get('Labels',{}))
|
||||
labels.update({'org.opencontainers.image.version':LOCK['release_version'],
|
||||
'org.opencontainers.image.revision':LOCK['source_commit'],'org.railiance.source.patch.sha256':LOCK['patch_sha256']})
|
||||
assert config['config'].get('Labels')==labels
|
||||
assert config['config']['User']=='1000:1000'
|
||||
expected_created=datetime.datetime.fromtimestamp(epoch,datetime.timezone.utc).isoformat(timespec='seconds').replace('+00:00','Z')
|
||||
assert config['created']==expected_created
|
||||
found=False
|
||||
last='blobs/sha256/'+manifest['layers'][-1]['digest'].split(':')[1]
|
||||
with tarfile.open(fileobj=archive.extractfile(last),mode='r|*') as layer:
|
||||
for member in layer:
|
||||
name=member.name.lstrip('./');assert name in ('app','app/gitea','app/gitea/gitea')
|
||||
if name=='app/gitea/gitea':
|
||||
h=hashlib.sha256();stream=layer.extractfile(member)
|
||||
for block in iter(lambda:stream.read(1024*1024),b''):h.update(block)
|
||||
assert h.hexdigest()==verified['binary_sha256'];found=True
|
||||
assert found
|
||||
save(work/'container-result.json',{'image_manifest_digest':descriptor['digest'],'config_digest':config_digest,
|
||||
'oci_index_sha256':hashlib.sha256(index_bytes).hexdigest(),'archive_sha256':digest(archive_path),
|
||||
'artifact_path':str(archive_path),'source_epoch':LOCK['source_date_epoch'],'container_epoch':epoch,
|
||||
'all_base_layers_and_rootless_runtime_config_preserved':True,'all_oci_blob_digests_verified':True,
|
||||
'final_layer_binary_exact':True,'registry_publication':False,'deployment_tag':None,
|
||||
'host_or_secret_mounts':False,'privileged_containers':False,'local_runtime_image_loaded':False})
|
||||
print((work/'container-result.json').read_text())
|
||||
|
||||
def main():
|
||||
parser=argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('phase',choices=['prepare','acquire-npm','build','verify','container'])
|
||||
parser.add_argument('--workspace',required=True)
|
||||
for n in ['source-archive','go-archive','node-archive','module-cache','go-cache-seed','npm-cache-seed']:parser.add_argument('--'+n)
|
||||
a=parser.parse_args();work=Path(a.workspace).resolve()
|
||||
if not str(work).startswith('/tmp/'):raise ValueError('isolated build workspace must be under /tmp')
|
||||
if a.phase=='prepare':return prepare(a,work)
|
||||
assert json.loads((work/'prepared.json').read_text())['lock_sha256']==digest(INPUTS/'inputs.lock.json')
|
||||
if a.phase=='acquire-npm':
|
||||
checked_inputs(work);env=base_env(work);env['NPM_CONFIG_OFFLINE']='false'
|
||||
return run(work,'npm-public-acquire',['npm','ci','--ignore-scripts','--no-audit','--no-fund'],env,cwd=work/'source')
|
||||
{'build':build,'verify':verify,'container':container}[a.phase](work)
|
||||
|
||||
if __name__=='__main__':main()
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
# Actual Forgejo source-state transaction primitive, r1
|
||||
|
||||
This is bounded source progress for NK-WP-0048-T34 / RAPPS-WP-0016-T05/T08,
|
||||
not an admitted operational backend or completed inverse. It adds real database
|
||||
primitives to the actual reviewed Forgejo 11.0.3 source under `/tmp`; it adds no
|
||||
second application source repository in Apps. No production/native database,
|
||||
configuration, credential, deployment or identity was read or changed. Existing
|
||||
frozen extension and release packets remain unchanged. No canonical owner edit,
|
||||
publication, installed image selection or native dispatch occurred.
|
||||
|
||||
## Implemented database mechanics
|
||||
|
||||
`models/auth/source_operation.go` operates on actual `login_source` rows through
|
||||
Forgejo's real transaction machinery. Prepare commits original whole source
|
||||
inventory and exact private desired configuration before a source mutation.
|
||||
Apply checks exact current/original equality, modifies one explicitly identified
|
||||
OAuth2 source, verifies the complete expected persisted post-state and commits
|
||||
post-state plus outcome atomically. Existing targets require exact ID/name/type;
|
||||
case creation requires name absence. Configuration decoding is deliberately
|
||||
avoided for snapshots and restoration, so unknown fields cannot silently vanish.
|
||||
Complete raw private/public equality, source ID and created/updated identity
|
||||
stay inside owner custody. Public results contain only operation ID, phase and
|
||||
numeric source ID; no private digest or configuration is returned.
|
||||
|
||||
Inverse independently checks complete current/post equality. It restores
|
||||
preexisting sources in place with the exact original raw bytes and ID. It never
|
||||
deletes a preexisting or unchanged source. Only confirmed case creation can
|
||||
select deletion; actual local user and external-login references refuse deletion
|
||||
without changing those users/links. Another unresolved managed operation on the
|
||||
same target refuses, rather than sharing creation provenance. Unknown or changed
|
||||
state refuses. A prepared intent can be cancelled without changing sources.
|
||||
Reconcile reads committed phase/current equality without retrying mutation.
|
||||
Errors provide no driver/configuration text and return no successful result.
|
||||
|
||||
SQLite acquires a write transaction before source reads, excluding even
|
||||
uncooperative SQL writers. The actual two-connection exclusion test sets the
|
||||
managed engine to DEFERRED mode, so it exercises the explicit initial write.
|
||||
The source contains PostgreSQL table locks covering source/journal/user/link
|
||||
writers; PostgreSQL behavior is **not tested or accepted by this SQLite proof**.
|
||||
MySQL and nested transactions refuse. Locks span each database transaction;
|
||||
they do not span an entire release or control provider registration in another
|
||||
process, DDL/admin bypasses, Helm/image writers or unmanaged shared consumers.
|
||||
Complete application/release writer exclusion still needs owner implementation.
|
||||
|
||||
The production DB hooks currently print arguments on slow/error queries and
|
||||
runtime traces, even with normal SQL logging disabled. This patch adds an
|
||||
explicit private-query context and redacts statement, arguments and driver-error
|
||||
text in those three hooks. The primitive refuses both configured SQL logging
|
||||
and the actual XORM logger's argument logging. Server/driver/audit/backup logging,
|
||||
custom hooks and private-runtime output custody remain owner review obligations.
|
||||
The actual hook tests include positive unredacted controls; no real secrets
|
||||
were used. The initial candidate only wrapped the Go context; inspection found
|
||||
that existing Engined contexts and db.WithTx did not propagate that marker into
|
||||
XORM's real query hooks. Direct hook fixtures alone missed this flaw. The final
|
||||
patch rebinds marked contexts in db.GetEngine, preserving the same transaction
|
||||
and actual SQL cancellation context; ordinary unmarked routing is unchanged.
|
||||
The added actual hooked-XORM failing-query test checks real slow/error/trace
|
||||
paths and positive/private controls. Earlier results remain in the evidence as
|
||||
insufficient for this propagation question.
|
||||
|
||||
## Private journal custody and recovery limits
|
||||
|
||||
`login_source_operation` is registered with Forgejo's existing model/schema sync.
|
||||
Its before/desired/post fields contain **private raw configurations and secret
|
||||
copies**, including unrelated-source snapshots. It is not a value-free receipt,
|
||||
independent authenticated evidence, an authorization token or an audit outbox.
|
||||
It inherits the owner's database/storage/backup custody and encryption limits;
|
||||
this patch adds no encryption or new read grant. Existing database access does
|
||||
not automatically authorize an additional journal reader. Owner credential
|
||||
inventory, recovery access, backup inclusion, retention and eventual deletion
|
||||
must explicitly account for these additional copies before admission.
|
||||
|
||||
Preparation refuses at 64 unresolved or 256 total journals, at a desired config
|
||||
above 64 KiB or an original inventory above 1 MiB. These are refusal bounds, not
|
||||
retention approval. Nothing automatically erases unresolved, failed/uncertain or
|
||||
terminal private recovery state. A separately authenticated owner retention and
|
||||
purge decision/implementation is still required; exhaustion remains refused.
|
||||
Completed rows preserve original/desired/post state for recovery. Individual
|
||||
refused or storage-failed calls are not a separately durable attempt log: the
|
||||
admitted runtime must preserve their original errors/uncertainty, current-session
|
||||
references and authenticated attempt/evidence history outside a rolled-back
|
||||
transaction. DB commit uncertainty always calls reconciliation; an ambiguous
|
||||
response does not justify blindly replaying the effect.
|
||||
|
||||
The actual file-backed SQLite test kills a child process immediately after the
|
||||
real ApplySourceOperation INSERT, before post-state/journal update/commit. A
|
||||
new process sees the original prepared journal and no source. Later independent
|
||||
processes observe confirmed apply, perform inverse and confirm reverted absence.
|
||||
This proves bounded actual local database crash/restart mechanics, not an
|
||||
external session, deployed database or independent evidence facility.
|
||||
|
||||
## Required integration before any exposed operation
|
||||
|
||||
There is intentionally no new unauthenticated CLI, API, privileged default
|
||||
factory or always-true authority callback. These exported low-level functions
|
||||
have the same private-database trust boundary as existing CRUD; they are not an
|
||||
authorization boundary. An existing owner CLI/service must authenticate a fresh
|
||||
current initiating principal/session and exact Access Engine decision **before
|
||||
every call**, including preparation/reconciliation journal writes and each
|
||||
inverse attempt. Admission must bind case, operation/transaction, source and
|
||||
accepted implementation revision, exact parameters, target, principal, current
|
||||
session, purpose/action and expiry/revocation. Neither an arbitrary Subject in
|
||||
a CheckRequest, a service caller token, an offline config pin nor this journal
|
||||
authenticates the initiating human/agent. Fresh inverse authority cannot reuse
|
||||
a revoked hand-over session. All exit paths still need revocation.
|
||||
|
||||
The shared producer-owned Access Engine adapter is separate coordinator work.
|
||||
The real owner session/evidence/outbox/revocation facilities, release-wide
|
||||
exclusion, cross-process provider refresh/restart and session-to-CLI binding
|
||||
remain unresolved here. These are existing hand-over/inverse obligations,
|
||||
not an eighth SecurityCase kernel step or another service. Actual app policy
|
||||
and exact desired-config validation remain the admitted owner's responsibility.
|
||||
|
||||
An image downgrade remains refused until authoritative source restoration/removal,
|
||||
complete unrelated-state checks and all extension-sensitive configurations are
|
||||
independently verified safe. Even a restored original source can contain a
|
||||
preexisting extension gate; a source inverse does not itself authorize downgrade.
|
||||
Helm omission/rollback never substitutes for source inverse. Keep the extended
|
||||
image and gate when linked-source deletion refuses or any current state is
|
||||
uncertain; preserve local sign-in and reconcile with the owner. Existing users
|
||||
or external links must never be removed to force deletion.
|
||||
|
||||
## Evidence
|
||||
|
||||
The exact eight-path additive patch, actual source pre/postimages, inventories,
|
||||
bounded real package tests, crash/exclusion tests and original failed attempts
|
||||
are retained in this directory. Tests run in network-disabled bwrap with only
|
||||
public source/toolchain/module inputs; toolchain/modules are read-only and build
|
||||
cache/source are isolated writable copies. `run-tests.py` records the exact
|
||||
sandbox and argv. Only synthetic fixtures are used. Initial Git-global-config
|
||||
and hook-test pointer-method harness failures remain evidence, not successes.
|
||||
Early test sandboxes used network isolation without a PID namespace; their
|
||||
host-proc visibility is retained as a profile limitation, not an isolation proof.
|
||||
No host process-argument scan was authored. Final and corrected control runs use
|
||||
both network and PID isolation before mounting proc. The first control trial
|
||||
started before its separate source copy completed; its missing-package setup
|
||||
failure is explicitly invalid, preserved and superseded, never a detected guard.
|
||||
No lifecycle, native gate or owner workplan is closed by these source tests.
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
{
|
||||
"argv": [
|
||||
"bwrap",
|
||||
"--unshare-net",
|
||||
"--ro-bind",
|
||||
"/usr",
|
||||
"/usr",
|
||||
"--ro-bind",
|
||||
"/lib",
|
||||
"/lib",
|
||||
"--ro-bind",
|
||||
"/lib64",
|
||||
"/lib64",
|
||||
"--symlink",
|
||||
"usr/bin",
|
||||
"/bin",
|
||||
"--proc",
|
||||
"/proc",
|
||||
"--dev",
|
||||
"/dev",
|
||||
"--tmpfs",
|
||||
"/tmp",
|
||||
"--ro-bind",
|
||||
"/tmp/forgejo-independent-r1/go",
|
||||
"/tmp/forgejo-independent-r1/go",
|
||||
"--ro-bind",
|
||||
"/tmp/forgejo-independent-r1/gomodcache",
|
||||
"/tmp/forgejo-independent-r1/gomodcache",
|
||||
"--bind",
|
||||
"/tmp/apps-operation-backend-r1",
|
||||
"/tmp/apps-operation-backend-r1",
|
||||
"--bind",
|
||||
"/tmp/apps-operation-gocache-r1",
|
||||
"/tmp/apps-operation-gocache-r1",
|
||||
"--chdir",
|
||||
"/tmp/apps-operation-backend-r1",
|
||||
"--",
|
||||
"env",
|
||||
"-i",
|
||||
"PATH=/tmp/forgejo-independent-r1/go/bin:/usr/bin:/bin",
|
||||
"HOME=/tmp/operation-home",
|
||||
"GOTOOLCHAIN=local",
|
||||
"GOPROXY=off",
|
||||
"GOSUMDB=off",
|
||||
"GOMODCACHE=/tmp/forgejo-independent-r1/gomodcache",
|
||||
"GOCACHE=/tmp/apps-operation-gocache-r1",
|
||||
"GOPATH=/tmp/operation-gopath",
|
||||
"GOVCS=*:off",
|
||||
"GIT_CONFIG_NOSYSTEM=1",
|
||||
"GIT_CONFIG_GLOBAL=/dev/null",
|
||||
"GIT_TERMINAL_PROMPT=0",
|
||||
"GOMAXPROCS=4",
|
||||
"CGO_ENABLED=1",
|
||||
"go",
|
||||
"test",
|
||||
"-p",
|
||||
"4",
|
||||
"-tags",
|
||||
"sqlite sqlite_unlock_notify",
|
||||
"./models/auth",
|
||||
"-run",
|
||||
"TestSourceOperation",
|
||||
"-count=1",
|
||||
"-v"
|
||||
],
|
||||
"returncode": 1,
|
||||
"seconds": 30.90784502029419
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
{
|
||||
"argv": [
|
||||
"bwrap",
|
||||
"--unshare-net",
|
||||
"--ro-bind",
|
||||
"/usr",
|
||||
"/usr",
|
||||
"--ro-bind",
|
||||
"/lib",
|
||||
"/lib",
|
||||
"--ro-bind",
|
||||
"/lib64",
|
||||
"/lib64",
|
||||
"--symlink",
|
||||
"usr/bin",
|
||||
"/bin",
|
||||
"--proc",
|
||||
"/proc",
|
||||
"--dev",
|
||||
"/dev",
|
||||
"--tmpfs",
|
||||
"/tmp",
|
||||
"--ro-bind",
|
||||
"/tmp/forgejo-independent-r1/go",
|
||||
"/tmp/forgejo-independent-r1/go",
|
||||
"--ro-bind",
|
||||
"/tmp/forgejo-independent-r1/gomodcache",
|
||||
"/tmp/forgejo-independent-r1/gomodcache",
|
||||
"--bind",
|
||||
"/tmp/apps-operation-backend-r1",
|
||||
"/tmp/apps-operation-backend-r1",
|
||||
"--bind",
|
||||
"/tmp/apps-operation-gocache-r1",
|
||||
"/tmp/apps-operation-gocache-r1",
|
||||
"--chdir",
|
||||
"/tmp/apps-operation-backend-r1",
|
||||
"--",
|
||||
"env",
|
||||
"-i",
|
||||
"PATH=/tmp/forgejo-independent-r1/go/bin:/usr/bin:/bin",
|
||||
"HOME=/tmp/operation-home",
|
||||
"GOTOOLCHAIN=local",
|
||||
"GOPROXY=off",
|
||||
"GOSUMDB=off",
|
||||
"GOMODCACHE=/tmp/forgejo-independent-r1/gomodcache",
|
||||
"GOCACHE=/tmp/apps-operation-gocache-r1",
|
||||
"GOPATH=/tmp/operation-gopath",
|
||||
"GOVCS=*:off",
|
||||
"GIT_CONFIG_NOSYSTEM=1",
|
||||
"GIT_CONFIG_GLOBAL=/tmp/operation-test.gitconfig",
|
||||
"GIT_TERMINAL_PROMPT=0",
|
||||
"GOMAXPROCS=4",
|
||||
"CGO_ENABLED=1",
|
||||
"go",
|
||||
"test",
|
||||
"-p",
|
||||
"4",
|
||||
"-tags",
|
||||
"sqlite sqlite_unlock_notify",
|
||||
"./models/auth",
|
||||
"-run",
|
||||
"TestSourceOperation",
|
||||
"-count=1",
|
||||
"-v"
|
||||
],
|
||||
"returncode": 0,
|
||||
"seconds": 16.431792497634888
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
{
|
||||
"argv": [
|
||||
"bwrap",
|
||||
"--unshare-net",
|
||||
"--ro-bind",
|
||||
"/usr",
|
||||
"/usr",
|
||||
"--ro-bind",
|
||||
"/lib",
|
||||
"/lib",
|
||||
"--ro-bind",
|
||||
"/lib64",
|
||||
"/lib64",
|
||||
"--symlink",
|
||||
"usr/bin",
|
||||
"/bin",
|
||||
"--proc",
|
||||
"/proc",
|
||||
"--dev",
|
||||
"/dev",
|
||||
"--tmpfs",
|
||||
"/tmp",
|
||||
"--ro-bind",
|
||||
"/tmp/forgejo-independent-r1/go",
|
||||
"/tmp/forgejo-independent-r1/go",
|
||||
"--ro-bind",
|
||||
"/tmp/forgejo-independent-r1/gomodcache",
|
||||
"/tmp/forgejo-independent-r1/gomodcache",
|
||||
"--bind",
|
||||
"/tmp/apps-operation-backend-r1",
|
||||
"/tmp/apps-operation-backend-r1",
|
||||
"--bind",
|
||||
"/tmp/apps-operation-gocache-r1",
|
||||
"/tmp/apps-operation-gocache-r1",
|
||||
"--chdir",
|
||||
"/tmp/apps-operation-backend-r1",
|
||||
"--",
|
||||
"env",
|
||||
"-i",
|
||||
"PATH=/tmp/forgejo-independent-r1/go/bin:/usr/bin:/bin",
|
||||
"HOME=/tmp/operation-home",
|
||||
"GOTOOLCHAIN=local",
|
||||
"GOPROXY=off",
|
||||
"GOSUMDB=off",
|
||||
"GOMODCACHE=/tmp/forgejo-independent-r1/gomodcache",
|
||||
"GOCACHE=/tmp/apps-operation-gocache-r1",
|
||||
"GOPATH=/tmp/operation-gopath",
|
||||
"GOVCS=*:off",
|
||||
"GIT_CONFIG_NOSYSTEM=1",
|
||||
"GIT_CONFIG_GLOBAL=/tmp/operation-test.gitconfig",
|
||||
"GIT_TERMINAL_PROMPT=0",
|
||||
"GOMAXPROCS=4",
|
||||
"CGO_ENABLED=1",
|
||||
"go",
|
||||
"test",
|
||||
"-p",
|
||||
"4",
|
||||
"-tags",
|
||||
"sqlite sqlite_unlock_notify",
|
||||
"./models/auth",
|
||||
"./models/db",
|
||||
"-run",
|
||||
"TestSourceOperation|TestSensitiveSQLHooks",
|
||||
"-count=1",
|
||||
"-v"
|
||||
],
|
||||
"returncode": 1,
|
||||
"seconds": 24.786890029907227
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
{
|
||||
"argv": [
|
||||
"bwrap",
|
||||
"--unshare-net",
|
||||
"--ro-bind",
|
||||
"/usr",
|
||||
"/usr",
|
||||
"--ro-bind",
|
||||
"/lib",
|
||||
"/lib",
|
||||
"--ro-bind",
|
||||
"/lib64",
|
||||
"/lib64",
|
||||
"--symlink",
|
||||
"usr/bin",
|
||||
"/bin",
|
||||
"--proc",
|
||||
"/proc",
|
||||
"--dev",
|
||||
"/dev",
|
||||
"--tmpfs",
|
||||
"/tmp",
|
||||
"--ro-bind",
|
||||
"/tmp/forgejo-independent-r1/go",
|
||||
"/tmp/forgejo-independent-r1/go",
|
||||
"--ro-bind",
|
||||
"/tmp/forgejo-independent-r1/gomodcache",
|
||||
"/tmp/forgejo-independent-r1/gomodcache",
|
||||
"--bind",
|
||||
"/tmp/apps-operation-backend-r1",
|
||||
"/tmp/apps-operation-backend-r1",
|
||||
"--bind",
|
||||
"/tmp/apps-operation-gocache-r1",
|
||||
"/tmp/apps-operation-gocache-r1",
|
||||
"--chdir",
|
||||
"/tmp/apps-operation-backend-r1",
|
||||
"--",
|
||||
"env",
|
||||
"-i",
|
||||
"PATH=/tmp/forgejo-independent-r1/go/bin:/usr/bin:/bin",
|
||||
"HOME=/tmp/operation-home",
|
||||
"GOTOOLCHAIN=local",
|
||||
"GOPROXY=off",
|
||||
"GOSUMDB=off",
|
||||
"GOMODCACHE=/tmp/forgejo-independent-r1/gomodcache",
|
||||
"GOCACHE=/tmp/apps-operation-gocache-r1",
|
||||
"GOPATH=/tmp/operation-gopath",
|
||||
"GOVCS=*:off",
|
||||
"GIT_CONFIG_NOSYSTEM=1",
|
||||
"GIT_CONFIG_GLOBAL=/tmp/operation-test.gitconfig",
|
||||
"GIT_TERMINAL_PROMPT=0",
|
||||
"GOMAXPROCS=4",
|
||||
"CGO_ENABLED=1",
|
||||
"go",
|
||||
"test",
|
||||
"-p",
|
||||
"4",
|
||||
"-tags",
|
||||
"sqlite sqlite_unlock_notify",
|
||||
"./models/auth",
|
||||
"./models/db",
|
||||
"-run",
|
||||
"TestSourceOperation|TestSensitiveSQLHooks",
|
||||
"-count=1",
|
||||
"-v"
|
||||
],
|
||||
"returncode": 0,
|
||||
"seconds": 92.08943581581116
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
{
|
||||
"argv": [
|
||||
"bwrap",
|
||||
"--unshare-net",
|
||||
"--ro-bind",
|
||||
"/usr",
|
||||
"/usr",
|
||||
"--ro-bind",
|
||||
"/lib",
|
||||
"/lib",
|
||||
"--ro-bind",
|
||||
"/lib64",
|
||||
"/lib64",
|
||||
"--symlink",
|
||||
"usr/bin",
|
||||
"/bin",
|
||||
"--proc",
|
||||
"/proc",
|
||||
"--dev",
|
||||
"/dev",
|
||||
"--tmpfs",
|
||||
"/tmp",
|
||||
"--ro-bind",
|
||||
"/tmp/forgejo-independent-r1/go",
|
||||
"/tmp/forgejo-independent-r1/go",
|
||||
"--ro-bind",
|
||||
"/tmp/forgejo-independent-r1/gomodcache",
|
||||
"/tmp/forgejo-independent-r1/gomodcache",
|
||||
"--bind",
|
||||
"/tmp/apps-operation-backend-r1",
|
||||
"/tmp/apps-operation-backend-r1",
|
||||
"--bind",
|
||||
"/tmp/apps-operation-gocache-r1",
|
||||
"/tmp/apps-operation-gocache-r1",
|
||||
"--chdir",
|
||||
"/tmp/apps-operation-backend-r1",
|
||||
"--",
|
||||
"env",
|
||||
"-i",
|
||||
"PATH=/tmp/forgejo-independent-r1/go/bin:/usr/bin:/bin",
|
||||
"HOME=/tmp/operation-home",
|
||||
"GOTOOLCHAIN=local",
|
||||
"GOPROXY=off",
|
||||
"GOSUMDB=off",
|
||||
"GOMODCACHE=/tmp/forgejo-independent-r1/gomodcache",
|
||||
"GOCACHE=/tmp/apps-operation-gocache-r1",
|
||||
"GOPATH=/tmp/operation-gopath",
|
||||
"GOVCS=*:off",
|
||||
"GIT_CONFIG_NOSYSTEM=1",
|
||||
"GIT_CONFIG_GLOBAL=/tmp/operation-test.gitconfig",
|
||||
"GIT_TERMINAL_PROMPT=0",
|
||||
"GOMAXPROCS=4",
|
||||
"CGO_ENABLED=1",
|
||||
"go",
|
||||
"test",
|
||||
"-p",
|
||||
"4",
|
||||
"-tags",
|
||||
"sqlite sqlite_unlock_notify",
|
||||
"./models/auth",
|
||||
"./models/db",
|
||||
"-run",
|
||||
"TestSourceOperation|TestSensitiveSQLHooks",
|
||||
"-count=1",
|
||||
"-v"
|
||||
],
|
||||
"returncode": 0,
|
||||
"seconds": 83.88910269737244
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
{
|
||||
"argv": [
|
||||
"bwrap",
|
||||
"--unshare-net",
|
||||
"--ro-bind",
|
||||
"/usr",
|
||||
"/usr",
|
||||
"--ro-bind",
|
||||
"/lib",
|
||||
"/lib",
|
||||
"--ro-bind",
|
||||
"/lib64",
|
||||
"/lib64",
|
||||
"--symlink",
|
||||
"usr/bin",
|
||||
"/bin",
|
||||
"--proc",
|
||||
"/proc",
|
||||
"--dev",
|
||||
"/dev",
|
||||
"--tmpfs",
|
||||
"/tmp",
|
||||
"--ro-bind",
|
||||
"/tmp/forgejo-independent-r1/go",
|
||||
"/tmp/forgejo-independent-r1/go",
|
||||
"--ro-bind",
|
||||
"/tmp/forgejo-independent-r1/gomodcache",
|
||||
"/tmp/forgejo-independent-r1/gomodcache",
|
||||
"--bind",
|
||||
"/tmp/apps-operation-backend-r1",
|
||||
"/tmp/apps-operation-backend-r1",
|
||||
"--bind",
|
||||
"/tmp/apps-operation-gocache-r1",
|
||||
"/tmp/apps-operation-gocache-r1",
|
||||
"--chdir",
|
||||
"/tmp/apps-operation-backend-r1",
|
||||
"--",
|
||||
"env",
|
||||
"-i",
|
||||
"PATH=/tmp/forgejo-independent-r1/go/bin:/usr/bin:/bin",
|
||||
"HOME=/tmp/operation-home",
|
||||
"GOTOOLCHAIN=local",
|
||||
"GOPROXY=off",
|
||||
"GOSUMDB=off",
|
||||
"GOMODCACHE=/tmp/forgejo-independent-r1/gomodcache",
|
||||
"GOCACHE=/tmp/apps-operation-gocache-r1",
|
||||
"GOPATH=/tmp/operation-gopath",
|
||||
"GOVCS=*:off",
|
||||
"GIT_CONFIG_NOSYSTEM=1",
|
||||
"GIT_CONFIG_GLOBAL=/tmp/operation-test.gitconfig",
|
||||
"GIT_TERMINAL_PROMPT=0",
|
||||
"GOMAXPROCS=4",
|
||||
"CGO_ENABLED=1",
|
||||
"go",
|
||||
"test",
|
||||
"-p",
|
||||
"4",
|
||||
"-tags",
|
||||
"sqlite sqlite_unlock_notify",
|
||||
"./models/auth",
|
||||
"./models/db",
|
||||
"-run",
|
||||
"TestSourceOperation|TestSensitiveSQLHooks",
|
||||
"-count=1",
|
||||
"-v"
|
||||
],
|
||||
"returncode": 0,
|
||||
"seconds": 30.82476782798767
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
{
|
||||
"argv": [
|
||||
"bwrap",
|
||||
"--unshare-net",
|
||||
"--ro-bind",
|
||||
"/usr",
|
||||
"/usr",
|
||||
"--ro-bind",
|
||||
"/lib",
|
||||
"/lib",
|
||||
"--ro-bind",
|
||||
"/lib64",
|
||||
"/lib64",
|
||||
"--symlink",
|
||||
"usr/bin",
|
||||
"/bin",
|
||||
"--proc",
|
||||
"/proc",
|
||||
"--dev",
|
||||
"/dev",
|
||||
"--tmpfs",
|
||||
"/tmp",
|
||||
"--ro-bind",
|
||||
"/tmp/forgejo-independent-r1/go",
|
||||
"/tmp/forgejo-independent-r1/go",
|
||||
"--ro-bind",
|
||||
"/tmp/forgejo-independent-r1/gomodcache",
|
||||
"/tmp/forgejo-independent-r1/gomodcache",
|
||||
"--bind",
|
||||
"/tmp/apps-operation-backend-r1",
|
||||
"/tmp/apps-operation-backend-r1",
|
||||
"--bind",
|
||||
"/tmp/apps-operation-gocache-r1",
|
||||
"/tmp/apps-operation-gocache-r1",
|
||||
"--chdir",
|
||||
"/tmp/apps-operation-backend-r1",
|
||||
"--",
|
||||
"env",
|
||||
"-i",
|
||||
"PATH=/tmp/forgejo-independent-r1/go/bin:/usr/bin:/bin",
|
||||
"HOME=/tmp/operation-home",
|
||||
"GOTOOLCHAIN=local",
|
||||
"GOPROXY=off",
|
||||
"GOSUMDB=off",
|
||||
"GOMODCACHE=/tmp/forgejo-independent-r1/gomodcache",
|
||||
"GOCACHE=/tmp/apps-operation-gocache-r1",
|
||||
"GOPATH=/tmp/operation-gopath",
|
||||
"GOVCS=*:off",
|
||||
"GIT_CONFIG_NOSYSTEM=1",
|
||||
"GIT_CONFIG_GLOBAL=/tmp/operation-test.gitconfig",
|
||||
"GIT_TERMINAL_PROMPT=0",
|
||||
"GOMAXPROCS=4",
|
||||
"CGO_ENABLED=1",
|
||||
"go",
|
||||
"test",
|
||||
"-p",
|
||||
"4",
|
||||
"-tags",
|
||||
"sqlite sqlite_unlock_notify",
|
||||
"./models/auth",
|
||||
"./models/db",
|
||||
"-run",
|
||||
"TestSourceOperation|TestSensitiveSQLHooks",
|
||||
"-count=1",
|
||||
"-v"
|
||||
],
|
||||
"returncode": 0,
|
||||
"seconds": 213.70628547668457
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
{
|
||||
"argv": [
|
||||
"bwrap",
|
||||
"--unshare-net",
|
||||
"--unshare-pid",
|
||||
"--ro-bind",
|
||||
"/usr",
|
||||
"/usr",
|
||||
"--ro-bind",
|
||||
"/lib",
|
||||
"/lib",
|
||||
"--ro-bind",
|
||||
"/lib64",
|
||||
"/lib64",
|
||||
"--symlink",
|
||||
"usr/bin",
|
||||
"/bin",
|
||||
"--proc",
|
||||
"/proc",
|
||||
"--dev",
|
||||
"/dev",
|
||||
"--tmpfs",
|
||||
"/tmp",
|
||||
"--ro-bind",
|
||||
"/tmp/forgejo-independent-r1/go",
|
||||
"/tmp/forgejo-independent-r1/go",
|
||||
"--ro-bind",
|
||||
"/tmp/forgejo-independent-r1/gomodcache",
|
||||
"/tmp/forgejo-independent-r1/gomodcache",
|
||||
"--bind",
|
||||
"/tmp/apps-operation-backend-r1",
|
||||
"/tmp/apps-operation-backend-r1",
|
||||
"--bind",
|
||||
"/tmp/apps-operation-gocache-r1",
|
||||
"/tmp/apps-operation-gocache-r1",
|
||||
"--chdir",
|
||||
"/tmp/apps-operation-backend-r1",
|
||||
"--",
|
||||
"env",
|
||||
"-i",
|
||||
"PATH=/tmp/forgejo-independent-r1/go/bin:/usr/bin:/bin",
|
||||
"HOME=/tmp/operation-home",
|
||||
"GOTOOLCHAIN=local",
|
||||
"GOPROXY=off",
|
||||
"GOSUMDB=off",
|
||||
"GOMODCACHE=/tmp/forgejo-independent-r1/gomodcache",
|
||||
"GOCACHE=/tmp/apps-operation-gocache-r1",
|
||||
"GOPATH=/tmp/operation-gopath",
|
||||
"GOVCS=*:off",
|
||||
"GIT_CONFIG_NOSYSTEM=1",
|
||||
"GIT_CONFIG_GLOBAL=/tmp/operation-test.gitconfig",
|
||||
"GIT_TERMINAL_PROMPT=0",
|
||||
"GOMAXPROCS=4",
|
||||
"CGO_ENABLED=1",
|
||||
"go",
|
||||
"test",
|
||||
"-p",
|
||||
"4",
|
||||
"-tags",
|
||||
"sqlite sqlite_unlock_notify",
|
||||
"./models/auth",
|
||||
"./models/db",
|
||||
"-run",
|
||||
"TestSourceOperation|TestSensitiveSQLHooks",
|
||||
"-count=1",
|
||||
"-v"
|
||||
],
|
||||
"returncode": 0,
|
||||
"seconds": 46.296929121017456
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,188 @@
|
|||
--- a/models/db/engine.go
|
||||
+++ b/models/db/engine.go
|
||||
@@ -330,8 +330,12 @@
|
||||
func (TracingHook) BeforeProcess(c *contexts.ContextHook) (context.Context, error) {
|
||||
ctx, task := trace.NewTask(c.Ctx, "sql")
|
||||
ctx = context.WithValue(ctx, sqlTask{}, task)
|
||||
- trace.Log(ctx, "query", c.SQL)
|
||||
- trace.Logf(ctx, "args", "%v", c.Args)
|
||||
+ if isSensitiveSQL(c.Ctx) {
|
||||
+ trace.Log(ctx, "query", "[owner-confined query]")
|
||||
+ } else {
|
||||
+ trace.Log(ctx, "query", c.SQL)
|
||||
+ trace.Logf(ctx, "args", "%v", c.Args)
|
||||
+ }
|
||||
return ctx, nil
|
||||
}
|
||||
|
||||
@@ -353,7 +357,11 @@
|
||||
|
||||
func (h *SlowQueryHook) AfterProcess(c *contexts.ContextHook) error {
|
||||
if c.ExecuteTime >= h.Treshold {
|
||||
- h.Logger.Log(8, log.WARN, "[Slow SQL Query] %s %v - %v", c.SQL, c.Args, c.ExecuteTime)
|
||||
+ if isSensitiveSQL(c.Ctx) {
|
||||
+ h.Logger.Log(8, log.WARN, "[Slow SQL Query] [owner-confined query] - %v", c.ExecuteTime)
|
||||
+ } else {
|
||||
+ h.Logger.Log(8, log.WARN, "[Slow SQL Query] %s %v - %v", c.SQL, c.Args, c.ExecuteTime)
|
||||
+ }
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -370,7 +378,11 @@
|
||||
|
||||
func (h *ErrorQueryHook) AfterProcess(c *contexts.ContextHook) error {
|
||||
if c.Err != nil && !errors.Is(c.Err, context.Canceled) {
|
||||
- h.Logger.Log(8, log.ERROR, "[Error SQL Query] %s %v - %v", c.SQL, c.Args, c.Err)
|
||||
- }
|
||||
- return nil
|
||||
-}
|
||||
+ if isSensitiveSQL(c.Ctx) {
|
||||
+ h.Logger.Log(8, log.ERROR, "[Error SQL Query] [owner-confined query]")
|
||||
+ } else {
|
||||
+ h.Logger.Log(8, log.ERROR, "[Error SQL Query] %s %v - %v", c.SQL, c.Args, c.Err)
|
||||
+ }
|
||||
+ }
|
||||
+ return nil
|
||||
+}
|
||||
--- a/models/db/context.go
|
||||
+++ b/models/db/context.go
|
||||
@@ -72,6 +72,12 @@
|
||||
// GetEngine will get a db Engine from this context or return an Engine restricted to this context
|
||||
func GetEngine(ctx context.Context) Engine {
|
||||
if e := getEngine(ctx); e != nil {
|
||||
+ // A private-query marker must reach XORM's real query hooks, including
|
||||
+ // inside an existing engine/transaction context. Wrapping the Go
|
||||
+ // context alone does not change XORM's stored session context.
|
||||
+ if isSensitiveSQL(ctx) {
|
||||
+ return e.Context(ctx)
|
||||
+ }
|
||||
return e
|
||||
}
|
||||
return x.Context(ctx)
|
||||
--- /dev/null
|
||||
+++ b/models/db/sensitive_sql.go
|
||||
@@ -0,0 +1,18 @@
|
||||
+// Copyright 2026 The Forgejo Authors. All rights reserved.
|
||||
+// SPDX-License-Identifier: MIT
|
||||
+package db
|
||||
+
|
||||
+import "context"
|
||||
+
|
||||
+type sensitiveSQLKey struct{}
|
||||
+
|
||||
+// WithSensitiveSQL suppresses argument, statement and driver-error disclosure
|
||||
+// in the application's query hooks. The caller must separately refuse raw XORM
|
||||
+// SQL logging; custom driver/server auditing remains an owner custody concern.
|
||||
+func WithSensitiveSQL(ctx context.Context) context.Context {
|
||||
+ return context.WithValue(ctx, sensitiveSQLKey{}, true)
|
||||
+}
|
||||
+func isSensitiveSQL(ctx context.Context) bool {
|
||||
+ sensitive, _ := ctx.Value(sensitiveSQLKey{}).(bool)
|
||||
+ return sensitive
|
||||
+}
|
||||
--- /dev/null
|
||||
+++ b/models/db/sensitive_sql_test.go
|
||||
@@ -0,0 +1,103 @@
|
||||
+// Copyright 2026 The Forgejo Authors. All rights reserved.
|
||||
+// SPDX-License-Identifier: MIT
|
||||
+package db
|
||||
+
|
||||
+import (
|
||||
+ "bytes"
|
||||
+ "context"
|
||||
+ "errors"
|
||||
+ "fmt"
|
||||
+ "runtime/trace"
|
||||
+ "strings"
|
||||
+ "testing"
|
||||
+ "time"
|
||||
+
|
||||
+ "forgejo.org/modules/log"
|
||||
+ "xorm.io/xorm"
|
||||
+ "xorm.io/xorm/contexts"
|
||||
+)
|
||||
+
|
||||
+type sensitiveSQLLog struct {
|
||||
+ log.Logger
|
||||
+ entries []string
|
||||
+}
|
||||
+
|
||||
+func (l *sensitiveSQLLog) Log(_ int, _ log.Level, f string, v ...any) {
|
||||
+ l.entries = append(l.entries, fmt.Sprintf(f, v...))
|
||||
+}
|
||||
+func TestSensitiveSQLHooks(t *testing.T) {
|
||||
+ const secret = "synthetic-query-private-sentinel"
|
||||
+ for _, sensitive := range []bool{false, true} {
|
||||
+ ctx := context.Background()
|
||||
+ if sensitive {
|
||||
+ ctx = WithSensitiveSQL(ctx)
|
||||
+ }
|
||||
+ h := &contexts.ContextHook{Ctx: ctx, SQL: "SELECT '" + secret + "'", Args: []any{secret}, Err: errors.New(secret), ExecuteTime: time.Second}
|
||||
+ logger := new(sensitiveSQLLog)
|
||||
+ if err := (&SlowQueryHook{Logger: logger}).AfterProcess(h); err != nil {
|
||||
+ t.Fatal(err)
|
||||
+ }
|
||||
+ if err := (&ErrorQueryHook{Logger: logger}).AfterProcess(h); err != nil {
|
||||
+ t.Fatal(err)
|
||||
+ }
|
||||
+ found := strings.Contains(strings.Join(logger.entries, "\n"), secret)
|
||||
+ if found == sensitive {
|
||||
+ t.Fatal("query hook disclosure/redaction mismatch")
|
||||
+ }
|
||||
+ var out bytes.Buffer
|
||||
+ if err := trace.Start(&out); err != nil {
|
||||
+ t.Fatal(err)
|
||||
+ }
|
||||
+ next, err := (TracingHook{}).BeforeProcess(h)
|
||||
+ if err != nil {
|
||||
+ trace.Stop()
|
||||
+ t.Fatal(err)
|
||||
+ }
|
||||
+ h.Ctx = next
|
||||
+ err = (TracingHook{}).AfterProcess(h)
|
||||
+ trace.Stop()
|
||||
+ if err != nil {
|
||||
+ t.Fatal(err)
|
||||
+ }
|
||||
+ if bytes.Contains(out.Bytes(), []byte(secret)) == sensitive {
|
||||
+ t.Fatal("trace disclosure/redaction mismatch")
|
||||
+ }
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+func TestSensitiveSQLActualContextPropagation(t *testing.T) {
|
||||
+ engine, ok := GetEngine(DefaultContext).(*xorm.Engine)
|
||||
+ if !ok {
|
||||
+ t.Fatal("missing actual fixture engine")
|
||||
+ }
|
||||
+ logger := new(sensitiveSQLLog)
|
||||
+ engine.AddHook(&SlowQueryHook{Logger: logger})
|
||||
+ engine.AddHook(&ErrorQueryHook{Logger: logger})
|
||||
+ engine.AddHook(TracingHook{})
|
||||
+ const secret = "synthetic-actual-xorm-private-sentinel"
|
||||
+ for _, sensitive := range []bool{false, true} {
|
||||
+ logger.entries = nil
|
||||
+ ctx := context.Context(DefaultContext)
|
||||
+ if sensitive {
|
||||
+ ctx = WithSensitiveSQL(ctx)
|
||||
+ }
|
||||
+ var out bytes.Buffer
|
||||
+ if err := trace.Start(&out); err != nil {
|
||||
+ t.Fatal(err)
|
||||
+ }
|
||||
+ _, err := GetEngine(ctx).Exec("SELECT missing_synthetic_function(?)", secret)
|
||||
+ trace.Stop()
|
||||
+ if err == nil {
|
||||
+ t.Fatal("expected actual SQLite function refusal")
|
||||
+ }
|
||||
+ if len(logger.entries) < 2 {
|
||||
+ t.Fatal("actual query hooks did not execute")
|
||||
+ }
|
||||
+ if strings.Contains(strings.Join(logger.entries, "\n"), secret) == sensitive {
|
||||
+ t.Fatal("actual XORM query context did not preserve redaction")
|
||||
+ }
|
||||
+ if bytes.Contains(out.Bytes(), []byte(secret)) == sensitive {
|
||||
+ t.Fatal("actual XORM trace context did not preserve redaction")
|
||||
+ }
|
||||
+ }
|
||||
+}
|
||||
|
|
@ -0,0 +1,430 @@
|
|||
// Copyright 2026 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package auth
|
||||
|
||||
// This owner-confined database primitive is NOT an authorization boundary.
|
||||
// No CLI/web/API route exposes it. The admitted runtime must separately obtain
|
||||
// fresh authenticated apply/inverse sessions and retain signed public evidence.
|
||||
// Configurations and private journal rows must never leave database custody.
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"sort"
|
||||
|
||||
"forgejo.org/models/db"
|
||||
"forgejo.org/modules/setting"
|
||||
"forgejo.org/modules/timeutil"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrSourceOperationConflict = errors.New("source operation requires owner reconciliation")
|
||||
ErrSourceOperationStorage = errors.New("source operation storage failure; reconcile before retry")
|
||||
ErrSourceOperationUnsupported = errors.New("source operation requires standalone SQLite/PostgreSQL transaction with SQL logging disabled")
|
||||
operationIDPattern = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9._:-]{7,127}$`)
|
||||
)
|
||||
|
||||
// Private source rows deliberately bypass Config decoding: decoding and
|
||||
// reserializing can erase unknown fields, including a gate on an older image.
|
||||
type sourceOperationRow struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
Type Type
|
||||
Name string
|
||||
IsActive bool
|
||||
IsSyncEnabled bool
|
||||
Cfg string `xorm:"TEXT"`
|
||||
CreatedUnix timeutil.TimeStamp
|
||||
UpdatedUnix timeutil.TimeStamp
|
||||
}
|
||||
|
||||
func (sourceOperationRow) TableName() string { return "login_source" }
|
||||
|
||||
// The whole inventory, including private configs, stays in the owner's database.
|
||||
// Neither journal payload nor its digest is a public result.
|
||||
type sourceOperationJournal struct {
|
||||
OperationID string `xorm:"pk VARCHAR(128)"`
|
||||
CaseID string `xorm:"VARCHAR(128)"`
|
||||
TargetName string
|
||||
TargetID int64
|
||||
Phase string
|
||||
BeforeState string `xorm:"TEXT"`
|
||||
DesiredState string `xorm:"TEXT"`
|
||||
PostState string `xorm:"TEXT"`
|
||||
}
|
||||
|
||||
func (sourceOperationJournal) TableName() string { return "login_source_operation" }
|
||||
func init() { db.RegisterModel(new(sourceOperationJournal)) }
|
||||
|
||||
// SourceOperationResult contains only public outcome metadata. An authenticated
|
||||
// session/evidence producer must bind this result to its actual principal,
|
||||
// admitted source revision, exact parameters and current operation session.
|
||||
type SourceOperationResult struct {
|
||||
OperationID string
|
||||
Phase string
|
||||
SourceID int64
|
||||
}
|
||||
|
||||
// sourceOperationTx acquires a database-enforced lock before any source read.
|
||||
// SQLite's first write serializes every database writer, including stock admin
|
||||
// CRUD. PostgreSQL table locks exclude ordinary SQL source and user-link writes.
|
||||
// Privileged DDL, process/provider refresh and release writers are outside this
|
||||
// bounded primitive; they still require owner runtime/release exclusion.
|
||||
func sourceOperationTx(ctx context.Context, f func(context.Context) error) error {
|
||||
if db.InTransaction(ctx) || setting.Database.LogSQL || (!setting.Database.Type.IsSQLite3() && !setting.Database.Type.IsPostgreSQL()) {
|
||||
return ErrSourceOperationUnsupported
|
||||
}
|
||||
err := db.WithTx(db.WithSensitiveSQL(ctx), func(tx context.Context) error {
|
||||
session, ok := db.GetEngine(tx).(*xorm.Session)
|
||||
if !ok || session.Engine().Logger().IsShowSQL() {
|
||||
return ErrSourceOperationUnsupported
|
||||
}
|
||||
if setting.Database.Type.IsPostgreSQL() {
|
||||
if _, err := db.GetEngine(tx).Exec(`LOCK TABLE login_source_operation, login_source, "user", external_login_user IN EXCLUSIVE MODE`); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
// Even with no matching row, UPDATE starts SQLite's write transaction.
|
||||
if _, err := db.GetEngine(tx).Exec(`UPDATE login_source_operation SET phase = phase WHERE operation_id = ''`); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return f(tx)
|
||||
})
|
||||
if err == nil || errors.Is(err, ErrSourceOperationConflict) || errors.Is(err, ErrSourceOperationUnsupported) || IsErrSourceInUse(err) {
|
||||
return err
|
||||
}
|
||||
return ErrSourceOperationStorage
|
||||
}
|
||||
|
||||
func sourceInventory(ctx context.Context) ([]sourceOperationRow, error) {
|
||||
rows := make([]sourceOperationRow, 0)
|
||||
err := db.GetEngine(ctx).OrderBy("id").Find(&rows)
|
||||
return rows, err
|
||||
}
|
||||
func encodeSourceInventory(rows []sourceOperationRow) string {
|
||||
b, _ := json.Marshal(rows)
|
||||
return string(b)
|
||||
}
|
||||
func decodeSourceInventory(s string) ([]sourceOperationRow, error) {
|
||||
var rows []sourceOperationRow
|
||||
if err := json.Unmarshal([]byte(s), &rows); err != nil {
|
||||
return nil, ErrSourceOperationConflict
|
||||
}
|
||||
return rows, nil
|
||||
}
|
||||
func sourceJournal(ctx context.Context, operationID string) (*sourceOperationJournal, error) {
|
||||
row := new(sourceOperationJournal)
|
||||
found, err := db.GetEngine(ctx).ID(operationID).Get(row)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !found {
|
||||
return nil, ErrSourceOperationConflict
|
||||
}
|
||||
return row, nil
|
||||
}
|
||||
func journalResult(j *sourceOperationJournal) SourceOperationResult {
|
||||
return SourceOperationResult{OperationID: j.OperationID, Phase: j.Phase, SourceID: j.TargetID}
|
||||
}
|
||||
|
||||
// PrepareSourceOperation durably captures exact source identity and complete
|
||||
// original inventory before mutation. desired is private owner input, not an
|
||||
// authorization token. targetID=0 requires authoritative name absence. Existing
|
||||
// sources require both exact ID and name. Reuse of an operation ID is refused.
|
||||
func PrepareSourceOperation(ctx context.Context, operationID, caseID string, targetID int64, desired *Source) (SourceOperationResult, error) {
|
||||
var result SourceOperationResult
|
||||
if !operationIDPattern.MatchString(operationID) || !operationIDPattern.MatchString(caseID) || desired == nil || desired.Type != OAuth2 || desired.Name == "" || desired.Cfg == nil || desired.IsSyncEnabled || desired.ID != targetID {
|
||||
return result, ErrSourceOperationConflict
|
||||
}
|
||||
cfg, err := desired.Cfg.ToDB()
|
||||
if err != nil || len(cfg) > 64*1024 || !json.Valid(cfg) {
|
||||
return result, ErrSourceOperationConflict
|
||||
}
|
||||
candidate := sourceOperationRow{ID: targetID, Type: OAuth2, Name: desired.Name, IsActive: desired.IsActive, Cfg: string(cfg)}
|
||||
candidateBytes, _ := json.Marshal(candidate)
|
||||
err = sourceOperationTx(ctx, func(tx context.Context) error {
|
||||
// Bound confidential duplicate custody. Refuse rather than erase journals
|
||||
// with pending recovery or silently expire terminal private snapshots.
|
||||
count, err := db.GetEngine(tx).Count(new(sourceOperationJournal))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if count >= 256 {
|
||||
return ErrSourceOperationConflict
|
||||
}
|
||||
active, err := db.GetEngine(tx).Where("phase IN (?, ?, ?)", "prepared", "applied", "unchanged").Count(new(sourceOperationJournal))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if active >= 64 {
|
||||
return ErrSourceOperationConflict
|
||||
}
|
||||
found, err := db.GetEngine(tx).ID(operationID).Exist(new(sourceOperationJournal))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if found {
|
||||
return ErrSourceOperationConflict
|
||||
}
|
||||
shared, err := db.GetEngine(tx).Where("target_name = ? AND phase IN (?, ?, ?)", desired.Name, "prepared", "applied", "unchanged").Exist(new(sourceOperationJournal))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if shared {
|
||||
return ErrSourceOperationConflict
|
||||
}
|
||||
rows, err := sourceInventory(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(encodeSourceInventory(rows)) > 1024*1024 {
|
||||
return ErrSourceOperationConflict
|
||||
}
|
||||
matched := false
|
||||
for _, row := range rows {
|
||||
if row.Name == desired.Name || row.ID == targetID {
|
||||
if targetID == 0 || row.ID != targetID || row.Name != desired.Name || row.Type != OAuth2 {
|
||||
return ErrSourceOperationConflict
|
||||
}
|
||||
matched = true
|
||||
}
|
||||
}
|
||||
if targetID != 0 && !matched {
|
||||
return ErrSourceOperationConflict
|
||||
}
|
||||
j := &sourceOperationJournal{OperationID: operationID, CaseID: caseID, TargetName: desired.Name, TargetID: targetID, Phase: "prepared", BeforeState: encodeSourceInventory(rows), DesiredState: string(candidateBytes)}
|
||||
if _, err := db.GetEngine(tx).Insert(j); err != nil {
|
||||
return err
|
||||
}
|
||||
result = journalResult(j)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return SourceOperationResult{}, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// ApplySourceOperation atomically compares every original raw source row,
|
||||
// mutates exactly the target and persists confirmed post-state. A lost commit
|
||||
// acknowledgement is reconciled using the same operation ID, never a fresh ID.
|
||||
// No source registration/network call occurs inside the database transaction.
|
||||
func ApplySourceOperation(ctx context.Context, operationID, caseID string) (SourceOperationResult, error) {
|
||||
var result SourceOperationResult
|
||||
err := sourceOperationTx(ctx, func(tx context.Context) error {
|
||||
j, err := sourceJournal(tx, operationID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if j.CaseID != caseID {
|
||||
return ErrSourceOperationConflict
|
||||
}
|
||||
rows, err := sourceInventory(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if j.Phase == "applied" || j.Phase == "unchanged" {
|
||||
if encodeSourceInventory(rows) != j.PostState {
|
||||
return ErrSourceOperationConflict
|
||||
}
|
||||
result = journalResult(j)
|
||||
return nil
|
||||
}
|
||||
if j.Phase != "prepared" || encodeSourceInventory(rows) != j.BeforeState {
|
||||
return ErrSourceOperationConflict
|
||||
}
|
||||
var desired sourceOperationRow
|
||||
if json.Unmarshal([]byte(j.DesiredState), &desired) != nil {
|
||||
return ErrSourceOperationConflict
|
||||
}
|
||||
unchanged := false
|
||||
for _, row := range rows {
|
||||
if row.ID == j.TargetID {
|
||||
desired.CreatedUnix = row.CreatedUnix
|
||||
desired.UpdatedUnix = row.UpdatedUnix
|
||||
unchanged = reflect.DeepEqual(row, desired)
|
||||
}
|
||||
}
|
||||
if !unchanged {
|
||||
desired.UpdatedUnix = timeutil.TimeStampNow()
|
||||
if j.TargetID == 0 {
|
||||
desired.CreatedUnix = desired.UpdatedUnix
|
||||
if _, err := db.GetEngine(tx).NoAutoTime().Insert(&desired); err != nil {
|
||||
return err
|
||||
}
|
||||
j.TargetID = desired.ID
|
||||
} else {
|
||||
n, err := db.GetEngine(tx).ID(j.TargetID).NoAutoTime().AllCols().Update(&desired)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n != 1 {
|
||||
return ErrSourceOperationConflict
|
||||
}
|
||||
}
|
||||
}
|
||||
post, err := sourceInventory(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
expected := append([]sourceOperationRow{}, rows...)
|
||||
if !unchanged {
|
||||
replaced := false
|
||||
for i := range expected {
|
||||
if expected[i].ID == desired.ID {
|
||||
expected[i] = desired
|
||||
replaced = true
|
||||
}
|
||||
}
|
||||
if !replaced {
|
||||
expected = append(expected, desired)
|
||||
}
|
||||
sort.Slice(expected, func(i, k int) bool { return expected[i].ID < expected[k].ID })
|
||||
}
|
||||
if !reflect.DeepEqual(post, expected) {
|
||||
return ErrSourceOperationConflict
|
||||
}
|
||||
j.PostState = encodeSourceInventory(post)
|
||||
j.Phase = "applied"
|
||||
if unchanged {
|
||||
j.Phase = "unchanged"
|
||||
}
|
||||
if _, err := db.GetEngine(tx).ID(j.OperationID).AllCols().Update(j); err != nil {
|
||||
return err
|
||||
}
|
||||
result = journalResult(j)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return SourceOperationResult{}, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// InverseSourceOperation requires fresh external inverse authority on EACH call.
|
||||
// It restores a preexisting source in place; only confirmed case creation can
|
||||
// delete. Any linked local/external account refuses deletion without alteration.
|
||||
func InverseSourceOperation(ctx context.Context, operationID, caseID string) (SourceOperationResult, error) {
|
||||
var result SourceOperationResult
|
||||
err := sourceOperationTx(ctx, func(tx context.Context) error {
|
||||
j, err := sourceJournal(tx, operationID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if j.CaseID != caseID {
|
||||
return ErrSourceOperationConflict
|
||||
}
|
||||
rows, err := sourceInventory(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if j.Phase == "reverted" || j.Phase == "cancelled" {
|
||||
if encodeSourceInventory(rows) != j.BeforeState {
|
||||
return ErrSourceOperationConflict
|
||||
}
|
||||
result = journalResult(j)
|
||||
return nil
|
||||
}
|
||||
if j.Phase == "prepared" {
|
||||
if encodeSourceInventory(rows) != j.BeforeState {
|
||||
return ErrSourceOperationConflict
|
||||
}
|
||||
j.Phase = "cancelled"
|
||||
} else {
|
||||
if (j.Phase != "applied" && j.Phase != "unchanged") || encodeSourceInventory(rows) != j.PostState {
|
||||
return ErrSourceOperationConflict
|
||||
}
|
||||
before, err := decodeSourceInventory(j.BeforeState)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var original *sourceOperationRow
|
||||
for i := range before {
|
||||
if before[i].ID == j.TargetID {
|
||||
original = &before[i]
|
||||
}
|
||||
}
|
||||
if original != nil {
|
||||
if _, err := db.GetEngine(tx).ID(j.TargetID).NoAutoTime().AllCols().Update(original); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
for _, table := range []struct{ name, column string }{{"user", "login_source"}, {"external_login_user", "login_source_id"}} {
|
||||
count, err := db.GetEngine(tx).Table(table.name).Where(table.column+" = ?", j.TargetID).Count()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if count != 0 {
|
||||
return ErrSourceInUse{ID: j.TargetID}
|
||||
}
|
||||
}
|
||||
n, err := db.GetEngine(tx).ID(j.TargetID).Delete(new(sourceOperationRow))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n != 1 {
|
||||
return ErrSourceOperationConflict
|
||||
}
|
||||
}
|
||||
restored, err := sourceInventory(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if encodeSourceInventory(restored) != j.BeforeState {
|
||||
return ErrSourceOperationConflict
|
||||
}
|
||||
j.Phase = "reverted"
|
||||
}
|
||||
if _, err := db.GetEngine(tx).ID(j.OperationID).AllCols().Update(j); err != nil {
|
||||
return err
|
||||
}
|
||||
result = journalResult(j)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return SourceOperationResult{}, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// ReconcileSourceOperation only checks committed journal/current equality. It
|
||||
// never infers that a prepared intent ran and never retries the mutation. The
|
||||
// separately authenticated runtime must call this after an ambiguous result.
|
||||
func ReconcileSourceOperation(ctx context.Context, operationID, caseID string) (SourceOperationResult, error) {
|
||||
var result SourceOperationResult
|
||||
err := sourceOperationTx(ctx, func(tx context.Context) error {
|
||||
j, err := sourceJournal(tx, operationID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if j.CaseID != caseID {
|
||||
return ErrSourceOperationConflict
|
||||
}
|
||||
rows, err := sourceInventory(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
expected := j.BeforeState
|
||||
switch j.Phase {
|
||||
case "prepared", "reverted", "cancelled":
|
||||
case "applied", "unchanged":
|
||||
expected = j.PostState
|
||||
default:
|
||||
return ErrSourceOperationConflict
|
||||
}
|
||||
if encodeSourceInventory(rows) != expected {
|
||||
return ErrSourceOperationConflict
|
||||
}
|
||||
result = journalResult(j)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return SourceOperationResult{}, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
// Copyright 2026 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"forgejo.org/models/db"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
func TestSourceOperationDatabaseWriterExclusion(t *testing.T) {
|
||||
// Use DEFERRED transaction mode in the managed engine as well, so this
|
||||
// test detects removal of the primitive's explicit first write.
|
||||
previousContext := db.DefaultContext
|
||||
previousEngine, ok := db.GetEngine(previousContext).(*xorm.Engine)
|
||||
if !ok {
|
||||
t.Fatal("missing actual fixture engine")
|
||||
}
|
||||
managed, err := xorm.NewEngine("sqlite3", "file::memory:?cache=shared&_txlock=deferred")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
db.SetDefaultEngine(context.Background(), managed)
|
||||
defer func() { db.SetDefaultEngine(previousContext, previousEngine); managed.Close() }()
|
||||
// A second actual connection executes uncooperative raw SQL, so this checks
|
||||
// database exclusion rather than an advisory-lock convention.
|
||||
raw, err := xorm.NewEngine("sqlite3", "file::memory:?cache=shared&_txlock=deferred")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer raw.Close()
|
||||
locked := make(chan struct{})
|
||||
release := make(chan struct{})
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
done <- sourceOperationTx(db.DefaultContext, func(ctx context.Context) error { close(locked); <-release; return nil })
|
||||
}()
|
||||
select {
|
||||
case <-locked:
|
||||
case err := <-done:
|
||||
t.Fatalf("lock failed: %v", err)
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("lock timed out")
|
||||
}
|
||||
writeDone := make(chan error, 1)
|
||||
go func() { _, err := raw.Exec("UPDATE login_source SET name = name WHERE id = -123"); writeDone <- err }()
|
||||
select {
|
||||
case err := <-writeDone:
|
||||
close(release)
|
||||
<-done
|
||||
t.Fatalf("uncooperative write escaped exclusion: %v", err)
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
}
|
||||
close(release)
|
||||
select {
|
||||
case err := <-done:
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("transaction did not finish")
|
||||
}
|
||||
select {
|
||||
case err := <-writeDone:
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("writer did not resume")
|
||||
}
|
||||
}
|
||||
func TestSourceOperationRefusesActualXORMArgumentLogging(t *testing.T) {
|
||||
engine, ok := db.GetEngine(db.DefaultContext).(*xorm.Engine)
|
||||
if !ok {
|
||||
t.Fatal("missing actual engine")
|
||||
}
|
||||
engine.ShowSQL(true)
|
||||
defer engine.ShowSQL(false)
|
||||
called := false
|
||||
err := sourceOperationTx(db.DefaultContext, func(context.Context) error { called = true; return nil })
|
||||
if err != ErrSourceOperationUnsupported || called {
|
||||
t.Fatal("actual SQL argument logger did not fail closed")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
// Copyright 2026 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
package auth_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
auth "forgejo.org/models/auth"
|
||||
"forgejo.org/models/db"
|
||||
oauth2 "forgejo.org/services/auth/source/oauth2"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"xorm.io/xorm"
|
||||
"xorm.io/xorm/contexts"
|
||||
"xorm.io/xorm/names"
|
||||
)
|
||||
|
||||
type operationCrashAfterSourceInsert struct{}
|
||||
|
||||
func (operationCrashAfterSourceInsert) BeforeProcess(c *contexts.ContextHook) (context.Context, error) {
|
||||
return c.Ctx, nil
|
||||
}
|
||||
func (operationCrashAfterSourceInsert) AfterProcess(c *contexts.ContextHook) error {
|
||||
if c.Err == nil && strings.Contains(c.SQL, "INSERT INTO") && strings.Contains(c.SQL, "login_source") && !strings.Contains(c.SQL, "login_source_operation") {
|
||||
// Process death after the real INSERT, before actual ApplySourceOperation's
|
||||
// post-read/journal update/commit. No artificial operation is substituted.
|
||||
os.Exit(73)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func TestSourceOperationProcessRecovery(t *testing.T) {
|
||||
if step := os.Getenv("SOURCE_OPERATION_CHILD_STEP"); step != "" {
|
||||
e, err := xorm.NewEngine("sqlite3", os.Getenv("SOURCE_OPERATION_CHILD_DB")+"?_txlock=deferred")
|
||||
require.NoError(t, err)
|
||||
e.SetMapper(names.GonicMapper{})
|
||||
db.SetDefaultEngine(context.Background(), e)
|
||||
require.NoError(t, db.SyncAllTables())
|
||||
auth.RegisterTypeConfig(auth.OAuth2, new(oauth2.Source))
|
||||
ctx := db.DefaultContext
|
||||
switch step {
|
||||
case "prepare":
|
||||
_, err = auth.PrepareSourceOperation(ctx, "operation-process-recovery", "case-process-recovery", 0, operationSource())
|
||||
require.NoError(t, err)
|
||||
case "crash":
|
||||
e.AddHook(operationCrashAfterSourceInsert{})
|
||||
_, err = auth.ApplySourceOperation(ctx, "operation-process-recovery", "case-process-recovery")
|
||||
require.NoError(t, err)
|
||||
t.Fatal("real insert did not reach crash hook")
|
||||
case "recover-prepared":
|
||||
r, err := auth.ReconcileSourceOperation(ctx, "operation-process-recovery", "case-process-recovery")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "prepared", r.Phase)
|
||||
n, err := db.GetEngine(ctx).Table("login_source").Count()
|
||||
require.NoError(t, err)
|
||||
require.Zero(t, n)
|
||||
_, err = auth.ApplySourceOperation(ctx, "operation-process-recovery", "case-process-recovery")
|
||||
require.NoError(t, err)
|
||||
case "recover-applied":
|
||||
r, err := auth.ReconcileSourceOperation(ctx, "operation-process-recovery", "case-process-recovery")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "applied", r.Phase)
|
||||
_, err = auth.InverseSourceOperation(ctx, "operation-process-recovery", "case-process-recovery")
|
||||
require.NoError(t, err)
|
||||
case "recover-inverse":
|
||||
r, err := auth.ReconcileSourceOperation(ctx, "operation-process-recovery", "case-process-recovery")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "reverted", r.Phase)
|
||||
n, err := db.GetEngine(ctx).Table("login_source").Count()
|
||||
require.NoError(t, err)
|
||||
require.Zero(t, n)
|
||||
default:
|
||||
t.Fatal("unknown synthetic child step")
|
||||
}
|
||||
require.NoError(t, e.Close())
|
||||
return
|
||||
}
|
||||
exe, err := os.Executable()
|
||||
require.NoError(t, err)
|
||||
path := filepath.Join(t.TempDir(), "owner-private.sqlite")
|
||||
for _, step := range []string{"prepare", "crash", "recover-prepared", "recover-applied", "recover-inverse"} {
|
||||
child := exec.Command(exe, "-test.run=^TestSourceOperationProcessRecovery$", "-test.v")
|
||||
child.Env = append(os.Environ(), "SOURCE_OPERATION_CHILD_STEP="+step, "SOURCE_OPERATION_CHILD_DB="+path)
|
||||
output, err := child.CombinedOutput()
|
||||
if step == "crash" {
|
||||
exit, ok := err.(*exec.ExitError)
|
||||
require.True(t, ok, string(output))
|
||||
require.Equal(t, 73, exit.ExitCode(), string(output))
|
||||
} else {
|
||||
require.NoError(t, err, string(output))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,235 @@
|
|||
// Copyright 2026 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
package auth_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
auth "forgejo.org/models/auth"
|
||||
"forgejo.org/models/db"
|
||||
"forgejo.org/models/unittest"
|
||||
"forgejo.org/modules/setting"
|
||||
oauth2 "forgejo.org/services/auth/source/oauth2"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func operationDB(t *testing.T) context.Context {
|
||||
t.Helper()
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
_, err := db.GetEngine(db.DefaultContext).Exec("DELETE FROM login_source_operation")
|
||||
require.NoError(t, err)
|
||||
auth.RegisterTypeConfig(auth.OAuth2, new(oauth2.Source))
|
||||
return db.DefaultContext
|
||||
}
|
||||
func operationSource() *auth.Source {
|
||||
return &auth.Source{Type: auth.OAuth2, Name: "operation-test", IsActive: false, Cfg: &oauth2.Source{Provider: "openidConnect", ClientID: "synthetic-client", ClientSecret: "synthetic-private-original", RequiredClaimName: "groups", RequiredClaimValues: []string{"g1", "g2", "g3"}, Scopes: []string{"openid", "profile", "groups"}}}
|
||||
}
|
||||
func operationRows(t *testing.T, ctx context.Context) string {
|
||||
t.Helper()
|
||||
rows, err := db.GetEngine(ctx).Query("SELECT * FROM login_source ORDER BY id")
|
||||
require.NoError(t, err)
|
||||
b, err := json.Marshal(rows)
|
||||
require.NoError(t, err)
|
||||
return string(b)
|
||||
}
|
||||
func TestSourceOperationCreateAndInverse(t *testing.T) {
|
||||
ctx := operationDB(t)
|
||||
before := operationRows(t, ctx)
|
||||
r, err := auth.PrepareSourceOperation(ctx, "operation-create", "case-create", 0, operationSource())
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "prepared", r.Phase)
|
||||
require.Equal(t, before, operationRows(t, ctx))
|
||||
r, err = auth.ApplySourceOperation(ctx, "operation-create", "case-create")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "applied", r.Phase)
|
||||
require.Positive(t, r.SourceID)
|
||||
id := r.SourceID
|
||||
post := operationRows(t, ctx)
|
||||
r, err = auth.ApplySourceOperation(ctx, "operation-create", "case-create")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, id, r.SourceID)
|
||||
require.Equal(t, post, operationRows(t, ctx))
|
||||
_, err = auth.InverseSourceOperation(ctx, "operation-create", "wrong-case")
|
||||
require.ErrorIs(t, err, auth.ErrSourceOperationConflict)
|
||||
r, err = auth.InverseSourceOperation(ctx, "operation-create", "case-create")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "reverted", r.Phase)
|
||||
require.Equal(t, before, operationRows(t, ctx))
|
||||
_, err = auth.InverseSourceOperation(ctx, "operation-create", "case-create")
|
||||
require.NoError(t, err)
|
||||
_, err = auth.ApplySourceOperation(ctx, "operation-create", "case-create")
|
||||
require.ErrorIs(t, err, auth.ErrSourceOperationConflict)
|
||||
}
|
||||
func TestSourceOperationRestoresExactRawPreexisting(t *testing.T) {
|
||||
ctx := operationDB(t)
|
||||
s := operationSource()
|
||||
require.NoError(t, auth.CreateSource(ctx, s))
|
||||
// The old/unknown field must survive restoration byte for byte, including the
|
||||
// private synthetic secret. A typed OAuth2 decoder would silently lose it.
|
||||
raw := `{"Provider":"openidConnect","ClientID":"synthetic-client","ClientSecret":"synthetic-private-original","FutureGuard":{"x":true}}`
|
||||
_, err := db.GetEngine(ctx).Exec("UPDATE login_source SET cfg = ? WHERE id = ?", raw, s.ID)
|
||||
require.NoError(t, err)
|
||||
before := operationRows(t, ctx)
|
||||
s.Cfg.(*oauth2.Source).ClientSecret = "synthetic-private-new"
|
||||
_, err = auth.PrepareSourceOperation(ctx, "operation-restore", "case-restore", s.ID, s)
|
||||
require.NoError(t, err)
|
||||
_, err = auth.ApplySourceOperation(ctx, "operation-restore", "case-restore")
|
||||
require.NoError(t, err)
|
||||
_, err = auth.InverseSourceOperation(ctx, "operation-restore", "case-restore")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, before, operationRows(t, ctx))
|
||||
source, err := auth.GetSourceByID(ctx, s.ID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, s.ID, source.ID)
|
||||
}
|
||||
func TestSourceOperationUnchangedNeverDeletesPreexisting(t *testing.T) {
|
||||
ctx := operationDB(t)
|
||||
s := operationSource()
|
||||
require.NoError(t, auth.CreateSource(ctx, s))
|
||||
before := operationRows(t, ctx)
|
||||
_, err := auth.PrepareSourceOperation(ctx, "operation-unchanged", "case-unchanged", s.ID, s)
|
||||
require.NoError(t, err)
|
||||
r, err := auth.ApplySourceOperation(ctx, "operation-unchanged", "case-unchanged")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "unchanged", r.Phase)
|
||||
_, err = auth.InverseSourceOperation(ctx, "operation-unchanged", "case-unchanged")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, before, operationRows(t, ctx))
|
||||
}
|
||||
func TestSourceOperationPrivateDriftRefuses(t *testing.T) {
|
||||
for _, stage := range []string{"before-apply", "before-inverse"} {
|
||||
t.Run(stage, func(t *testing.T) {
|
||||
ctx := operationDB(t)
|
||||
s := operationSource()
|
||||
require.NoError(t, auth.CreateSource(ctx, s))
|
||||
_, err := auth.PrepareSourceOperation(ctx, "operation-private-drift", "case-private-drift", s.ID, s)
|
||||
require.NoError(t, err)
|
||||
if stage == "before-inverse" {
|
||||
_, err = auth.ApplySourceOperation(ctx, "operation-private-drift", "case-private-drift")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
s.Cfg.(*oauth2.Source).ClientSecret = "synthetic-private-drift"
|
||||
require.NoError(t, auth.UpdateSource(ctx, s))
|
||||
drift := operationRows(t, ctx)
|
||||
if stage == "before-apply" {
|
||||
_, err = auth.ApplySourceOperation(ctx, "operation-private-drift", "case-private-drift")
|
||||
} else {
|
||||
_, err = auth.InverseSourceOperation(ctx, "operation-private-drift", "case-private-drift")
|
||||
}
|
||||
require.ErrorIs(t, err, auth.ErrSourceOperationConflict)
|
||||
require.NotContains(t, err.Error(), "synthetic-private")
|
||||
require.Equal(t, drift, operationRows(t, ctx))
|
||||
})
|
||||
}
|
||||
}
|
||||
func TestSourceOperationStockCRUDDriftRefuses(t *testing.T) {
|
||||
ctx := operationDB(t)
|
||||
_, err := auth.PrepareSourceOperation(ctx, "operation-other-writer", "case-other-writer", 0, operationSource())
|
||||
require.NoError(t, err)
|
||||
other := operationSource()
|
||||
other.Name = "unrelated-source"
|
||||
require.NoError(t, auth.CreateSource(ctx, other))
|
||||
drift := operationRows(t, ctx)
|
||||
_, err = auth.ApplySourceOperation(ctx, "operation-other-writer", "case-other-writer")
|
||||
require.ErrorIs(t, err, auth.ErrSourceOperationConflict)
|
||||
require.Equal(t, drift, operationRows(t, ctx))
|
||||
}
|
||||
func TestSourceOperationLinkedSourceRefusesDeletion(t *testing.T) {
|
||||
for _, kind := range []string{"user", "external"} {
|
||||
t.Run(kind, func(t *testing.T) {
|
||||
ctx := operationDB(t)
|
||||
_, err := auth.PrepareSourceOperation(ctx, "operation-linked", "case-linked", 0, operationSource())
|
||||
require.NoError(t, err)
|
||||
r, err := auth.ApplySourceOperation(ctx, "operation-linked", "case-linked")
|
||||
require.NoError(t, err)
|
||||
if kind == "user" {
|
||||
_, err = db.GetEngine(ctx).Exec("UPDATE `user` SET login_source = ? WHERE id = 1", r.SourceID)
|
||||
} else {
|
||||
_, err = db.GetEngine(ctx).Exec("INSERT INTO external_login_user (external_id, user_id, login_source_id) VALUES (?, ?, ?)", "synthetic-external", 1, r.SourceID)
|
||||
}
|
||||
require.NoError(t, err)
|
||||
before := operationRows(t, ctx)
|
||||
_, err = auth.InverseSourceOperation(ctx, "operation-linked", "case-linked")
|
||||
require.True(t, auth.IsErrSourceInUse(err))
|
||||
require.Equal(t, before, operationRows(t, ctx))
|
||||
if kind == "user" {
|
||||
rows, e := db.GetEngine(ctx).Query("SELECT login_source FROM `user` WHERE id = 1")
|
||||
require.NoError(t, e)
|
||||
require.NotEqual(t, []byte("0"), rows[0]["login_source"])
|
||||
} else {
|
||||
n, e := db.GetEngine(ctx).Table("external_login_user").Count()
|
||||
require.NoError(t, e)
|
||||
require.EqualValues(t, 1, n)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
func TestSourceOperationPreparedCancellationAndIDReuse(t *testing.T) {
|
||||
ctx := operationDB(t)
|
||||
before := operationRows(t, ctx)
|
||||
_, err := auth.PrepareSourceOperation(ctx, "operation-cancelled", "case-cancelled", 0, operationSource())
|
||||
require.NoError(t, err)
|
||||
_, err = auth.PrepareSourceOperation(ctx, "operation-cancelled", "case-cancelled", 0, operationSource())
|
||||
require.ErrorIs(t, err, auth.ErrSourceOperationConflict)
|
||||
r, err := auth.InverseSourceOperation(ctx, "operation-cancelled", "case-cancelled")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "cancelled", r.Phase)
|
||||
require.Equal(t, before, operationRows(t, ctx))
|
||||
}
|
||||
func TestSourceOperationRejectsUnsafeRuntime(t *testing.T) {
|
||||
ctx := operationDB(t)
|
||||
setting.Database.LogSQL = true
|
||||
t.Cleanup(func() { setting.Database.LogSQL = false })
|
||||
_, err := auth.PrepareSourceOperation(ctx, "operation-no-logging", "case-no-logging", 0, operationSource())
|
||||
require.ErrorIs(t, err, auth.ErrSourceOperationUnsupported)
|
||||
setting.Database.LogSQL = false
|
||||
err = db.WithTx(ctx, func(tx context.Context) error {
|
||||
_, err := auth.PrepareSourceOperation(tx, "operation-nested", "case-nested", 0, operationSource())
|
||||
return err
|
||||
})
|
||||
require.ErrorIs(t, err, auth.ErrSourceOperationUnsupported)
|
||||
}
|
||||
func TestSourceOperationRefusesSharedManagedSource(t *testing.T) {
|
||||
ctx := operationDB(t)
|
||||
_, err := auth.PrepareSourceOperation(ctx, "operation-first-case", "case-first-case", 0, operationSource())
|
||||
require.NoError(t, err)
|
||||
r, err := auth.ApplySourceOperation(ctx, "operation-first-case", "case-first-case")
|
||||
require.NoError(t, err)
|
||||
shared := operationSource()
|
||||
shared.ID = r.SourceID
|
||||
_, err = auth.PrepareSourceOperation(ctx, "operation-second-case", "case-second-case", r.SourceID, shared)
|
||||
require.ErrorIs(t, err, auth.ErrSourceOperationConflict)
|
||||
}
|
||||
func TestSourceOperationBoundsPrivateJournalCustody(t *testing.T) {
|
||||
ctx := operationDB(t)
|
||||
// A full unresolved-journal budget refuses new preparation, without deleting
|
||||
// any intent or replacing private recovery data with an expiry assumption.
|
||||
for i := 0; i < 64; i++ {
|
||||
_, err := db.GetEngine(ctx).Exec("INSERT INTO login_source_operation (operation_id, case_id, target_name, phase) VALUES (?, ?, ?, ?)", fmt.Sprintf("bounded-operation-%03d", i), "bounded-case", fmt.Sprintf("bounded-target-%03d", i), "prepared")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
_, err := auth.PrepareSourceOperation(ctx, "operation-budget-full", "case-budget-full", 0, operationSource())
|
||||
require.ErrorIs(t, err, auth.ErrSourceOperationConflict)
|
||||
n, err := db.GetEngine(ctx).Table("login_source_operation").Count()
|
||||
require.NoError(t, err)
|
||||
require.EqualValues(t, 64, n)
|
||||
}
|
||||
func TestSourceOperationRequiresExactPostState(t *testing.T) {
|
||||
ctx := operationDB(t)
|
||||
before := operationRows(t, ctx)
|
||||
_, err := auth.PrepareSourceOperation(ctx, "operation-post-exact", "case-post-exact", 0, operationSource())
|
||||
require.NoError(t, err)
|
||||
_, err = db.GetEngine(ctx).Exec("CREATE TRIGGER source_operation_test_drift AFTER INSERT ON login_source BEGIN UPDATE login_source SET name = 'unexpected-rewrite' WHERE id = NEW.id; END")
|
||||
require.NoError(t, err)
|
||||
defer db.GetEngine(ctx).Exec("DROP TRIGGER source_operation_test_drift")
|
||||
_, err = auth.ApplySourceOperation(ctx, "operation-post-exact", "case-post-exact")
|
||||
require.ErrorIs(t, err, auth.ErrSourceOperationConflict)
|
||||
require.Equal(t, before, operationRows(t, ctx))
|
||||
r, err := auth.ReconcileSourceOperation(ctx, "operation-post-exact", "case-post-exact")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "prepared", r.Phase)
|
||||
}
|
||||
|
|
@ -0,0 +1,388 @@
|
|||
// Copyright 2014 The Gogs Authors. All rights reserved.
|
||||
// Copyright 2018 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
"runtime/trace"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"forgejo.org/modules/log"
|
||||
"forgejo.org/modules/setting"
|
||||
|
||||
"xorm.io/xorm"
|
||||
"xorm.io/xorm/contexts"
|
||||
"xorm.io/xorm/names"
|
||||
"xorm.io/xorm/schemas"
|
||||
|
||||
_ "github.com/go-sql-driver/mysql" // Needed for the MySQL driver
|
||||
_ "github.com/lib/pq" // Needed for the Postgresql driver
|
||||
)
|
||||
|
||||
var (
|
||||
x *xorm.Engine
|
||||
tables []any
|
||||
initFuncs []func() error
|
||||
)
|
||||
|
||||
// Engine represents a xorm engine or session.
|
||||
type Engine interface {
|
||||
Table(tableNameOrBean any) *xorm.Session
|
||||
Count(...any) (int64, error)
|
||||
Decr(column string, arg ...any) *xorm.Session
|
||||
Delete(...any) (int64, error)
|
||||
Truncate(...any) (int64, error)
|
||||
Exec(...any) (sql.Result, error)
|
||||
Find(any, ...any) error
|
||||
Get(beans ...any) (bool, error)
|
||||
ID(any) *xorm.Session
|
||||
In(string, ...any) *xorm.Session
|
||||
Incr(column string, arg ...any) *xorm.Session
|
||||
Insert(...any) (int64, error)
|
||||
Iterate(any, xorm.IterFunc) error
|
||||
IsTableExist(any) (bool, error)
|
||||
Join(joinOperator string, tablename, condition any, args ...any) *xorm.Session
|
||||
SQL(any, ...any) *xorm.Session
|
||||
Where(any, ...any) *xorm.Session
|
||||
Asc(colNames ...string) *xorm.Session
|
||||
Desc(colNames ...string) *xorm.Session
|
||||
Limit(limit int, start ...int) *xorm.Session
|
||||
NoAutoTime() *xorm.Session
|
||||
SumInt(bean any, columnName string) (res int64, err error)
|
||||
Sync(...any) error
|
||||
Select(string) *xorm.Session
|
||||
SetExpr(string, any) *xorm.Session
|
||||
NotIn(string, ...any) *xorm.Session
|
||||
OrderBy(any, ...any) *xorm.Session
|
||||
Exist(...any) (bool, error)
|
||||
Distinct(...string) *xorm.Session
|
||||
Query(...any) ([]map[string][]byte, error)
|
||||
Cols(...string) *xorm.Session
|
||||
Context(ctx context.Context) *xorm.Session
|
||||
Ping() error
|
||||
}
|
||||
|
||||
// TableInfo returns table's information via an object
|
||||
func TableInfo(v any) (*schemas.Table, error) {
|
||||
return x.TableInfo(v)
|
||||
}
|
||||
|
||||
// DumpTables dump tables information
|
||||
func DumpTables(tables []*schemas.Table, w io.Writer, tp ...schemas.DBType) error {
|
||||
return x.DumpTables(tables, w, tp...)
|
||||
}
|
||||
|
||||
// RegisterModel registers model, if initfunc provided, it will be invoked after data model sync
|
||||
func RegisterModel(bean any, initFunc ...func() error) {
|
||||
tables = append(tables, bean)
|
||||
if len(initFuncs) > 0 && initFunc[0] != nil {
|
||||
initFuncs = append(initFuncs, initFunc[0])
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
gonicNames := []string{"SSL", "UID"}
|
||||
for _, name := range gonicNames {
|
||||
names.LintGonicMapper[name] = true
|
||||
}
|
||||
}
|
||||
|
||||
// newXORMEngine returns a new XORM engine from the configuration
|
||||
func newXORMEngine() (*xorm.Engine, error) {
|
||||
connStr, err := setting.DBConnStr()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var engine *xorm.Engine
|
||||
|
||||
if setting.Database.Type.IsPostgreSQL() && len(setting.Database.Schema) > 0 {
|
||||
// OK whilst we sort out our schema issues - create a schema aware postgres
|
||||
registerPostgresSchemaDriver()
|
||||
engine, err = xorm.NewEngine("postgresschema", connStr)
|
||||
} else {
|
||||
engine, err = xorm.NewEngine(setting.Database.Type.String(), connStr)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if setting.Database.Type.IsMySQL() {
|
||||
engine.Dialect().SetParams(map[string]string{"rowFormat": "DYNAMIC"})
|
||||
}
|
||||
engine.SetSchema(setting.Database.Schema)
|
||||
return engine, nil
|
||||
}
|
||||
|
||||
// SyncAllTables sync the schemas of all tables, is required by unit test code
|
||||
func SyncAllTables() error {
|
||||
_, err := x.StoreEngine("InnoDB").SyncWithOptions(xorm.SyncOptions{
|
||||
WarnIfDatabaseColumnMissed: true,
|
||||
}, tables...)
|
||||
return err
|
||||
}
|
||||
|
||||
// InitEngine initializes the xorm.Engine and sets it as db.DefaultContext
|
||||
func InitEngine(ctx context.Context) error {
|
||||
xormEngine, err := newXORMEngine()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect to database: %w", err)
|
||||
}
|
||||
|
||||
xormEngine.SetMapper(names.GonicMapper{})
|
||||
// WARNING: for serv command, MUST remove the output to os.stdout,
|
||||
// so use log file to instead print to stdout.
|
||||
xormEngine.SetLogger(NewXORMLogger(setting.Database.LogSQL))
|
||||
xormEngine.ShowSQL(setting.Database.LogSQL)
|
||||
xormEngine.SetMaxOpenConns(setting.Database.MaxOpenConns)
|
||||
xormEngine.SetMaxIdleConns(setting.Database.MaxIdleConns)
|
||||
xormEngine.SetConnMaxLifetime(setting.Database.ConnMaxLifetime)
|
||||
xormEngine.SetConnMaxIdleTime(setting.Database.ConnMaxIdleTime)
|
||||
xormEngine.SetDefaultContext(ctx)
|
||||
|
||||
if setting.Database.SlowQueryThreshold > 0 {
|
||||
xormEngine.AddHook(&SlowQueryHook{
|
||||
Treshold: setting.Database.SlowQueryThreshold,
|
||||
Logger: log.GetLogger("xorm"),
|
||||
})
|
||||
}
|
||||
|
||||
errorLogger := log.GetLogger("xorm")
|
||||
if setting.IsInTesting {
|
||||
errorLogger = log.GetLogger(log.DEFAULT)
|
||||
}
|
||||
|
||||
xormEngine.AddHook(&ErrorQueryHook{
|
||||
Logger: errorLogger,
|
||||
})
|
||||
|
||||
xormEngine.AddHook(&TracingHook{})
|
||||
|
||||
SetDefaultEngine(ctx, xormEngine)
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetDefaultEngine sets the default engine for db
|
||||
func SetDefaultEngine(ctx context.Context, eng *xorm.Engine) {
|
||||
x = eng
|
||||
DefaultContext = &Context{
|
||||
Context: ctx,
|
||||
e: x,
|
||||
}
|
||||
}
|
||||
|
||||
// UnsetDefaultEngine closes and unsets the default engine
|
||||
// We hope the SetDefaultEngine and UnsetDefaultEngine can be paired, but it's impossible now,
|
||||
// there are many calls to InitEngine -> SetDefaultEngine directly to overwrite the `x` and DefaultContext without close
|
||||
// Global database engine related functions are all racy and there is no graceful close right now.
|
||||
func UnsetDefaultEngine() {
|
||||
if x != nil {
|
||||
_ = x.Close()
|
||||
x = nil
|
||||
}
|
||||
DefaultContext = nil
|
||||
}
|
||||
|
||||
// InitEngineWithMigration initializes a new xorm.Engine and sets it as the db.DefaultContext
|
||||
// This function must never call .Sync() if the provided migration function fails.
|
||||
// When called from the "doctor" command, the migration function is a version check
|
||||
// that prevents the doctor from fixing anything in the database if the migration level
|
||||
// is different from the expected value.
|
||||
func InitEngineWithMigration(ctx context.Context, migrateFunc func(*xorm.Engine) error) (err error) {
|
||||
if err = InitEngine(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = x.Ping(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
preprocessDatabaseCollation(x)
|
||||
|
||||
// We have to run migrateFunc here in case the user is re-running installation on a previously created DB.
|
||||
// If we do not then table schemas will be changed and there will be conflicts when the migrations run properly.
|
||||
//
|
||||
// Installation should only be being re-run if users want to recover an old database.
|
||||
// However, we should think carefully about should we support re-install on an installed instance,
|
||||
// as there may be other problems due to secret reinitialization.
|
||||
if err = migrateFunc(x); err != nil {
|
||||
return fmt.Errorf("migrate: %w", err)
|
||||
}
|
||||
|
||||
if err = SyncAllTables(); err != nil {
|
||||
return fmt.Errorf("sync database struct error: %w", err)
|
||||
}
|
||||
|
||||
for _, initFunc := range initFuncs {
|
||||
if err := initFunc(); err != nil {
|
||||
return fmt.Errorf("initFunc failed: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// NamesToBean return a list of beans or an error
|
||||
func NamesToBean(names ...string) ([]any, error) {
|
||||
beans := []any{}
|
||||
if len(names) == 0 {
|
||||
beans = append(beans, tables...)
|
||||
return beans, nil
|
||||
}
|
||||
// Need to map provided names to beans...
|
||||
beanMap := make(map[string]any)
|
||||
for _, bean := range tables {
|
||||
beanMap[strings.ToLower(reflect.Indirect(reflect.ValueOf(bean)).Type().Name())] = bean
|
||||
beanMap[strings.ToLower(x.TableName(bean))] = bean
|
||||
beanMap[strings.ToLower(x.TableName(bean, true))] = bean
|
||||
}
|
||||
|
||||
gotBean := make(map[any]bool)
|
||||
for _, name := range names {
|
||||
bean, ok := beanMap[strings.ToLower(strings.TrimSpace(name))]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("no table found that matches: %s", name)
|
||||
}
|
||||
if !gotBean[bean] {
|
||||
beans = append(beans, bean)
|
||||
gotBean[bean] = true
|
||||
}
|
||||
}
|
||||
return beans, nil
|
||||
}
|
||||
|
||||
// DumpDatabase dumps all data from database according the special database SQL syntax to file system.
|
||||
func DumpDatabase(filePath, dbType string) error {
|
||||
var tbs []*schemas.Table
|
||||
for _, t := range tables {
|
||||
t, err := x.TableInfo(t)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tbs = append(tbs, t)
|
||||
}
|
||||
|
||||
type Version struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
Version int64
|
||||
}
|
||||
t, err := x.TableInfo(&Version{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tbs = append(tbs, t)
|
||||
|
||||
if len(dbType) > 0 {
|
||||
return x.DumpTablesToFile(tbs, filePath, schemas.DBType(dbType))
|
||||
}
|
||||
return x.DumpTablesToFile(tbs, filePath)
|
||||
}
|
||||
|
||||
// MaxBatchInsertSize returns the table's max batch insert size
|
||||
func MaxBatchInsertSize(bean any) int {
|
||||
t, err := x.TableInfo(bean)
|
||||
if err != nil {
|
||||
return 50
|
||||
}
|
||||
return 999 / len(t.ColumnsSeq())
|
||||
}
|
||||
|
||||
// IsTableNotEmpty returns true if table has at least one record
|
||||
func IsTableNotEmpty(beanOrTableName any) (bool, error) {
|
||||
return x.Table(beanOrTableName).Exist()
|
||||
}
|
||||
|
||||
// DeleteAllRecords will delete all the records of this table
|
||||
func DeleteAllRecords(tableName string) error {
|
||||
_, err := x.Exec(fmt.Sprintf("DELETE FROM %s", tableName))
|
||||
return err
|
||||
}
|
||||
|
||||
// GetMaxID will return max id of the table
|
||||
func GetMaxID(beanOrTableName any) (maxID int64, err error) {
|
||||
_, err = x.Select("MAX(id)").Table(beanOrTableName).Get(&maxID)
|
||||
return maxID, err
|
||||
}
|
||||
|
||||
func SetLogSQL(ctx context.Context, on bool) {
|
||||
e := GetEngine(ctx)
|
||||
if x, ok := e.(*xorm.Engine); ok {
|
||||
x.ShowSQL(on)
|
||||
} else if sess, ok := e.(*xorm.Session); ok {
|
||||
sess.Engine().ShowSQL(on)
|
||||
}
|
||||
}
|
||||
|
||||
type TracingHook struct{}
|
||||
|
||||
var _ contexts.Hook = &TracingHook{}
|
||||
|
||||
type sqlTask struct{}
|
||||
|
||||
func (TracingHook) BeforeProcess(c *contexts.ContextHook) (context.Context, error) {
|
||||
ctx, task := trace.NewTask(c.Ctx, "sql")
|
||||
ctx = context.WithValue(ctx, sqlTask{}, task)
|
||||
if isSensitiveSQL(c.Ctx) {
|
||||
trace.Log(ctx, "query", "[owner-confined query]")
|
||||
} else {
|
||||
trace.Log(ctx, "query", c.SQL)
|
||||
trace.Logf(ctx, "args", "%v", c.Args)
|
||||
}
|
||||
return ctx, nil
|
||||
}
|
||||
|
||||
func (TracingHook) AfterProcess(c *contexts.ContextHook) error {
|
||||
c.Ctx.Value(sqlTask{}).(*trace.Task).End()
|
||||
return nil
|
||||
}
|
||||
|
||||
type SlowQueryHook struct {
|
||||
Treshold time.Duration
|
||||
Logger log.Logger
|
||||
}
|
||||
|
||||
var _ contexts.Hook = &SlowQueryHook{}
|
||||
|
||||
func (SlowQueryHook) BeforeProcess(c *contexts.ContextHook) (context.Context, error) {
|
||||
return c.Ctx, nil
|
||||
}
|
||||
|
||||
func (h *SlowQueryHook) AfterProcess(c *contexts.ContextHook) error {
|
||||
if c.ExecuteTime >= h.Treshold {
|
||||
if isSensitiveSQL(c.Ctx) {
|
||||
h.Logger.Log(8, log.WARN, "[Slow SQL Query] [owner-confined query] - %v", c.ExecuteTime)
|
||||
} else {
|
||||
h.Logger.Log(8, log.WARN, "[Slow SQL Query] %s %v - %v", c.SQL, c.Args, c.ExecuteTime)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type ErrorQueryHook struct {
|
||||
Logger log.Logger
|
||||
}
|
||||
|
||||
var _ contexts.Hook = &ErrorQueryHook{}
|
||||
|
||||
func (ErrorQueryHook) BeforeProcess(c *contexts.ContextHook) (context.Context, error) {
|
||||
return c.Ctx, nil
|
||||
}
|
||||
|
||||
func (h *ErrorQueryHook) AfterProcess(c *contexts.ContextHook) error {
|
||||
if c.Err != nil && !errors.Is(c.Err, context.Canceled) {
|
||||
if isSensitiveSQL(c.Ctx) {
|
||||
h.Logger.Log(8, log.ERROR, "[Error SQL Query] [owner-confined query]")
|
||||
} else {
|
||||
h.Logger.Log(8, log.ERROR, "[Error SQL Query] %s %v - %v", c.SQL, c.Args, c.Err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
// Copyright 2026 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
package db
|
||||
|
||||
import "context"
|
||||
|
||||
type sensitiveSQLKey struct{}
|
||||
|
||||
// WithSensitiveSQL suppresses argument, statement and driver-error disclosure
|
||||
// in the application's query hooks. The caller must separately refuse raw XORM
|
||||
// SQL logging; custom driver/server auditing remains an owner custody concern.
|
||||
func WithSensitiveSQL(ctx context.Context) context.Context {
|
||||
return context.WithValue(ctx, sensitiveSQLKey{}, true)
|
||||
}
|
||||
func isSensitiveSQL(ctx context.Context) bool {
|
||||
sensitive, _ := ctx.Value(sensitiveSQLKey{}).(bool)
|
||||
return sensitive
|
||||
}
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
// Copyright 2026 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
package db
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"runtime/trace"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"forgejo.org/modules/log"
|
||||
"xorm.io/xorm"
|
||||
"xorm.io/xorm/contexts"
|
||||
)
|
||||
|
||||
type sensitiveSQLLog struct {
|
||||
log.Logger
|
||||
entries []string
|
||||
}
|
||||
|
||||
func (l *sensitiveSQLLog) Log(_ int, _ log.Level, f string, v ...any) {
|
||||
l.entries = append(l.entries, fmt.Sprintf(f, v...))
|
||||
}
|
||||
func TestSensitiveSQLHooks(t *testing.T) {
|
||||
const secret = "synthetic-query-private-sentinel"
|
||||
for _, sensitive := range []bool{false, true} {
|
||||
ctx := context.Background()
|
||||
if sensitive {
|
||||
ctx = WithSensitiveSQL(ctx)
|
||||
}
|
||||
h := &contexts.ContextHook{Ctx: ctx, SQL: "SELECT '" + secret + "'", Args: []any{secret}, Err: errors.New(secret), ExecuteTime: time.Second}
|
||||
logger := new(sensitiveSQLLog)
|
||||
if err := (&SlowQueryHook{Logger: logger}).AfterProcess(h); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := (&ErrorQueryHook{Logger: logger}).AfterProcess(h); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
found := strings.Contains(strings.Join(logger.entries, "\n"), secret)
|
||||
if found == sensitive {
|
||||
t.Fatal("query hook disclosure/redaction mismatch")
|
||||
}
|
||||
var out bytes.Buffer
|
||||
if err := trace.Start(&out); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
next, err := (TracingHook{}).BeforeProcess(h)
|
||||
if err != nil {
|
||||
trace.Stop()
|
||||
t.Fatal(err)
|
||||
}
|
||||
h.Ctx = next
|
||||
err = (TracingHook{}).AfterProcess(h)
|
||||
trace.Stop()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if bytes.Contains(out.Bytes(), []byte(secret)) == sensitive {
|
||||
t.Fatal("trace disclosure/redaction mismatch")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSensitiveSQLActualContextPropagation(t *testing.T) {
|
||||
engine, ok := GetEngine(DefaultContext).(*xorm.Engine)
|
||||
if !ok {
|
||||
t.Fatal("missing actual fixture engine")
|
||||
}
|
||||
logger := new(sensitiveSQLLog)
|
||||
engine.AddHook(&SlowQueryHook{Logger: logger})
|
||||
engine.AddHook(&ErrorQueryHook{Logger: logger})
|
||||
engine.AddHook(TracingHook{})
|
||||
const secret = "synthetic-actual-xorm-private-sentinel"
|
||||
for _, sensitive := range []bool{false, true} {
|
||||
logger.entries = nil
|
||||
ctx := context.Context(DefaultContext)
|
||||
if sensitive {
|
||||
ctx = WithSensitiveSQL(ctx)
|
||||
}
|
||||
var out bytes.Buffer
|
||||
if err := trace.Start(&out); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err := GetEngine(ctx).Exec("SELECT missing_synthetic_function(?)", secret)
|
||||
trace.Stop()
|
||||
if err == nil {
|
||||
t.Fatal("expected actual SQLite function refusal")
|
||||
}
|
||||
if len(logger.entries) < 2 {
|
||||
t.Fatal("actual query hooks did not execute")
|
||||
}
|
||||
if strings.Contains(strings.Join(logger.entries, "\n"), secret) == sensitive {
|
||||
t.Fatal("actual XORM query context did not preserve redaction")
|
||||
}
|
||||
if bytes.Contains(out.Bytes(), []byte(secret)) == sensitive {
|
||||
t.Fatal("actual XORM trace context did not preserve redaction")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,376 @@
|
|||
// Copyright 2014 The Gogs Authors. All rights reserved.
|
||||
// Copyright 2018 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
"runtime/trace"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"forgejo.org/modules/log"
|
||||
"forgejo.org/modules/setting"
|
||||
|
||||
"xorm.io/xorm"
|
||||
"xorm.io/xorm/contexts"
|
||||
"xorm.io/xorm/names"
|
||||
"xorm.io/xorm/schemas"
|
||||
|
||||
_ "github.com/go-sql-driver/mysql" // Needed for the MySQL driver
|
||||
_ "github.com/lib/pq" // Needed for the Postgresql driver
|
||||
)
|
||||
|
||||
var (
|
||||
x *xorm.Engine
|
||||
tables []any
|
||||
initFuncs []func() error
|
||||
)
|
||||
|
||||
// Engine represents a xorm engine or session.
|
||||
type Engine interface {
|
||||
Table(tableNameOrBean any) *xorm.Session
|
||||
Count(...any) (int64, error)
|
||||
Decr(column string, arg ...any) *xorm.Session
|
||||
Delete(...any) (int64, error)
|
||||
Truncate(...any) (int64, error)
|
||||
Exec(...any) (sql.Result, error)
|
||||
Find(any, ...any) error
|
||||
Get(beans ...any) (bool, error)
|
||||
ID(any) *xorm.Session
|
||||
In(string, ...any) *xorm.Session
|
||||
Incr(column string, arg ...any) *xorm.Session
|
||||
Insert(...any) (int64, error)
|
||||
Iterate(any, xorm.IterFunc) error
|
||||
IsTableExist(any) (bool, error)
|
||||
Join(joinOperator string, tablename, condition any, args ...any) *xorm.Session
|
||||
SQL(any, ...any) *xorm.Session
|
||||
Where(any, ...any) *xorm.Session
|
||||
Asc(colNames ...string) *xorm.Session
|
||||
Desc(colNames ...string) *xorm.Session
|
||||
Limit(limit int, start ...int) *xorm.Session
|
||||
NoAutoTime() *xorm.Session
|
||||
SumInt(bean any, columnName string) (res int64, err error)
|
||||
Sync(...any) error
|
||||
Select(string) *xorm.Session
|
||||
SetExpr(string, any) *xorm.Session
|
||||
NotIn(string, ...any) *xorm.Session
|
||||
OrderBy(any, ...any) *xorm.Session
|
||||
Exist(...any) (bool, error)
|
||||
Distinct(...string) *xorm.Session
|
||||
Query(...any) ([]map[string][]byte, error)
|
||||
Cols(...string) *xorm.Session
|
||||
Context(ctx context.Context) *xorm.Session
|
||||
Ping() error
|
||||
}
|
||||
|
||||
// TableInfo returns table's information via an object
|
||||
func TableInfo(v any) (*schemas.Table, error) {
|
||||
return x.TableInfo(v)
|
||||
}
|
||||
|
||||
// DumpTables dump tables information
|
||||
func DumpTables(tables []*schemas.Table, w io.Writer, tp ...schemas.DBType) error {
|
||||
return x.DumpTables(tables, w, tp...)
|
||||
}
|
||||
|
||||
// RegisterModel registers model, if initfunc provided, it will be invoked after data model sync
|
||||
func RegisterModel(bean any, initFunc ...func() error) {
|
||||
tables = append(tables, bean)
|
||||
if len(initFuncs) > 0 && initFunc[0] != nil {
|
||||
initFuncs = append(initFuncs, initFunc[0])
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
gonicNames := []string{"SSL", "UID"}
|
||||
for _, name := range gonicNames {
|
||||
names.LintGonicMapper[name] = true
|
||||
}
|
||||
}
|
||||
|
||||
// newXORMEngine returns a new XORM engine from the configuration
|
||||
func newXORMEngine() (*xorm.Engine, error) {
|
||||
connStr, err := setting.DBConnStr()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var engine *xorm.Engine
|
||||
|
||||
if setting.Database.Type.IsPostgreSQL() && len(setting.Database.Schema) > 0 {
|
||||
// OK whilst we sort out our schema issues - create a schema aware postgres
|
||||
registerPostgresSchemaDriver()
|
||||
engine, err = xorm.NewEngine("postgresschema", connStr)
|
||||
} else {
|
||||
engine, err = xorm.NewEngine(setting.Database.Type.String(), connStr)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if setting.Database.Type.IsMySQL() {
|
||||
engine.Dialect().SetParams(map[string]string{"rowFormat": "DYNAMIC"})
|
||||
}
|
||||
engine.SetSchema(setting.Database.Schema)
|
||||
return engine, nil
|
||||
}
|
||||
|
||||
// SyncAllTables sync the schemas of all tables, is required by unit test code
|
||||
func SyncAllTables() error {
|
||||
_, err := x.StoreEngine("InnoDB").SyncWithOptions(xorm.SyncOptions{
|
||||
WarnIfDatabaseColumnMissed: true,
|
||||
}, tables...)
|
||||
return err
|
||||
}
|
||||
|
||||
// InitEngine initializes the xorm.Engine and sets it as db.DefaultContext
|
||||
func InitEngine(ctx context.Context) error {
|
||||
xormEngine, err := newXORMEngine()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect to database: %w", err)
|
||||
}
|
||||
|
||||
xormEngine.SetMapper(names.GonicMapper{})
|
||||
// WARNING: for serv command, MUST remove the output to os.stdout,
|
||||
// so use log file to instead print to stdout.
|
||||
xormEngine.SetLogger(NewXORMLogger(setting.Database.LogSQL))
|
||||
xormEngine.ShowSQL(setting.Database.LogSQL)
|
||||
xormEngine.SetMaxOpenConns(setting.Database.MaxOpenConns)
|
||||
xormEngine.SetMaxIdleConns(setting.Database.MaxIdleConns)
|
||||
xormEngine.SetConnMaxLifetime(setting.Database.ConnMaxLifetime)
|
||||
xormEngine.SetConnMaxIdleTime(setting.Database.ConnMaxIdleTime)
|
||||
xormEngine.SetDefaultContext(ctx)
|
||||
|
||||
if setting.Database.SlowQueryThreshold > 0 {
|
||||
xormEngine.AddHook(&SlowQueryHook{
|
||||
Treshold: setting.Database.SlowQueryThreshold,
|
||||
Logger: log.GetLogger("xorm"),
|
||||
})
|
||||
}
|
||||
|
||||
errorLogger := log.GetLogger("xorm")
|
||||
if setting.IsInTesting {
|
||||
errorLogger = log.GetLogger(log.DEFAULT)
|
||||
}
|
||||
|
||||
xormEngine.AddHook(&ErrorQueryHook{
|
||||
Logger: errorLogger,
|
||||
})
|
||||
|
||||
xormEngine.AddHook(&TracingHook{})
|
||||
|
||||
SetDefaultEngine(ctx, xormEngine)
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetDefaultEngine sets the default engine for db
|
||||
func SetDefaultEngine(ctx context.Context, eng *xorm.Engine) {
|
||||
x = eng
|
||||
DefaultContext = &Context{
|
||||
Context: ctx,
|
||||
e: x,
|
||||
}
|
||||
}
|
||||
|
||||
// UnsetDefaultEngine closes and unsets the default engine
|
||||
// We hope the SetDefaultEngine and UnsetDefaultEngine can be paired, but it's impossible now,
|
||||
// there are many calls to InitEngine -> SetDefaultEngine directly to overwrite the `x` and DefaultContext without close
|
||||
// Global database engine related functions are all racy and there is no graceful close right now.
|
||||
func UnsetDefaultEngine() {
|
||||
if x != nil {
|
||||
_ = x.Close()
|
||||
x = nil
|
||||
}
|
||||
DefaultContext = nil
|
||||
}
|
||||
|
||||
// InitEngineWithMigration initializes a new xorm.Engine and sets it as the db.DefaultContext
|
||||
// This function must never call .Sync() if the provided migration function fails.
|
||||
// When called from the "doctor" command, the migration function is a version check
|
||||
// that prevents the doctor from fixing anything in the database if the migration level
|
||||
// is different from the expected value.
|
||||
func InitEngineWithMigration(ctx context.Context, migrateFunc func(*xorm.Engine) error) (err error) {
|
||||
if err = InitEngine(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = x.Ping(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
preprocessDatabaseCollation(x)
|
||||
|
||||
// We have to run migrateFunc here in case the user is re-running installation on a previously created DB.
|
||||
// If we do not then table schemas will be changed and there will be conflicts when the migrations run properly.
|
||||
//
|
||||
// Installation should only be being re-run if users want to recover an old database.
|
||||
// However, we should think carefully about should we support re-install on an installed instance,
|
||||
// as there may be other problems due to secret reinitialization.
|
||||
if err = migrateFunc(x); err != nil {
|
||||
return fmt.Errorf("migrate: %w", err)
|
||||
}
|
||||
|
||||
if err = SyncAllTables(); err != nil {
|
||||
return fmt.Errorf("sync database struct error: %w", err)
|
||||
}
|
||||
|
||||
for _, initFunc := range initFuncs {
|
||||
if err := initFunc(); err != nil {
|
||||
return fmt.Errorf("initFunc failed: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// NamesToBean return a list of beans or an error
|
||||
func NamesToBean(names ...string) ([]any, error) {
|
||||
beans := []any{}
|
||||
if len(names) == 0 {
|
||||
beans = append(beans, tables...)
|
||||
return beans, nil
|
||||
}
|
||||
// Need to map provided names to beans...
|
||||
beanMap := make(map[string]any)
|
||||
for _, bean := range tables {
|
||||
beanMap[strings.ToLower(reflect.Indirect(reflect.ValueOf(bean)).Type().Name())] = bean
|
||||
beanMap[strings.ToLower(x.TableName(bean))] = bean
|
||||
beanMap[strings.ToLower(x.TableName(bean, true))] = bean
|
||||
}
|
||||
|
||||
gotBean := make(map[any]bool)
|
||||
for _, name := range names {
|
||||
bean, ok := beanMap[strings.ToLower(strings.TrimSpace(name))]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("no table found that matches: %s", name)
|
||||
}
|
||||
if !gotBean[bean] {
|
||||
beans = append(beans, bean)
|
||||
gotBean[bean] = true
|
||||
}
|
||||
}
|
||||
return beans, nil
|
||||
}
|
||||
|
||||
// DumpDatabase dumps all data from database according the special database SQL syntax to file system.
|
||||
func DumpDatabase(filePath, dbType string) error {
|
||||
var tbs []*schemas.Table
|
||||
for _, t := range tables {
|
||||
t, err := x.TableInfo(t)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tbs = append(tbs, t)
|
||||
}
|
||||
|
||||
type Version struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
Version int64
|
||||
}
|
||||
t, err := x.TableInfo(&Version{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tbs = append(tbs, t)
|
||||
|
||||
if len(dbType) > 0 {
|
||||
return x.DumpTablesToFile(tbs, filePath, schemas.DBType(dbType))
|
||||
}
|
||||
return x.DumpTablesToFile(tbs, filePath)
|
||||
}
|
||||
|
||||
// MaxBatchInsertSize returns the table's max batch insert size
|
||||
func MaxBatchInsertSize(bean any) int {
|
||||
t, err := x.TableInfo(bean)
|
||||
if err != nil {
|
||||
return 50
|
||||
}
|
||||
return 999 / len(t.ColumnsSeq())
|
||||
}
|
||||
|
||||
// IsTableNotEmpty returns true if table has at least one record
|
||||
func IsTableNotEmpty(beanOrTableName any) (bool, error) {
|
||||
return x.Table(beanOrTableName).Exist()
|
||||
}
|
||||
|
||||
// DeleteAllRecords will delete all the records of this table
|
||||
func DeleteAllRecords(tableName string) error {
|
||||
_, err := x.Exec(fmt.Sprintf("DELETE FROM %s", tableName))
|
||||
return err
|
||||
}
|
||||
|
||||
// GetMaxID will return max id of the table
|
||||
func GetMaxID(beanOrTableName any) (maxID int64, err error) {
|
||||
_, err = x.Select("MAX(id)").Table(beanOrTableName).Get(&maxID)
|
||||
return maxID, err
|
||||
}
|
||||
|
||||
func SetLogSQL(ctx context.Context, on bool) {
|
||||
e := GetEngine(ctx)
|
||||
if x, ok := e.(*xorm.Engine); ok {
|
||||
x.ShowSQL(on)
|
||||
} else if sess, ok := e.(*xorm.Session); ok {
|
||||
sess.Engine().ShowSQL(on)
|
||||
}
|
||||
}
|
||||
|
||||
type TracingHook struct{}
|
||||
|
||||
var _ contexts.Hook = &TracingHook{}
|
||||
|
||||
type sqlTask struct{}
|
||||
|
||||
func (TracingHook) BeforeProcess(c *contexts.ContextHook) (context.Context, error) {
|
||||
ctx, task := trace.NewTask(c.Ctx, "sql")
|
||||
ctx = context.WithValue(ctx, sqlTask{}, task)
|
||||
trace.Log(ctx, "query", c.SQL)
|
||||
trace.Logf(ctx, "args", "%v", c.Args)
|
||||
return ctx, nil
|
||||
}
|
||||
|
||||
func (TracingHook) AfterProcess(c *contexts.ContextHook) error {
|
||||
c.Ctx.Value(sqlTask{}).(*trace.Task).End()
|
||||
return nil
|
||||
}
|
||||
|
||||
type SlowQueryHook struct {
|
||||
Treshold time.Duration
|
||||
Logger log.Logger
|
||||
}
|
||||
|
||||
var _ contexts.Hook = &SlowQueryHook{}
|
||||
|
||||
func (SlowQueryHook) BeforeProcess(c *contexts.ContextHook) (context.Context, error) {
|
||||
return c.Ctx, nil
|
||||
}
|
||||
|
||||
func (h *SlowQueryHook) AfterProcess(c *contexts.ContextHook) error {
|
||||
if c.ExecuteTime >= h.Treshold {
|
||||
h.Logger.Log(8, log.WARN, "[Slow SQL Query] %s %v - %v", c.SQL, c.Args, c.ExecuteTime)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type ErrorQueryHook struct {
|
||||
Logger log.Logger
|
||||
}
|
||||
|
||||
var _ contexts.Hook = &ErrorQueryHook{}
|
||||
|
||||
func (ErrorQueryHook) BeforeProcess(c *contexts.ContextHook) (context.Context, error) {
|
||||
return c.Ctx, nil
|
||||
}
|
||||
|
||||
func (h *ErrorQueryHook) AfterProcess(c *contexts.ContextHook) error {
|
||||
if c.Err != nil && !errors.Is(c.Err, context.Canceled) {
|
||||
h.Logger.Log(8, log.ERROR, "[Error SQL Query] %s %v - %v", c.SQL, c.Args, c.Err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
import pathlib,subprocess,json,hashlib,time
|
||||
root=pathlib.Path('/tmp/apps-operation-controls-source-r1');target=root/'models/auth/source_operation.go';original=target.read_text()
|
||||
runner=pathlib.Path('/tmp/run-apps-operation-tests-r1.py').read_text().replace('/tmp/apps-operation-backend-r1','/tmp/apps-operation-controls-source-r1').replace("'./models/auth','./models/db','-run','TestSourceOperation|TestSensitiveSQL'","'./models/auth','-run',TEST_FILTER")
|
||||
controls=[('apply-cas','j.Phase != "prepared" || encodeSourceInventory(rows) != j.BeforeState','j.Phase != "prepared"','TestSourceOperationPrivateDriftRefuses/before-apply'),('inverse-cas','(j.Phase != "applied" && j.Phase != "unchanged") || encodeSourceInventory(rows) != j.PostState','(j.Phase != "applied" && j.Phase != "unchanged")','TestSourceOperationPrivateDriftRefuses/before-inverse'),('linked-delete','if count != 0 {','if false && count != 0 {','TestSourceOperationLinkedSourceRefusesDeletion'),('sqlite-first-write','''if _, err := db.GetEngine(tx).Exec(`UPDATE login_source_operation SET phase = phase WHERE operation_id = ''`); err != nil {
|
||||
return err
|
||||
}''','', 'TestSourceOperationDatabaseWriterExclusion')]
|
||||
results=[]
|
||||
try:
|
||||
for label,old,new,filt in controls:
|
||||
assert old in original,label
|
||||
target.write_text(original.replace(old,new,1))
|
||||
script=runner.replace('TEST_FILTER',repr(filt)).replace('/tmp/apps-operation-tests-r1.log',f'/tmp/apps-operation-control-{label}.log').replace('/tmp/apps-operation-tests-r1.json',f'/tmp/apps-operation-control-{label}.json')
|
||||
p=pathlib.Path(f'/tmp/run-apps-operation-control-{label}.py');p.write_text(script)
|
||||
outcome=subprocess.run(['python3',str(p)],capture_output=True,text=True)
|
||||
meta=json.loads(pathlib.Path(f'/tmp/apps-operation-control-{label}.json').read_text())
|
||||
actual_log=pathlib.Path(f'/tmp/apps-operation-control-{label}.log').read_text()
|
||||
results.append({'test_assertion_failed':'--- FAIL: TestSourceOperation' in actual_log,'control':label,'expected':'test failure after guard removal','detected':meta['returncode']!=0 and '--- FAIL: TestSourceOperation' in actual_log,'returncode':meta['returncode'],'seconds':meta['seconds']})
|
||||
print(label,results[-1],flush=True)
|
||||
finally:
|
||||
target.write_text(original)
|
||||
pathlib.Path('/tmp/apps-operation-controls-r1.json').write_text(json.dumps({'source_sha256':hashlib.sha256(original.encode()).hexdigest(),'controls':results},indent=2))
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
import os,pathlib,subprocess,json,time
|
||||
r=pathlib.Path('/tmp/apps-operation-backend-r1')
|
||||
env={'PATH':'/tmp/forgejo-independent-r1/go/bin:/usr/bin:/bin','HOME':'/tmp/operation-home','GOTOOLCHAIN':'local','GOPROXY':'off','GOSUMDB':'off','GOMODCACHE':'/tmp/forgejo-independent-r1/gomodcache','GOCACHE':'/tmp/apps-operation-gocache-r1','GOPATH':'/tmp/operation-gopath','GOVCS':'*:off','GIT_CONFIG_NOSYSTEM':'1','GIT_CONFIG_GLOBAL':'/tmp/operation-test.gitconfig','GIT_TERMINAL_PROMPT':'0','GOMAXPROCS':'4','CGO_ENABLED':'1'}
|
||||
args=['bwrap','--unshare-net','--ro-bind','/usr','/usr','--ro-bind','/lib','/lib','--ro-bind','/lib64','/lib64','--symlink','usr/bin','/bin','--proc','/proc','--dev','/dev','--tmpfs','/tmp','--ro-bind','/tmp/forgejo-independent-r1/go','/tmp/forgejo-independent-r1/go','--ro-bind','/tmp/forgejo-independent-r1/gomodcache','/tmp/forgejo-independent-r1/gomodcache','--bind',str(r),str(r),'--bind','/tmp/apps-operation-gocache-r1','/tmp/apps-operation-gocache-r1','--chdir',str(r),'--','env','-i']+[k+'='+v for k,v in env.items()]+['go','test','-p','4','-tags','sqlite sqlite_unlock_notify','./models/auth','./models/db','-run','TestSourceOperation|TestSensitiveSQLHooks','-count=1','-v']
|
||||
start=time.time()
|
||||
with open('/tmp/apps-operation-tests-r1.log','w') as out: p=subprocess.run(args,stdout=out,stderr=subprocess.STDOUT)
|
||||
pathlib.Path('/tmp/apps-operation-tests-r1.json').write_text(json.dumps({'argv':args,'returncode':p.returncode,'seconds':time.time()-start},indent=2))
|
||||
print(p.returncode);print(pathlib.Path('/tmp/apps-operation-tests-r1.log').read_text()[-9000:])
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
import os,pathlib,subprocess,json,time
|
||||
r=pathlib.Path('/tmp/apps-operation-backend-r1')
|
||||
env={'PATH':'/tmp/forgejo-independent-r1/go/bin:/usr/bin:/bin','HOME':'/tmp/operation-home','GOTOOLCHAIN':'local','GOPROXY':'off','GOSUMDB':'off','GOMODCACHE':'/tmp/forgejo-independent-r1/gomodcache','GOCACHE':'/tmp/apps-operation-gocache-r1','GOPATH':'/tmp/operation-gopath','GOVCS':'*:off','GIT_CONFIG_NOSYSTEM':'1','GIT_CONFIG_GLOBAL':'/tmp/operation-test.gitconfig','GIT_TERMINAL_PROMPT':'0','GOMAXPROCS':'4','CGO_ENABLED':'1'}
|
||||
args=['bwrap','--unshare-net','--unshare-pid','--ro-bind','/usr','/usr','--ro-bind','/lib','/lib','--ro-bind','/lib64','/lib64','--symlink','usr/bin','/bin','--proc','/proc','--dev','/dev','--tmpfs','/tmp','--ro-bind','/tmp/forgejo-independent-r1/go','/tmp/forgejo-independent-r1/go','--ro-bind','/tmp/forgejo-independent-r1/gomodcache','/tmp/forgejo-independent-r1/gomodcache','--bind',str(r),str(r),'--bind','/tmp/apps-operation-gocache-r1','/tmp/apps-operation-gocache-r1','--chdir',str(r),'--','env','-i']+[k+'='+v for k,v in env.items()]+['go','test','-p','4','-tags','sqlite sqlite_unlock_notify','./models/auth','./models/db','-run','TestSourceOperation|TestSensitiveSQL','-count=1','-v']
|
||||
start=time.time()
|
||||
with open('/tmp/apps-operation-tests-r1.log','w') as out: p=subprocess.run(args,stdout=out,stderr=subprocess.STDOUT)
|
||||
pathlib.Path('/tmp/apps-operation-tests-r1.json').write_text(json.dumps({'argv':args,'returncode':p.returncode,'seconds':time.time()-start},indent=2))
|
||||
print(p.returncode);print(pathlib.Path('/tmp/apps-operation-tests-r1.log').read_text()[-9000:])
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
{
|
||||
"source": "actual reviewed Forgejo 11.0.3 extended source at /tmp/forgejo-independent-r1/forgejo",
|
||||
"candidate_root": "/tmp/apps-operation-backend-r1",
|
||||
"paths": [
|
||||
{
|
||||
"path": "models/auth/source_operation.go",
|
||||
"before_sha256": null,
|
||||
"after_sha256": "ef1b07ef6cf454015d00cde8588835f5118d829da91eb0303d37184859345a31",
|
||||
"preimage_absent": true
|
||||
},
|
||||
{
|
||||
"path": "models/auth/source_operation_test.go",
|
||||
"before_sha256": null,
|
||||
"after_sha256": "77d311bca42f48faf3fbdc15c53582e3d8a6d9108a72ed7bb6172e5b88951428",
|
||||
"preimage_absent": true
|
||||
},
|
||||
{
|
||||
"path": "models/auth/source_operation_internal_test.go",
|
||||
"before_sha256": null,
|
||||
"after_sha256": "af0f424d3a1fd2b29793b64baa1fa07284fee7ea0b7721c503db4efac0d871cb",
|
||||
"preimage_absent": true
|
||||
},
|
||||
{
|
||||
"path": "models/auth/source_operation_process_test.go",
|
||||
"before_sha256": null,
|
||||
"after_sha256": "40d33b11a01cb659f0bf6f8b545c4d79910e8c082a789060bcbc9cadeee965bd",
|
||||
"preimage_absent": true
|
||||
},
|
||||
{
|
||||
"path": "models/db/engine.go",
|
||||
"before_sha256": "774cedc74e1a8d9ae6f9e5f106c6cbb66f12c11b42e99ef07a7affd03269c3d2",
|
||||
"after_sha256": "43683804d0c5bc6ead1dba0344c69dd196825cf3d5eb502af8d932a7b8207b96",
|
||||
"preimage_absent": false
|
||||
},
|
||||
{
|
||||
"path": "models/db/context.go",
|
||||
"before_sha256": "10774e272206c7a8d6ebfb106b37d535de77f7e96ba57657b4dad960639f8519",
|
||||
"after_sha256": "d4c4acfcb8987071aecf5cd957b3821c8d2619a2696398d37c2970f2527686f7",
|
||||
"preimage_absent": false
|
||||
},
|
||||
{
|
||||
"path": "models/db/sensitive_sql.go",
|
||||
"before_sha256": null,
|
||||
"after_sha256": "8d4e6b3f08e9fdd919e97e47bd857cbd8935fdcbf367d20b5b8c545f279e796e",
|
||||
"preimage_absent": true
|
||||
},
|
||||
{
|
||||
"path": "models/db/sensitive_sql_test.go",
|
||||
"before_sha256": null,
|
||||
"after_sha256": "de6353c955047c3311747e4777b9f9b2195e9efb19160b971dd758dfc94cf31b",
|
||||
"preimage_absent": true
|
||||
}
|
||||
],
|
||||
"authority": "source-only low-level private database primitives; no exposed runtime/admission/backend completion"
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
Apps release source r1 requires one source correction before integration. Its
|
||||
`tools/forgejo-release-build.py:46` build sandbox mounts `/proc` without a PID
|
||||
namespace. A process running through the exact shipped `sandbox()` can read an
|
||||
unrelated process's command line, even though that process and its arguments are
|
||||
outside the build workspace. This breaks the intended separation from ambient
|
||||
credential-bearing processes. No real credential or host process command line
|
||||
was inspected: the reproducer ran inside an outer PID/network namespace and
|
||||
read only a synthetic helper's public canary. Adding only `--unshare-pid` made
|
||||
the identical read return `DENIED`. The parent-root filesystem traversal control
|
||||
returned permission denied; no filesystem escape is claimed.
|
||||
|
||||
The exact adverse evidence is `proc-boundary-result.json`, with the executable
|
||||
reproducer and invocation in `proc-boundary-test.py` and
|
||||
`proc-argv-execution.json`. The synthetic helper exited normally and every owned
|
||||
execution handle is terminal. The narrow requested correction is PID isolation
|
||||
in `sandbox()`. This review does not modify the proposal or its owner source.
|
||||
|
||||
All other reviewed build/artifact checks passed. The input patch is
|
||||
`f0bcb2903592874d355e2a403de0c22aeda79b00070f30e7ea02b45106dd67fa`;
|
||||
the frozen map is
|
||||
`ec499038fa9a1e612bb81ed63bd67dad2ca52a178bb7bcb89a33bb64832880bb`.
|
||||
All 71 map entries match current bytes. Applying the patch to the exact canonical
|
||||
Makefile preimage produces exactly the six frozen postimages. Canonical Apps
|
||||
remains `944b30ad63ab054cc24617e374e177d34cd572da`, its Makefile and both deployed
|
||||
values files match the source inventory, and the five proposed added paths are
|
||||
absent there. Source, Go and Node archives match the lock; source PAX commit and
|
||||
Node's retained official checksum text agree. The accepted prior independent
|
||||
review supplies the source/Go upstream-origin checks; no network was used here.
|
||||
|
||||
Independent offline checks rehashed both existing source workspaces: each has
|
||||
628 identical frontend assets, four identical bindata files, the seven exact
|
||||
previously reviewed source outputs, preserved dependency locks, and the same
|
||||
110,936,928-byte executable, SHA256
|
||||
`4df429454b0289c6df76d7fd95d41394ebc286191809349b2c1e655b6be494e4`.
|
||||
A fresh execution of the exact candidate's verifier in nested PID/network
|
||||
isolation extracted all 2,234 public/options/template assets with exact byte
|
||||
equality, exposed the new CLI flag, and found no ELF interpreter. The executable
|
||||
sandbox had only the binary and empty output directory; no source/assets/library
|
||||
fallback could satisfy that test. Seven independent adverse controls refused
|
||||
outside-/tmp workspaces, existing workspaces, bad archives, repeated extraction,
|
||||
changed dependency locks, changed reviewed source and changed container binary.
|
||||
|
||||
OCI inspection used only a read-only archive, without Docker or a registry.
|
||||
Every descriptor size, compressed blob digest and uncompressed diff_id matches;
|
||||
the complete original public base layer descriptor prefix is exact, rootless
|
||||
runtime configuration and labels agree, and the final layer contains only the
|
||||
expected directories and exact executable with uid/gid 0 and mode0755.
|
||||
Archive SHA256 is
|
||||
`e0e74653321383e133485191f0ac5af53aafb065fb5809e20ede2808cec91539`,
|
||||
manifest SHA256 is
|
||||
`9d12779f0936b9ccdbde402a9869eaad8bb91ba724d579b6c2983e8e58dabf96`,
|
||||
and config SHA256 is
|
||||
`e9e81da43c5ea6895a95b6304337a40874e3275833be5f21fc6ffc104f79a2af`.
|
||||
The two full builds and two OCI builds remain recorded producer evidence; this
|
||||
review rechecked their retained outputs and did not repeat expensive builds.
|
||||
|
||||
The initial reviewer harness incorrectly required every final-layer timestamp
|
||||
to equal the image epoch. BuildKit correctly retains older source/base times and
|
||||
clips later timestamps to the epoch. That harness failure, original script and
|
||||
result are retained as attempt1. The corrected test checks the source binary's
|
||||
exact source epoch and each entry's upper bound; attempt2 passes. This is a
|
||||
reviewer test refinement, with no candidate correction or extra build success.
|
||||
The later proc controls are a separate test of the same exact source revision.
|
||||
|
||||
This packet is an independent technical source review, not owner assent,
|
||||
architecture acceptance, image admission or deployment authorization. Existing
|
||||
operational gaps remain: authenticated Access Engine decision enforcement,
|
||||
private runtime secret input (the proposal is unimplemented), owner integration
|
||||
and image admission/publication, exact type/case/descriptor re-freeze, current
|
||||
operation/inverse sessions, durable authenticated journal, authoritative persisted
|
||||
application before/post/current state, all release/application writer exclusion,
|
||||
browser/group/login probes, capacity and rollout. Helm omission or rollback
|
||||
does not delete persisted OAuth sources; image downgrade before verified inverse
|
||||
is unsafe. Linked users or external logins must not be removed to force inverse.
|
||||
These remain existing T34/T37 boundaries, not new gates introduced by this review.
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"README.md": "2f1643212661b1604f9be35300fdc9368b5f8256c94b38b201d35c77d95e27cf",
|
||||
"controls/archive-tamper.log": "a6eaa5d1f58cf23abd7627f354b782abd98beaab27a8a8bb0d2d876a457823e9",
|
||||
"controls/binary-change-before-container.log": "e54cbd65d7abda0f1eb0d1c3c942df586dd90046f746081f1ed7ce77f8a9dee8",
|
||||
"controls/dependency-lock-tamper.log": "4e36b2c28547e306c4e1a69fbc9ebe032ffc282dad8067324f582bee792b20ee",
|
||||
"controls/existing-workspace.log": "7a09f8a50ea0374f2471bd6e19c6f63c6d9396c6e11adcea25e908c4a9d883bd",
|
||||
"controls/outside-tmp.log": "15e2ff62c356b04f22bae120faaabb190f40a4b57911971df0e406edbd0ff096",
|
||||
"controls/repeat-verification.log": "248f3eccad77673747a6bc7d0a746a770c9d00e953bc3e7a9d0d43ed19c9dfb9",
|
||||
"controls/reviewed-source-tamper.log": "c8a263c96c7e60b8f62e0a995c4a45a61db814860670870b91add69900543467",
|
||||
"final-input-check.json": "64901fa6e0d6bff50983106605e0a2f690b93c04a5149bb0a9c9f8668b2229d3",
|
||||
"fresh-process.json": "5bdb80885c005f9886e29a10c579f9f15cf2653d4a1b1259e4ec799e84261645",
|
||||
"fresh-verification.json": "cd9b24143d73231cc2029b8a8788d32f677fd51255953da6c1aed3d907ed7dca",
|
||||
"offline-attempt1.json": "b6d6171abf3221dd2da0b79b9631076258a3e638555e3fb2d4f796a53490d8bf",
|
||||
"offline-attempt1.log": "617277c76cf8c6bbb05100f8ca0f5efd72e77a8c5337a974f70185c292db1b7b",
|
||||
"offline-attempt2.json": "a04e84668beef61b1ae62093514204bbed3ebb939f13b3461641dedba8b5c009",
|
||||
"offline-attempt2.log": "0838feceb8317e6e19d3d398fe1476de356ac7aa9ab7348b158406f8b5c28dd4",
|
||||
"offline-results-attempt1.json": "6eb2430dd38d572abb2a579f2ffd508ad8720e53e7d41dbb7f077178290ca35c",
|
||||
"offline-results.json": "fb6da7aaaf43fe613d27a025a74ca42e747e7bb5ddd658b62ed7a8a1f3ba3383",
|
||||
"proc-argv-execution.json": "e32c97e8e56229be96a4e69ddb1d8c47028d876fef2b61e79532e6fc9e26811e",
|
||||
"proc-argv.log": "d8c7d4ec492dcfbd4bb1584dff74da9c05ec13a730fb58b09138f33d4db00b94",
|
||||
"proc-boundary-execution.json": "a75dd2e210c88c343b8bfca82f4ca40c7402cf32cbc0f50e5ff8e985db1e9c33",
|
||||
"proc-boundary-result-attempt1.json": "57878fb236c8c5253cfe5424f7ff136397c0cc08ff700ab0bffcef1e963ac23d",
|
||||
"proc-boundary-result.json": "d8c7d4ec492dcfbd4bb1584dff74da9c05ec13a730fb58b09138f33d4db00b94",
|
||||
"proc-boundary-test.py": "45eb2d6eab745ad4e7bfd5a1fc0b33e0757cab20f314b45aaeca15afe76b8703",
|
||||
"proc-boundary.log": "57878fb236c8c5253cfe5424f7ff136397c0cc08ff700ab0bffcef1e963ac23d",
|
||||
"public-archives.json": "e67bdfe41e887d380b307e065258838f8c6e2891f63ff842d6a934326f6410f6",
|
||||
"review-offline-attempt1.py": "1800ca5f24898e3e6fe993461ba50a78ed6bceebe4bc32d87d5527e224330438",
|
||||
"review-offline.py": "2d4c6f0160eae873fa475042f99f183a818c06925f00de789c5650defb874712",
|
||||
"review.json": "df2c23df953e1d4c10bd3abd7aea350cc33d8bbfdb2a99d92a436feba0946f73",
|
||||
"status.json": "3c9427425181504207e5496f52a42aac231b35d413cb1ca70016778fd3da3451"
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"apps_head": "944b30ad63ab054cc24617e374e177d34cd572da",
|
||||
"canonical_files": {
|
||||
"Makefile": "a319e264e6f04378f6b3d2c7781860fc4bac1abdb248cffa8c26065e08f2f290",
|
||||
"helm/forgejo-values.yaml": "cf87a3dccd666bf8870619755afac4af64fc0bee77b0f0ea54718d6384b70d4f",
|
||||
"helm/forgejo-registry-values.yaml": "a38f551734843c6367448f267c7376b1aeee010927a49690fdd7c1471e0ae450"
|
||||
},
|
||||
"Makefile_preimage_equals_canonical_HEAD_and_worktree": true,
|
||||
"five_added_paths_absent_in_canonical_owner": true,
|
||||
"all_frozen_entries_still_exact": 71,
|
||||
"patch_sha256": "f0bcb2903592874d355e2a403de0c22aeda79b00070f30e7ea02b45106dd67fa",
|
||||
"map_sha256": "ec499038fa9a1e612bb81ed63bd67dad2ca52a178bb7bcb89a33bb64832880bb",
|
||||
"canonical_owner_changed_by_reviewer": false,
|
||||
"candidate_changed_by_reviewer": false
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
{
|
||||
"argv": [
|
||||
"bwrap",
|
||||
"--unshare-net",
|
||||
"--unshare-pid",
|
||||
"--proc",
|
||||
"/proc",
|
||||
"--dev",
|
||||
"/dev",
|
||||
"--tmpfs",
|
||||
"/tmp",
|
||||
"--dir",
|
||||
"/app",
|
||||
"--ro-bind",
|
||||
"/tmp/review/verification/source/gitea",
|
||||
"/app/forgejo",
|
||||
"--bind",
|
||||
"/tmp/review/verification/embedded-extracted",
|
||||
"/results",
|
||||
"--chdir",
|
||||
"/tmp",
|
||||
"--clearenv",
|
||||
"--setenv",
|
||||
"PATH",
|
||||
"/nonexistent",
|
||||
"--setenv",
|
||||
"HOME",
|
||||
"/tmp/home",
|
||||
"--setenv",
|
||||
"USER",
|
||||
"builder",
|
||||
"--setenv",
|
||||
"USERNAME",
|
||||
"builder",
|
||||
"--setenv",
|
||||
"GITEA_WORK_DIR",
|
||||
"/tmp/work",
|
||||
"--setenv",
|
||||
"GITEA_CUSTOM",
|
||||
"/tmp/custom",
|
||||
"--setenv",
|
||||
"GITEA_APP_INI",
|
||||
"/tmp/absent-app.ini",
|
||||
"--setenv",
|
||||
"TZ",
|
||||
"UTC",
|
||||
"--setenv",
|
||||
"LC_ALL",
|
||||
"C",
|
||||
"--",
|
||||
"/app/forgejo",
|
||||
"embedded",
|
||||
"extract",
|
||||
"--include-vendored",
|
||||
"--destination",
|
||||
"/results",
|
||||
"**"
|
||||
],
|
||||
"driver_pid": null,
|
||||
"phase": "embedded-extract",
|
||||
"pid": null,
|
||||
"returncode": 0,
|
||||
"seconds": 1.91,
|
||||
"state": "complete"
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"all_public_options_templates_exact": true,
|
||||
"architecture_approval": false,
|
||||
"binary_sha256": "4df429454b0289c6df76d7fd95d41394ebc286191809349b2c1e655b6be494e4",
|
||||
"embedded_file_manifest_sha256": "74a0ec278ab99ac86ee434d7620229dc4a70146efd51876a1e4738d2c9f4ffdb",
|
||||
"embedded_files": 2234,
|
||||
"native_admission": false,
|
||||
"network": "bwrap --unshare-net",
|
||||
"new_cli_flag_present": true,
|
||||
"no_dynamic_interpreter": true,
|
||||
"no_source_assets_libraries_or_host_config_mounted": true,
|
||||
"source_locks_preserved": true
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
{
|
||||
"argv": [
|
||||
"/usr/bin/bwrap",
|
||||
"--unshare-net",
|
||||
"--unshare-pid",
|
||||
"--new-session",
|
||||
"--die-with-parent",
|
||||
"--ro-bind",
|
||||
"/usr",
|
||||
"/usr",
|
||||
"--ro-bind",
|
||||
"/lib",
|
||||
"/lib",
|
||||
"--ro-bind",
|
||||
"/lib64",
|
||||
"/lib64",
|
||||
"--symlink",
|
||||
"usr/bin",
|
||||
"/bin",
|
||||
"--proc",
|
||||
"/proc",
|
||||
"--dev",
|
||||
"/dev",
|
||||
"--tmpfs",
|
||||
"/tmp",
|
||||
"--bind",
|
||||
"/tmp/apps-release-independent-review-r1-hkx9xvc0",
|
||||
"/tmp/review",
|
||||
"--ro-bind",
|
||||
"/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-17/apps-forgejo-release-build-r1",
|
||||
"/proposal",
|
||||
"--ro-bind",
|
||||
"/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-17/forgejo-required-claim-values-review-r1",
|
||||
"/prior-review",
|
||||
"--ro-bind",
|
||||
"/tmp/apps-forgejo-release-build-r1/source",
|
||||
"/source1",
|
||||
"--ro-bind",
|
||||
"/tmp/apps-forgejo-release-repro-r1/source",
|
||||
"/source2",
|
||||
"--ro-bind",
|
||||
"/tmp/apps-forgejo-release-repro-r1/image.oci.tar",
|
||||
"/image.oci.tar",
|
||||
"--ro-bind",
|
||||
"/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-17/apps-release-independent-review-r1/review-offline.py",
|
||||
"/review-offline.py",
|
||||
"--chdir",
|
||||
"/tmp/review",
|
||||
"--clearenv",
|
||||
"--setenv",
|
||||
"PATH",
|
||||
"/usr/bin:/bin",
|
||||
"--setenv",
|
||||
"HOME",
|
||||
"/tmp/home",
|
||||
"--",
|
||||
"/usr/bin/python3",
|
||||
"-I",
|
||||
"/review-offline.py"
|
||||
],
|
||||
"returncode": 1,
|
||||
"elapsed_seconds": 6.03,
|
||||
"owned_pid_terminal": true,
|
||||
"workspace": "/tmp/apps-release-independent-review-r1-hkx9xvc0",
|
||||
"network": "unshare-net",
|
||||
"pid": "unshare-pid",
|
||||
"host_home_credentials_config_mounted": false
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
{
|
||||
"argv": [
|
||||
"/usr/bin/bwrap",
|
||||
"--unshare-net",
|
||||
"--unshare-pid",
|
||||
"--new-session",
|
||||
"--die-with-parent",
|
||||
"--ro-bind",
|
||||
"/usr",
|
||||
"/usr",
|
||||
"--ro-bind",
|
||||
"/lib",
|
||||
"/lib",
|
||||
"--ro-bind",
|
||||
"/lib64",
|
||||
"/lib64",
|
||||
"--symlink",
|
||||
"usr/bin",
|
||||
"/bin",
|
||||
"--proc",
|
||||
"/proc",
|
||||
"--dev",
|
||||
"/dev",
|
||||
"--tmpfs",
|
||||
"/tmp",
|
||||
"--bind",
|
||||
"/tmp/apps-release-independent-review-r1-attempt2-86saom6w",
|
||||
"/tmp/review",
|
||||
"--ro-bind",
|
||||
"/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-17/apps-forgejo-release-build-r1",
|
||||
"/proposal",
|
||||
"--ro-bind",
|
||||
"/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-17/forgejo-required-claim-values-review-r1",
|
||||
"/prior-review",
|
||||
"--ro-bind",
|
||||
"/tmp/apps-forgejo-release-build-r1/source",
|
||||
"/source1",
|
||||
"--ro-bind",
|
||||
"/tmp/apps-forgejo-release-repro-r1/source",
|
||||
"/source2",
|
||||
"--ro-bind",
|
||||
"/tmp/apps-forgejo-release-repro-r1/image.oci.tar",
|
||||
"/image.oci.tar",
|
||||
"--ro-bind",
|
||||
"/home/worsch/the-custodian/docs/changes/netkingdom-target-2026-10-17/apps-release-independent-review-r1/review-offline.py",
|
||||
"/review-offline.py",
|
||||
"--chdir",
|
||||
"/tmp/review",
|
||||
"--clearenv",
|
||||
"--setenv",
|
||||
"PATH",
|
||||
"/usr/bin:/bin",
|
||||
"--setenv",
|
||||
"HOME",
|
||||
"/tmp/home",
|
||||
"--",
|
||||
"/usr/bin/python3",
|
||||
"-I",
|
||||
"/review-offline.py"
|
||||
],
|
||||
"returncode": 0,
|
||||
"elapsed_seconds": 11.08,
|
||||
"owned_pid_terminal": true,
|
||||
"workspace": "/tmp/apps-release-independent-review-r1-attempt2-86saom6w",
|
||||
"network": "unshare-net",
|
||||
"pid": "unshare-pid",
|
||||
"host_home_credentials_config_mounted": false,
|
||||
"refinement": "Reviewer-only timestamp upper bound correction; attempt1 preserved, no candidate correction or repeated full build."
|
||||
}
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
{
|
||||
"adversarial_refusals": {
|
||||
"detail": {
|
||||
"controls": [
|
||||
{
|
||||
"case": "outside-tmp",
|
||||
"refused": true,
|
||||
"returncode": 1
|
||||
},
|
||||
{
|
||||
"case": "existing-workspace",
|
||||
"refused": true,
|
||||
"returncode": 1
|
||||
},
|
||||
{
|
||||
"case": "archive-tamper",
|
||||
"refused": true,
|
||||
"returncode": 1
|
||||
},
|
||||
{
|
||||
"case": "repeat-verification",
|
||||
"refused": true,
|
||||
"returncode": 1
|
||||
},
|
||||
{
|
||||
"case": "dependency-lock-tamper",
|
||||
"refused": true,
|
||||
"returncode": 1
|
||||
},
|
||||
{
|
||||
"case": "reviewed-source-tamper",
|
||||
"refused": true,
|
||||
"returncode": 1
|
||||
},
|
||||
{
|
||||
"case": "binary-change-before-container",
|
||||
"refused": true,
|
||||
"returncode": 1
|
||||
}
|
||||
],
|
||||
"no_build_container_or_network_action_in_controls": true
|
||||
},
|
||||
"passed": true
|
||||
},
|
||||
"artifact_reproduction_current_bytes": {
|
||||
"detail": {
|
||||
"binary_equal": true,
|
||||
"bindata_sources_each": 4,
|
||||
"frontend_files_each": 628,
|
||||
"public_integrity_bound_npm_packages": 1150
|
||||
},
|
||||
"passed": true
|
||||
},
|
||||
"fresh_bare_binary_asset_extraction": {
|
||||
"detail": {
|
||||
"all_public_options_templates_exact": true,
|
||||
"architecture_approval": false,
|
||||
"binary_sha256": "4df429454b0289c6df76d7fd95d41394ebc286191809349b2c1e655b6be494e4",
|
||||
"embedded_file_manifest_sha256": "74a0ec278ab99ac86ee434d7620229dc4a70146efd51876a1e4738d2c9f4ffdb",
|
||||
"embedded_files": 2234,
|
||||
"native_admission": false,
|
||||
"network": "bwrap --unshare-net",
|
||||
"new_cli_flag_present": true,
|
||||
"no_dynamic_interpreter": true,
|
||||
"no_source_assets_libraries_or_host_config_mounted": true,
|
||||
"source_locks_preserved": true
|
||||
},
|
||||
"passed": true
|
||||
},
|
||||
"frozen_packet": {
|
||||
"detail": {
|
||||
"exact_patch_and_map": true,
|
||||
"frozen_entries": 71
|
||||
},
|
||||
"passed": true
|
||||
},
|
||||
"independent_oci_structure_and_content": {
|
||||
"error": "AssertionError()",
|
||||
"passed": false
|
||||
},
|
||||
"patch_pre_postimages": {
|
||||
"detail": {
|
||||
"exact_six_postimages": 6,
|
||||
"no_other_paths_created": true
|
||||
},
|
||||
"passed": true
|
||||
},
|
||||
"reviewed_source_binding": {
|
||||
"detail": {
|
||||
"both_workspace_outputs_current": true,
|
||||
"prior_accepted_extension_bound": true
|
||||
},
|
||||
"passed": true
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue