penguin/utils

my env utils

bash/sources/19_k8s_funcs

raw ยท 8323 bytes

#!/bin/bash

function klabels {
	local header=
	local padding=
	local trim_length=
	local line_text=
	local first_line=
	local max_key_length=0
	local max_label_length=0
	local key=
	local value=
	local filter_value="$1"
	local filter_key="$2"

	while IFS= read -r line; do
		if [ -z "$header" ]; then
			header="$line"
			padding="${header//LABELS/}"
			padding="${padding//?/ }"
			echo "$header"
			trim_length=${#padding}
			continue
		fi

		line_text="${line:0:trim_length}"
		first_line=
		IFS=',' read -ra labels <<< "${line:trim_length}"

		# Calculate the maximum key length to align key=value pairs
		for label in "${labels[@]}"; do
			key="${label%%=*}"
			max_key_length=$(( ${#key} > max_key_length ? ${#key} : max_key_length ))
		done

		# Loop through labels and align them
		for label in "${labels[@]}"; do
			key="${label%%=*}"
			value="${label#*=}"

			if [ -n "$filter_key" ] && ! [[ "$key" == *$filter_key* ]] ; then
				continue
			fi

			if [ -n "$filter_value" ] && ! [[ "$value" == *$filter_value* ]] ; then
				continue
			fi


			# Format the label to align the '=' sign
			printf -v formatted_label "%-${max_key_length}s = %s" "$key" "$value"

			if [ -z "$first_line" ]; then
				first_line=1

				# Print a line separator based on the longest key length
				separator_length=$((max_key_length + trim_length))
				printf "%${separator_length}s\n" | tr ' ' "-"

				echo "$line_text$formatted_label"
			else
				echo "$padding$formatted_label"
			fi
		done
	done
}

function __kcreate_help {
	cat <<'EOH'
usage:
  kcreate [-n NAMESPACE] [options] TARGET_POD NEW_POD [-exec [--] CMD...]

examples:
  kcreate -n www app-pod debug-pod
  kcreate -n www app-pod debug-pod -exec -- bash -l
  kcreate -n www -c app --rm app-pod debug-pod -exec -- bash -l

options:
  -n, --namespace NS       Namespace. Defaults to current kubectl namespace, then default.
  -c, --container NAME     Container to wait for / exec into.
  -e, -exec, --exec        After creating the Pod, wait for container(s) to run, then kubectl exec -it.
  --rm                    Delete the new Pod after the exec command exits.
  --sleep, --sleep-infinity
                          Start copied containers with: sleep infinity.
  --force                 Delete NEW_POD first if it already exists.
  --dry-run               Print the generated Pod manifest instead of creating it.
  --copy-labels           Copy labels from the target Pod. Dangerous: Services may route traffic to it.
  --copy-annotations      Copy annotations from the target Pod. Usually not needed.
  --same-node             Keep spec.nodeName, forcing the new Pod onto the same node.
  --no-probes             Remove liveness/readiness/startup probes from copied containers.
  --timeout SECONDS       Exec wait timeout. Default: 90.
EOH
}

function __kcreate_current_namespace {
	local ns

	ns=$(kubectl config view --minify -o 'jsonpath={..namespace}' 2>/dev/null)
	if [ -z "$ns" ]; then
		ns=default
	fi

	printf '%s\n' "$ns"
}

function __kcreate_wait_container_running {
	local ns="$1"
	local pod="$2"
	local container="$3"
	local timeout="$4"
	local end
	local ok

	end=$((SECONDS + timeout))

	while [ "$SECONDS" -le "$end" ]; do
		if [ -n "$container" ]; then
			ok=$(
				kubectl -n "$ns" get pod "$pod" -o json 2>/dev/null | jq -r --arg c "$container" '
					[.status.containerStatuses[]? | select(.name == $c and (.state.running != null))] | length > 0
				'
			) || ok=false
		else
			ok=$(
				kubectl -n "$ns" get pod "$pod" -o json 2>/dev/null | jq -r '
					[.status.containerStatuses[]?] as $s
					| ($s | length > 0) and ([ $s[] | select(.state.running != null) ] | length == ($s | length))
				'
			) || ok=false
		fi

		if [ "$ok" = true ]; then
			return 0
		fi

		sleep 1
	done

	echo "kcreate: timed out waiting for pod/$pod container(s) to be running" >&2
	kubectl -n "$ns" get pod "$pod" >&2
	return 1
}

function kcreate {
	local ns=
	local target_pod=
	local new_pod=
	local container=
	local sleep_forever=false
	local exec_after=false
	local rm_after=false
	local force=false
	local dry_run=false
	local copy_labels=false
	local copy_annotations=false
	local same_node=false
	local no_probes=false
	local timeout=90
	local manifest
	local rc
	local exec_cmd=()
	local exec_args=()

	while [ "$#" -gt 0 ]; do
		case "$1" in
			-h|--help)
				__kcreate_help
				return 0
				;;
			-n|--namespace)
				if [ -z "$2" ]; then echo "kcreate: $1 needs a namespace" >&2; return 2; fi
				ns="$2"
				shift 2
				;;
			-c|--container)
				if [ -z "$2" ]; then echo "kcreate: $1 needs a container name" >&2; return 2; fi
				container="$2"
				shift 2
				;;
			--sleep|--sleep-infinity)
				sleep_forever=true
				shift
				;;
			-e|-exec|--exec)
				exec_after=true
				shift
				if [ "${1:-}" = "--" ]; then shift; fi
				exec_cmd=("$@")
				break
				;;
			--rm)
				rm_after=true
				shift
				;;
			--force)
				force=true
				shift
				;;
			--dry-run)
				dry_run=true
				shift
				;;
			--copy-labels)
				copy_labels=true
				shift
				;;
			--copy-annotations)
				copy_annotations=true
				shift
				;;
			--same-node)
				same_node=true
				shift
				;;
			--no-probes)
				no_probes=true
				shift
				;;
			--timeout)
				if [ -z "$2" ]; then echo "kcreate: $1 needs seconds" >&2; return 2; fi
				timeout="$2"
				shift 2
				;;
			-*)
				echo "kcreate: unknown option: $1" >&2
				return 2
				;;
			*)
				if [ -z "$target_pod" ]; then
					target_pod="$1"
				elif [ -z "$new_pod" ]; then
					new_pod="$1"
				else
					echo "kcreate: unexpected argument: $1" >&2
					return 2
				fi
				shift
				;;
		esac
	done

	if [ -z "$ns" ]; then
		ns=$(__kcreate_current_namespace)
	fi

	if [ -z "$target_pod" ] || [ -z "$new_pod" ]; then
		__kcreate_help >&2
		return 2
	fi

	if ! command -v kubectl >/dev/null 2>&1; then
		echo "kcreate: kubectl is required" >&2
		return 127
	fi

	if ! command -v jq >/dev/null 2>&1; then
		echo "kcreate: jq is required" >&2
		return 127
	fi

	if ! [[ "$timeout" =~ ^[0-9]+$ ]]; then
		echo "kcreate: --timeout must be plain seconds, e.g. 90" >&2
		return 2
	fi

	manifest=$(
		kubectl -n "$ns" get pod "$target_pod" -o json | jq \
			--arg name "$new_pod" \
			--arg src "$target_pod" \
			--arg srcns "$ns" \
			--argjson sleep_forever "$sleep_forever" \
			--argjson copy_labels "$copy_labels" \
			--argjson copy_annotations "$copy_annotations" \
			--argjson same_node "$same_node" \
			--argjson no_probes "$no_probes" '
			{
				apiVersion: "v1",
				kind: "Pod",
				metadata: {
					name: $name,
					labels: (if $copy_labels then (.metadata.labels // {}) else {"app.kubernetes.io/managed-by": "kcreate"} end),
					annotations: (if $copy_annotations then (.metadata.annotations // {}) else {} end)
				},
				spec: .spec
			}
			| .metadata.annotations["kcreate/source-pod"] = $src
			| .metadata.annotations["kcreate/source-namespace"] = $srcns
			| del(.metadata.labels["pod-template-hash"]?)
			| del(.spec.priority?)
			| del(.spec.serviceAccount?)
			| if $same_node then . else del(.spec.nodeName?) end
			| if $no_probes then
				(.spec.containers |= map(del(.livenessProbe?, .readinessProbe?, .startupProbe?)))
			  else
				.
			  end
			| if $sleep_forever then
				(.spec.containers |= map(
					.command = ["sleep"]
					| .args = ["infinity"]
					| del(.livenessProbe?, .readinessProbe?, .startupProbe?)
				))
			  else
				.
			end
		'
	) || return $?

	if [ "$dry_run" = true ]; then
		printf '%s\n' "$manifest"
		return 0
	fi

	if [ "$copy_labels" = true ]; then
		echo "kcreate: warning: --copy-labels may let Services route traffic to pod/$new_pod" >&2
	fi

	if [ "$force" = true ]; then
		kubectl -n "$ns" delete pod "$new_pod" --ignore-not-found --wait=true || return $?
	fi

	printf '%s\n' "$manifest" | kubectl -n "$ns" create -f - || return $?

	if [ "$exec_after" != true ]; then
		kubectl -n "$ns" get pod "$new_pod"
		return $?
	fi

	if [ "${#exec_cmd[@]}" -eq 0 ]; then
		exec_cmd=(sh)
	fi

	__kcreate_wait_container_running "$ns" "$new_pod" "$container" "$timeout" || return $?

	if [ -n "$container" ]; then
		exec_args=(-c "$container")
	fi

	kubectl -n "$ns" exec -it "$new_pod" "${exec_args[@]}" -- "${exec_cmd[@]}"
	rc=$?

	if [ "$rm_after" = true ]; then
		kubectl -n "$ns" delete pod "$new_pod" --ignore-not-found >/dev/null
	fi

	return "$rc"
}