Repository navigation
Expand file tree
/
Copy pathbuild.sh
More file actions
executable file
·210 lines (186 loc) · 8.59 KB
/
Copy pathbuild.sh
File metadata and controls
executable file
·210 lines (186 loc) · 8.59 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
#!/usr/bin/env bash
# build.sh — build and push Gameplane module bundles to an OCI registry.
#
# Each module lives in modules/<name>/{template.yaml, module.yaml,
# README.md, icon.png}. This script wraps `oras push` with the
# Gameplane media types so the operator's pull path can find each
# layer by its filename annotation.
#
# Usage:
# modules/build.sh push --registry ghcr.io/gameplanepanel/gameplane-modules
# modules/build.sh push --registry localhost:5001 --name minecraft-java --insecure
# modules/build.sh push --registry $REG --plain-http # all modules, plain-http
#
# Required: oras >= 1.2.0 (https://oras.land/docs/installation)
# For --sign: cosign >= 2.0 (https://docs.sigstore.dev/cosign/installation)
# Signing stays on cosign's classic signature format
# (--new-bundle-format=false --use-signing-config=false) because that is what
# the operator's cosign/v2 verifier reads; the newer Rekor v2 flow also needs
# a timestamp authority that keyed signing cannot satisfy.
set -euo pipefail
ARTIFACT_TYPE="application/vnd.gameplane.module.v1+json"
MEDIA_METADATA="application/vnd.gameplane.module.metadata.v1+yaml"
MEDIA_TEMPLATE="application/vnd.gameplane.module.template.v1+yaml"
MEDIA_README="application/vnd.gameplane.module.readme.v1+md"
MEDIA_ICON="image/png"
usage() {
cat <<USAGE
Usage: $0 push [flags]
Flags:
--registry <ref> Registry/repo prefix (e.g. ghcr.io/gameplanepanel/gameplane-modules) [required]
--name <name> Push only this module (defaults: every dir under modules/)
--plain-http Use plain HTTP (for local kind registries)
--insecure Skip TLS verification
--tag-latest Also tag :latest in addition to module.yaml's version
--sign cosign-sign each pushed bundle (keyed; offline by default)
--tlog-upload With --sign, also record each signature in the public
Rekor transparency log (for official published bundles)
Reads each modules/<name>/module.yaml for the version. The bundle is
pushed to <registry>/<name>:<version>.
With --sign, set COSIGN_PRIVATE_KEY to the PEM private key (and
COSIGN_PASSWORD if it is encrypted); each bundle is signed by its manifest
digest. By default signing is offline (\`--tlog-upload=false\`), which the
operator verifies offline via ModuleSource.spec.verify.key — that keeps
local/air-gapped authoring and the plain-HTTP e2e working without Sigstore
connectivity. Add --tlog-upload for official bundles so the signature is
publicly logged. See docs/module-authoring.md.
USAGE
}
# parse_manifest_digest reads `oras push` output on stdin and prints the pushed
# MANIFEST digest, anchoring on oras's authoritative "Digest: sha256:…" summary
# line. Scanning the whole output for the first sha256 token would risk picking
# up a LAYER digest and signing the wrong object. Kept as a function (and
# reachable via the internal `_parse-digest` command) so it can be regression
# tested without a registry — see test-build-sh.sh.
parse_manifest_digest() {
sed -n 's/^[[:space:]]*Digest:[[:space:]]*\(sha256:[0-9a-f]\{64\}\).*/\1/p' | head -n1
}
cmd="${1:-}"
shift || true
case "$cmd" in
push) ;;
# Internal, for tests: read oras output on stdin, print the manifest digest.
_parse-digest) parse_manifest_digest; exit 0 ;;
""|-h|--help) usage; exit 0 ;;
*) echo "unknown command: $cmd" >&2; usage; exit 2 ;;
esac
REGISTRY=""
TARGET=""
PLAIN_HTTP=""
INSECURE=""
TAG_LATEST=0
SIGN=0
TLOG_UPLOAD=0
while [[ $# -gt 0 ]]; do
case "$1" in
--registry) REGISTRY="$2"; shift 2 ;;
--name) TARGET="$2"; shift 2 ;;
--plain-http) PLAIN_HTTP=1; shift ;;
--insecure) INSECURE=1; shift ;;
--tag-latest) TAG_LATEST=1; shift ;;
--sign) SIGN=1; shift ;;
--tlog-upload) TLOG_UPLOAD=1; shift ;;
-h|--help) usage; exit 0 ;;
*) echo "unknown flag: $1" >&2; usage; exit 2 ;;
esac
done
[[ -n "$REGISTRY" ]] || { echo "--registry required" >&2; exit 2; }
command -v oras >/dev/null || { echo "oras not in PATH" >&2; exit 2; }
if (( SIGN )); then
command -v cosign >/dev/null || { echo "cosign not in PATH (required for --sign)" >&2; exit 2; }
[[ -n "${COSIGN_PRIVATE_KEY:-}" ]] || { echo "--sign requires COSIGN_PRIVATE_KEY (PEM private key) in the environment" >&2; exit 2; }
elif (( TLOG_UPLOAD )); then
echo "--tlog-upload has no effect without --sign" >&2; exit 2
fi
# Resolve modules dir from the script's location, so callers can run
# this from anywhere.
MODULES_DIR="$(cd "$(dirname "$0")" && pwd)"
# yaml-grep: extract a top-level YAML scalar by key (poor-man's parser
# good enough for module.yaml — there are no nested mappings or quoted
# colons in the fields we read).
ymv() {
local file="$1" key="$2"
awk -v k="^$key:" 'BEGIN{IGNORECASE=0} $0 ~ k {sub(/^[^:]+:[ \t]*/, ""); gsub(/^["'"'"']|["'"'"']$/, ""); print; exit}' "$file"
}
push_one() {
local name="$1"
local dir="$MODULES_DIR/$name"
local meta="$dir/module.yaml"
local tmpl="$dir/template.yaml"
[[ -f "$meta" ]] || { echo "skip $name: no module.yaml" >&2; return 0; }
[[ -f "$tmpl" ]] || { echo "skip $name: no template.yaml" >&2; return 0; }
local declared_name version
declared_name="$(ymv "$meta" name)"
version="$(ymv "$meta" version)"
[[ -n "$version" ]] || { echo "$name: module.yaml is missing version" >&2; return 1; }
[[ "$declared_name" == "$name" ]] || {
echo "$name: module.yaml#name=$declared_name does not match directory $name" >&2
return 1
}
local ref="$REGISTRY/$name:$version"
echo ">> pushing $ref"
local args=( push )
[[ -n "$PLAIN_HTTP" ]] && args+=( --plain-http )
[[ -n "$INSECURE" ]] && args+=( --insecure )
args+=( --artifact-type "$ARTIFACT_TYPE" "$ref" )
# Use a subshell with cd so oras records each layer as just the filename
# (no leading directory) — that's what the puller expects in the title
# annotation.
local layer_args=( "module.yaml:$MEDIA_METADATA" "template.yaml:$MEDIA_TEMPLATE" )
[[ -f "$dir/README.md" ]] && layer_args+=( "README.md:$MEDIA_README" )
[[ -f "$dir/icon.png" ]] && layer_args+=( "icon.png:$MEDIA_ICON" )
# Capture combined output so we can recover the pushed manifest digest
# for signing; still echo it so the push stays visible in CI logs.
local out
out="$( ( cd "$dir" && oras "${args[@]}" "${layer_args[@]}" ) 2>&1 )"
printf '%s\n' "$out"
if (( SIGN )); then
local digest
digest="$(printf '%s\n' "$out" | parse_manifest_digest)"
[[ -n "$digest" ]] || { echo "$name: could not parse pushed manifest digest from oras output" >&2; return 1; }
echo ">> signing $REGISTRY/$name@$digest"
# cosign reads the key from $COSIGN_PRIVATE_KEY and the passphrase from
# $COSIGN_PASSWORD. Two modes:
# default keyed + offline (--tlog-upload=false), matching the
# operator's offline keyed verify path (IgnoreTlog/Offline)
# — this is what local authors and the plain-HTTP e2e use.
# --tlog-upload also record the signature in the public Rekor
# transparency log, for official published bundles.
# --new-bundle-format=false --use-signing-config=false keep cosign on the
# classic signature/hashedrekord path in both modes: it is the format the
# operator's cosign/v2 verifier reads, and cosign's newer Rekor v2 flow
# additionally requires a TSA that keyed signing cannot satisfy.
local sargs=( sign --key env://COSIGN_PRIVATE_KEY --yes
--new-bundle-format=false --use-signing-config=false )
if (( TLOG_UPLOAD )); then
echo ">> (recording signature in the public Rekor transparency log)"
else
sargs+=( --tlog-upload=false )
fi
# --plain-http needs --allow-http-registry (force HTTP); --allow-insecure-
# registry only skips TLS verification on an HTTPS connection, so against a
# plain-HTTP registry it errors "server gave HTTP response to HTTPS client"
# (except for localhost, which go-containerregistry auto-treats as HTTP).
[[ -n "$PLAIN_HTTP" ]] && sargs+=( --allow-http-registry )
[[ -n "$INSECURE" ]] && sargs+=( --allow-insecure-registry )
cosign "${sargs[@]}" "$REGISTRY/$name@$digest"
fi
if (( TAG_LATEST )); then
local latest="$REGISTRY/$name:latest"
echo ">> tagging $latest"
local targs=( tag )
[[ -n "$PLAIN_HTTP" ]] && targs+=( --plain-http )
[[ -n "$INSECURE" ]] && targs+=( --insecure )
oras "${targs[@]}" "$ref" latest
fi
}
if [[ -n "$TARGET" ]]; then
push_one "$TARGET"
exit 0
fi
# Push every directory that has a module.yaml.
for d in "$MODULES_DIR"/*/; do
name="$(basename "$d")"
[[ -f "$d/module.yaml" ]] || continue
push_one "$name"
done