|
#!/bin/bash |
|
|
|
COMMAND="" |
|
NAMESPACE="" |
|
CONTEXT="" |
|
KUBECONFIG="" |
|
COMMAND_ARGS=() |
|
COMMAND_OPTIONS=() |
|
|
|
# kubectl() { |
|
# echo "kubectl $@" >> /tmp/kubectl-keda.log |
|
# /usr/local/sbin/kubectl $@ |
|
# } |
|
|
|
# Function to parse command line arguments |
|
# Arguments: "$@" - all command line arguments |
|
# Returns: Sets global variables COMMAND, NAMESPACE, CONTEXT, KUBECONFIG, COMMAND_ARGS, COMMAND_OPTIONS |
|
parse_arguments() { |
|
local args=("$@") |
|
local i=0 |
|
local current_arg="" |
|
|
|
# Reset global variables |
|
COMMAND="" |
|
NAMESPACE="" |
|
CONTEXT="" |
|
KUBECONFIG="" |
|
COMMAND_ARGS=() |
|
COMMAND_OPTIONS=() |
|
|
|
# Process all arguments |
|
while ((i < ${#args[@]})); do |
|
current_arg="${args[$i]}" |
|
|
|
case "$current_arg" in |
|
"-n"|"--namespace") |
|
if ((i + 1 >= ${#args[@]})); then |
|
usage "Namespace value is required for $current_arg" |
|
return 1 |
|
fi |
|
NAMESPACE="${args[$((i+1))]}" |
|
((i++)) |
|
;; |
|
"--context") |
|
if ((i + 1 >= ${#args[@]})); then |
|
usage "Context value is required for $current_arg" |
|
return 1 |
|
fi |
|
CONTEXT="${args[$((i+1))]}" |
|
((i++)) |
|
;; |
|
"--kubeconfig") |
|
if ((i + 1 >= ${#args[@]})); then |
|
usage "Kubeconfig value is required for $current_arg" |
|
return 1 |
|
fi |
|
KUBECONFIG="${args[$((i+1))]}" |
|
((i++)) |
|
;; |
|
"--"*) |
|
if ((i + 1 >= ${#args[@]})); then |
|
usage "Value is required for $current_arg" |
|
return 1 |
|
fi |
|
COMMAND_OPTIONS+=("$current_arg" "${args[$((i+1))]}") |
|
((i++)) |
|
;; |
|
-*) |
|
usage "Unknown option: $current_arg" |
|
return 1 |
|
;; |
|
*) |
|
if [[ -z "$COMMAND" ]]; then |
|
COMMAND="$current_arg" |
|
else |
|
COMMAND_ARGS+=("$current_arg") |
|
fi |
|
;; |
|
esac |
|
((i++)) |
|
done |
|
|
|
return 0 |
|
} |
|
|
|
# Function to update ScaledObject configuration |
|
# Arguments: |
|
# $1 - ScaledObject name |
|
# $2 - JSON patch |
|
# $3 - Namespace (optional) |
|
# $4 - Context (optional) |
|
# $5 - Kubeconfig (optional) |
|
# Returns: 0 on success, 1 on failure |
|
update_scaled_object() { |
|
local name="$1" |
|
local patch="$2" |
|
local namespace="$3" |
|
local context="$4" |
|
local kubeconfig="$5" |
|
local kubectl_args=() |
|
|
|
if [[ -z "$name" ]]; then |
|
handle_error "ScaledObject name is required" 1 false |
|
return 1 |
|
fi |
|
|
|
if [[ -z "$patch" ]]; then |
|
handle_error "JSON patch is required" 1 false |
|
return 1 |
|
fi |
|
|
|
kubectl_args+=("patch" "scaledobject" "$name" "--type=merge" "-p" "$patch") |
|
|
|
if [[ -n "$namespace" ]]; then |
|
kubectl_args+=("--namespace" "$namespace") |
|
fi |
|
|
|
if [[ -n "$context" ]]; then |
|
kubectl_args+=("--context" "$context") |
|
fi |
|
|
|
if [[ -n "$kubeconfig" ]]; then |
|
kubectl_args+=("--kubeconfig" "$kubeconfig") |
|
fi |
|
|
|
if ! output=$(kubectl "${kubectl_args[@]}"); then |
|
echo "$output" |
|
return 1 |
|
fi |
|
echo "$output" |
|
|
|
return 0 |
|
} |
|
|
|
# Function to display usage information |
|
# Arguments: |
|
# $1 - Additional message to display before usage (optional) |
|
usage() { |
|
local message="$1" |
|
|
|
if [[ -n "$message" ]]; then |
|
echo "$message" |
|
echo |
|
fi |
|
|
|
echo "Usage: kubectl keda <command> [arguments]" |
|
echo "Commands:" |
|
echo " version" |
|
echo " pause <scaled-object-name1> [<scaled-object-name2>...] [--replicas <count>]" |
|
echo " unpause <scaled-object-name1> [<scaled-object-name2>...]" |
|
echo " status <scaled-object-name1> [<scaled-object-name2>...]" |
|
echo |
|
echo "Global options:" |
|
echo " -n, --namespace: Kubernetes namespace (default: current context's namespace)" |
|
echo " --context: Kubernetes context to use (default: current context)" |
|
echo " --kubeconfig: Kubernetes kubeconfig to use (default: current kubeconfig)" |
|
echo |
|
echo "Examples:" |
|
echo " kubectl keda pause my-scaled-object # Pause the scaled object" |
|
echo " kubectl keda pause my-scaled-object --replicas 10 # Set 10 replicas and pause the scaled object" |
|
echo " kubectl keda unpause my-scaled-object # Unpause the scaled object" |
|
echo " kubectl keda status my-scaled-object # Get the status of the scaled object" |
|
echo " kubectl keda status my-so-1 my-so-2 # Get the status of multiple scaled objects" |
|
echo " kubectl keda status -n my-namespace # Get the status of all scaled objects in a specific namespace" |
|
} |
|
|
|
# Function to handle errors |
|
# Arguments: |
|
# $1 - Error message |
|
# $2 - Exit code (optional, defaults to 1) |
|
# $3 - Show usage (optional, defaults to false) |
|
handle_error() { |
|
local message="$1" |
|
local exit_code="${2:-1}" |
|
local show_usage="${3:-true}" |
|
|
|
echo "Error: $message" |
|
|
|
# Show usage if requested |
|
if [[ "$show_usage" == "true" ]]; then |
|
usage |
|
fi |
|
|
|
exit "$exit_code" |
|
} |
|
|
|
# Function to pause a ScaledObject |
|
# Arguments: |
|
# $1 - Comma-separated list of ScaledObject names |
|
# $2 - Number of replicas to maintain (optional) |
|
# $3 - Namespace (optional) |
|
# $4 - Context (optional) |
|
# $5 - Kubeconfig (optional) |
|
pause_so() { |
|
local names=() |
|
local replicas="$2" |
|
local namespace="$3" |
|
local context="$4" |
|
local kubeconfig="$5" |
|
local kubectl_args=() |
|
|
|
IFS=',' read -ra names <<< "$1" |
|
if [[ ${#names[@]} -eq 0 ]]; then |
|
handle_error "At least one ScaledObject name is required" 1 true |
|
return 1 |
|
fi |
|
|
|
if [[ -n "$namespace" ]]; then |
|
kubectl_args+=("--namespace" "$namespace") |
|
fi |
|
if [[ -n "$context" ]]; then |
|
kubectl_args+=("--context" "$context") |
|
fi |
|
|
|
local patch='{"metadata":{"annotations":{"autoscaling.keda.sh/paused":"true"}}}' |
|
|
|
if [[ -n "$replicas" ]]; then |
|
if ! [[ "$replicas" =~ ^[0-9]+$ ]]; then |
|
handle_error "Replicas must be a positive number" |
|
return 1 |
|
fi |
|
patch=$(echo "$patch" | jq --arg reps "$replicas" '.metadata.annotations["autoscaling.keda.sh/paused-replicas"] = $reps') |
|
fi |
|
|
|
local success=true |
|
for name in "${names[@]}"; do |
|
local update_output |
|
if ! update_output=$(update_scaled_object "$name" "$patch" "$namespace" "$context" "$kubeconfig" 2>&1); then |
|
echo "Error: Failed to pause ScaledObject '$name': $update_output" >&2 |
|
success=false |
|
else |
|
echo "Successfully paused ScaledObject '$name'" |
|
if [[ -n "$replicas" ]]; then |
|
echo "Maintaining $replicas replicas while paused" |
|
fi |
|
fi |
|
done |
|
|
|
if [[ "$success" == "false" ]]; then |
|
return 1 |
|
fi |
|
return 0 |
|
} |
|
|
|
# Function to unpause a ScaledObject |
|
# Arguments: |
|
# $1 - Comma-separated list of ScaledObject names |
|
# $2 - Namespace (optional) |
|
# $3 - Context (optional) |
|
# $4 - Kubeconfig (optional) |
|
unpause_so() { |
|
local names=() |
|
local namespace="$2" |
|
local context="$3" |
|
local kubeconfig="$4" |
|
local kubectl_args=() |
|
|
|
IFS=',' read -ra names <<< "$1" |
|
if [[ ${#names[@]} -eq 0 ]]; then |
|
handle_error "At least one ScaledObject name is required" 1 true |
|
return 1 |
|
fi |
|
|
|
if [[ -n "$namespace" ]]; then |
|
kubectl_args+=("--namespace" "$namespace") |
|
fi |
|
if [[ -n "$context" ]]; then |
|
kubectl_args+=("--context" "$context") |
|
fi |
|
|
|
local patch='{"metadata":{"annotations":{"autoscaling.keda.sh/paused":null,"autoscaling.keda.sh/paused-replicas":null}}}' |
|
|
|
local success=true |
|
for name in "${names[@]}"; do |
|
local update_output |
|
if ! update_output=$(update_scaled_object "$name" "$patch" "$namespace" "$context" "$kubeconfig" 2>&1); then |
|
echo "Error: Failed to unpause ScaledObject '$name': $update_output" >&2 |
|
success=false |
|
else |
|
echo "Successfully unpaused ScaledObject '$name'" |
|
fi |
|
done |
|
|
|
if [[ "$success" == "false" ]]; then |
|
return 1 |
|
fi |
|
return 0 |
|
} |
|
|
|
# Function to get status of ScaledObject(s) |
|
# Arguments: |
|
# $1 - Comma-separated list of ScaledObject names (optional) |
|
# $2 - Namespace (optional) |
|
# $3 - Context (optional) |
|
# $4 - Kubeconfig (optional) |
|
status_so() { |
|
local names=() |
|
local namespace="$2" |
|
local context="$3" |
|
local kubeconfig="$4" |
|
local kubectl_args=() |
|
|
|
if [[ -n "$namespace" ]]; then |
|
kubectl_args+=("--namespace" "$namespace") |
|
fi |
|
if [[ -n "$context" ]]; then |
|
kubectl_args+=("--context" "$context") |
|
fi |
|
if [[ -n "$kubeconfig" ]]; then |
|
kubectl_args+=("--kubeconfig" "$kubeconfig") |
|
fi |
|
|
|
local so_list |
|
if [[ -n "$1" ]]; then |
|
IFS=',' read -ra names <<< "$1" |
|
local name_args=() |
|
for name in "${names[@]}"; do |
|
name_args+=("$name") |
|
done |
|
if ! so_list=$(kubectl get scaledobject "${name_args[@]}" -o json "${kubectl_args[@]}" 2>&1); then |
|
echo "Error: Failed to get ScaledObjects: $so_list" >&2 |
|
return 1 |
|
fi |
|
# If single resource, wrap it in a List object |
|
if [[ ${#names[@]} -eq 1 ]]; then |
|
so_list=$(echo "$so_list" | jq '{apiVersion: "v1", kind: "List", items: [.]}') |
|
fi |
|
else |
|
if ! so_list=$(kubectl get scaledobject -o json "${kubectl_args[@]}" 2>&1); then |
|
echo "Error: Failed to get ScaledObjects: $so_list" >&2 |
|
return 1 |
|
fi |
|
|
|
local item_count=$(echo "$so_list" | jq '.items | length') |
|
if [[ "$item_count" -eq 0 ]]; then |
|
echo "No ScaledObjects found in namespace ${namespace:-default}" |
|
return 0 |
|
fi |
|
fi |
|
|
|
local scale_targets=() |
|
while IFS= read -r target; do |
|
if [[ -n "$target" ]]; then |
|
scale_targets+=("$target") |
|
fi |
|
done < <(echo "$so_list" | jq -r '.items[] | "\(.spec.scaleTargetRef.kind // "Deployment") \(.spec.scaleTargetRef.name)"') |
|
|
|
local scale_target_json |
|
if [[ ${#scale_targets[@]} -gt 0 ]]; then |
|
# Get all scale target resources in a single API call |
|
local kubectl_get_args=() |
|
for target in "${scale_targets[@]}"; do |
|
read -r kind name <<< "$target" |
|
kubectl_get_args+=("$kind/$name") |
|
done |
|
if ! scale_target_json=$(kubectl get "${kubectl_get_args[@]}" -o json "${kubectl_args[@]}" 2>&1); then |
|
echo "Error: Failed to get scale target resources: $scale_target_json" >&2 |
|
return 1 |
|
fi |
|
|
|
if [[ ${#scale_targets[@]} -eq 1 ]]; then |
|
scale_target_json=$(echo "$scale_target_json" | jq '{apiVersion: "v1", kind: "List", items: [.]}') |
|
fi |
|
fi |
|
|
|
local success=true |
|
while IFS= read -r so_json; do |
|
local name=$(echo "$so_json" | jq -r '.metadata.name') |
|
local namespace=$(echo "$so_json" | jq -r '.metadata.namespace') |
|
local min_replicas=$(echo "$so_json" | jq -r '.spec.minReplicaCount // 0') |
|
local max_replicas=$(echo "$so_json" | jq -r '.spec.maxReplicaCount // 100') |
|
local is_paused=$(echo "$so_json" | jq -r '.metadata.annotations["autoscaling.keda.sh/paused"] // "false"') |
|
local paused_replicas=$(echo "$so_json" | jq -r '.metadata.annotations["autoscaling.keda.sh/paused-replicas"] // ""') |
|
local scale_target_ref=$(echo "$so_json" | jq -r '.spec.scaleTargetRef.name') |
|
local scale_target_kind=$(echo "$so_json" | jq -r '.spec.scaleTargetRef.kind // "Deployment"') |
|
|
|
local current_replicas=$(echo "$scale_target_json" | jq -r --arg kind "$scale_target_kind" --arg name "$scale_target_ref" ' |
|
.items[] | select(.kind == $kind and .metadata.name == $name) | .status.replicas // 0 |
|
') |
|
|
|
echo "ScaledObject: $namespace/$name" |
|
echo " Min Replicas: $min_replicas" |
|
echo " Max Replicas: $max_replicas" |
|
echo " Paused: $is_paused" |
|
echo " Paused Replicas: $paused_replicas" |
|
echo " Current Replicas: $current_replicas" |
|
echo |
|
done < <(echo "$so_list" | jq -c '.items[]') |
|
|
|
if [[ "$success" == "false" ]]; then |
|
return 1 |
|
fi |
|
return 0 |
|
} |
|
|
|
# Function to extract command option value by option name |
|
# Arguments: |
|
# $1 - Option name without dashes (e.g., "replicas" for "--replicas") |
|
# Returns: The value of the option if found, empty string if not found |
|
get_option_value() { |
|
local option_name="$1" |
|
local i |
|
|
|
for ((i=0; i<${#COMMAND_OPTIONS[@]}; i+=2)); do |
|
if [[ "${COMMAND_OPTIONS[$i]}" == "--$option_name" ]]; then |
|
echo "${COMMAND_OPTIONS[$((i+1))]}" |
|
return 0 |
|
fi |
|
done |
|
echo "" |
|
return 1 |
|
} |
|
|
|
# Main script execution |
|
main() { |
|
# Parse all arguments |
|
parse_arguments "$@" |
|
|
|
if [[ -z "$COMMAND" ]]; then |
|
usage "No command specified" |
|
return 1 |
|
fi |
|
|
|
case "$COMMAND" in |
|
"version") |
|
echo "kubectl KEDA plugin for managing KEDA ScaledObjects" |
|
echo "The plugin is vibe-coded, it can be buggy." |
|
echo "Version: v0.3.2" |
|
;; |
|
"pause") |
|
local replicas |
|
replicas=$(get_option_value "replicas") |
|
pause_so "$(IFS=,; echo "${COMMAND_ARGS[*]}")" "$replicas" "$NAMESPACE" "$CONTEXT" "$KUBECONFIG" |
|
;; |
|
"unpause") |
|
unpause_so "$(IFS=,; echo "${COMMAND_ARGS[*]}")" "$NAMESPACE" "$CONTEXT" "$KUBECONFIG" |
|
;; |
|
"status") |
|
status_so "$(IFS=,; echo "${COMMAND_ARGS[*]}")" "$NAMESPACE" "$CONTEXT" "$KUBECONFIG" |
|
;; |
|
*) |
|
usage "Unknown command: $COMMAND" |
|
return 1 |
|
;; |
|
esac |
|
} |
|
|
|
main "$@" |