-
Notifications
You must be signed in to change notification settings - Fork 55
🧯 fix: Fence Worker Pairing Revocation #70
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
danny-avila
wants to merge
18
commits into
danny-avila/principal-code-workers
from
danny-avila/code-worker-lifecycle
Open
Changes from all commits
Commits
Show all changes
18 commits
Select commit
Hold shift + click to select a range
50c58e9
fix: invalidate pending worker pairings on revoke
danny-avila 886bdd5
fix: package bridge protocol in API image
danny-avila fb08f15
fix: fence mixed-version pairing revocation
danny-avila c32a0ce
fix: redeem valid legacy pairing codes
danny-avila bd4a0d0
fix: harden pairing rollout compatibility
danny-avila 36b3311
fix: make pairing revocation atomic
danny-avila 758c820
fix: preserve pairing identity across rollouts
danny-avila 6ea76cc
fix: reopen pairing cleanup after rollbacks
danny-avila 310e129
fix: bound legacy pairing migration scans
danny-avila dd7fe02
fix: retry interrupted pairing migrations
danny-avila 83d9315
fix: harden pairing migration cleanup
danny-avila 6178e15
fix: make pairing cleanup recoverable
danny-avila d3c6129
fix: preserve pairing recovery across lifecycle rollout
danny-avila 7555cc6
fix: drain API pods before pairing rollback
danny-avila e277ea7
fix: fence rollback reentry and pairing epochs
danny-avila ca53b09
fix: close rollback verification gaps
danny-avila 175398a
fix: keep rollback drain on one cluster
danny-avila 716fea5
fix: bound rollback recovery triggers
danny-avila File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,190 @@ | ||
| #!/usr/bin/env bash | ||
| set -euo pipefail | ||
|
|
||
| usage() { | ||
| echo "usage: $0 RELEASE REVISION [NAMESPACE] [helm rollback flags...]" >&2 | ||
| exit 64 | ||
| } | ||
|
|
||
| release=${1:-} | ||
| revision=${2:-} | ||
| namespace=${3:-default} | ||
| if [[ -z "$release" || ! "$revision" =~ ^[1-9][0-9]*$ ]]; then | ||
| usage | ||
| fi | ||
| shift $(( $# >= 3 ? 3 : $# )) | ||
|
|
||
| if [[ ! "$release" =~ ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ ]]; then | ||
| echo "invalid Helm release name: $release" >&2 | ||
| exit 64 | ||
| fi | ||
| if [[ ! "$namespace" =~ ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ ]]; then | ||
| echo "invalid Kubernetes namespace: $namespace" >&2 | ||
| exit 64 | ||
| fi | ||
| for flag in "$@"; do | ||
| case "$flag" in | ||
| -n|-n?*|--namespace|--namespace=*|--kube-context|--kube-context=*|\ | ||
| --kubeconfig|--kubeconfig=*|--kube-apiserver|--kube-apiserver=*|\ | ||
| --kube-ca-file|--kube-ca-file=*|--kube-token|--kube-token=*|\ | ||
| --kube-tls-server-name|--kube-tls-server-name=*|\ | ||
| --kube-as-user|--kube-as-user=*|--kube-as-group|--kube-as-group=*|\ | ||
| --kube-insecure-skip-tls-verify|--kube-insecure-skip-tls-verify=*) | ||
| echo "refusing target-changing Helm rollback flag: $flag" >&2 | ||
| exit 64 | ||
| ;; | ||
| esac | ||
| done | ||
| for variable in \ | ||
| HELM_KUBEAPISERVER \ | ||
| HELM_KUBEASGROUPS \ | ||
| HELM_KUBEASUSER \ | ||
| HELM_KUBECAFILE \ | ||
| HELM_KUBECONTEXT \ | ||
| HELM_KUBEINSECURE_SKIP_TLS_VERIFY \ | ||
| HELM_KUBETLS_SERVER_NAME \ | ||
| HELM_KUBETOKEN \ | ||
| HELM_NAMESPACE; do | ||
| if [[ -n ${!variable:-} ]]; then | ||
| echo "refusing Helm target override from environment: $variable" >&2 | ||
| exit 64 | ||
| fi | ||
| done | ||
|
|
||
| timeout=${CODEAPI_ROLLBACK_TIMEOUT:-10m} | ||
| selector="app.kubernetes.io/instance=${release},app.kubernetes.io/component=api" | ||
|
|
||
| discover_api_deployments() { | ||
| local output | ||
| output=$(kubectl --namespace "$namespace" get deployment \ | ||
| --selector "$selector" --output name) || return | ||
| deployments=() | ||
| if [[ -n "$output" ]]; then | ||
| mapfile -t deployments <<< "$output" | ||
| fi | ||
| } | ||
|
|
||
| list_api_pods() { | ||
| local output | ||
| output=$(kubectl --namespace "$namespace" get pod \ | ||
| --selector "$selector" --output name) || return | ||
| pods=() | ||
| if [[ -n "$output" ]]; then | ||
| mapfile -t pods <<< "$output" | ||
| fi | ||
| } | ||
|
|
||
| discover_api_deployments | ||
| if (( ${#deployments[@]} != 1 )); then | ||
| echo "expected exactly one Code API deployment for $selector" >&2 | ||
| exit 1 | ||
| fi | ||
| deployment=${deployments[0]} | ||
|
|
||
| fence=$(kubectl --namespace "$namespace" get "$deployment" \ | ||
| --output 'jsonpath={.spec.template.metadata.annotations.codeapi\.librechat\.ai/pairing-fence-version}') | ||
| if [[ -z "$fence" ]]; then | ||
| echo "refusing rollback: the live API deployment has no pairing fence" >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| deployment_name=${deployment#*/} | ||
| rollback_config_map=${deployment_name%-api}-pairing-rollback | ||
| rollback_epoch="$(date +%s)-${RANDOM}-${RANDOM}" | ||
|
|
||
| echo "Recording pairing rollback epoch $rollback_epoch..." >&2 | ||
| kubectl --namespace "$namespace" create configmap "$rollback_config_map" \ | ||
| --from-literal="epoch=$rollback_epoch" --dry-run=client --output yaml | \ | ||
| kubectl --namespace "$namespace" apply --filename - | ||
|
|
||
| drain_api() { | ||
| local pod_action=${1:-wait} | ||
| local replica_state desired current ready available updated | ||
|
|
||
| # Helm may have partially installed a target with a different fullname. | ||
| # Resolve every matching API Deployment on each drain attempt. | ||
| discover_api_deployments | ||
| if (( ${#deployments[@]} == 0 )) && [[ "$pod_action" != delete ]]; then | ||
| echo "refusing rollback: no API deployment matched $selector" >&2 | ||
| return 1 | ||
| fi | ||
|
|
||
| echo "Deleting API autoscalers before the rollback fence is lowered..." >&2 | ||
| kubectl --namespace "$namespace" delete horizontalpodautoscaler \ | ||
| --selector "$selector" --ignore-not-found --wait=true | ||
|
|
||
| echo "Scaling the fenced API deployment to zero..." >&2 | ||
| for deployment in "${deployments[@]}"; do | ||
| kubectl --namespace "$namespace" scale "$deployment" --replicas=0 | ||
| kubectl --namespace "$namespace" rollout status "$deployment" \ | ||
| --timeout "$timeout" | ||
| done | ||
|
|
||
| list_api_pods | ||
| if (( ${#pods[@]} > 0 )); then | ||
| if [[ "$pod_action" == delete ]]; then | ||
| kubectl --namespace "$namespace" delete pod \ | ||
| --selector "$selector" --wait=true --timeout "$timeout" | ||
| else | ||
| kubectl --namespace "$namespace" wait "${pods[@]}" \ | ||
| --for=delete --timeout "$timeout" | ||
| fi | ||
| fi | ||
|
|
||
| # Relist immediately before Helm can lower the fence. This catches a new | ||
| # matching pod that appeared after the first snapshot. | ||
| discover_api_deployments | ||
| for deployment in "${deployments[@]}"; do | ||
| replica_state=$(kubectl --namespace "$namespace" get "$deployment" \ | ||
| --output 'jsonpath={.spec.replicas},{.status.replicas},{.status.readyReplicas},{.status.availableReplicas},{.status.updatedReplicas}') || return | ||
| IFS=, read -r desired current ready available updated <<< "$replica_state" | ||
| if [[ ${desired:-0} != 0 || ${current:-0} != 0 || ${ready:-0} != 0 || | ||
| ${available:-0} != 0 || ${updated:-0} != 0 ]]; then | ||
| echo "refusing rollback: API deployment did not converge to zero replicas" >&2 | ||
| return 1 | ||
| fi | ||
| done | ||
| list_api_pods | ||
| if (( ${#pods[@]} > 0 )); then | ||
| echo "refusing rollback: API pods appeared after the drain" >&2 | ||
| return 1 | ||
| fi | ||
| } | ||
|
|
||
| drain_api wait | ||
|
|
||
| echo "All fenced API pods are gone; starting Helm rollback..." >&2 | ||
| rollback_pid= | ||
| recover_interrupted_rollback() { | ||
| local exit_status=$1 | ||
| trap - HUP INT TERM | ||
| if [[ -n "$rollback_pid" ]]; then | ||
| kill -TERM "$rollback_pid" 2>/dev/null || true | ||
| wait "$rollback_pid" 2>/dev/null || true | ||
| fi | ||
| echo "Helm rollback interrupted; restoring the fail-closed API drain..." >&2 | ||
| set -e | ||
| drain_api delete | ||
| exit "$exit_status" | ||
| } | ||
| trap 'recover_interrupted_rollback 129' HUP | ||
| trap 'recover_interrupted_rollback 130' INT | ||
| trap 'recover_interrupted_rollback 143' TERM | ||
|
|
||
| helm rollback "$release" "$revision" \ | ||
| --namespace "$namespace" --wait --wait-for-jobs --timeout "$timeout" "$@" & | ||
| rollback_pid=$! | ||
| set +e | ||
| wait "$rollback_pid" | ||
| rollback_status=$? | ||
| set -e | ||
| rollback_pid= | ||
| trap - HUP INT TERM | ||
|
|
||
| if (( rollback_status == 0 )); then | ||
| exit 0 | ||
| else | ||
| echo "Helm rollback failed; restoring the fail-closed API drain..." >&2 | ||
| drain_api delete | ||
| exit "$rollback_status" | ||
| fi | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.