summarylogtreecommitdiffstats
path: root/capo-shell
blob: 9fc00ca0099737884795f4d21c795bd1a4b346ce (plain)
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}}"