Skip to content
Open
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
55 changes: 52 additions & 3 deletions kubefwd/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@ https://kubefwd.com/

- `bash`
- `kubefwd`
- `entr`
- GNU core utils (`tr`, `sort`) - `brew install coreutils`
- `uuidgen` (or `od` via GNU core utils)
- `entr` (required if kubefwd < 1.25.0)
- `jq` (required if kubefwd >= 1.25.0)
- GNU core utils (`tr`, `sort`, `od`) - `brew install coreutils`

## Examples

Expand All @@ -32,6 +34,24 @@ v1alpha1.extension_repo(name='default', url='https://github.com/tilt-dev/tilt-ex
v1alpha1.extension(name='kubefwd:config', repo_name='default', repo_path='kubefwd', args=['--namespaces=kubesystem'])
```

```python
# explicitly override the kubefwd mode (idle or legacy) instead of auto-detecting.
v1alpha1.extension_repo(name='default', url='https://github.com/tilt-dev/tilt-extensions')
v1alpha1.extension(name='kubefwd:config', repo_name='default', repo_path='kubefwd', args=['--kubefwd-mode=legacy'])
```

```python
# kubefwd all namespaces Tilt deploys to, but purge stale IPs first.
v1alpha1.extension_repo(name='default', url='https://github.com/tilt-dev/tilt-extensions')
v1alpha1.extension(name='kubefwd:config', repo_name='default', repo_path='kubefwd', args=['--kubefwd-purge=true'])
```

```python
# Use non-interactive sudo (sudo -n) instead of graphical prompts (useful in WSL).
v1alpha1.extension_repo(name='default', url='https://github.com/tilt-dev/tilt-extensions')
v1alpha1.extension(name='kubefwd:config', repo_name='default', repo_path='kubefwd', args=['--kubefwd-sudo-non-interactive=true'])
```

## Usage

When Tilt starts up for the first time, it will prompt up your native OS GUI for
Expand All @@ -44,6 +64,35 @@ to, then configure `kubefwd` for that namespace.
If `kubefwd` gets wedged, Tilt has a custom button to the UI to refresh it.
Click the `kubefwd:run` resource and press the button labelled "Refresh".

## Bypassing the Sudo Prompt

To avoid entering your password every time Tilt starts `kubefwd`, you can permanently grant passwordless execution rights to the `run-kubefwd.sh` script. You can achieve this using one of the following methods, depending on your system's configuration.

### Method 1: sudoers (for `sudo`)
If your system uses `sudo` (e.g., when using `--kubefwd-sudo-non-interactive=true`), you can add a rule to your `sudoers` configuration.

Run `sudo visudo -f /etc/sudoers.d/tilt-kubefwd` and add the following line, replacing `<username>` with your actual username:

```
<username> ALL=(ALL:ALL) NOPASSWD: /tmp/kubefwd.tilt/run-kubefwd.sh
```

### Method 2: PolicyKit / polkit (for `pkexec`)
If your system uses `pkexec` for graphical prompts, you can add a polkit rule.

Create a file at `/etc/polkit-1/rules.d/49-nopasswd-kubefwd.rules` and add the following content, replacing `<username>` with your actual username:

```javascript
polkit.addRule(function(action, subject) {
if (action.id == "org.freedesktop.policykit.exec" &&
action.lookup("program") == "/tmp/kubefwd.tilt/run-kubefwd.sh" &&
subject.user == "<username>") {
return polkit.Result.YES;
}
});
```


## Future Work

### Pod Discovery
Expand All @@ -60,7 +109,7 @@ Possible solutions:
2) Even better, a version of `kubefwd` that reads the pods from the Tilt API.

Currently, this extension uses `entr` to restart `kubefwd`
without requesting new credentials.
without requesting new credentials when `kubefwd` < 1.25.0.

### Multi-cluster kubefwd

Expand Down
67 changes: 62 additions & 5 deletions kubefwd/Tiltfile
Original file line number Diff line number Diff line change
@@ -1,18 +1,75 @@
# Accept namespaces as args. This requires tilt > v0.22.7

config.define_string_list("namespaces")
config.define_string("kubefwd-mode", usage="Set to 'idle' or 'legacy'. Defaults to auto-detect based on kubefwd version.")
config.define_bool("kubefwd-purge", usage="Remove stale kubefwd host entries before starting")
config.define_bool("kubefwd-sudo-non-interactive", usage="Use non-interactive sudo (sudo -n) instead of graphical prompts")
cfg = config.parse()

namespaces = cfg.get('namespaces', [])
mode = cfg.get('kubefwd-mode', None)
purge = cfg.get('kubefwd-purge', False)
sudo_non_interactive = cfg.get('kubefwd-sudo-non-interactive', False)

trigger_file = str(local('mktemp "${TMPDIR:-/tmp}/tilt-kubefwd.XXXXXXXXX"')).strip()
# Auto-detect mode based on kubefwd version if not explicitly provided
if not mode:
# Attempt to parse version from `kubefwd version` output
# Expected output: "Kubefwd version: 1.22.4" or "Version 1.25.0"
version_output = str(local('kubefwd version 2>&1 | grep -i "version" | grep --max-count=1 -oE "[0-9]+\\.[0-9]+" || echo ""')).strip()

mode = "legacy" # Fallback to legacy if we can't parse the version
if version_output:
parts = version_output.split('.')
if len(parts) >= 2:
major = int(parts[0])
minor = int(parts[1])
if major > 1 or (major == 1 and minor >= 25):
mode = "idle"

trigger_file = ""
deps = ['sudo-kubefwd.sh', 'run-kubefwd.sh', 'watch-namespaces.sh']

# Generate a static API key for the session so our scripts can talk to kubefwd
api_key = str(local('uuidgen 2>/dev/null || od -vN "16" -An -tx1 /dev/urandom | tr -d " \n" 2>/dev/null || echo ""')).strip()
if not api_key:
fail("kubefwd extension requires 'uuidgen' or 'od' to generate an API key. Please install uuidgen or coreutils.")

if mode == "legacy":
trigger_file = str(local('mktemp "${TMPDIR:-/tmp}/tilt-kubefwd.XXXXXXXXX"')).strip()
deps.append('run-kubefwd-internal.sh')
btn_args = ["touch", trigger_file]
else:
btn_args = ["curl", "-s", "-X", "POST", "-H", "Authorization: Bearer %s" % api_key, "http://kubefwd.internal/api/v1/services/reconnect?force=true"]

# Creates a button that refreshes the kubefwd without
# requiring new credentials.
local(['./create-refresh-button.sh', trigger_file])
# Use ui_button function. This requires tilt >= v0.22.10
v1alpha1.ui_button(
name='kubefwd:refresh',
text='Refresh',
icon_name='refresh',
location={
'component_type': 'Resource',
'component_id': 'kubefwd:run'
}
)

v1alpha1.cmd(
name='kubefwd:refresh',
args=btn_args,
annotations={'tilt.dev/resource': 'kubefwd:run'},
start_on=v1alpha1.start_on_spec(ui_buttons=['kubefwd:refresh'])
)

local_resource(
name='kubefwd:run',
serve_cmd=['./sudo-kubefwd.sh', trigger_file],
deps=['sudo-kubefwd.sh', 'run-kubefwd.sh', 'run-kubefwd-internal.sh', 'watch-namespaces.sh'],
serve_env={'TILT_CFG_NAMESPACES': ' '.join(namespaces)})
serve_cmd=['./sudo-kubefwd.sh'],
deps=deps,
serve_env={
'TILT_CFG_NAMESPACES': ' '.join(namespaces),
'TILT_KUBEFWD_MODE': mode,
'TILT_KUBEFWD_TRIGGER': trigger_file,
'KUBEFWD_PURGE': str(purge).lower(),
'TILT_SUDO_NON_INTERACTIVE': str(sudo_non_interactive).lower(),
'KUBEFWD_API_KEY': api_key
})
29 changes: 0 additions & 29 deletions kubefwd/create-refresh-button.sh

This file was deleted.

4 changes: 2 additions & 2 deletions kubefwd/run-kubefwd-internal.sh
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env bash
#
# Run kubefwd, assuming we already have sudo privs.
# Build up the arguments to kubefwd from the namespaces in the trigger file.
Expand All @@ -11,7 +11,7 @@ while read -r namespace; do
continue
fi
flags+=("-n=$namespace")
done < "$TRIGGER"
done < "$TILT_KUBEFWD_TRIGGER"

if [ ${#flags[@]} -eq 0 ]; then
echo "No namespaces to kubefwd"
Expand Down
31 changes: 28 additions & 3 deletions kubefwd/run-kubefwd.sh
Original file line number Diff line number Diff line change
@@ -1,13 +1,38 @@
#!/bin/bash
#!/usr/bin/env bash
# Run kubefwd, assuming we already have sudo privs.

export KUBEFWD="$1"
export KUBECONFIG="$2"
export ENTR="$3"
export TRIGGER="$4"
export TILT_KUBEFWD_MODE="$4"
export TILT_KUBEFWD_TRIGGER="$5"
export KUBEFWD_PURGE="$6"
export KUBEFWD_API_KEY="$7"

DIR=$(realpath "$(dirname "$0")")

set -exuo pipefail

echo "$TRIGGER" | "$ENTR" -rn "$DIR/run-kubefwd-internal.sh"
if [[ "$TILT_KUBEFWD_MODE" == "legacy" ]]; then
echo "$TILT_KUBEFWD_TRIGGER" | "$ENTR" -rn "$DIR/run-kubefwd-internal.sh"
else
flags=()
if [[ "${KUBEFWD_PURGE:-false}" == "true" ]]; then
flags+=("--purge-stale-ips")
fi

# Kill any lingering kubefwd processes from previous Tilt runs
# and wait for them to exit to prevent race conditions with /etc/hosts
(pkill -x kubefwd && pidwait -x kubefwd) || true

# Check array length before expanding to avoid unbound variable errors
# in Bash 3.2 (macOS default) when 'set -u' is enabled and the array is empty.

echo "API Key: ${KUBEFWD_API_KEY}"

if [ ${#flags[@]} -eq 0 ]; then
exec "$KUBEFWD"
else
exec "$KUBEFWD" "${flags[@]}"
fi
fi
82 changes: 50 additions & 32 deletions kubefwd/sudo-kubefwd.sh
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
#!/bin/bash
#!/usr/bin/env bash
#
# Runs kubefwd with admin privileges.
# Invokes an OS-specific sudo UI.

TRIGGER="$1"
ENTR=$(command -v entr)
OSASCRIPT=$(command -v osascript)
PKEXEC=$(command -v pkexec)
SUDO=$(command -v sudo)
KDESUDO=$(command -v kdesudo)
GKSUDO=$(command -v gksudo)
KUBEFWD=$(command -v kubefwd)
JQ=$(command -v jq)
DIR=$(realpath "$(dirname "$0")")
KUBECONFIG=${KUBECONFIG:-${HOME}/.kube/config}
TILT_KUBEFWD_MODE=${TILT_KUBEFWD_MODE:-idle}
TILT_KUBEFWD_TRIGGER=${TILT_KUBEFWD_TRIGGER:-}
KUBEFWD_PURGE=${KUBEFWD_PURGE:-false}
KUBEFWD_API_KEY=${KUBEFWD_API_KEY:-}

set -euo pipefail

Expand All @@ -22,10 +27,18 @@ if [[ "$KUBEFWD" == "" ]]; then
exit 1
fi

if [[ "$ENTR" == "" ]]; then
echo "entr not found. Did you forget to install it?"
echo "Run: brew install entr"
exit 1
if [[ "$TILT_KUBEFWD_MODE" == "legacy" ]]; then
if [[ "$ENTR" == "" ]]; then
echo "entr not found. Did you forget to install it?"
echo "Run: brew install entr"
exit 1
fi
else
if [[ "$JQ" == "" ]]; then
echo "jq not found. Did you forget to install it?"
echo "Run: brew install jq"
exit 1
fi
fi

# Copy kubefwd script into a new directory to avoid path normalization issues.
Expand All @@ -34,64 +47,69 @@ mkdir -p /tmp/kubefwd.tilt
run_kubefwd_path="/tmp/kubefwd.tilt/run-kubefwd.sh"
cp "$(dirname "$0")/run-kubefwd.sh" $run_kubefwd_path

run_kubefwd_internal_path="/tmp/kubefwd.tilt/run-kubefwd-internal.sh"
cp "$(dirname "$0")/run-kubefwd-internal.sh" $run_kubefwd_internal_path

RUN_KUBEFWD=$run_kubefwd_path

# Initialize the trigger file
touch "$TRIGGER"
chmod a+rw "$TRIGGER"
if [[ "$TILT_KUBEFWD_MODE" == "legacy" ]]; then
run_kubefwd_internal_path="/tmp/kubefwd.tilt/run-kubefwd-internal.sh"
cp "$(dirname "$0")/run-kubefwd-internal.sh" $run_kubefwd_internal_path

# Initialize the trigger file
touch "$TILT_KUBEFWD_TRIGGER"
chmod a+rw "$TILT_KUBEFWD_TRIGGER"
fi

# In the background, populate the trigger file
# with the namespaces we need to watch.
"$DIR/watch-namespaces.sh" "$TRIGGER" &
# In the background, synchronize the namespaces we need to watch.
"$DIR/watch-namespaces.sh" &
WATCH_PID="$!"

# shellcheck disable=SC2317
function cleanup {
set -x
set +e

# Remove the trigger file. This will make any dangling entr/kubefwd
# processes exit. (We can't kill them directly b/c they're running with sudo
# privs.)
rm -f "$TRIGGER"

if [[ "$TILT_KUBEFWD_MODE" == "legacy" ]]; then
rm -f "$TILT_KUBEFWD_TRIGGER"
fi

kill "$WATCH_PID"
wait "$WATCH_PID"

# Remove the trigger file twice, in case the watcher created a new one.
rm -f "$TRIGGER"
if [[ "$TILT_KUBEFWD_MODE" == "legacy" ]]; then
rm -f "$TILT_KUBEFWD_TRIGGER"
fi
}
trap cleanup EXIT

if [[ "${TILT_SUDO_NON_INTERACTIVE:-false}" == "true" ]] && [[ "$SUDO" != "" ]]; then
set -x
"$SUDO" -n "$RUN_KUBEFWD" "$KUBEFWD" "$KUBECONFIG" "$ENTR" "$TILT_KUBEFWD_MODE" "$TILT_KUBEFWD_TRIGGER" "$KUBEFWD_PURGE" "$KUBEFWD_API_KEY"
exit "$?"
fi

if [[ "$OSASCRIPT" != "" ]]; then
set -x
"$OSASCRIPT" -e "do shell script \"$RUN_KUBEFWD $KUBEFWD $KUBECONFIG $ENTR $TRIGGER\" with administrator privileges"
CMD="'$RUN_KUBEFWD' '$KUBEFWD' '$KUBECONFIG' '$ENTR' '$TILT_KUBEFWD_MODE' '$TILT_KUBEFWD_TRIGGER' '$KUBEFWD_PURGE' '$KUBEFWD_API_KEY'"
"$OSASCRIPT" -e "do shell script \"$CMD\" with administrator privileges"
exit "$?"
fi

if [[ "$PKEXEC" != "" ]]; then
set -x
"$PKEXEC" --disable-internal-agent "$RUN_KUBEFWD" "$KUBEFWD" "$KUBECONFIG" "$ENTR" "$TRIGGER"
"$PKEXEC" --disable-internal-agent "$RUN_KUBEFWD" "$KUBEFWD" "$KUBECONFIG" "$ENTR" "$TILT_KUBEFWD_MODE" "$TILT_KUBEFWD_TRIGGER" "$KUBEFWD_PURGE" "$KUBEFWD_API_KEY"
exit "$?"
fi
fi

if [[ "$KDESUDO" != "" ]]; then
set -x
"$KDESUDO" --comment 'Tilt needs admin privs to run kubefwd. Please enter your password.' "$RUN_KUBEFWD" "$KUBEFWD" "$KUBECONFIG" "$ENTR" "$TRIGGER"
"$KDESUDO" --comment 'Tilt needs admin privs to run kubefwd. Please enter your password.' "$RUN_KUBEFWD" "$KUBEFWD" "$KUBECONFIG" "$ENTR" "$TILT_KUBEFWD_MODE" "$TILT_KUBEFWD_TRIGGER" "$KUBEFWD_PURGE" "$KUBEFWD_API_KEY"
exit "$?"
fi
fi

if [[ "$GKSUDO" != "" ]]; then
set -x
"$GKSUDO" --preserve-env --sudo-mode --description 'tilt/kubefwd' "$RUN_KUBEFWD" "$KUBEFWD" "$KUBECONFIG" "$ENTR" "$TRIGGER"
"$GKSUDO" --preserve-env --sudo-mode --description 'tilt/kubefwd' "$RUN_KUBEFWD" "$KUBEFWD" "$KUBECONFIG" "$ENTR" "$TILT_KUBEFWD_MODE" "$TILT_KUBEFWD_TRIGGER" "$KUBEFWD_PURGE" "$KUBEFWD_API_KEY"
exit "$?"
fi
fi

echo "No sudo runner found"
exit 1



Loading