1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
|
#!/usr/bin/env bash
set -eu
set -o pipefail
if [[ "${1:-}" == "--help" ]] || [[ "${1:-}" == "-h" ]]; then
cat <<EOF
Usage: capo-shell <namespace> <name> [command...]
Opens a shell with kubeconfig and OpenStack environment variables set for
a CAPO-managed CAPI cluster.
Arguments:
namespace Kubernetes namespace of the cluster
name Name of the cluster
command Command to run (default: \$SHELL or bash)
Environment variables (override config file):
KUBECONFIG_CONTEXT kubectl context to use for the management cluster
CAPO_SHELL_KUBECONFIG_FILTER yq expression to filter the workload kubeconfig
CAPO_SHELL_SSHUTTLE_HOST SSH host to proxy API server traffic via sshuttle
Config file: \${XDG_CONFIG_HOME:-\$HOME/.config}/capo-shell/config.yaml
transforms.{clusters,contexts,users} jq expressions applied to workload kubeconfig sections
custom_fields.friendly_name jq expression (against the Cluster resource) used to
rename the workload kubeconfig cluster (CAPI already
names it after the cluster, so this only takes effect
if the expression resolves non-empty); the kubeconfig
user is renamed to the identity reported by
"kubectl auth whoami", if available; if either rename
happened, the context is renamed to "<user>|<cluster>"
sshuttle_host global SSH proxy host
management_clusters[].sshuttle_host per-cluster SSH proxy host (matched by KUBECONFIG path)
EOF
exit 0
fi
namespace="${1?}"
name="${2?}"
shift
shift
envs=()
_capo_config="${XDG_CONFIG_HOME:-$HOME/.config}/capo-shell/config.yaml"
_friendly_name_expr=""
if [[ -f "$_capo_config" ]]; then
_friendly_name_expr="$(yq -r '.custom_fields.friendly_name // empty' "$_capo_config")"
if [[ ! -v CAPO_SHELL_SSHUTTLE_HOST ]]; then
_sshuttle=""
if [[ -v KUBECONFIG ]]; then
while IFS= read -r _kc && IFS= read -r _sh; do
if [[ "$(envsubst <<< "$_kc")" == "$KUBECONFIG" ]] && [[ -n "$_sh" ]]; then
_sshuttle="$(envsubst <<< "$_sh")"
break
fi
done < <(yq -r '.management_clusters[] | (.kubeconfig, (.sshuttle_host // ""))' "$_capo_config")
fi
if [[ -z "$_sshuttle" ]]; then
_sshuttle="$(yq -r '.sshuttle_host // empty' "$_capo_config")"
[[ -n "$_sshuttle" ]] && _sshuttle="$(envsubst <<< "$_sshuttle")"
fi
[[ -n "$_sshuttle" ]] && CAPO_SHELL_SSHUTTLE_HOST="$_sshuttle"
fi
if [[ ! -v CAPO_SHELL_KUBECONFIG_FILTER ]]; then
_filter=""
_t="$(yq -r '.transforms.clusters // empty' "$_capo_config")"
[[ -n "$_t" ]] && _filter="${_filter:+$_filter | }.clusters |= map($_t)"
_t="$(yq -r '.transforms.contexts // empty' "$_capo_config")"
[[ -n "$_t" ]] && _filter="${_filter:+$_filter | }.contexts |= map($_t)"
_t="$(yq -r '.transforms.users // empty' "$_capo_config")"
[[ -n "$_t" ]] && _filter="${_filter:+$_filter | }.users |= map($_t)"
[[ -n "$_filter" ]] && CAPO_SHELL_KUBECONFIG_FILTER="$_filter"
fi
fi
function cleanup() {
[[ -v NEW_KUBECONFIG ]] && [[ -f "$NEW_KUBECONFIG" ]] && rm -f "$NEW_KUBECONFIG"
[[ -v PROXY_PID ]] && kill "$PROXY_PID"
}
trap 'EC=$?; cleanup || true; exit $EC' EXIT INT TERM
NEW_KUBECONFIG="$(mktemp -p "$XDG_RUNTIME_DIR")"
if [[ -v KUBECONFIG_CONTEXT ]]; then
function kubectl() {
command kubectl --context "$KUBECONFIG_CONTEXT" "$@"
}
fi
if kubectl -n "$namespace" get secrets "${name}-kubeconfig" -o jsonpath='{.data.value}' 2>/dev/null | base64 -d >"$NEW_KUBECONFIG"; then
hasKubeconfig=true
envs+=(KUBECONFIG="$NEW_KUBECONFIG")
if [[ -v CAPO_SHELL_KUBECONFIG_FILTER ]]; then
yq -i -Y "$CAPO_SHELL_KUBECONFIG_FILTER" "$NEW_KUBECONFIG"
fi
_renamed=false
if [[ -n "$_friendly_name_expr" ]]; then
if _friendly="$(kubectl -n "$namespace" get cluster "$name" -o yaml 2>/dev/null | yq -er "(${_friendly_name_expr}) // empty" 2>/dev/null)"; then
yq -i -Y --arg name "$_friendly" '
(.contexts[0].context.cluster) as $oldCluster |
(.clusters[] | select(.name == $oldCluster) | .name) = $name |
(.contexts[0].context.cluster) = $name
' "$NEW_KUBECONFIG"
_renamed=true
fi
fi
if _whoami="$(command kubectl --kubeconfig "$NEW_KUBECONFIG" auth whoami -o jsonpath='{.status.userInfo.username}' 2>/dev/null)" && [[ -n "$_whoami" ]]; then
yq -i -Y --arg user "$_whoami" '
(.contexts[0].context.user) as $oldUser |
(.users[] | select(.name == $oldUser) | .name) = $user |
(.contexts[0].context.user) = $user
' "$NEW_KUBECONFIG"
_renamed=true
fi
if [[ "$_renamed" == true ]]; then
yq -i -Y '
(.contexts[0].context.user) as $user |
(.contexts[0].context.cluster) as $cluster |
(.contexts[0].name) = "\($user)|\($cluster)" |
(."current-context") = "\($user)|\($cluster)"
' "$NEW_KUBECONFIG"
fi
else
hasKubeconfig=false
envs+=(KUBECONFIG="")
fi
if secretName="$(kubectl -n "$namespace" get openstackcluster -l cluster.x-k8s.io/cluster-name="$name" -o yaml 2>/dev/null | yq -er '.items[0].spec.identityRef.name')"; then
hasOSConfig=true
mapfile -t osEnvs < <(kubectl -n "$namespace" get secret "$secretName" -o jsonpath='{.data.clouds\.yaml}' | base64 -d | yq -er '.clouds.openstack | {OS_AUTH_TYPE: .["auth_type"], OS_AUTH_URL: .auth["auth_url"], OS_APPLICATION_CREDENTIAL_ID: .auth["application_credential_id"], OS_APPLICATION_CREDENTIAL_SECRET: .auth["application_credential_secret"], OS_REGION_NAME: .["region_name"], OS_INTERFACE: .interface, OS_IDENTITY_API_VERSION: .["identity_api_version"]} | to_entries[] | "\(.key)=\(.value)"')
envs+=(OS_SHELL=true "${osEnvs[@]}")
else
hasOSConfig=false
envs+=(OS_AUTH_URL="")
fi
if [[ "$hasOSConfig" == false ]] && [[ "$hasKubeconfig" == false ]]; then
echo "All secrets are missing!" >/dev/stderr
exit 1
fi
if [[ "$hasOSConfig" == false ]]; then
echo "OpenStack config missing, only setting KUBECONFIG" >/dev/stderr
fi
if [[ "$hasKubeconfig" == false ]]; then
echo "KUBECONFIG missing, only setting OpenStack env" >/dev/stderr
elif clusterEndpoint="$(kubectl -n "$namespace" get openstackcluster -l cluster.x-k8s.io/cluster-name="$name" -o yaml 2>/dev/null | yq -er '.items[0] | if .spec.apiServerLoadBalancer.allowedCIDRs then .spec.controlPlaneEndpoint | "\(.host) \(.port)" else null end')"; then
read -r apiServerIP apiServerPort <<< "$clusterEndpoint"
if [[ -v CAPO_SHELL_SSHUTTLE_HOST ]]; then
sshuttle -r "$CAPO_SHELL_SSHUTTLE_HOST" "$apiServerIP:$apiServerPort" &>/dev/null &
PROXY_PID=$!
fi
fi
env -u KUBECONFIG -u KUBECONFIG_CONTEXT -u PROXY_PID "${envs[@]}" "${@:-${SHELL:-/usr/bin/env bash}}"
|