Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions bin/y-bin.optional.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -25,14 +25,14 @@ k3d:
linux_arm64: 0b8110f2229631af7402fb828259330985918b08fefd38b7f1b788a1c8687216

kubefwd:
version: 1.25.12
version: 1.25.16
templates:
download: https://github.com/txn2/kubefwd/releases/download/v${version}/kubefwd_${Os}_${xarch}.tar.gz
sha256:
darwin_amd64: a9edf5398e0bcebf1a77eb8a74185498035cd624749d9b9ab08cf3ba832b7543
darwin_arm64: 70d60486d7d03b2dda99eee1a10d12549b861ae354df0225956a7552d8fb9a83
linux_amd64: 047b596f0d672111199265ccc7f830f1578eb9ad9be7dc5a30f2174b261534f2
linux_arm64: 04bc8d17e2417ead85bd2ba12a4c0eb517aa873c5611903656d7f579fe146a00
darwin_amd64: 905a3dd8a70d5acfcfe2a03f41feec264ac0c11cf7e5633b489716d719b64a6b
darwin_arm64: fc7a0126559cdd9e5cf8ddaf6a271b2d06381787bc3d3005ae48739a88f7d56c
linux_amd64: 07275cad05b2427069071160125b8cb29e94dd44582f685ce6d966fa9e7fb7d7
linux_arm64: e01ade02d919be2c7e306543f0a65de2e629c254ef16b51ecb45830b0044a3e8
archive:
tool: tar
path: kubefwd
Expand Down
5 changes: 4 additions & 1 deletion bin/y-cluster-sudoers
Original file line numberDiff line numberDiff line change
Expand Up@@ -50,7 +50,10 @@ rules() {
$u ALL=(root) NOPASSWD: $YBIN/y-localhost *

# y-k8s-ingress-hosts: /etc/hosts management ($u)
$u ALL=(root) NOPASSWD: $YBIN/y-k8s-ingress-hosts-v*-bin *
# Argument-free on purpose: the rule this replaced covered the
# k8s-ingress-hosts binary with any flags, and its -host-file flag
# turned that into a write-any-file-as-root grant.
$u ALL=(root) NOPASSWD: $YBIN/y-etc-hosts-write
EOF
done
}
Expand Down
68 changes: 68 additions & 0 deletions bin/y-etc-hosts-write
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
#!/usr/bin/env bash
[ -z "${DEBUG:-}" ] || set -x
set -eo pipefail

YHELP='y-etc-hosts-write - Replace /etc/hosts with content read from stdin

Usage: y-etc-hosts-write < newhosts

The single privileged step of the /etc/hosts tooling. It takes no arguments, so
the NOPASSWD rule y-cluster-sudoers grants for it cannot be pointed at another
path the way a rule for a flag-taking binary can.

Refuses input that is empty or has no 127.0.0.1 entry, and swaps the file in
with rename(2) so a reader never sees a half-written hosts file.

Environment:
Y_ETC_HOSTS Target path (default: /etc/hosts)

Dependencies:

Exit codes:
0 Written, or already identical
1 Usage error
2 Input refused by the safety checks
3 Target not writable (run through sudo)
'

case "${1:-}" in
help) echo "$YHELP"; exit 0 ;;
--help) echo "$YHELP"; exit 0 ;;
esac

if [ $# -ne 0 ]; then
echo "ERROR: takes no arguments, reads the new file from stdin" >&2
exit 1
fi

TARGET="${Y_ETC_HOSTS:-/etc/hosts}"
TARGET_DIR="$(dirname "$TARGET")"

if [ ! -w "$TARGET" ] || [ ! -w "$TARGET_DIR" ]; then
echo "ERROR: $TARGET is not writable as $(id -un), run through sudo" >&2
exit 3
fi

# Same directory as the target, so the swap below is a rename within one filesystem
TMP=$(mktemp "$TARGET_DIR/.hosts.XXXXXX")
trap 'rm -f "$TMP"' EXIT

cat > "$TMP"

if [ ! -s "$TMP" ]; then
echo "ERROR: refusing to write an empty $TARGET" >&2
exit 2
fi
if ! grep -qE '^[[:space:]]*127\.0\.0\.1[[:space:]]' "$TMP"; then
echo "ERROR: refusing input without a 127.0.0.1 entry" >&2
exit 2
fi

if cmp -s "$TMP" "$TARGET"; then
echo "# $TARGET already up to date"
exit 0
fi

chmod 0644 "$TMP"
mv "$TMP" "$TARGET"
echo "# wrote $TARGET"
155 changes: 95 additions & 60 deletions bin/y-k8s-ingress-hosts
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,68 @@
#!/usr/bin/env bash
[ -z "$DEBUG" ] || set -x
[ -z "${DEBUG:-}" ] || set -x
set -eo pipefail
YBIN="$(dirname $0)"
YBIN="$(dirname "$0")"

[ -z "$KUBECONFIG" ] && echo "This script requires a KUBECONFIG" && exit 1
YHELP='y-k8s-ingress-hosts - Sync /etc/hosts with cluster ingress and gateway hosts

CTX=""
CHECK=false
ENSURE=false
EXPLICIT_HOST_IP=""
PASSTHROUGH=()
Usage: y-k8s-ingress-hosts --context=NAME [options]

while [ $# -gt 0 ]; do
case "$1" in
-h|--help)
cat >&2 <<EOF
Usage: y-k8s-ingress-hosts [flags]

Flags:
--context=NAME kubeconfig context name (required)
-write rewrite host file
-check|--check check if /etc/hosts includes required entries (no sudo)
--ensure check, then write if needed (combines -check and -write)
--host-ip=IP override IP for all entries (otherwise resolved from
the GatewayClass yolean.se/dns-hint-ip annotation)
-h, --help show this help
Options:
--context=NAME kubeconfig context name (required)
-write write the entries to the hosts file
-check report drift without writing (never escalates)
--ensure check, then write if needed
--host-ip=IP override IP for all entries (otherwise resolved from
the GatewayClass yolean.se/dns-hint-ip annotation)

Cluster reads and the rendering of the new hosts file both run as the invoking
user. Only the swap of /etc/hosts is escalated, via y-etc-hosts-write, which
takes no arguments. Nothing here runs kubectl or the k8s-ingress-hosts binary
as root.

If --host-ip is not given, resolution walks (in order):
1. Gateway/ystack.ystack -> spec.gatewayClassName -> GatewayClass
metadata.annotations[yolean.se/dns-hint-ip]
2. Any GatewayClass carrying yolean.se/dns-hint-ip (used when no
consumer Gateway/ystack.ystack exists, e.g. y-cluster's
consumer Gateway/ystack.ystack exists, e.g. the y-cluster
appliance flow where the GatewayClass is the only artefact).
3. Gateway/ystack.ystack metadata.annotations[yolean.se/override-ip]
(legacy, pre-dates the dns-hint-ip contract).
y-cluster provision stamps the dns-hint-ip annotation when the
host forwards guest:80.
EOF
exit 0 ;;

Environment:
KUBECONFIG required
Y_HOST_IP same as --host-ip
Y_ETC_HOSTS target hosts file (default: /etc/hosts)

Dependencies:

Exit codes:
0 Up to date, written, or no entries to write
1 Usage error, or -check found drift without --ensure
3 The k8s-ingress-hosts binary failed
'

case "${1:-}" in
help) echo "$YHELP"; exit 0 ;;
--help) echo "$YHELP"; exit 0 ;;
-h) echo "$YHELP"; exit 0 ;;
esac

[ -z "$KUBECONFIG" ] && echo "ERROR: this script requires a KUBECONFIG" >&2 && exit 1

CTX=""
CHECK=false
ENSURE=false
WRITE=false
EXPLICIT_HOST_IP=""
PASSTHROUGH=()

while [ $# -gt 0 ]; do
case "$1" in
--context=*) CTX="${1#*=}"; shift ;;
-write|--write) WRITE=true; shift ;;
-check|--check) CHECK=true; shift ;;
--ensure) ENSURE=true; shift ;;
--host-ip=*) EXPLICIT_HOST_IP="${1#*=}"; shift ;;
Expand All@@ -55,10 +79,13 @@ EOF
esac
done

[ -z "$CTX" ] && echo "Required: --context=NAME" && exit 1
[ -z "$CTX" ] && echo "ERROR: required: --context=NAME" >&2 && exit 1

HOSTS_FILE="${Y_ETC_HOSTS:-/etc/hosts}"

CONTEXT_KUBECONFIG=$(mktemp)
trap "rm -f $CONTEXT_KUBECONFIG" EXIT
NEW_HOSTS=$(mktemp)
trap 'rm -f "$CONTEXT_KUBECONFIG" "$NEW_HOSTS"' EXIT
kubectl config view --raw --minify --context="$CTX" --request-timeout=5s > "$CONTEXT_KUBECONFIG"

# Resolve the host-side dial IP, in priority order:
Expand DownExpand Up@@ -96,7 +123,7 @@ if [ -z "$HOST_IP" ]; then
-o go-template='{{range .items}}{{$ip := index .metadata.annotations "yolean.se/dns-hint-ip"}}{{if $ip}}{{.metadata.name}}={{$ip}}{{"\n"}}{{end}}{{end}}' \
2>/dev/null || true) # y-script-lint:disable=or-true # no GatewayClasses is a normal pre-install state
DISTINCT_IPS=$(echo "$HINT_LIST" | awk -F= 'NF==2 && $2!="" {print $2}' | sort -u)
IP_COUNT=$(echo "$DISTINCT_IPS" | grep -c .)
IP_COUNT=$(echo "$DISTINCT_IPS" | grep -c . || true) # y-script-lint:disable=or-true # grep -c prints 0 but exits 1 with no matches, which is the no-annotation case
if [ "$IP_COUNT" = "1" ]; then
HOST_IP="$DISTINCT_IPS"
GC_NAME=$(echo "$HINT_LIST" | awk -F= 'NF==2 && $2!="" {print $1; exit}')
Expand All@@ -118,16 +145,24 @@ if [ -n "$HOST_IP" ]; then
PASSTHROUGH+=("-override-ip" "$HOST_IP")
fi

version=$(y-bin-download $YBIN/y-bin.optional.yaml k8s-ingress-hosts)
version=$(y-bin-download "$YBIN/y-bin.optional.yaml" k8s-ingress-hosts)
BIN="$YBIN/y-k8s-ingress-hosts-v${version}-bin"

echo "# reading k8s ingress resources..."
if ! RENDERED=$("$BIN" -kubeconfig "$CONTEXT_KUBECONFIG" "${PASSTHROUGH[@]}" 2>&1); then
echo "ERROR: $BIN failed:" >&2
echo "$RENDERED" >&2
exit 3
fi
ENTRIES=$(echo "$RENDERED" | grep -v '^#' || true) # y-script-lint:disable=or-true # grep exits 1 when the binary emitted comments only, which is the no-entries case handled below

if $CHECK || $ENSURE; then
NEEDED=$($YBIN/y-k8s-ingress-hosts-v${version}-bin -kubeconfig "$CONTEXT_KUBECONFIG" "${PASSTHROUGH[@]}" 2>/dev/null | grep -v '^#')
STALE=0
while IFS= read -r line; do
[ -z "$line" ] && continue
EXPECTED_IP=$(echo "$line" | awk '{print $1}')
HOST=$(echo "$line" | awk '{print $2}')
ACTUAL=$(grep -E "^[^#]*[[:space:]]$HOST([[:space:]]|$)" /etc/hosts 2>/dev/null || true) # y-script-lint:disable=or-true # grep exits 1 on no match -- expected for a missing-host check
ACTUAL=$(grep -E "^[^#]*[[:space:]]$HOST([[:space:]]|$)" "$HOSTS_FILE" 2>/dev/null || true) # y-script-lint:disable=or-true # grep exits 1 on no match -- expected for a missing-host check
if [ -z "$ACTUAL" ]; then
echo "Missing: $line"
STALE=1
Expand All@@ -136,46 +171,46 @@ if $CHECK || $ENSURE; then
echo "Stale: $HOST has $ACTUAL_IP, expected $EXPECTED_IP"
STALE=1
fi
done <<< "$NEEDED"
done <<< "$ENTRIES"
if [ $STALE -eq 0 ]; then
echo "# /etc/hosts is up to date"
echo "# $HOSTS_FILE is up to date"
exit 0
fi
if ! $ENSURE; then
echo "# /etc/hosts needs updating. Run with -write or --ensure to fix."
echo "# $HOSTS_FILE needs updating. Run with -write or --ensure to fix."
exit 1
fi
echo "# /etc/hosts needs updating, writing ..."
PASSTHROUGH+=("-write")
echo "# $HOSTS_FILE needs updating, writing ..."
WRITE=true
fi

# Guard: don't write an empty block that clears existing entries.
# Preview without -write to check if there are entries.
_PREVIEW_ARGS=()
for _a in "${PASSTHROUGH[@]}"; do
[ "$_a" = "-write" ] || _PREVIEW_ARGS+=("$_a")
done
echo "# reading k8s ingress resources..."
_PREVIEW=$($YBIN/y-k8s-ingress-hosts-v${version}-bin -kubeconfig "$CONTEXT_KUBECONFIG" "${_PREVIEW_ARGS[@]}" 2>/dev/null | grep -v '^#')
if [ -z "$_PREVIEW" ]; then
echo "# no ingress/gateway entries found, skipping write to preserve existing /etc/hosts"
# Without -write/-check/--ensure this is a preview, like the binary itself
if ! $WRITE; then
echo "$RENDERED"
exit 0
fi

# One-line stdout log when this invocation will actually mutate
# /etc/hosts (i.e. -write is in PASSTHROUGH, set either explicitly
# by the caller or appended above by --ensure on detected drift).
# Useful as a converge-trace breadcrumb so a yconverge exec check
# that ran y-k8s-ingress-hosts is visibly attributable.
WRITE_MODE=false
for _a in "${PASSTHROUGH[@]}"; do
[ "$_a" = "-write" ] && WRITE_MODE=true
done
if $WRITE_MODE; then
HOST_COUNT=$(echo "$_PREVIEW" | wc -l | tr -d ' ')
echo "y-k8s-ingress-hosts: writing $HOST_COUNT host entries to /etc/hosts"
# Guard: an empty render would otherwise clear the managed block
if [ -z "$ENTRIES" ]; then
echo "# no ingress/gateway entries found, skipping write to preserve existing $HOSTS_FILE"
exit 0
fi

[ $(id -u) -ne 0 ] && exec sudo $YBIN/y-k8s-ingress-hosts-v${version}-bin -kubeconfig "$CONTEXT_KUBECONFIG" "${PASSTHROUGH[@]}"
HOST_COUNT=$(echo "$ENTRIES" | wc -l | tr -d ' ')
echo "y-k8s-ingress-hosts: writing $HOST_COUNT host entries to $HOSTS_FILE"

# Render as the invoking user: the binary merges its managed block into a copy,
# and only that copy is handed to the privileged step.
cp "$HOSTS_FILE" "$NEW_HOSTS"
"$BIN" -kubeconfig "$CONTEXT_KUBECONFIG" -host-file "$NEW_HOSTS" -write "${PASSTHROUGH[@]}" >/dev/null

$YBIN/y-k8s-ingress-hosts-v${version}-bin -kubeconfig "$CONTEXT_KUBECONFIG" "${PASSTHROUGH[@]}" || exit $?
if cmp -s "$NEW_HOSTS" "$HOSTS_FILE"; then
echo "# $HOSTS_FILE already up to date"
exit 0
fi

if [ "$HOSTS_FILE" = "/etc/hosts" ] && [ "$(id -u)" -ne 0 ]; then
sudo "$YBIN/y-etc-hosts-write" < "$NEW_HOSTS"
else
Y_ETC_HOSTS="$HOSTS_FILE" "$YBIN/y-etc-hosts-write" < "$NEW_HOSTS"
fi
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions bin/y-bin.optional.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -25,14 +25,14 @@ k3d:
linux_arm64: 0b8110f2229631af7402fb828259330985918b08fefd38b7f1b788a1c8687216

kubefwd:
version: 1.25.12
version: 1.25.16
templates:
download: https://github.com/txn2/kubefwd/releases/download/v${version}/kubefwd_${Os}_${xarch}.tar.gz
sha256:
darwin_amd64: a9edf5398e0bcebf1a77eb8a74185498035cd624749d9b9ab08cf3ba832b7543
darwin_arm64: 70d60486d7d03b2dda99eee1a10d12549b861ae354df0225956a7552d8fb9a83
linux_amd64: 047b596f0d672111199265ccc7f830f1578eb9ad9be7dc5a30f2174b261534f2
linux_arm64: 04bc8d17e2417ead85bd2ba12a4c0eb517aa873c5611903656d7f579fe146a00
darwin_amd64: 905a3dd8a70d5acfcfe2a03f41feec264ac0c11cf7e5633b489716d719b64a6b
darwin_arm64: fc7a0126559cdd9e5cf8ddaf6a271b2d06381787bc3d3005ae48739a88f7d56c
linux_amd64: 07275cad05b2427069071160125b8cb29e94dd44582f685ce6d966fa9e7fb7d7
linux_arm64: e01ade02d919be2c7e306543f0a65de2e629c254ef16b51ecb45830b0044a3e8
archive:
tool: tar
path: kubefwd
Expand Down
5 changes: 4 additions & 1 deletion bin/y-cluster-sudoers
Original file line numberDiff line numberDiff line change
Expand Up@@ -50,7 +50,10 @@ rules() {
$u ALL=(root) NOPASSWD: $YBIN/y-localhost *

# y-k8s-ingress-hosts: /etc/hosts management ($u)
$u ALL=(root) NOPASSWD: $YBIN/y-k8s-ingress-hosts-v*-bin *
# Argument-free on purpose: the rule this replaced covered the
# k8s-ingress-hosts binary with any flags, and its -host-file flag
# turned that into a write-any-file-as-root grant.
$u ALL=(root) NOPASSWD: $YBIN/y-etc-hosts-write
EOF
done
}
Expand Down
68 changes: 68 additions & 0 deletions bin/y-etc-hosts-write
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
#!/usr/bin/env bash
[ -z "${DEBUG:-}" ] || set -x
set -eo pipefail

YHELP='y-etc-hosts-write - Replace /etc/hosts with content read from stdin

Usage: y-etc-hosts-write < newhosts

The single privileged step of the /etc/hosts tooling. It takes no arguments, so
the NOPASSWD rule y-cluster-sudoers grants for it cannot be pointed at another
path the way a rule for a flag-taking binary can.

Refuses input that is empty or has no 127.0.0.1 entry, and swaps the file in
with rename(2) so a reader never sees a half-written hosts file.

Environment:
Y_ETC_HOSTS Target path (default: /etc/hosts)

Dependencies:

Exit codes:
0 Written, or already identical
1 Usage error
2 Input refused by the safety checks
3 Target not writable (run through sudo)
'

case "${1:-}" in
help) echo "$YHELP"; exit 0 ;;
--help) echo "$YHELP"; exit 0 ;;
esac

if [ $# -ne 0 ]; then
echo "ERROR: takes no arguments, reads the new file from stdin" >&2
exit 1
fi

TARGET="${Y_ETC_HOSTS:-/etc/hosts}"
TARGET_DIR="$(dirname "$TARGET")"

if [ ! -w "$TARGET" ] || [ ! -w "$TARGET_DIR" ]; then
echo "ERROR: $TARGET is not writable as $(id -un), run through sudo" >&2
exit 3
fi

# Same directory as the target, so the swap below is a rename within one filesystem
TMP=$(mktemp "$TARGET_DIR/.hosts.XXXXXX")
trap 'rm -f "$TMP"' EXIT

cat > "$TMP"

if [ ! -s "$TMP" ]; then
echo "ERROR: refusing to write an empty $TARGET" >&2
exit 2
fi
if ! grep -qE '^[[:space:]]*127\.0\.0\.1[[:space:]]' "$TMP"; then
echo "ERROR: refusing input without a 127.0.0.1 entry" >&2
exit 2
fi

if cmp -s "$TMP" "$TARGET"; then
echo "# $TARGET already up to date"
exit 0
fi

chmod 0644 "$TMP"
mv "$TMP" "$TARGET"
echo "# wrote $TARGET"
155 changes: 95 additions & 60 deletions bin/y-k8s-ingress-hosts
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,68 @@
#!/usr/bin/env bash
[ -z "$DEBUG" ] || set -x
[ -z "${DEBUG:-}" ] || set -x
set -eo pipefail
YBIN="$(dirname $0)"
YBIN="$(dirname "$0")"

[ -z "$KUBECONFIG" ] && echo "This script requires a KUBECONFIG" && exit 1
YHELP='y-k8s-ingress-hosts - Sync /etc/hosts with cluster ingress and gateway hosts

CTX=""
CHECK=false
ENSURE=false
EXPLICIT_HOST_IP=""
PASSTHROUGH=()
Usage: y-k8s-ingress-hosts --context=NAME [options]

while [ $# -gt 0 ]; do
case "$1" in
-h|--help)
cat >&2 <<EOF
Usage: y-k8s-ingress-hosts [flags]

Flags:
--context=NAME kubeconfig context name (required)
-write rewrite host file
-check|--check check if /etc/hosts includes required entries (no sudo)
--ensure check, then write if needed (combines -check and -write)
--host-ip=IP override IP for all entries (otherwise resolved from
the GatewayClass yolean.se/dns-hint-ip annotation)
-h, --help show this help
Options:
--context=NAME kubeconfig context name (required)
-write write the entries to the hosts file
-check report drift without writing (never escalates)
--ensure check, then write if needed
--host-ip=IP override IP for all entries (otherwise resolved from
the GatewayClass yolean.se/dns-hint-ip annotation)

Cluster reads and the rendering of the new hosts file both run as the invoking
user. Only the swap of /etc/hosts is escalated, via y-etc-hosts-write, which
takes no arguments. Nothing here runs kubectl or the k8s-ingress-hosts binary
as root.

If --host-ip is not given, resolution walks (in order):
1. Gateway/ystack.ystack -> spec.gatewayClassName -> GatewayClass
metadata.annotations[yolean.se/dns-hint-ip]
2. Any GatewayClass carrying yolean.se/dns-hint-ip (used when no
consumer Gateway/ystack.ystack exists, e.g. y-cluster's
consumer Gateway/ystack.ystack exists, e.g. the y-cluster
appliance flow where the GatewayClass is the only artefact).
3. Gateway/ystack.ystack metadata.annotations[yolean.se/override-ip]
(legacy, pre-dates the dns-hint-ip contract).
y-cluster provision stamps the dns-hint-ip annotation when the
host forwards guest:80.
EOF
exit 0 ;;

Environment:
KUBECONFIG required
Y_HOST_IP same as --host-ip
Y_ETC_HOSTS target hosts file (default: /etc/hosts)

Dependencies:

Exit codes:
0 Up to date, written, or no entries to write
1 Usage error, or -check found drift without --ensure
3 The k8s-ingress-hosts binary failed
'

case "${1:-}" in
help) echo "$YHELP"; exit 0 ;;
--help) echo "$YHELP"; exit 0 ;;
-h) echo "$YHELP"; exit 0 ;;
esac

[ -z "$KUBECONFIG" ] && echo "ERROR: this script requires a KUBECONFIG" >&2 && exit 1

CTX=""
CHECK=false
ENSURE=false
WRITE=false
EXPLICIT_HOST_IP=""
PASSTHROUGH=()

while [ $# -gt 0 ]; do
case "$1" in
--context=*) CTX="${1#*=}"; shift ;;
-write|--write) WRITE=true; shift ;;
-check|--check) CHECK=true; shift ;;
--ensure) ENSURE=true; shift ;;
--host-ip=*) EXPLICIT_HOST_IP="${1#*=}"; shift ;;
Expand All@@ -55,10 +79,13 @@ EOF
esac
done

[ -z "$CTX" ] && echo "Required: --context=NAME" && exit 1
[ -z "$CTX" ] && echo "ERROR: required: --context=NAME" >&2 && exit 1

HOSTS_FILE="${Y_ETC_HOSTS:-/etc/hosts}"

CONTEXT_KUBECONFIG=$(mktemp)
trap "rm -f $CONTEXT_KUBECONFIG" EXIT
NEW_HOSTS=$(mktemp)
trap 'rm -f "$CONTEXT_KUBECONFIG" "$NEW_HOSTS"' EXIT
kubectl config view --raw --minify --context="$CTX" --request-timeout=5s > "$CONTEXT_KUBECONFIG"

# Resolve the host-side dial IP, in priority order:
Expand DownExpand Up@@ -96,7 +123,7 @@ if [ -z "$HOST_IP" ]; then
-o go-template='{{range .items}}{{$ip := index .metadata.annotations "yolean.se/dns-hint-ip"}}{{if $ip}}{{.metadata.name}}={{$ip}}{{"\n"}}{{end}}{{end}}' \
2>/dev/null || true) # y-script-lint:disable=or-true # no GatewayClasses is a normal pre-install state
DISTINCT_IPS=$(echo "$HINT_LIST" | awk -F= 'NF==2 && $2!="" {print $2}' | sort -u)
IP_COUNT=$(echo "$DISTINCT_IPS" | grep -c .)
IP_COUNT=$(echo "$DISTINCT_IPS" | grep -c . || true) # y-script-lint:disable=or-true # grep -c prints 0 but exits 1 with no matches, which is the no-annotation case
if [ "$IP_COUNT" = "1" ]; then
HOST_IP="$DISTINCT_IPS"
GC_NAME=$(echo "$HINT_LIST" | awk -F= 'NF==2 && $2!="" {print $1; exit}')
Expand All@@ -118,16 +145,24 @@ if [ -n "$HOST_IP" ]; then
PASSTHROUGH+=("-override-ip" "$HOST_IP")
fi

version=$(y-bin-download $YBIN/y-bin.optional.yaml k8s-ingress-hosts)
version=$(y-bin-download "$YBIN/y-bin.optional.yaml" k8s-ingress-hosts)
BIN="$YBIN/y-k8s-ingress-hosts-v${version}-bin"

echo "# reading k8s ingress resources..."
if ! RENDERED=$("$BIN" -kubeconfig "$CONTEXT_KUBECONFIG" "${PASSTHROUGH[@]}" 2>&1); then
echo "ERROR: $BIN failed:" >&2
echo "$RENDERED" >&2
exit 3
fi
ENTRIES=$(echo "$RENDERED" | grep -v '^#' || true) # y-script-lint:disable=or-true # grep exits 1 when the binary emitted comments only, which is the no-entries case handled below

if $CHECK || $ENSURE; then
NEEDED=$($YBIN/y-k8s-ingress-hosts-v${version}-bin -kubeconfig "$CONTEXT_KUBECONFIG" "${PASSTHROUGH[@]}" 2>/dev/null | grep -v '^#')
STALE=0
while IFS= read -r line; do
[ -z "$line" ] && continue
EXPECTED_IP=$(echo "$line" | awk '{print $1}')
HOST=$(echo "$line" | awk '{print $2}')
ACTUAL=$(grep -E "^[^#]*[[:space:]]$HOST([[:space:]]|$)" /etc/hosts 2>/dev/null || true) # y-script-lint:disable=or-true # grep exits 1 on no match -- expected for a missing-host check
ACTUAL=$(grep -E "^[^#]*[[:space:]]$HOST([[:space:]]|$)" "$HOSTS_FILE" 2>/dev/null || true) # y-script-lint:disable=or-true # grep exits 1 on no match -- expected for a missing-host check
if [ -z "$ACTUAL" ]; then
echo "Missing: $line"
STALE=1
Expand All@@ -136,46 +171,46 @@ if $CHECK || $ENSURE; then
echo "Stale: $HOST has $ACTUAL_IP, expected $EXPECTED_IP"
STALE=1
fi
done <<< "$NEEDED"
done <<< "$ENTRIES"
if [ $STALE -eq 0 ]; then
echo "# /etc/hosts is up to date"
echo "# $HOSTS_FILE is up to date"
exit 0
fi
if ! $ENSURE; then
echo "# /etc/hosts needs updating. Run with -write or --ensure to fix."
echo "# $HOSTS_FILE needs updating. Run with -write or --ensure to fix."
exit 1
fi
echo "# /etc/hosts needs updating, writing ..."
PASSTHROUGH+=("-write")
echo "# $HOSTS_FILE needs updating, writing ..."
WRITE=true
fi

# Guard: don't write an empty block that clears existing entries.
# Preview without -write to check if there are entries.
_PREVIEW_ARGS=()
for _a in "${PASSTHROUGH[@]}"; do
[ "$_a" = "-write" ] || _PREVIEW_ARGS+=("$_a")
done
echo "# reading k8s ingress resources..."
_PREVIEW=$($YBIN/y-k8s-ingress-hosts-v${version}-bin -kubeconfig "$CONTEXT_KUBECONFIG" "${_PREVIEW_ARGS[@]}" 2>/dev/null | grep -v '^#')
if [ -z "$_PREVIEW" ]; then
echo "# no ingress/gateway entries found, skipping write to preserve existing /etc/hosts"
# Without -write/-check/--ensure this is a preview, like the binary itself
if ! $WRITE; then
echo "$RENDERED"
exit 0
fi

# One-line stdout log when this invocation will actually mutate
# /etc/hosts (i.e. -write is in PASSTHROUGH, set either explicitly
# by the caller or appended above by --ensure on detected drift).
# Useful as a converge-trace breadcrumb so a yconverge exec check
# that ran y-k8s-ingress-hosts is visibly attributable.
WRITE_MODE=false
for _a in "${PASSTHROUGH[@]}"; do
[ "$_a" = "-write" ] && WRITE_MODE=true
done
if $WRITE_MODE; then
HOST_COUNT=$(echo "$_PREVIEW" | wc -l | tr -d ' ')
echo "y-k8s-ingress-hosts: writing $HOST_COUNT host entries to /etc/hosts"
# Guard: an empty render would otherwise clear the managed block
if [ -z "$ENTRIES" ]; then
echo "# no ingress/gateway entries found, skipping write to preserve existing $HOSTS_FILE"
exit 0
fi

[ $(id -u) -ne 0 ] && exec sudo $YBIN/y-k8s-ingress-hosts-v${version}-bin -kubeconfig "$CONTEXT_KUBECONFIG" "${PASSTHROUGH[@]}"
HOST_COUNT=$(echo "$ENTRIES" | wc -l | tr -d ' ')
echo "y-k8s-ingress-hosts: writing $HOST_COUNT host entries to $HOSTS_FILE"

# Render as the invoking user: the binary merges its managed block into a copy,
# and only that copy is handed to the privileged step.
cp "$HOSTS_FILE" "$NEW_HOSTS"
"$BIN" -kubeconfig "$CONTEXT_KUBECONFIG" -host-file "$NEW_HOSTS" -write "${PASSTHROUGH[@]}" >/dev/null

$YBIN/y-k8s-ingress-hosts-v${version}-bin -kubeconfig "$CONTEXT_KUBECONFIG" "${PASSTHROUGH[@]}" || exit $?
if cmp -s "$NEW_HOSTS" "$HOSTS_FILE"; then
echo "# $HOSTS_FILE already up to date"
exit 0
fi

if [ "$HOSTS_FILE" = "/etc/hosts" ] && [ "$(id -u)" -ne 0 ]; then
sudo "$YBIN/y-etc-hosts-write" < "$NEW_HOSTS"
else
Y_ETC_HOSTS="$HOSTS_FILE" "$YBIN/y-etc-hosts-write" < "$NEW_HOSTS"
fi
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions bin/y-bin.optional.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -25,14 +25,14 @@ k3d:
linux_arm64: 0b8110f2229631af7402fb828259330985918b08fefd38b7f1b788a1c8687216

kubefwd:
version: 1.25.12
version: 1.25.16
templates:
download: https://github.com/txn2/kubefwd/releases/download/v${version}/kubefwd_${Os}_${xarch}.tar.gz
sha256:
darwin_amd64: a9edf5398e0bcebf1a77eb8a74185498035cd624749d9b9ab08cf3ba832b7543
darwin_arm64: 70d60486d7d03b2dda99eee1a10d12549b861ae354df0225956a7552d8fb9a83
linux_amd64: 047b596f0d672111199265ccc7f830f1578eb9ad9be7dc5a30f2174b261534f2
linux_arm64: 04bc8d17e2417ead85bd2ba12a4c0eb517aa873c5611903656d7f579fe146a00
darwin_amd64: 905a3dd8a70d5acfcfe2a03f41feec264ac0c11cf7e5633b489716d719b64a6b
darwin_arm64: fc7a0126559cdd9e5cf8ddaf6a271b2d06381787bc3d3005ae48739a88f7d56c
linux_amd64: 07275cad05b2427069071160125b8cb29e94dd44582f685ce6d966fa9e7fb7d7
linux_arm64: e01ade02d919be2c7e306543f0a65de2e629c254ef16b51ecb45830b0044a3e8
archive:
tool: tar
path: kubefwd
Expand Down
5 changes: 4 additions & 1 deletion bin/y-cluster-sudoers
Original file line numberDiff line numberDiff line change
Expand Up@@ -50,7 +50,10 @@ rules() {
$u ALL=(root) NOPASSWD: $YBIN/y-localhost *

# y-k8s-ingress-hosts: /etc/hosts management ($u)
$u ALL=(root) NOPASSWD: $YBIN/y-k8s-ingress-hosts-v*-bin *
# Argument-free on purpose: the rule this replaced covered the
# k8s-ingress-hosts binary with any flags, and its -host-file flag
# turned that into a write-any-file-as-root grant.
$u ALL=(root) NOPASSWD: $YBIN/y-etc-hosts-write
EOF
done
}
Expand Down
68 changes: 68 additions & 0 deletions bin/y-etc-hosts-write
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
#!/usr/bin/env bash
[ -z "${DEBUG:-}" ] || set -x
set -eo pipefail

YHELP='y-etc-hosts-write - Replace /etc/hosts with content read from stdin

Usage: y-etc-hosts-write < newhosts

The single privileged step of the /etc/hosts tooling. It takes no arguments, so
the NOPASSWD rule y-cluster-sudoers grants for it cannot be pointed at another
path the way a rule for a flag-taking binary can.

Refuses input that is empty or has no 127.0.0.1 entry, and swaps the file in
with rename(2) so a reader never sees a half-written hosts file.

Environment:
Y_ETC_HOSTS Target path (default: /etc/hosts)

Dependencies:

Exit codes:
0 Written, or already identical
1 Usage error
2 Input refused by the safety checks
3 Target not writable (run through sudo)
'

case "${1:-}" in
help) echo "$YHELP"; exit 0 ;;
--help) echo "$YHELP"; exit 0 ;;
esac

if [ $# -ne 0 ]; then
echo "ERROR: takes no arguments, reads the new file from stdin" >&2
exit 1
fi

TARGET="${Y_ETC_HOSTS:-/etc/hosts}"
TARGET_DIR="$(dirname "$TARGET")"

if [ ! -w "$TARGET" ] || [ ! -w "$TARGET_DIR" ]; then
echo "ERROR: $TARGET is not writable as $(id -un), run through sudo" >&2
exit 3
fi

# Same directory as the target, so the swap below is a rename within one filesystem
TMP=$(mktemp "$TARGET_DIR/.hosts.XXXXXX")
trap 'rm -f "$TMP"' EXIT

cat > "$TMP"

if [ ! -s "$TMP" ]; then
echo "ERROR: refusing to write an empty $TARGET" >&2
exit 2
fi
if ! grep -qE '^[[:space:]]*127\.0\.0\.1[[:space:]]' "$TMP"; then
echo "ERROR: refusing input without a 127.0.0.1 entry" >&2
exit 2
fi

if cmp -s "$TMP" "$TARGET"; then
echo "# $TARGET already up to date"
exit 0
fi

chmod 0644 "$TMP"
mv "$TMP" "$TARGET"
echo "# wrote $TARGET"
155 changes: 95 additions & 60 deletions bin/y-k8s-ingress-hosts
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,68 @@
#!/usr/bin/env bash
[ -z "$DEBUG" ] || set -x
[ -z "${DEBUG:-}" ] || set -x
set -eo pipefail
YBIN="$(dirname $0)"
YBIN="$(dirname "$0")"

[ -z "$KUBECONFIG" ] && echo "This script requires a KUBECONFIG" && exit 1
YHELP='y-k8s-ingress-hosts - Sync /etc/hosts with cluster ingress and gateway hosts

CTX=""
CHECK=false
ENSURE=false
EXPLICIT_HOST_IP=""
PASSTHROUGH=()
Usage: y-k8s-ingress-hosts --context=NAME [options]

while [ $# -gt 0 ]; do
case "$1" in
-h|--help)
cat >&2 <<EOF
Usage: y-k8s-ingress-hosts [flags]

Flags:
--context=NAME kubeconfig context name (required)
-write rewrite host file
-check|--check check if /etc/hosts includes required entries (no sudo)
--ensure check, then write if needed (combines -check and -write)
--host-ip=IP override IP for all entries (otherwise resolved from
the GatewayClass yolean.se/dns-hint-ip annotation)
-h, --help show this help
Options:
--context=NAME kubeconfig context name (required)
-write write the entries to the hosts file
-check report drift without writing (never escalates)
--ensure check, then write if needed
--host-ip=IP override IP for all entries (otherwise resolved from
the GatewayClass yolean.se/dns-hint-ip annotation)

Cluster reads and the rendering of the new hosts file both run as the invoking
user. Only the swap of /etc/hosts is escalated, via y-etc-hosts-write, which
takes no arguments. Nothing here runs kubectl or the k8s-ingress-hosts binary
as root.

If --host-ip is not given, resolution walks (in order):
1. Gateway/ystack.ystack -> spec.gatewayClassName -> GatewayClass
metadata.annotations[yolean.se/dns-hint-ip]
2. Any GatewayClass carrying yolean.se/dns-hint-ip (used when no
consumer Gateway/ystack.ystack exists, e.g. y-cluster's
consumer Gateway/ystack.ystack exists, e.g. the y-cluster
appliance flow where the GatewayClass is the only artefact).
3. Gateway/ystack.ystack metadata.annotations[yolean.se/override-ip]
(legacy, pre-dates the dns-hint-ip contract).
y-cluster provision stamps the dns-hint-ip annotation when the
host forwards guest:80.
EOF
exit 0 ;;

Environment:
KUBECONFIG required
Y_HOST_IP same as --host-ip
Y_ETC_HOSTS target hosts file (default: /etc/hosts)

Dependencies:

Exit codes:
0 Up to date, written, or no entries to write
1 Usage error, or -check found drift without --ensure
3 The k8s-ingress-hosts binary failed
'

case "${1:-}" in
help) echo "$YHELP"; exit 0 ;;
--help) echo "$YHELP"; exit 0 ;;
-h) echo "$YHELP"; exit 0 ;;
esac

[ -z "$KUBECONFIG" ] && echo "ERROR: this script requires a KUBECONFIG" >&2 && exit 1

CTX=""
CHECK=false
ENSURE=false
WRITE=false
EXPLICIT_HOST_IP=""
PASSTHROUGH=()

while [ $# -gt 0 ]; do
case "$1" in
--context=*) CTX="${1#*=}"; shift ;;
-write|--write) WRITE=true; shift ;;
-check|--check) CHECK=true; shift ;;
--ensure) ENSURE=true; shift ;;
--host-ip=*) EXPLICIT_HOST_IP="${1#*=}"; shift ;;
Expand All@@ -55,10 +79,13 @@ EOF
esac
done

[ -z "$CTX" ] && echo "Required: --context=NAME" && exit 1
[ -z "$CTX" ] && echo "ERROR: required: --context=NAME" >&2 && exit 1

HOSTS_FILE="${Y_ETC_HOSTS:-/etc/hosts}"

CONTEXT_KUBECONFIG=$(mktemp)
trap "rm -f $CONTEXT_KUBECONFIG" EXIT
NEW_HOSTS=$(mktemp)
trap 'rm -f "$CONTEXT_KUBECONFIG" "$NEW_HOSTS"' EXIT
kubectl config view --raw --minify --context="$CTX" --request-timeout=5s > "$CONTEXT_KUBECONFIG"

# Resolve the host-side dial IP, in priority order:
Expand DownExpand Up@@ -96,7 +123,7 @@ if [ -z "$HOST_IP" ]; then
-o go-template='{{range .items}}{{$ip := index .metadata.annotations "yolean.se/dns-hint-ip"}}{{if $ip}}{{.metadata.name}}={{$ip}}{{"\n"}}{{end}}{{end}}' \
2>/dev/null || true) # y-script-lint:disable=or-true # no GatewayClasses is a normal pre-install state
DISTINCT_IPS=$(echo "$HINT_LIST" | awk -F= 'NF==2 && $2!="" {print $2}' | sort -u)
IP_COUNT=$(echo "$DISTINCT_IPS" | grep -c .)
IP_COUNT=$(echo "$DISTINCT_IPS" | grep -c . || true) # y-script-lint:disable=or-true # grep -c prints 0 but exits 1 with no matches, which is the no-annotation case
if [ "$IP_COUNT" = "1" ]; then
HOST_IP="$DISTINCT_IPS"
GC_NAME=$(echo "$HINT_LIST" | awk -F= 'NF==2 && $2!="" {print $1; exit}')
Expand All@@ -118,16 +145,24 @@ if [ -n "$HOST_IP" ]; then
PASSTHROUGH+=("-override-ip" "$HOST_IP")
fi

version=$(y-bin-download $YBIN/y-bin.optional.yaml k8s-ingress-hosts)
version=$(y-bin-download "$YBIN/y-bin.optional.yaml" k8s-ingress-hosts)
BIN="$YBIN/y-k8s-ingress-hosts-v${version}-bin"

echo "# reading k8s ingress resources..."
if ! RENDERED=$("$BIN" -kubeconfig "$CONTEXT_KUBECONFIG" "${PASSTHROUGH[@]}" 2>&1); then
echo "ERROR: $BIN failed:" >&2
echo "$RENDERED" >&2
exit 3
fi
ENTRIES=$(echo "$RENDERED" | grep -v '^#' || true) # y-script-lint:disable=or-true # grep exits 1 when the binary emitted comments only, which is the no-entries case handled below

if $CHECK || $ENSURE; then
NEEDED=$($YBIN/y-k8s-ingress-hosts-v${version}-bin -kubeconfig "$CONTEXT_KUBECONFIG" "${PASSTHROUGH[@]}" 2>/dev/null | grep -v '^#')
STALE=0
while IFS= read -r line; do
[ -z "$line" ] && continue
EXPECTED_IP=$(echo "$line" | awk '{print $1}')
HOST=$(echo "$line" | awk '{print $2}')
ACTUAL=$(grep -E "^[^#]*[[:space:]]$HOST([[:space:]]|$)" /etc/hosts 2>/dev/null || true) # y-script-lint:disable=or-true # grep exits 1 on no match -- expected for a missing-host check
ACTUAL=$(grep -E "^[^#]*[[:space:]]$HOST([[:space:]]|$)" "$HOSTS_FILE" 2>/dev/null || true) # y-script-lint:disable=or-true # grep exits 1 on no match -- expected for a missing-host check
if [ -z "$ACTUAL" ]; then
echo "Missing: $line"
STALE=1
Expand All@@ -136,46 +171,46 @@ if $CHECK || $ENSURE; then
echo "Stale: $HOST has $ACTUAL_IP, expected $EXPECTED_IP"
STALE=1
fi
done <<< "$NEEDED"
done <<< "$ENTRIES"
if [ $STALE -eq 0 ]; then
echo "# /etc/hosts is up to date"
echo "# $HOSTS_FILE is up to date"
exit 0
fi
if ! $ENSURE; then
echo "# /etc/hosts needs updating. Run with -write or --ensure to fix."
echo "# $HOSTS_FILE needs updating. Run with -write or --ensure to fix."
exit 1
fi
echo "# /etc/hosts needs updating, writing ..."
PASSTHROUGH+=("-write")
echo "# $HOSTS_FILE needs updating, writing ..."
WRITE=true
fi

# Guard: don't write an empty block that clears existing entries.
# Preview without -write to check if there are entries.
_PREVIEW_ARGS=()
for _a in "${PASSTHROUGH[@]}"; do
[ "$_a" = "-write" ] || _PREVIEW_ARGS+=("$_a")
done
echo "# reading k8s ingress resources..."
_PREVIEW=$($YBIN/y-k8s-ingress-hosts-v${version}-bin -kubeconfig "$CONTEXT_KUBECONFIG" "${_PREVIEW_ARGS[@]}" 2>/dev/null | grep -v '^#')
if [ -z "$_PREVIEW" ]; then
echo "# no ingress/gateway entries found, skipping write to preserve existing /etc/hosts"
# Without -write/-check/--ensure this is a preview, like the binary itself
if ! $WRITE; then
echo "$RENDERED"
exit 0
fi

# One-line stdout log when this invocation will actually mutate
# /etc/hosts (i.e. -write is in PASSTHROUGH, set either explicitly
# by the caller or appended above by --ensure on detected drift).
# Useful as a converge-trace breadcrumb so a yconverge exec check
# that ran y-k8s-ingress-hosts is visibly attributable.
WRITE_MODE=false
for _a in "${PASSTHROUGH[@]}"; do
[ "$_a" = "-write" ] && WRITE_MODE=true
done
if $WRITE_MODE; then
HOST_COUNT=$(echo "$_PREVIEW" | wc -l | tr -d ' ')
echo "y-k8s-ingress-hosts: writing $HOST_COUNT host entries to /etc/hosts"
# Guard: an empty render would otherwise clear the managed block
if [ -z "$ENTRIES" ]; then
echo "# no ingress/gateway entries found, skipping write to preserve existing $HOSTS_FILE"
exit 0
fi

[ $(id -u) -ne 0 ] && exec sudo $YBIN/y-k8s-ingress-hosts-v${version}-bin -kubeconfig "$CONTEXT_KUBECONFIG" "${PASSTHROUGH[@]}"
HOST_COUNT=$(echo "$ENTRIES" | wc -l | tr -d ' ')
echo "y-k8s-ingress-hosts: writing $HOST_COUNT host entries to $HOSTS_FILE"

# Render as the invoking user: the binary merges its managed block into a copy,
# and only that copy is handed to the privileged step.
cp "$HOSTS_FILE" "$NEW_HOSTS"
"$BIN" -kubeconfig "$CONTEXT_KUBECONFIG" -host-file "$NEW_HOSTS" -write "${PASSTHROUGH[@]}" >/dev/null

$YBIN/y-k8s-ingress-hosts-v${version}-bin -kubeconfig "$CONTEXT_KUBECONFIG" "${PASSTHROUGH[@]}" || exit $?
if cmp -s "$NEW_HOSTS" "$HOSTS_FILE"; then
echo "# $HOSTS_FILE already up to date"
exit 0
fi

if [ "$HOSTS_FILE" = "/etc/hosts" ] && [ "$(id -u)" -ne 0 ]; then
sudo "$YBIN/y-etc-hosts-write" < "$NEW_HOSTS"
else
Y_ETC_HOSTS="$HOSTS_FILE" "$YBIN/y-etc-hosts-write" < "$NEW_HOSTS"
fi
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions bin/y-bin.optional.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -25,14 +25,14 @@ k3d:
linux_arm64: 0b8110f2229631af7402fb828259330985918b08fefd38b7f1b788a1c8687216

kubefwd:
version: 1.25.12
version: 1.25.16
templates:
download: https://github.com/txn2/kubefwd/releases/download/v${version}/kubefwd_${Os}_${xarch}.tar.gz
sha256:
darwin_amd64: a9edf5398e0bcebf1a77eb8a74185498035cd624749d9b9ab08cf3ba832b7543
darwin_arm64: 70d60486d7d03b2dda99eee1a10d12549b861ae354df0225956a7552d8fb9a83
linux_amd64: 047b596f0d672111199265ccc7f830f1578eb9ad9be7dc5a30f2174b261534f2
linux_arm64: 04bc8d17e2417ead85bd2ba12a4c0eb517aa873c5611903656d7f579fe146a00
darwin_amd64: 905a3dd8a70d5acfcfe2a03f41feec264ac0c11cf7e5633b489716d719b64a6b
darwin_arm64: fc7a0126559cdd9e5cf8ddaf6a271b2d06381787bc3d3005ae48739a88f7d56c
linux_amd64: 07275cad05b2427069071160125b8cb29e94dd44582f685ce6d966fa9e7fb7d7
linux_arm64: e01ade02d919be2c7e306543f0a65de2e629c254ef16b51ecb45830b0044a3e8
archive:
tool: tar
path: kubefwd
Expand Down
5 changes: 4 additions & 1 deletion bin/y-cluster-sudoers
Original file line numberDiff line numberDiff line change
Expand Up@@ -50,7 +50,10 @@ rules() {
$u ALL=(root) NOPASSWD: $YBIN/y-localhost *

# y-k8s-ingress-hosts: /etc/hosts management ($u)
$u ALL=(root) NOPASSWD: $YBIN/y-k8s-ingress-hosts-v*-bin *
# Argument-free on purpose: the rule this replaced covered the
# k8s-ingress-hosts binary with any flags, and its -host-file flag
# turned that into a write-any-file-as-root grant.
$u ALL=(root) NOPASSWD: $YBIN/y-etc-hosts-write
EOF
done
}
Expand Down
68 changes: 68 additions & 0 deletions bin/y-etc-hosts-write
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
#!/usr/bin/env bash
[ -z "${DEBUG:-}" ] || set -x
set -eo pipefail

YHELP='y-etc-hosts-write - Replace /etc/hosts with content read from stdin

Usage: y-etc-hosts-write < newhosts

The single privileged step of the /etc/hosts tooling. It takes no arguments, so
the NOPASSWD rule y-cluster-sudoers grants for it cannot be pointed at another
path the way a rule for a flag-taking binary can.

Refuses input that is empty or has no 127.0.0.1 entry, and swaps the file in
with rename(2) so a reader never sees a half-written hosts file.

Environment:
Y_ETC_HOSTS Target path (default: /etc/hosts)

Dependencies:

Exit codes:
0 Written, or already identical
1 Usage error
2 Input refused by the safety checks
3 Target not writable (run through sudo)
'

case "${1:-}" in
help) echo "$YHELP"; exit 0 ;;
--help) echo "$YHELP"; exit 0 ;;
esac

if [ $# -ne 0 ]; then
echo "ERROR: takes no arguments, reads the new file from stdin" >&2
exit 1
fi

TARGET="${Y_ETC_HOSTS:-/etc/hosts}"
TARGET_DIR="$(dirname "$TARGET")"

if [ ! -w "$TARGET" ] || [ ! -w "$TARGET_DIR" ]; then
echo "ERROR: $TARGET is not writable as $(id -un), run through sudo" >&2
exit 3
fi

# Same directory as the target, so the swap below is a rename within one filesystem
TMP=$(mktemp "$TARGET_DIR/.hosts.XXXXXX")
trap 'rm -f "$TMP"' EXIT

cat > "$TMP"

if [ ! -s "$TMP" ]; then
echo "ERROR: refusing to write an empty $TARGET" >&2
exit 2
fi
if ! grep -qE '^[[:space:]]*127\.0\.0\.1[[:space:]]' "$TMP"; then
echo "ERROR: refusing input without a 127.0.0.1 entry" >&2
exit 2
fi

if cmp -s "$TMP" "$TARGET"; then
echo "# $TARGET already up to date"
exit 0
fi

chmod 0644 "$TMP"
mv "$TMP" "$TARGET"
echo "# wrote $TARGET"
155 changes: 95 additions & 60 deletions bin/y-k8s-ingress-hosts
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,68 @@
#!/usr/bin/env bash
[ -z "$DEBUG" ] || set -x
[ -z "${DEBUG:-}" ] || set -x
set -eo pipefail
YBIN="$(dirname $0)"
YBIN="$(dirname "$0")"

[ -z "$KUBECONFIG" ] && echo "This script requires a KUBECONFIG" && exit 1
YHELP='y-k8s-ingress-hosts - Sync /etc/hosts with cluster ingress and gateway hosts

CTX=""
CHECK=false
ENSURE=false
EXPLICIT_HOST_IP=""
PASSTHROUGH=()
Usage: y-k8s-ingress-hosts --context=NAME [options]

while [ $# -gt 0 ]; do
case "$1" in
-h|--help)
cat >&2 <<EOF
Usage: y-k8s-ingress-hosts [flags]

Flags:
--context=NAME kubeconfig context name (required)
-write rewrite host file
-check|--check check if /etc/hosts includes required entries (no sudo)
--ensure check, then write if needed (combines -check and -write)
--host-ip=IP override IP for all entries (otherwise resolved from
the GatewayClass yolean.se/dns-hint-ip annotation)
-h, --help show this help
Options:
--context=NAME kubeconfig context name (required)
-write write the entries to the hosts file
-check report drift without writing (never escalates)
--ensure check, then write if needed
--host-ip=IP override IP for all entries (otherwise resolved from
the GatewayClass yolean.se/dns-hint-ip annotation)

Cluster reads and the rendering of the new hosts file both run as the invoking
user. Only the swap of /etc/hosts is escalated, via y-etc-hosts-write, which
takes no arguments. Nothing here runs kubectl or the k8s-ingress-hosts binary
as root.

If --host-ip is not given, resolution walks (in order):
1. Gateway/ystack.ystack -> spec.gatewayClassName -> GatewayClass
metadata.annotations[yolean.se/dns-hint-ip]
2. Any GatewayClass carrying yolean.se/dns-hint-ip (used when no
consumer Gateway/ystack.ystack exists, e.g. y-cluster's
consumer Gateway/ystack.ystack exists, e.g. the y-cluster
appliance flow where the GatewayClass is the only artefact).
3. Gateway/ystack.ystack metadata.annotations[yolean.se/override-ip]
(legacy, pre-dates the dns-hint-ip contract).
y-cluster provision stamps the dns-hint-ip annotation when the
host forwards guest:80.
EOF
exit 0 ;;

Environment:
KUBECONFIG required
Y_HOST_IP same as --host-ip
Y_ETC_HOSTS target hosts file (default: /etc/hosts)

Dependencies:

Exit codes:
0 Up to date, written, or no entries to write
1 Usage error, or -check found drift without --ensure
3 The k8s-ingress-hosts binary failed
'

case "${1:-}" in
help) echo "$YHELP"; exit 0 ;;
--help) echo "$YHELP"; exit 0 ;;
-h) echo "$YHELP"; exit 0 ;;
esac

[ -z "$KUBECONFIG" ] && echo "ERROR: this script requires a KUBECONFIG" >&2 && exit 1

CTX=""
CHECK=false
ENSURE=false
WRITE=false
EXPLICIT_HOST_IP=""
PASSTHROUGH=()

while [ $# -gt 0 ]; do
case "$1" in
--context=*) CTX="${1#*=}"; shift ;;
-write|--write) WRITE=true; shift ;;
-check|--check) CHECK=true; shift ;;
--ensure) ENSURE=true; shift ;;
--host-ip=*) EXPLICIT_HOST_IP="${1#*=}"; shift ;;
Expand All@@ -55,10 +79,13 @@ EOF
esac
done

[ -z "$CTX" ] && echo "Required: --context=NAME" && exit 1
[ -z "$CTX" ] && echo "ERROR: required: --context=NAME" >&2 && exit 1

HOSTS_FILE="${Y_ETC_HOSTS:-/etc/hosts}"

CONTEXT_KUBECONFIG=$(mktemp)
trap "rm -f $CONTEXT_KUBECONFIG" EXIT
NEW_HOSTS=$(mktemp)
trap 'rm -f "$CONTEXT_KUBECONFIG" "$NEW_HOSTS"' EXIT
kubectl config view --raw --minify --context="$CTX" --request-timeout=5s > "$CONTEXT_KUBECONFIG"

# Resolve the host-side dial IP, in priority order:
Expand DownExpand Up@@ -96,7 +123,7 @@ if [ -z "$HOST_IP" ]; then
-o go-template='{{range .items}}{{$ip := index .metadata.annotations "yolean.se/dns-hint-ip"}}{{if $ip}}{{.metadata.name}}={{$ip}}{{"\n"}}{{end}}{{end}}' \
2>/dev/null || true) # y-script-lint:disable=or-true # no GatewayClasses is a normal pre-install state
DISTINCT_IPS=$(echo "$HINT_LIST" | awk -F= 'NF==2 && $2!="" {print $2}' | sort -u)
IP_COUNT=$(echo "$DISTINCT_IPS" | grep -c .)
IP_COUNT=$(echo "$DISTINCT_IPS" | grep -c . || true) # y-script-lint:disable=or-true # grep -c prints 0 but exits 1 with no matches, which is the no-annotation case
if [ "$IP_COUNT" = "1" ]; then
HOST_IP="$DISTINCT_IPS"
GC_NAME=$(echo "$HINT_LIST" | awk -F= 'NF==2 && $2!="" {print $1; exit}')
Expand All@@ -118,16 +145,24 @@ if [ -n "$HOST_IP" ]; then
PASSTHROUGH+=("-override-ip" "$HOST_IP")
fi

version=$(y-bin-download $YBIN/y-bin.optional.yaml k8s-ingress-hosts)
version=$(y-bin-download "$YBIN/y-bin.optional.yaml" k8s-ingress-hosts)
BIN="$YBIN/y-k8s-ingress-hosts-v${version}-bin"

echo "# reading k8s ingress resources..."
if ! RENDERED=$("$BIN" -kubeconfig "$CONTEXT_KUBECONFIG" "${PASSTHROUGH[@]}" 2>&1); then
echo "ERROR: $BIN failed:" >&2
echo "$RENDERED" >&2
exit 3
fi
ENTRIES=$(echo "$RENDERED" | grep -v '^#' || true) # y-script-lint:disable=or-true # grep exits 1 when the binary emitted comments only, which is the no-entries case handled below

if $CHECK || $ENSURE; then
NEEDED=$($YBIN/y-k8s-ingress-hosts-v${version}-bin -kubeconfig "$CONTEXT_KUBECONFIG" "${PASSTHROUGH[@]}" 2>/dev/null | grep -v '^#')
STALE=0
while IFS= read -r line; do
[ -z "$line" ] && continue
EXPECTED_IP=$(echo "$line" | awk '{print $1}')
HOST=$(echo "$line" | awk '{print $2}')
ACTUAL=$(grep -E "^[^#]*[[:space:]]$HOST([[:space:]]|$)" /etc/hosts 2>/dev/null || true) # y-script-lint:disable=or-true # grep exits 1 on no match -- expected for a missing-host check
ACTUAL=$(grep -E "^[^#]*[[:space:]]$HOST([[:space:]]|$)" "$HOSTS_FILE" 2>/dev/null || true) # y-script-lint:disable=or-true # grep exits 1 on no match -- expected for a missing-host check
if [ -z "$ACTUAL" ]; then
echo "Missing: $line"
STALE=1
Expand All@@ -136,46 +171,46 @@ if $CHECK || $ENSURE; then
echo "Stale: $HOST has $ACTUAL_IP, expected $EXPECTED_IP"
STALE=1
fi
done <<< "$NEEDED"
done <<< "$ENTRIES"
if [ $STALE -eq 0 ]; then
echo "# /etc/hosts is up to date"
echo "# $HOSTS_FILE is up to date"
exit 0
fi
if ! $ENSURE; then
echo "# /etc/hosts needs updating. Run with -write or --ensure to fix."
echo "# $HOSTS_FILE needs updating. Run with -write or --ensure to fix."
exit 1
fi
echo "# /etc/hosts needs updating, writing ..."
PASSTHROUGH+=("-write")
echo "# $HOSTS_FILE needs updating, writing ..."
WRITE=true
fi

# Guard: don't write an empty block that clears existing entries.
# Preview without -write to check if there are entries.
_PREVIEW_ARGS=()
for _a in "${PASSTHROUGH[@]}"; do
[ "$_a" = "-write" ] || _PREVIEW_ARGS+=("$_a")
done
echo "# reading k8s ingress resources..."
_PREVIEW=$($YBIN/y-k8s-ingress-hosts-v${version}-bin -kubeconfig "$CONTEXT_KUBECONFIG" "${_PREVIEW_ARGS[@]}" 2>/dev/null | grep -v '^#')
if [ -z "$_PREVIEW" ]; then
echo "# no ingress/gateway entries found, skipping write to preserve existing /etc/hosts"
# Without -write/-check/--ensure this is a preview, like the binary itself
if ! $WRITE; then
echo "$RENDERED"
exit 0
fi

# One-line stdout log when this invocation will actually mutate
# /etc/hosts (i.e. -write is in PASSTHROUGH, set either explicitly
# by the caller or appended above by --ensure on detected drift).
# Useful as a converge-trace breadcrumb so a yconverge exec check
# that ran y-k8s-ingress-hosts is visibly attributable.
WRITE_MODE=false
for _a in "${PASSTHROUGH[@]}"; do
[ "$_a" = "-write" ] && WRITE_MODE=true
done
if $WRITE_MODE; then
HOST_COUNT=$(echo "$_PREVIEW" | wc -l | tr -d ' ')
echo "y-k8s-ingress-hosts: writing $HOST_COUNT host entries to /etc/hosts"
# Guard: an empty render would otherwise clear the managed block
if [ -z "$ENTRIES" ]; then
echo "# no ingress/gateway entries found, skipping write to preserve existing $HOSTS_FILE"
exit 0
fi

[ $(id -u) -ne 0 ] && exec sudo $YBIN/y-k8s-ingress-hosts-v${version}-bin -kubeconfig "$CONTEXT_KUBECONFIG" "${PASSTHROUGH[@]}"
HOST_COUNT=$(echo "$ENTRIES" | wc -l | tr -d ' ')
echo "y-k8s-ingress-hosts: writing $HOST_COUNT host entries to $HOSTS_FILE"

# Render as the invoking user: the binary merges its managed block into a copy,
# and only that copy is handed to the privileged step.
cp "$HOSTS_FILE" "$NEW_HOSTS"
"$BIN" -kubeconfig "$CONTEXT_KUBECONFIG" -host-file "$NEW_HOSTS" -write "${PASSTHROUGH[@]}" >/dev/null

$YBIN/y-k8s-ingress-hosts-v${version}-bin -kubeconfig "$CONTEXT_KUBECONFIG" "${PASSTHROUGH[@]}" || exit $?
if cmp -s "$NEW_HOSTS" "$HOSTS_FILE"; then
echo "# $HOSTS_FILE already up to date"
exit 0
fi

if [ "$HOSTS_FILE" = "/etc/hosts" ] && [ "$(id -u)" -ne 0 ]; then
sudo "$YBIN/y-etc-hosts-write" < "$NEW_HOSTS"
else
Y_ETC_HOSTS="$HOSTS_FILE" "$YBIN/y-etc-hosts-write" < "$NEW_HOSTS"
fi
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions bin/y-bin.optional.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -25,14 +25,14 @@ k3d:
linux_arm64: 0b8110f2229631af7402fb828259330985918b08fefd38b7f1b788a1c8687216

kubefwd:
version: 1.25.12
version: 1.25.16
templates:
download: https://github.com/txn2/kubefwd/releases/download/v${version}/kubefwd_${Os}_${xarch}.tar.gz
sha256:
darwin_amd64: a9edf5398e0bcebf1a77eb8a74185498035cd624749d9b9ab08cf3ba832b7543
darwin_arm64: 70d60486d7d03b2dda99eee1a10d12549b861ae354df0225956a7552d8fb9a83
linux_amd64: 047b596f0d672111199265ccc7f830f1578eb9ad9be7dc5a30f2174b261534f2
linux_arm64: 04bc8d17e2417ead85bd2ba12a4c0eb517aa873c5611903656d7f579fe146a00
darwin_amd64: 905a3dd8a70d5acfcfe2a03f41feec264ac0c11cf7e5633b489716d719b64a6b
darwin_arm64: fc7a0126559cdd9e5cf8ddaf6a271b2d06381787bc3d3005ae48739a88f7d56c
linux_amd64: 07275cad05b2427069071160125b8cb29e94dd44582f685ce6d966fa9e7fb7d7
linux_arm64: e01ade02d919be2c7e306543f0a65de2e629c254ef16b51ecb45830b0044a3e8
archive:
tool: tar
path: kubefwd
Expand Down
5 changes: 4 additions & 1 deletion bin/y-cluster-sudoers
Original file line numberDiff line numberDiff line change
Expand Up@@ -50,7 +50,10 @@ rules() {
$u ALL=(root) NOPASSWD: $YBIN/y-localhost *

# y-k8s-ingress-hosts: /etc/hosts management ($u)
$u ALL=(root) NOPASSWD: $YBIN/y-k8s-ingress-hosts-v*-bin *
# Argument-free on purpose: the rule this replaced covered the
# k8s-ingress-hosts binary with any flags, and its -host-file flag
# turned that into a write-any-file-as-root grant.
$u ALL=(root) NOPASSWD: $YBIN/y-etc-hosts-write
EOF
done
}
Expand Down
68 changes: 68 additions & 0 deletions bin/y-etc-hosts-write
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
#!/usr/bin/env bash
[ -z "${DEBUG:-}" ] || set -x
set -eo pipefail

YHELP='y-etc-hosts-write - Replace /etc/hosts with content read from stdin

Usage: y-etc-hosts-write < newhosts

The single privileged step of the /etc/hosts tooling. It takes no arguments, so
the NOPASSWD rule y-cluster-sudoers grants for it cannot be pointed at another
path the way a rule for a flag-taking binary can.

Refuses input that is empty or has no 127.0.0.1 entry, and swaps the file in
with rename(2) so a reader never sees a half-written hosts file.

Environment:
Y_ETC_HOSTS Target path (default: /etc/hosts)

Dependencies:

Exit codes:
0 Written, or already identical
1 Usage error
2 Input refused by the safety checks
3 Target not writable (run through sudo)
'

case "${1:-}" in
help) echo "$YHELP"; exit 0 ;;
--help) echo "$YHELP"; exit 0 ;;
esac

if [ $# -ne 0 ]; then
echo "ERROR: takes no arguments, reads the new file from stdin" >&2
exit 1
fi

TARGET="${Y_ETC_HOSTS:-/etc/hosts}"
TARGET_DIR="$(dirname "$TARGET")"

if [ ! -w "$TARGET" ] || [ ! -w "$TARGET_DIR" ]; then
echo "ERROR: $TARGET is not writable as $(id -un), run through sudo" >&2
exit 3
fi

# Same directory as the target, so the swap below is a rename within one filesystem
TMP=$(mktemp "$TARGET_DIR/.hosts.XXXXXX")
trap 'rm -f "$TMP"' EXIT

cat > "$TMP"

if [ ! -s "$TMP" ]; then
echo "ERROR: refusing to write an empty $TARGET" >&2
exit 2
fi
if ! grep -qE '^[[:space:]]*127\.0\.0\.1[[:space:]]' "$TMP"; then
echo "ERROR: refusing input without a 127.0.0.1 entry" >&2
exit 2
fi

if cmp -s "$TMP" "$TARGET"; then
echo "# $TARGET already up to date"
exit 0
fi

chmod 0644 "$TMP"
mv "$TMP" "$TARGET"
echo "# wrote $TARGET"
155 changes: 95 additions & 60 deletions bin/y-k8s-ingress-hosts
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,68 @@
#!/usr/bin/env bash
[ -z "$DEBUG" ] || set -x
[ -z "${DEBUG:-}" ] || set -x
set -eo pipefail
YBIN="$(dirname $0)"
YBIN="$(dirname "$0")"

[ -z "$KUBECONFIG" ] && echo "This script requires a KUBECONFIG" && exit 1
YHELP='y-k8s-ingress-hosts - Sync /etc/hosts with cluster ingress and gateway hosts

CTX=""
CHECK=false
ENSURE=false
EXPLICIT_HOST_IP=""
PASSTHROUGH=()
Usage: y-k8s-ingress-hosts --context=NAME [options]

while [ $# -gt 0 ]; do
case "$1" in
-h|--help)
cat >&2 <<EOF
Usage: y-k8s-ingress-hosts [flags]

Flags:
--context=NAME kubeconfig context name (required)
-write rewrite host file
-check|--check check if /etc/hosts includes required entries (no sudo)
--ensure check, then write if needed (combines -check and -write)
--host-ip=IP override IP for all entries (otherwise resolved from
the GatewayClass yolean.se/dns-hint-ip annotation)
-h, --help show this help
Options:
--context=NAME kubeconfig context name (required)
-write write the entries to the hosts file
-check report drift without writing (never escalates)
--ensure check, then write if needed
--host-ip=IP override IP for all entries (otherwise resolved from
the GatewayClass yolean.se/dns-hint-ip annotation)

Cluster reads and the rendering of the new hosts file both run as the invoking
user. Only the swap of /etc/hosts is escalated, via y-etc-hosts-write, which
takes no arguments. Nothing here runs kubectl or the k8s-ingress-hosts binary
as root.

If --host-ip is not given, resolution walks (in order):
1. Gateway/ystack.ystack -> spec.gatewayClassName -> GatewayClass
metadata.annotations[yolean.se/dns-hint-ip]
2. Any GatewayClass carrying yolean.se/dns-hint-ip (used when no
consumer Gateway/ystack.ystack exists, e.g. y-cluster's
consumer Gateway/ystack.ystack exists, e.g. the y-cluster
appliance flow where the GatewayClass is the only artefact).
3. Gateway/ystack.ystack metadata.annotations[yolean.se/override-ip]
(legacy, pre-dates the dns-hint-ip contract).
y-cluster provision stamps the dns-hint-ip annotation when the
host forwards guest:80.
EOF
exit 0 ;;

Environment:
KUBECONFIG required
Y_HOST_IP same as --host-ip
Y_ETC_HOSTS target hosts file (default: /etc/hosts)

Dependencies:

Exit codes:
0 Up to date, written, or no entries to write
1 Usage error, or -check found drift without --ensure
3 The k8s-ingress-hosts binary failed
'

case "${1:-}" in
help) echo "$YHELP"; exit 0 ;;
--help) echo "$YHELP"; exit 0 ;;
-h) echo "$YHELP"; exit 0 ;;
esac

[ -z "$KUBECONFIG" ] && echo "ERROR: this script requires a KUBECONFIG" >&2 && exit 1

CTX=""
CHECK=false
ENSURE=false
WRITE=false
EXPLICIT_HOST_IP=""
PASSTHROUGH=()

while [ $# -gt 0 ]; do
case "$1" in
--context=*) CTX="${1#*=}"; shift ;;
-write|--write) WRITE=true; shift ;;
-check|--check) CHECK=true; shift ;;
--ensure) ENSURE=true; shift ;;
--host-ip=*) EXPLICIT_HOST_IP="${1#*=}"; shift ;;
Expand All@@ -55,10 +79,13 @@ EOF
esac
done

[ -z "$CTX" ] && echo "Required: --context=NAME" && exit 1
[ -z "$CTX" ] && echo "ERROR: required: --context=NAME" >&2 && exit 1

HOSTS_FILE="${Y_ETC_HOSTS:-/etc/hosts}"

CONTEXT_KUBECONFIG=$(mktemp)
trap "rm -f $CONTEXT_KUBECONFIG" EXIT
NEW_HOSTS=$(mktemp)
trap 'rm -f "$CONTEXT_KUBECONFIG" "$NEW_HOSTS"' EXIT
kubectl config view --raw --minify --context="$CTX" --request-timeout=5s > "$CONTEXT_KUBECONFIG"

# Resolve the host-side dial IP, in priority order:
Expand DownExpand Up@@ -96,7 +123,7 @@ if [ -z "$HOST_IP" ]; then
-o go-template='{{range .items}}{{$ip := index .metadata.annotations "yolean.se/dns-hint-ip"}}{{if $ip}}{{.metadata.name}}={{$ip}}{{"\n"}}{{end}}{{end}}' \
2>/dev/null || true) # y-script-lint:disable=or-true # no GatewayClasses is a normal pre-install state
DISTINCT_IPS=$(echo "$HINT_LIST" | awk -F= 'NF==2 && $2!="" {print $2}' | sort -u)
IP_COUNT=$(echo "$DISTINCT_IPS" | grep -c .)
IP_COUNT=$(echo "$DISTINCT_IPS" | grep -c . || true) # y-script-lint:disable=or-true # grep -c prints 0 but exits 1 with no matches, which is the no-annotation case
if [ "$IP_COUNT" = "1" ]; then
HOST_IP="$DISTINCT_IPS"
GC_NAME=$(echo "$HINT_LIST" | awk -F= 'NF==2 && $2!="" {print $1; exit}')
Expand All@@ -118,16 +145,24 @@ if [ -n "$HOST_IP" ]; then
PASSTHROUGH+=("-override-ip" "$HOST_IP")
fi

version=$(y-bin-download $YBIN/y-bin.optional.yaml k8s-ingress-hosts)
version=$(y-bin-download "$YBIN/y-bin.optional.yaml" k8s-ingress-hosts)
BIN="$YBIN/y-k8s-ingress-hosts-v${version}-bin"

echo "# reading k8s ingress resources..."
if ! RENDERED=$("$BIN" -kubeconfig "$CONTEXT_KUBECONFIG" "${PASSTHROUGH[@]}" 2>&1); then
echo "ERROR: $BIN failed:" >&2
echo "$RENDERED" >&2
exit 3
fi
ENTRIES=$(echo "$RENDERED" | grep -v '^#' || true) # y-script-lint:disable=or-true # grep exits 1 when the binary emitted comments only, which is the no-entries case handled below

if $CHECK || $ENSURE; then
NEEDED=$($YBIN/y-k8s-ingress-hosts-v${version}-bin -kubeconfig "$CONTEXT_KUBECONFIG" "${PASSTHROUGH[@]}" 2>/dev/null | grep -v '^#')
STALE=0
while IFS= read -r line; do
[ -z "$line" ] && continue
EXPECTED_IP=$(echo "$line" | awk '{print $1}')
HOST=$(echo "$line" | awk '{print $2}')
ACTUAL=$(grep -E "^[^#]*[[:space:]]$HOST([[:space:]]|$)" /etc/hosts 2>/dev/null || true) # y-script-lint:disable=or-true # grep exits 1 on no match -- expected for a missing-host check
ACTUAL=$(grep -E "^[^#]*[[:space:]]$HOST([[:space:]]|$)" "$HOSTS_FILE" 2>/dev/null || true) # y-script-lint:disable=or-true # grep exits 1 on no match -- expected for a missing-host check
if [ -z "$ACTUAL" ]; then
echo "Missing: $line"
STALE=1
Expand All@@ -136,46 +171,46 @@ if $CHECK || $ENSURE; then
echo "Stale: $HOST has $ACTUAL_IP, expected $EXPECTED_IP"
STALE=1
fi
done <<< "$NEEDED"
done <<< "$ENTRIES"
if [ $STALE -eq 0 ]; then
echo "# /etc/hosts is up to date"
echo "# $HOSTS_FILE is up to date"
exit 0
fi
if ! $ENSURE; then
echo "# /etc/hosts needs updating. Run with -write or --ensure to fix."
echo "# $HOSTS_FILE needs updating. Run with -write or --ensure to fix."
exit 1
fi
echo "# /etc/hosts needs updating, writing ..."
PASSTHROUGH+=("-write")
echo "# $HOSTS_FILE needs updating, writing ..."
WRITE=true
fi

# Guard: don't write an empty block that clears existing entries.
# Preview without -write to check if there are entries.
_PREVIEW_ARGS=()
for _a in "${PASSTHROUGH[@]}"; do
[ "$_a" = "-write" ] || _PREVIEW_ARGS+=("$_a")
done
echo "# reading k8s ingress resources..."
_PREVIEW=$($YBIN/y-k8s-ingress-hosts-v${version}-bin -kubeconfig "$CONTEXT_KUBECONFIG" "${_PREVIEW_ARGS[@]}" 2>/dev/null | grep -v '^#')
if [ -z "$_PREVIEW" ]; then
echo "# no ingress/gateway entries found, skipping write to preserve existing /etc/hosts"
# Without -write/-check/--ensure this is a preview, like the binary itself
if ! $WRITE; then
echo "$RENDERED"
exit 0
fi

# One-line stdout log when this invocation will actually mutate
# /etc/hosts (i.e. -write is in PASSTHROUGH, set either explicitly
# by the caller or appended above by --ensure on detected drift).
# Useful as a converge-trace breadcrumb so a yconverge exec check
# that ran y-k8s-ingress-hosts is visibly attributable.
WRITE_MODE=false
for _a in "${PASSTHROUGH[@]}"; do
[ "$_a" = "-write" ] && WRITE_MODE=true
done
if $WRITE_MODE; then
HOST_COUNT=$(echo "$_PREVIEW" | wc -l | tr -d ' ')
echo "y-k8s-ingress-hosts: writing $HOST_COUNT host entries to /etc/hosts"
# Guard: an empty render would otherwise clear the managed block
if [ -z "$ENTRIES" ]; then
echo "# no ingress/gateway entries found, skipping write to preserve existing $HOSTS_FILE"
exit 0
fi

[ $(id -u) -ne 0 ] && exec sudo $YBIN/y-k8s-ingress-hosts-v${version}-bin -kubeconfig "$CONTEXT_KUBECONFIG" "${PASSTHROUGH[@]}"
HOST_COUNT=$(echo "$ENTRIES" | wc -l | tr -d ' ')
echo "y-k8s-ingress-hosts: writing $HOST_COUNT host entries to $HOSTS_FILE"

# Render as the invoking user: the binary merges its managed block into a copy,
# and only that copy is handed to the privileged step.
cp "$HOSTS_FILE" "$NEW_HOSTS"
"$BIN" -kubeconfig "$CONTEXT_KUBECONFIG" -host-file "$NEW_HOSTS" -write "${PASSTHROUGH[@]}" >/dev/null

$YBIN/y-k8s-ingress-hosts-v${version}-bin -kubeconfig "$CONTEXT_KUBECONFIG" "${PASSTHROUGH[@]}" || exit $?
if cmp -s "$NEW_HOSTS" "$HOSTS_FILE"; then
echo "# $HOSTS_FILE already up to date"
exit 0
fi

if [ "$HOSTS_FILE" = "/etc/hosts" ] && [ "$(id -u)" -ne 0 ]; then
sudo "$YBIN/y-etc-hosts-write" < "$NEW_HOSTS"
else
Y_ETC_HOSTS="$HOSTS_FILE" "$YBIN/y-etc-hosts-write" < "$NEW_HOSTS"
fi
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions bin/y-bin.optional.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -25,14 +25,14 @@ k3d:
linux_arm64: 0b8110f2229631af7402fb828259330985918b08fefd38b7f1b788a1c8687216

kubefwd:
version: 1.25.12
version: 1.25.16
templates:
download: https://github.com/txn2/kubefwd/releases/download/v${version}/kubefwd_${Os}_${xarch}.tar.gz
sha256:
darwin_amd64: a9edf5398e0bcebf1a77eb8a74185498035cd624749d9b9ab08cf3ba832b7543
darwin_arm64: 70d60486d7d03b2dda99eee1a10d12549b861ae354df0225956a7552d8fb9a83
linux_amd64: 047b596f0d672111199265ccc7f830f1578eb9ad9be7dc5a30f2174b261534f2
linux_arm64: 04bc8d17e2417ead85bd2ba12a4c0eb517aa873c5611903656d7f579fe146a00
darwin_amd64: 905a3dd8a70d5acfcfe2a03f41feec264ac0c11cf7e5633b489716d719b64a6b
darwin_arm64: fc7a0126559cdd9e5cf8ddaf6a271b2d06381787bc3d3005ae48739a88f7d56c
linux_amd64: 07275cad05b2427069071160125b8cb29e94dd44582f685ce6d966fa9e7fb7d7
linux_arm64: e01ade02d919be2c7e306543f0a65de2e629c254ef16b51ecb45830b0044a3e8
archive:
tool: tar
path: kubefwd
Expand Down
5 changes: 4 additions & 1 deletion bin/y-cluster-sudoers
Original file line numberDiff line numberDiff line change
Expand Up@@ -50,7 +50,10 @@ rules() {
$u ALL=(root) NOPASSWD: $YBIN/y-localhost *

# y-k8s-ingress-hosts: /etc/hosts management ($u)
$u ALL=(root) NOPASSWD: $YBIN/y-k8s-ingress-hosts-v*-bin *
# Argument-free on purpose: the rule this replaced covered the
# k8s-ingress-hosts binary with any flags, and its -host-file flag
# turned that into a write-any-file-as-root grant.
$u ALL=(root) NOPASSWD: $YBIN/y-etc-hosts-write
EOF
done
}
Expand Down
68 changes: 68 additions & 0 deletions bin/y-etc-hosts-write
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
#!/usr/bin/env bash
[ -z "${DEBUG:-}" ] || set -x
set -eo pipefail

YHELP='y-etc-hosts-write - Replace /etc/hosts with content read from stdin

Usage: y-etc-hosts-write < newhosts

The single privileged step of the /etc/hosts tooling. It takes no arguments, so
the NOPASSWD rule y-cluster-sudoers grants for it cannot be pointed at another
path the way a rule for a flag-taking binary can.

Refuses input that is empty or has no 127.0.0.1 entry, and swaps the file in
with rename(2) so a reader never sees a half-written hosts file.

Environment:
Y_ETC_HOSTS Target path (default: /etc/hosts)

Dependencies:

Exit codes:
0 Written, or already identical
1 Usage error
2 Input refused by the safety checks
3 Target not writable (run through sudo)
'

case "${1:-}" in
help) echo "$YHELP"; exit 0 ;;
--help) echo "$YHELP"; exit 0 ;;
esac

if [ $# -ne 0 ]; then
echo "ERROR: takes no arguments, reads the new file from stdin" >&2
exit 1
fi

TARGET="${Y_ETC_HOSTS:-/etc/hosts}"
TARGET_DIR="$(dirname "$TARGET")"

if [ ! -w "$TARGET" ] || [ ! -w "$TARGET_DIR" ]; then
echo "ERROR: $TARGET is not writable as $(id -un), run through sudo" >&2
exit 3
fi

# Same directory as the target, so the swap below is a rename within one filesystem
TMP=$(mktemp "$TARGET_DIR/.hosts.XXXXXX")
trap 'rm -f "$TMP"' EXIT

cat > "$TMP"

if [ ! -s "$TMP" ]; then
echo "ERROR: refusing to write an empty $TARGET" >&2
exit 2
fi
if ! grep -qE '^[[:space:]]*127\.0\.0\.1[[:space:]]' "$TMP"; then
echo "ERROR: refusing input without a 127.0.0.1 entry" >&2
exit 2
fi

if cmp -s "$TMP" "$TARGET"; then
echo "# $TARGET already up to date"
exit 0
fi

chmod 0644 "$TMP"
mv "$TMP" "$TARGET"
echo "# wrote $TARGET"
155 changes: 95 additions & 60 deletions bin/y-k8s-ingress-hosts
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,68 @@
#!/usr/bin/env bash
[ -z "$DEBUG" ] || set -x
[ -z "${DEBUG:-}" ] || set -x
set -eo pipefail
YBIN="$(dirname $0)"
YBIN="$(dirname "$0")"

[ -z "$KUBECONFIG" ] && echo "This script requires a KUBECONFIG" && exit 1
YHELP='y-k8s-ingress-hosts - Sync /etc/hosts with cluster ingress and gateway hosts

CTX=""
CHECK=false
ENSURE=false
EXPLICIT_HOST_IP=""
PASSTHROUGH=()
Usage: y-k8s-ingress-hosts --context=NAME [options]

while [ $# -gt 0 ]; do
case "$1" in
-h|--help)
cat >&2 <<EOF
Usage: y-k8s-ingress-hosts [flags]

Flags:
--context=NAME kubeconfig context name (required)
-write rewrite host file
-check|--check check if /etc/hosts includes required entries (no sudo)
--ensure check, then write if needed (combines -check and -write)
--host-ip=IP override IP for all entries (otherwise resolved from
the GatewayClass yolean.se/dns-hint-ip annotation)
-h, --help show this help
Options:
--context=NAME kubeconfig context name (required)
-write write the entries to the hosts file
-check report drift without writing (never escalates)
--ensure check, then write if needed
--host-ip=IP override IP for all entries (otherwise resolved from
the GatewayClass yolean.se/dns-hint-ip annotation)

Cluster reads and the rendering of the new hosts file both run as the invoking
user. Only the swap of /etc/hosts is escalated, via y-etc-hosts-write, which
takes no arguments. Nothing here runs kubectl or the k8s-ingress-hosts binary
as root.

If --host-ip is not given, resolution walks (in order):
1. Gateway/ystack.ystack -> spec.gatewayClassName -> GatewayClass
metadata.annotations[yolean.se/dns-hint-ip]
2. Any GatewayClass carrying yolean.se/dns-hint-ip (used when no
consumer Gateway/ystack.ystack exists, e.g. y-cluster's
consumer Gateway/ystack.ystack exists, e.g. the y-cluster
appliance flow where the GatewayClass is the only artefact).
3. Gateway/ystack.ystack metadata.annotations[yolean.se/override-ip]
(legacy, pre-dates the dns-hint-ip contract).
y-cluster provision stamps the dns-hint-ip annotation when the
host forwards guest:80.
EOF
exit 0 ;;

Environment:
KUBECONFIG required
Y_HOST_IP same as --host-ip
Y_ETC_HOSTS target hosts file (default: /etc/hosts)

Dependencies:

Exit codes:
0 Up to date, written, or no entries to write
1 Usage error, or -check found drift without --ensure
3 The k8s-ingress-hosts binary failed
'

case "${1:-}" in
help) echo "$YHELP"; exit 0 ;;
--help) echo "$YHELP"; exit 0 ;;
-h) echo "$YHELP"; exit 0 ;;
esac

[ -z "$KUBECONFIG" ] && echo "ERROR: this script requires a KUBECONFIG" >&2 && exit 1

CTX=""
CHECK=false
ENSURE=false
WRITE=false
EXPLICIT_HOST_IP=""
PASSTHROUGH=()

while [ $# -gt 0 ]; do
case "$1" in
--context=*) CTX="${1#*=}"; shift ;;
-write|--write) WRITE=true; shift ;;
-check|--check) CHECK=true; shift ;;
--ensure) ENSURE=true; shift ;;
--host-ip=*) EXPLICIT_HOST_IP="${1#*=}"; shift ;;
Expand All@@ -55,10 +79,13 @@ EOF
esac
done

[ -z "$CTX" ] && echo "Required: --context=NAME" && exit 1
[ -z "$CTX" ] && echo "ERROR: required: --context=NAME" >&2 && exit 1

HOSTS_FILE="${Y_ETC_HOSTS:-/etc/hosts}"

CONTEXT_KUBECONFIG=$(mktemp)
trap "rm -f $CONTEXT_KUBECONFIG" EXIT
NEW_HOSTS=$(mktemp)
trap 'rm -f "$CONTEXT_KUBECONFIG" "$NEW_HOSTS"' EXIT
kubectl config view --raw --minify --context="$CTX" --request-timeout=5s > "$CONTEXT_KUBECONFIG"

# Resolve the host-side dial IP, in priority order:
Expand DownExpand Up@@ -96,7 +123,7 @@ if [ -z "$HOST_IP" ]; then
-o go-template='{{range .items}}{{$ip := index .metadata.annotations "yolean.se/dns-hint-ip"}}{{if $ip}}{{.metadata.name}}={{$ip}}{{"\n"}}{{end}}{{end}}' \
2>/dev/null || true) # y-script-lint:disable=or-true # no GatewayClasses is a normal pre-install state
DISTINCT_IPS=$(echo "$HINT_LIST" | awk -F= 'NF==2 && $2!="" {print $2}' | sort -u)
IP_COUNT=$(echo "$DISTINCT_IPS" | grep -c .)
IP_COUNT=$(echo "$DISTINCT_IPS" | grep -c . || true) # y-script-lint:disable=or-true # grep -c prints 0 but exits 1 with no matches, which is the no-annotation case
if [ "$IP_COUNT" = "1" ]; then
HOST_IP="$DISTINCT_IPS"
GC_NAME=$(echo "$HINT_LIST" | awk -F= 'NF==2 && $2!="" {print $1; exit}')
Expand All@@ -118,16 +145,24 @@ if [ -n "$HOST_IP" ]; then
PASSTHROUGH+=("-override-ip" "$HOST_IP")
fi

version=$(y-bin-download $YBIN/y-bin.optional.yaml k8s-ingress-hosts)
version=$(y-bin-download "$YBIN/y-bin.optional.yaml" k8s-ingress-hosts)
BIN="$YBIN/y-k8s-ingress-hosts-v${version}-bin"

echo "# reading k8s ingress resources..."
if ! RENDERED=$("$BIN" -kubeconfig "$CONTEXT_KUBECONFIG" "${PASSTHROUGH[@]}" 2>&1); then
echo "ERROR: $BIN failed:" >&2
echo "$RENDERED" >&2
exit 3
fi
ENTRIES=$(echo "$RENDERED" | grep -v '^#' || true) # y-script-lint:disable=or-true # grep exits 1 when the binary emitted comments only, which is the no-entries case handled below

if $CHECK || $ENSURE; then
NEEDED=$($YBIN/y-k8s-ingress-hosts-v${version}-bin -kubeconfig "$CONTEXT_KUBECONFIG" "${PASSTHROUGH[@]}" 2>/dev/null | grep -v '^#')
STALE=0
while IFS= read -r line; do
[ -z "$line" ] && continue
EXPECTED_IP=$(echo "$line" | awk '{print $1}')
HOST=$(echo "$line" | awk '{print $2}')
ACTUAL=$(grep -E "^[^#]*[[:space:]]$HOST([[:space:]]|$)" /etc/hosts 2>/dev/null || true) # y-script-lint:disable=or-true # grep exits 1 on no match -- expected for a missing-host check
ACTUAL=$(grep -E "^[^#]*[[:space:]]$HOST([[:space:]]|$)" "$HOSTS_FILE" 2>/dev/null || true) # y-script-lint:disable=or-true # grep exits 1 on no match -- expected for a missing-host check
if [ -z "$ACTUAL" ]; then
echo "Missing: $line"
STALE=1
Expand All@@ -136,46 +171,46 @@ if $CHECK || $ENSURE; then
echo "Stale: $HOST has $ACTUAL_IP, expected $EXPECTED_IP"
STALE=1
fi
done <<< "$NEEDED"
done <<< "$ENTRIES"
if [ $STALE -eq 0 ]; then
echo "# /etc/hosts is up to date"
echo "# $HOSTS_FILE is up to date"
exit 0
fi
if ! $ENSURE; then
echo "# /etc/hosts needs updating. Run with -write or --ensure to fix."
echo "# $HOSTS_FILE needs updating. Run with -write or --ensure to fix."
exit 1
fi
echo "# /etc/hosts needs updating, writing ..."
PASSTHROUGH+=("-write")
echo "# $HOSTS_FILE needs updating, writing ..."
WRITE=true
fi

# Guard: don't write an empty block that clears existing entries.
# Preview without -write to check if there are entries.
_PREVIEW_ARGS=()
for _a in "${PASSTHROUGH[@]}"; do
[ "$_a" = "-write" ] || _PREVIEW_ARGS+=("$_a")
done
echo "# reading k8s ingress resources..."
_PREVIEW=$($YBIN/y-k8s-ingress-hosts-v${version}-bin -kubeconfig "$CONTEXT_KUBECONFIG" "${_PREVIEW_ARGS[@]}" 2>/dev/null | grep -v '^#')
if [ -z "$_PREVIEW" ]; then
echo "# no ingress/gateway entries found, skipping write to preserve existing /etc/hosts"
# Without -write/-check/--ensure this is a preview, like the binary itself
if ! $WRITE; then
echo "$RENDERED"
exit 0
fi

# One-line stdout log when this invocation will actually mutate
# /etc/hosts (i.e. -write is in PASSTHROUGH, set either explicitly
# by the caller or appended above by --ensure on detected drift).
# Useful as a converge-trace breadcrumb so a yconverge exec check
# that ran y-k8s-ingress-hosts is visibly attributable.
WRITE_MODE=false
for _a in "${PASSTHROUGH[@]}"; do
[ "$_a" = "-write" ] && WRITE_MODE=true
done
if $WRITE_MODE; then
HOST_COUNT=$(echo "$_PREVIEW" | wc -l | tr -d ' ')
echo "y-k8s-ingress-hosts: writing $HOST_COUNT host entries to /etc/hosts"
# Guard: an empty render would otherwise clear the managed block
if [ -z "$ENTRIES" ]; then
echo "# no ingress/gateway entries found, skipping write to preserve existing $HOSTS_FILE"
exit 0
fi

[ $(id -u) -ne 0 ] && exec sudo $YBIN/y-k8s-ingress-hosts-v${version}-bin -kubeconfig "$CONTEXT_KUBECONFIG" "${PASSTHROUGH[@]}"
HOST_COUNT=$(echo "$ENTRIES" | wc -l | tr -d ' ')
echo "y-k8s-ingress-hosts: writing $HOST_COUNT host entries to $HOSTS_FILE"

# Render as the invoking user: the binary merges its managed block into a copy,
# and only that copy is handed to the privileged step.
cp "$HOSTS_FILE" "$NEW_HOSTS"
"$BIN" -kubeconfig "$CONTEXT_KUBECONFIG" -host-file "$NEW_HOSTS" -write "${PASSTHROUGH[@]}" >/dev/null

$YBIN/y-k8s-ingress-hosts-v${version}-bin -kubeconfig "$CONTEXT_KUBECONFIG" "${PASSTHROUGH[@]}" || exit $?
if cmp -s "$NEW_HOSTS" "$HOSTS_FILE"; then
echo "# $HOSTS_FILE already up to date"
exit 0
fi

if [ "$HOSTS_FILE" = "/etc/hosts" ] && [ "$(id -u)" -ne 0 ]; then
sudo "$YBIN/y-etc-hosts-write" < "$NEW_HOSTS"
else
Y_ETC_HOSTS="$HOSTS_FILE" "$YBIN/y-etc-hosts-write" < "$NEW_HOSTS"
fi
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions bin/y-bin.optional.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -25,14 +25,14 @@ k3d:
linux_arm64: 0b8110f2229631af7402fb828259330985918b08fefd38b7f1b788a1c8687216

kubefwd:
version: 1.25.12
version: 1.25.16
templates:
download: https://github.com/txn2/kubefwd/releases/download/v${version}/kubefwd_${Os}_${xarch}.tar.gz
sha256:
darwin_amd64: a9edf5398e0bcebf1a77eb8a74185498035cd624749d9b9ab08cf3ba832b7543
darwin_arm64: 70d60486d7d03b2dda99eee1a10d12549b861ae354df0225956a7552d8fb9a83
linux_amd64: 047b596f0d672111199265ccc7f830f1578eb9ad9be7dc5a30f2174b261534f2
linux_arm64: 04bc8d17e2417ead85bd2ba12a4c0eb517aa873c5611903656d7f579fe146a00
darwin_amd64: 905a3dd8a70d5acfcfe2a03f41feec264ac0c11cf7e5633b489716d719b64a6b
darwin_arm64: fc7a0126559cdd9e5cf8ddaf6a271b2d06381787bc3d3005ae48739a88f7d56c
linux_amd64: 07275cad05b2427069071160125b8cb29e94dd44582f685ce6d966fa9e7fb7d7
linux_arm64: e01ade02d919be2c7e306543f0a65de2e629c254ef16b51ecb45830b0044a3e8
archive:
tool: tar
path: kubefwd
Expand Down
5 changes: 4 additions & 1 deletion bin/y-cluster-sudoers
Original file line numberDiff line numberDiff line change
Expand Up@@ -50,7 +50,10 @@ rules() {
$u ALL=(root) NOPASSWD: $YBIN/y-localhost *

# y-k8s-ingress-hosts: /etc/hosts management ($u)
$u ALL=(root) NOPASSWD: $YBIN/y-k8s-ingress-hosts-v*-bin *
# Argument-free on purpose: the rule this replaced covered the
# k8s-ingress-hosts binary with any flags, and its -host-file flag
# turned that into a write-any-file-as-root grant.
$u ALL=(root) NOPASSWD: $YBIN/y-etc-hosts-write
EOF
done
}
Expand Down
68 changes: 68 additions & 0 deletions bin/y-etc-hosts-write
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
#!/usr/bin/env bash
[ -z "${DEBUG:-}" ] || set -x
set -eo pipefail

YHELP='y-etc-hosts-write - Replace /etc/hosts with content read from stdin

Usage: y-etc-hosts-write < newhosts

The single privileged step of the /etc/hosts tooling. It takes no arguments, so
the NOPASSWD rule y-cluster-sudoers grants for it cannot be pointed at another
path the way a rule for a flag-taking binary can.

Refuses input that is empty or has no 127.0.0.1 entry, and swaps the file in
with rename(2) so a reader never sees a half-written hosts file.

Environment:
Y_ETC_HOSTS Target path (default: /etc/hosts)

Dependencies:

Exit codes:
0 Written, or already identical
1 Usage error
2 Input refused by the safety checks
3 Target not writable (run through sudo)
'

case "${1:-}" in
help) echo "$YHELP"; exit 0 ;;
--help) echo "$YHELP"; exit 0 ;;
esac

if [ $# -ne 0 ]; then
echo "ERROR: takes no arguments, reads the new file from stdin" >&2
exit 1
fi

TARGET="${Y_ETC_HOSTS:-/etc/hosts}"
TARGET_DIR="$(dirname "$TARGET")"

if [ ! -w "$TARGET" ] || [ ! -w "$TARGET_DIR" ]; then
echo "ERROR: $TARGET is not writable as $(id -un), run through sudo" >&2
exit 3
fi

# Same directory as the target, so the swap below is a rename within one filesystem
TMP=$(mktemp "$TARGET_DIR/.hosts.XXXXXX")
trap 'rm -f "$TMP"' EXIT

cat > "$TMP"

if [ ! -s "$TMP" ]; then
echo "ERROR: refusing to write an empty $TARGET" >&2
exit 2
fi
if ! grep -qE '^[[:space:]]*127\.0\.0\.1[[:space:]]' "$TMP"; then
echo "ERROR: refusing input without a 127.0.0.1 entry" >&2
exit 2
fi

if cmp -s "$TMP" "$TARGET"; then
echo "# $TARGET already up to date"
exit 0
fi

chmod 0644 "$TMP"
mv "$TMP" "$TARGET"
echo "# wrote $TARGET"
155 changes: 95 additions & 60 deletions bin/y-k8s-ingress-hosts
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,68 @@
#!/usr/bin/env bash
[ -z "$DEBUG" ] || set -x
[ -z "${DEBUG:-}" ] || set -x
set -eo pipefail
YBIN="$(dirname $0)"
YBIN="$(dirname "$0")"

[ -z "$KUBECONFIG" ] && echo "This script requires a KUBECONFIG" && exit 1
YHELP='y-k8s-ingress-hosts - Sync /etc/hosts with cluster ingress and gateway hosts

CTX=""
CHECK=false
ENSURE=false
EXPLICIT_HOST_IP=""
PASSTHROUGH=()
Usage: y-k8s-ingress-hosts --context=NAME [options]

while [ $# -gt 0 ]; do
case "$1" in
-h|--help)
cat >&2 <<EOF
Usage: y-k8s-ingress-hosts [flags]

Flags:
--context=NAME kubeconfig context name (required)
-write rewrite host file
-check|--check check if /etc/hosts includes required entries (no sudo)
--ensure check, then write if needed (combines -check and -write)
--host-ip=IP override IP for all entries (otherwise resolved from
the GatewayClass yolean.se/dns-hint-ip annotation)
-h, --help show this help
Options:
--context=NAME kubeconfig context name (required)
-write write the entries to the hosts file
-check report drift without writing (never escalates)
--ensure check, then write if needed
--host-ip=IP override IP for all entries (otherwise resolved from
the GatewayClass yolean.se/dns-hint-ip annotation)

Cluster reads and the rendering of the new hosts file both run as the invoking
user. Only the swap of /etc/hosts is escalated, via y-etc-hosts-write, which
takes no arguments. Nothing here runs kubectl or the k8s-ingress-hosts binary
as root.

If --host-ip is not given, resolution walks (in order):
1. Gateway/ystack.ystack -> spec.gatewayClassName -> GatewayClass
metadata.annotations[yolean.se/dns-hint-ip]
2. Any GatewayClass carrying yolean.se/dns-hint-ip (used when no
consumer Gateway/ystack.ystack exists, e.g. y-cluster's
consumer Gateway/ystack.ystack exists, e.g. the y-cluster
appliance flow where the GatewayClass is the only artefact).
3. Gateway/ystack.ystack metadata.annotations[yolean.se/override-ip]
(legacy, pre-dates the dns-hint-ip contract).
y-cluster provision stamps the dns-hint-ip annotation when the
host forwards guest:80.
EOF
exit 0 ;;

Environment:
KUBECONFIG required
Y_HOST_IP same as --host-ip
Y_ETC_HOSTS target hosts file (default: /etc/hosts)

Dependencies:

Exit codes:
0 Up to date, written, or no entries to write
1 Usage error, or -check found drift without --ensure
3 The k8s-ingress-hosts binary failed
'

case "${1:-}" in
help) echo "$YHELP"; exit 0 ;;
--help) echo "$YHELP"; exit 0 ;;
-h) echo "$YHELP"; exit 0 ;;
esac

[ -z "$KUBECONFIG" ] && echo "ERROR: this script requires a KUBECONFIG" >&2 && exit 1

CTX=""
CHECK=false
ENSURE=false
WRITE=false
EXPLICIT_HOST_IP=""
PASSTHROUGH=()

while [ $# -gt 0 ]; do
case "$1" in
--context=*) CTX="${1#*=}"; shift ;;
-write|--write) WRITE=true; shift ;;
-check|--check) CHECK=true; shift ;;
--ensure) ENSURE=true; shift ;;
--host-ip=*) EXPLICIT_HOST_IP="${1#*=}"; shift ;;
Expand All@@ -55,10 +79,13 @@ EOF
esac
done

[ -z "$CTX" ] && echo "Required: --context=NAME" && exit 1
[ -z "$CTX" ] && echo "ERROR: required: --context=NAME" >&2 && exit 1

HOSTS_FILE="${Y_ETC_HOSTS:-/etc/hosts}"

CONTEXT_KUBECONFIG=$(mktemp)
trap "rm -f $CONTEXT_KUBECONFIG" EXIT
NEW_HOSTS=$(mktemp)
trap 'rm -f "$CONTEXT_KUBECONFIG" "$NEW_HOSTS"' EXIT
kubectl config view --raw --minify --context="$CTX" --request-timeout=5s > "$CONTEXT_KUBECONFIG"

# Resolve the host-side dial IP, in priority order:
Expand DownExpand Up@@ -96,7 +123,7 @@ if [ -z "$HOST_IP" ]; then
-o go-template='{{range .items}}{{$ip := index .metadata.annotations "yolean.se/dns-hint-ip"}}{{if $ip}}{{.metadata.name}}={{$ip}}{{"\n"}}{{end}}{{end}}' \
2>/dev/null || true) # y-script-lint:disable=or-true # no GatewayClasses is a normal pre-install state
DISTINCT_IPS=$(echo "$HINT_LIST" | awk -F= 'NF==2 && $2!="" {print $2}' | sort -u)
IP_COUNT=$(echo "$DISTINCT_IPS" | grep -c .)
IP_COUNT=$(echo "$DISTINCT_IPS" | grep -c . || true) # y-script-lint:disable=or-true # grep -c prints 0 but exits 1 with no matches, which is the no-annotation case
if [ "$IP_COUNT" = "1" ]; then
HOST_IP="$DISTINCT_IPS"
GC_NAME=$(echo "$HINT_LIST" | awk -F= 'NF==2 && $2!="" {print $1; exit}')
Expand All@@ -118,16 +145,24 @@ if [ -n "$HOST_IP" ]; then
PASSTHROUGH+=("-override-ip" "$HOST_IP")
fi

version=$(y-bin-download $YBIN/y-bin.optional.yaml k8s-ingress-hosts)
version=$(y-bin-download "$YBIN/y-bin.optional.yaml" k8s-ingress-hosts)
BIN="$YBIN/y-k8s-ingress-hosts-v${version}-bin"

echo "# reading k8s ingress resources..."
if ! RENDERED=$("$BIN" -kubeconfig "$CONTEXT_KUBECONFIG" "${PASSTHROUGH[@]}" 2>&1); then
echo "ERROR: $BIN failed:" >&2
echo "$RENDERED" >&2
exit 3
fi
ENTRIES=$(echo "$RENDERED" | grep -v '^#' || true) # y-script-lint:disable=or-true # grep exits 1 when the binary emitted comments only, which is the no-entries case handled below

if $CHECK || $ENSURE; then
NEEDED=$($YBIN/y-k8s-ingress-hosts-v${version}-bin -kubeconfig "$CONTEXT_KUBECONFIG" "${PASSTHROUGH[@]}" 2>/dev/null | grep -v '^#')
STALE=0
while IFS= read -r line; do
[ -z "$line" ] && continue
EXPECTED_IP=$(echo "$line" | awk '{print $1}')
HOST=$(echo "$line" | awk '{print $2}')
ACTUAL=$(grep -E "^[^#]*[[:space:]]$HOST([[:space:]]|$)" /etc/hosts 2>/dev/null || true) # y-script-lint:disable=or-true # grep exits 1 on no match -- expected for a missing-host check
ACTUAL=$(grep -E "^[^#]*[[:space:]]$HOST([[:space:]]|$)" "$HOSTS_FILE" 2>/dev/null || true) # y-script-lint:disable=or-true # grep exits 1 on no match -- expected for a missing-host check
if [ -z "$ACTUAL" ]; then
echo "Missing: $line"
STALE=1
Expand All@@ -136,46 +171,46 @@ if $CHECK || $ENSURE; then
echo "Stale: $HOST has $ACTUAL_IP, expected $EXPECTED_IP"
STALE=1
fi
done <<< "$NEEDED"
done <<< "$ENTRIES"
if [ $STALE -eq 0 ]; then
echo "# /etc/hosts is up to date"
echo "# $HOSTS_FILE is up to date"
exit 0
fi
if ! $ENSURE; then
echo "# /etc/hosts needs updating. Run with -write or --ensure to fix."
echo "# $HOSTS_FILE needs updating. Run with -write or --ensure to fix."
exit 1
fi
echo "# /etc/hosts needs updating, writing ..."
PASSTHROUGH+=("-write")
echo "# $HOSTS_FILE needs updating, writing ..."
WRITE=true
fi

# Guard: don't write an empty block that clears existing entries.
# Preview without -write to check if there are entries.
_PREVIEW_ARGS=()
for _a in "${PASSTHROUGH[@]}"; do
[ "$_a" = "-write" ] || _PREVIEW_ARGS+=("$_a")
done
echo "# reading k8s ingress resources..."
_PREVIEW=$($YBIN/y-k8s-ingress-hosts-v${version}-bin -kubeconfig "$CONTEXT_KUBECONFIG" "${_PREVIEW_ARGS[@]}" 2>/dev/null | grep -v '^#')
if [ -z "$_PREVIEW" ]; then
echo "# no ingress/gateway entries found, skipping write to preserve existing /etc/hosts"
# Without -write/-check/--ensure this is a preview, like the binary itself
if ! $WRITE; then
echo "$RENDERED"
exit 0
fi

# One-line stdout log when this invocation will actually mutate
# /etc/hosts (i.e. -write is in PASSTHROUGH, set either explicitly
# by the caller or appended above by --ensure on detected drift).
# Useful as a converge-trace breadcrumb so a yconverge exec check
# that ran y-k8s-ingress-hosts is visibly attributable.
WRITE_MODE=false
for _a in "${PASSTHROUGH[@]}"; do
[ "$_a" = "-write" ] && WRITE_MODE=true
done
if $WRITE_MODE; then
HOST_COUNT=$(echo "$_PREVIEW" | wc -l | tr -d ' ')
echo "y-k8s-ingress-hosts: writing $HOST_COUNT host entries to /etc/hosts"
# Guard: an empty render would otherwise clear the managed block
if [ -z "$ENTRIES" ]; then
echo "# no ingress/gateway entries found, skipping write to preserve existing $HOSTS_FILE"
exit 0
fi

[ $(id -u) -ne 0 ] && exec sudo $YBIN/y-k8s-ingress-hosts-v${version}-bin -kubeconfig "$CONTEXT_KUBECONFIG" "${PASSTHROUGH[@]}"
HOST_COUNT=$(echo "$ENTRIES" | wc -l | tr -d ' ')
echo "y-k8s-ingress-hosts: writing $HOST_COUNT host entries to $HOSTS_FILE"

# Render as the invoking user: the binary merges its managed block into a copy,
# and only that copy is handed to the privileged step.
cp "$HOSTS_FILE" "$NEW_HOSTS"
"$BIN" -kubeconfig "$CONTEXT_KUBECONFIG" -host-file "$NEW_HOSTS" -write "${PASSTHROUGH[@]}" >/dev/null

$YBIN/y-k8s-ingress-hosts-v${version}-bin -kubeconfig "$CONTEXT_KUBECONFIG" "${PASSTHROUGH[@]}" || exit $?
if cmp -s "$NEW_HOSTS" "$HOSTS_FILE"; then
echo "# $HOSTS_FILE already up to date"
exit 0
fi

if [ "$HOSTS_FILE" = "/etc/hosts" ] && [ "$(id -u)" -ne 0 ]; then
sudo "$YBIN/y-etc-hosts-write" < "$NEW_HOSTS"
else
Y_ETC_HOSTS="$HOSTS_FILE" "$YBIN/y-etc-hosts-write" < "$NEW_HOSTS"
fi
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions bin/y-bin.optional.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -25,14 +25,14 @@ k3d:
linux_arm64: 0b8110f2229631af7402fb828259330985918b08fefd38b7f1b788a1c8687216

kubefwd:
version: 1.25.12
version: 1.25.16
templates:
download: https://github.com/txn2/kubefwd/releases/download/v${version}/kubefwd_${Os}_${xarch}.tar.gz
sha256:
darwin_amd64: a9edf5398e0bcebf1a77eb8a74185498035cd624749d9b9ab08cf3ba832b7543
darwin_arm64: 70d60486d7d03b2dda99eee1a10d12549b861ae354df0225956a7552d8fb9a83
linux_amd64: 047b596f0d672111199265ccc7f830f1578eb9ad9be7dc5a30f2174b261534f2
linux_arm64: 04bc8d17e2417ead85bd2ba12a4c0eb517aa873c5611903656d7f579fe146a00
darwin_amd64: 905a3dd8a70d5acfcfe2a03f41feec264ac0c11cf7e5633b489716d719b64a6b
darwin_arm64: fc7a0126559cdd9e5cf8ddaf6a271b2d06381787bc3d3005ae48739a88f7d56c
linux_amd64: 07275cad05b2427069071160125b8cb29e94dd44582f685ce6d966fa9e7fb7d7
linux_arm64: e01ade02d919be2c7e306543f0a65de2e629c254ef16b51ecb45830b0044a3e8
archive:
tool: tar
path: kubefwd
Expand Down
5 changes: 4 additions & 1 deletion bin/y-cluster-sudoers
Original file line numberDiff line numberDiff line change
Expand Up@@ -50,7 +50,10 @@ rules() {
$u ALL=(root) NOPASSWD: $YBIN/y-localhost *

# y-k8s-ingress-hosts: /etc/hosts management ($u)
$u ALL=(root) NOPASSWD: $YBIN/y-k8s-ingress-hosts-v*-bin *
# Argument-free on purpose: the rule this replaced covered the
# k8s-ingress-hosts binary with any flags, and its -host-file flag
# turned that into a write-any-file-as-root grant.
$u ALL=(root) NOPASSWD: $YBIN/y-etc-hosts-write
EOF
done
}
Expand Down
68 changes: 68 additions & 0 deletions bin/y-etc-hosts-write
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
#!/usr/bin/env bash
[ -z "${DEBUG:-}" ] || set -x
set -eo pipefail

YHELP='y-etc-hosts-write - Replace /etc/hosts with content read from stdin

Usage: y-etc-hosts-write < newhosts

The single privileged step of the /etc/hosts tooling. It takes no arguments, so
the NOPASSWD rule y-cluster-sudoers grants for it cannot be pointed at another
path the way a rule for a flag-taking binary can.

Refuses input that is empty or has no 127.0.0.1 entry, and swaps the file in
with rename(2) so a reader never sees a half-written hosts file.

Environment:
Y_ETC_HOSTS Target path (default: /etc/hosts)

Dependencies:

Exit codes:
0 Written, or already identical
1 Usage error
2 Input refused by the safety checks
3 Target not writable (run through sudo)
'

case "${1:-}" in
help) echo "$YHELP"; exit 0 ;;
--help) echo "$YHELP"; exit 0 ;;
esac

if [ $# -ne 0 ]; then
echo "ERROR: takes no arguments, reads the new file from stdin" >&2
exit 1
fi

TARGET="${Y_ETC_HOSTS:-/etc/hosts}"
TARGET_DIR="$(dirname "$TARGET")"

if [ ! -w "$TARGET" ] || [ ! -w "$TARGET_DIR" ]; then
echo "ERROR: $TARGET is not writable as $(id -un), run through sudo" >&2
exit 3
fi

# Same directory as the target, so the swap below is a rename within one filesystem
TMP=$(mktemp "$TARGET_DIR/.hosts.XXXXXX")
trap 'rm -f "$TMP"' EXIT

cat > "$TMP"

if [ ! -s "$TMP" ]; then
echo "ERROR: refusing to write an empty $TARGET" >&2
exit 2
fi
if ! grep -qE '^[[:space:]]*127\.0\.0\.1[[:space:]]' "$TMP"; then
echo "ERROR: refusing input without a 127.0.0.1 entry" >&2
exit 2
fi

if cmp -s "$TMP" "$TARGET"; then
echo "# $TARGET already up to date"
exit 0
fi

chmod 0644 "$TMP"
mv "$TMP" "$TARGET"
echo "# wrote $TARGET"
155 changes: 95 additions & 60 deletions bin/y-k8s-ingress-hosts
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,68 @@
#!/usr/bin/env bash
[ -z "$DEBUG" ] || set -x
[ -z "${DEBUG:-}" ] || set -x
set -eo pipefail
YBIN="$(dirname $0)"
YBIN="$(dirname "$0")"

[ -z "$KUBECONFIG" ] && echo "This script requires a KUBECONFIG" && exit 1
YHELP='y-k8s-ingress-hosts - Sync /etc/hosts with cluster ingress and gateway hosts

CTX=""
CHECK=false
ENSURE=false
EXPLICIT_HOST_IP=""
PASSTHROUGH=()
Usage: y-k8s-ingress-hosts --context=NAME [options]

while [ $# -gt 0 ]; do
case "$1" in
-h|--help)
cat >&2 <<EOF
Usage: y-k8s-ingress-hosts [flags]

Flags:
--context=NAME kubeconfig context name (required)
-write rewrite host file
-check|--check check if /etc/hosts includes required entries (no sudo)
--ensure check, then write if needed (combines -check and -write)
--host-ip=IP override IP for all entries (otherwise resolved from
the GatewayClass yolean.se/dns-hint-ip annotation)
-h, --help show this help
Options:
--context=NAME kubeconfig context name (required)
-write write the entries to the hosts file
-check report drift without writing (never escalates)
--ensure check, then write if needed
--host-ip=IP override IP for all entries (otherwise resolved from
the GatewayClass yolean.se/dns-hint-ip annotation)

Cluster reads and the rendering of the new hosts file both run as the invoking
user. Only the swap of /etc/hosts is escalated, via y-etc-hosts-write, which
takes no arguments. Nothing here runs kubectl or the k8s-ingress-hosts binary
as root.

If --host-ip is not given, resolution walks (in order):
1. Gateway/ystack.ystack -> spec.gatewayClassName -> GatewayClass
metadata.annotations[yolean.se/dns-hint-ip]
2. Any GatewayClass carrying yolean.se/dns-hint-ip (used when no
consumer Gateway/ystack.ystack exists, e.g. y-cluster's
consumer Gateway/ystack.ystack exists, e.g. the y-cluster
appliance flow where the GatewayClass is the only artefact).
3. Gateway/ystack.ystack metadata.annotations[yolean.se/override-ip]
(legacy, pre-dates the dns-hint-ip contract).
y-cluster provision stamps the dns-hint-ip annotation when the
host forwards guest:80.
EOF
exit 0 ;;

Environment:
KUBECONFIG required
Y_HOST_IP same as --host-ip
Y_ETC_HOSTS target hosts file (default: /etc/hosts)

Dependencies:

Exit codes:
0 Up to date, written, or no entries to write
1 Usage error, or -check found drift without --ensure
3 The k8s-ingress-hosts binary failed
'

case "${1:-}" in
help) echo "$YHELP"; exit 0 ;;
--help) echo "$YHELP"; exit 0 ;;
-h) echo "$YHELP"; exit 0 ;;
esac

[ -z "$KUBECONFIG" ] && echo "ERROR: this script requires a KUBECONFIG" >&2 && exit 1

CTX=""
CHECK=false
ENSURE=false
WRITE=false
EXPLICIT_HOST_IP=""
PASSTHROUGH=()

while [ $# -gt 0 ]; do
case "$1" in
--context=*) CTX="${1#*=}"; shift ;;
-write|--write) WRITE=true; shift ;;
-check|--check) CHECK=true; shift ;;
--ensure) ENSURE=true; shift ;;
--host-ip=*) EXPLICIT_HOST_IP="${1#*=}"; shift ;;
Expand All@@ -55,10 +79,13 @@ EOF
esac
done

[ -z "$CTX" ] && echo "Required: --context=NAME" && exit 1
[ -z "$CTX" ] && echo "ERROR: required: --context=NAME" >&2 && exit 1

HOSTS_FILE="${Y_ETC_HOSTS:-/etc/hosts}"

CONTEXT_KUBECONFIG=$(mktemp)
trap "rm -f $CONTEXT_KUBECONFIG" EXIT
NEW_HOSTS=$(mktemp)
trap 'rm -f "$CONTEXT_KUBECONFIG" "$NEW_HOSTS"' EXIT
kubectl config view --raw --minify --context="$CTX" --request-timeout=5s > "$CONTEXT_KUBECONFIG"

# Resolve the host-side dial IP, in priority order:
Expand DownExpand Up@@ -96,7 +123,7 @@ if [ -z "$HOST_IP" ]; then
-o go-template='{{range .items}}{{$ip := index .metadata.annotations "yolean.se/dns-hint-ip"}}{{if $ip}}{{.metadata.name}}={{$ip}}{{"\n"}}{{end}}{{end}}' \
2>/dev/null || true) # y-script-lint:disable=or-true # no GatewayClasses is a normal pre-install state
DISTINCT_IPS=$(echo "$HINT_LIST" | awk -F= 'NF==2 && $2!="" {print $2}' | sort -u)
IP_COUNT=$(echo "$DISTINCT_IPS" | grep -c .)
IP_COUNT=$(echo "$DISTINCT_IPS" | grep -c . || true) # y-script-lint:disable=or-true # grep -c prints 0 but exits 1 with no matches, which is the no-annotation case
if [ "$IP_COUNT" = "1" ]; then
HOST_IP="$DISTINCT_IPS"
GC_NAME=$(echo "$HINT_LIST" | awk -F= 'NF==2 && $2!="" {print $1; exit}')
Expand All@@ -118,16 +145,24 @@ if [ -n "$HOST_IP" ]; then
PASSTHROUGH+=("-override-ip" "$HOST_IP")
fi

version=$(y-bin-download $YBIN/y-bin.optional.yaml k8s-ingress-hosts)
version=$(y-bin-download "$YBIN/y-bin.optional.yaml" k8s-ingress-hosts)
BIN="$YBIN/y-k8s-ingress-hosts-v${version}-bin"

echo "# reading k8s ingress resources..."
if ! RENDERED=$("$BIN" -kubeconfig "$CONTEXT_KUBECONFIG" "${PASSTHROUGH[@]}" 2>&1); then
echo "ERROR: $BIN failed:" >&2
echo "$RENDERED" >&2
exit 3
fi
ENTRIES=$(echo "$RENDERED" | grep -v '^#' || true) # y-script-lint:disable=or-true # grep exits 1 when the binary emitted comments only, which is the no-entries case handled below

if $CHECK || $ENSURE; then
NEEDED=$($YBIN/y-k8s-ingress-hosts-v${version}-bin -kubeconfig "$CONTEXT_KUBECONFIG" "${PASSTHROUGH[@]}" 2>/dev/null | grep -v '^#')
STALE=0
while IFS= read -r line; do
[ -z "$line" ] && continue
EXPECTED_IP=$(echo "$line" | awk '{print $1}')
HOST=$(echo "$line" | awk '{print $2}')
ACTUAL=$(grep -E "^[^#]*[[:space:]]$HOST([[:space:]]|$)" /etc/hosts 2>/dev/null || true) # y-script-lint:disable=or-true # grep exits 1 on no match -- expected for a missing-host check
ACTUAL=$(grep -E "^[^#]*[[:space:]]$HOST([[:space:]]|$)" "$HOSTS_FILE" 2>/dev/null || true) # y-script-lint:disable=or-true # grep exits 1 on no match -- expected for a missing-host check
if [ -z "$ACTUAL" ]; then
echo "Missing: $line"
STALE=1
Expand All@@ -136,46 +171,46 @@ if $CHECK || $ENSURE; then
echo "Stale: $HOST has $ACTUAL_IP, expected $EXPECTED_IP"
STALE=1
fi
done <<< "$NEEDED"
done <<< "$ENTRIES"
if [ $STALE -eq 0 ]; then
echo "# /etc/hosts is up to date"
echo "# $HOSTS_FILE is up to date"
exit 0
fi
if ! $ENSURE; then
echo "# /etc/hosts needs updating. Run with -write or --ensure to fix."
echo "# $HOSTS_FILE needs updating. Run with -write or --ensure to fix."
exit 1
fi
echo "# /etc/hosts needs updating, writing ..."
PASSTHROUGH+=("-write")
echo "# $HOSTS_FILE needs updating, writing ..."
WRITE=true
fi

# Guard: don't write an empty block that clears existing entries.
# Preview without -write to check if there are entries.
_PREVIEW_ARGS=()
for _a in "${PASSTHROUGH[@]}"; do
[ "$_a" = "-write" ] || _PREVIEW_ARGS+=("$_a")
done
echo "# reading k8s ingress resources..."
_PREVIEW=$($YBIN/y-k8s-ingress-hosts-v${version}-bin -kubeconfig "$CONTEXT_KUBECONFIG" "${_PREVIEW_ARGS[@]}" 2>/dev/null | grep -v '^#')
if [ -z "$_PREVIEW" ]; then
echo "# no ingress/gateway entries found, skipping write to preserve existing /etc/hosts"
# Without -write/-check/--ensure this is a preview, like the binary itself
if ! $WRITE; then
echo "$RENDERED"
exit 0
fi

# One-line stdout log when this invocation will actually mutate
# /etc/hosts (i.e. -write is in PASSTHROUGH, set either explicitly
# by the caller or appended above by --ensure on detected drift).
# Useful as a converge-trace breadcrumb so a yconverge exec check
# that ran y-k8s-ingress-hosts is visibly attributable.
WRITE_MODE=false
for _a in "${PASSTHROUGH[@]}"; do
[ "$_a" = "-write" ] && WRITE_MODE=true
done
if $WRITE_MODE; then
HOST_COUNT=$(echo "$_PREVIEW" | wc -l | tr -d ' ')
echo "y-k8s-ingress-hosts: writing $HOST_COUNT host entries to /etc/hosts"
# Guard: an empty render would otherwise clear the managed block
if [ -z "$ENTRIES" ]; then
echo "# no ingress/gateway entries found, skipping write to preserve existing $HOSTS_FILE"
exit 0
fi

[ $(id -u) -ne 0 ] && exec sudo $YBIN/y-k8s-ingress-hosts-v${version}-bin -kubeconfig "$CONTEXT_KUBECONFIG" "${PASSTHROUGH[@]}"
HOST_COUNT=$(echo "$ENTRIES" | wc -l | tr -d ' ')
echo "y-k8s-ingress-hosts: writing $HOST_COUNT host entries to $HOSTS_FILE"

# Render as the invoking user: the binary merges its managed block into a copy,
# and only that copy is handed to the privileged step.
cp "$HOSTS_FILE" "$NEW_HOSTS"
"$BIN" -kubeconfig "$CONTEXT_KUBECONFIG" -host-file "$NEW_HOSTS" -write "${PASSTHROUGH[@]}" >/dev/null

$YBIN/y-k8s-ingress-hosts-v${version}-bin -kubeconfig "$CONTEXT_KUBECONFIG" "${PASSTHROUGH[@]}" || exit $?
if cmp -s "$NEW_HOSTS" "$HOSTS_FILE"; then
echo "# $HOSTS_FILE already up to date"
exit 0
fi

if [ "$HOSTS_FILE" = "/etc/hosts" ] && [ "$(id -u)" -ne 0 ]; then
sudo "$YBIN/y-etc-hosts-write" < "$NEW_HOSTS"
else
Y_ETC_HOSTS="$HOSTS_FILE" "$YBIN/y-etc-hosts-write" < "$NEW_HOSTS"
fi
Loading
Loading