Skip to content

Commit c81ec0a

Browse files
authored
feat: support security_token auth for local development (#504)
1 parent 46f624c commit c81ec0a

File tree

6 files changed

+714
-13
lines changed

6 files changed

+714
-13
lines changed

Tiltfile

Lines changed: 209 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ update_settings(k8s_upsert_timeout_secs = 60) # on first tilt up, often can tak
99
#Add tools to path
1010
os.putenv("PATH", os.getenv("PATH") + ":" + tools_bin)
1111

12-
keys = ["OCI_TENANCY_ID", "OCI_USER_ID", "OCI_CREDENTIALS_FINGERPRINT", "OCI_REGION", "OCI_CREDENTIALS_KEY_PATH"]
12+
legacy_auth_keys = ["OCI_TENANCY_ID", "OCI_USER_ID", "OCI_CREDENTIALS_FINGERPRINT", "OCI_REGION", "OCI_CREDENTIALS_KEY"]
13+
session_auth_keys = ["OCI_TENANCY_ID", "OCI_CREDENTIALS_FINGERPRINT", "OCI_REGION", "OCI_SESSION_TOKEN", "OCI_SESSION_PRIVATE_KEY"]
1314

1415
# set defaults
1516
settings = {
@@ -19,7 +20,7 @@ settings = {
1920
"deploy_cert_manager": True,
2021
"preload_images_for_kind": True,
2122
"kind_cluster_name": "capoci",
22-
"capi_version": "v1.10.7",
23+
"capi_version": "v1.11.0",
2324
"cert_manager_version": "v1.16.2",
2425
"kubernetes_version": "v1.30.0",
2526
}
@@ -66,10 +67,104 @@ def validate_auth():
6667
for sub in substitutions:
6768
if sub[-4:] == "_B64":
6869
os.environ[sub[:-4]] = base64_decode(os.environ[sub])
69-
print("{} was not specified in tilt-settings.json, attempting to load {}".format(base64_decode(os.environ[sub]), sub))
70-
missing = [k for k in keys if not os.environ.get(k)]
70+
print("loaded {} from {}".format(sub[:-4], sub))
71+
72+
if not os.environ.get("OCI_CREDENTIALS_KEY") and os.environ.get("OCI_CREDENTIALS_KEY_PATH"):
73+
os.environ["OCI_CREDENTIALS_KEY"] = read_file_from_path(os.environ.get("OCI_CREDENTIALS_KEY_PATH"))
74+
75+
use_instance_principal = str(os.environ.get("USE_INSTANCE_PRINCIPAL", "false")).strip().lower() == "true"
76+
use_session_token = str(os.environ.get("USE_SESSION_TOKEN", "false")).strip().lower() == "true"
77+
if not use_instance_principal and not use_session_token and settings.get("oci_session_profile"):
78+
# If a session profile is configured, default to session-token mode to avoid silent fallback to API-key auth.
79+
use_session_token = True
80+
os.environ["USE_SESSION_TOKEN"] = "true"
81+
print("oci_session_profile is set; defaulting auth mode to session-token")
82+
83+
if use_instance_principal:
84+
return "instance-principal"
85+
86+
if use_session_token:
87+
apply_session_profile_defaults()
88+
missing = [k for k in session_auth_keys if not os.environ.get(k)]
89+
if missing:
90+
fail("session-token auth selected but missing kustomize_substitutions values for {}".format(missing))
91+
return "session-token"
92+
93+
missing = [k for k in legacy_auth_keys if not os.environ.get(k)]
7194
if missing:
72-
fail("missing kustomize_substitutions keys {} in tilt-setting.json".format(missing))
95+
fail("legacy API-key auth selected but missing kustomize_substitutions values for {}. Set USE_SESSION_TOKEN_B64 or USE_INSTANCE_PRINCIPAL_B64 to switch modes.".format(missing))
96+
return "legacy-api-key"
97+
98+
def add_session_token_refresh_resource(auth_mode):
99+
if auth_mode != "session-token":
100+
return
101+
102+
session_profile = settings.get("oci_session_profile", "DEFAULT")
103+
token_path = expand_path(settings.get("oci_session_token_path"))
104+
private_key_path = expand_path(settings.get("oci_session_private_key_path"))
105+
if not token_path or not private_key_path:
106+
local_resource(
107+
"oci-session-refresh",
108+
cmd = "echo 'Set oci_session_token_path and oci_session_private_key_path in tilt-settings.json for session refresh.' >&2; exit 1",
109+
trigger_mode = TRIGGER_MODE_MANUAL,
110+
auto_init = False,
111+
labels = ["cluster-api"],
112+
)
113+
return
114+
115+
refresh_cmd = """set -euo pipefail
116+
if command -v oci >/dev/null 2>&1; then
117+
if ! oci session refresh --profile '{profile}'; then
118+
echo "warning: oci session refresh failed for profile '{profile}', continuing with local session files" >&2
119+
fi
120+
fi
121+
CAPOCI_NAMESPACE="$({kubectl_cmd} get deploy -A -o jsonpath='{{range .items[?(@.metadata.name==\"capoci-controller-manager\") ]}}{{.metadata.namespace}}{{end}}' 2>/dev/null || true)"
122+
if [ -z "$CAPOCI_NAMESPACE" ]; then
123+
CAPOCI_NAMESPACE="cluster-api-provider-oci-system"
124+
fi
125+
CAPOCI_AUTH_SECRET_NAME="$({kubectl_cmd} get deploy capoci-controller-manager -n "$CAPOCI_NAMESPACE" -o jsonpath='{{.spec.template.spec.volumes[?(@.name==\"auth-config-dir\")].secret.secretName}}' 2>/dev/null || true)"
126+
if [ -z "$CAPOCI_AUTH_SECRET_NAME" ]; then
127+
CAPOCI_AUTH_SECRET_NAME="capoci-auth-config"
128+
fi
129+
export CAPOCI_NAMESPACE
130+
export CAPOCI_AUTH_SECRET_NAME
131+
export USE_INSTANCE_PRINCIPAL_B64="ZmFsc2U="
132+
export USE_SESSION_TOKEN_B64="dHJ1ZQ=="
133+
export OCI_SESSION_TOKEN_B64="$(base64 < '{token_path}' | tr -d '\\n')"
134+
export OCI_SESSION_PRIVATE_KEY_B64="$(awk 'BEGIN{{in_key=0}}
135+
!in_key {{
136+
if (match($0, /-----BEGIN [A-Z ]*PRIVATE KEY-----/)) {{
137+
in_key=1
138+
}} else {{
139+
next
140+
}}
141+
}}
142+
{{
143+
line=$0
144+
if (match(line, /-----END [A-Z ]*PRIVATE KEY-----/)) {{
145+
print substr(line, 1, RSTART + RLENGTH - 1)
146+
exit
147+
}}
148+
print line
149+
}}' '{private_key_path}' | base64 | tr -d '\\n')"
150+
{envsubst_cmd} < config/default/credentials.yaml \
151+
| sed "s/^ name: auth-config$/ name: $CAPOCI_AUTH_SECRET_NAME/" \
152+
| sed "s/^ namespace: system$/ namespace: $CAPOCI_NAMESPACE/" \
153+
| {kubectl_cmd} apply -f -
154+
""".format(
155+
profile = session_profile,
156+
token_path = token_path,
157+
private_key_path = private_key_path,
158+
envsubst_cmd = envsubst_cmd,
159+
kubectl_cmd = kubectl_cmd,
160+
)
161+
local_resource(
162+
"oci-session-refresh",
163+
cmd = refresh_cmd,
164+
trigger_mode = TRIGGER_MODE_MANUAL,
165+
auto_init = True,
166+
labels = ["cluster-api"],
167+
)
73168

74169
# Users may define their own Tilt customizations in tilt.d. This directory is excluded from git and these files will
75170
# not be checked in to version control.
@@ -184,13 +279,118 @@ def base64_encode(to_encode):
184279
return str(encode_blob)
185280

186281
def base64_encode_file(path_to_encode):
187-
encode_blob = local("cat {} | tr -d '\n' | base64 - | tr -d '\n'".format(path_to_encode), quiet = True)
282+
encode_blob = local("base64 < {} | tr -d '\n'".format(shell_single_quote(path_to_encode)), quiet = True)
188283
return str(encode_blob)
189284

190285
def read_file_from_path(path_to_read):
191-
str_blob = local("cat {} | tr -d '\n'".format(path_to_read), quiet = True)
286+
str_blob = local("cat {}".format(shell_single_quote(path_to_read)), quiet = True)
192287
return str(str_blob)
193288

289+
def read_private_key_pem_from_path(path_to_read):
290+
awk_cmd = """awk 'BEGIN{{in_key=0}}
291+
!in_key {{
292+
if (match($0, /-----BEGIN [A-Z ]*PRIVATE KEY-----/)) {{
293+
in_key=1
294+
}} else {{
295+
next
296+
}}
297+
}}
298+
{{
299+
line=$0
300+
if (match(line, /-----END [A-Z ]*PRIVATE KEY-----/)) {{
301+
print substr(line, 1, RSTART + RLENGTH - 1)
302+
exit
303+
}}
304+
print line
305+
}}' {path}""".format(path = shell_single_quote(path_to_read))
306+
pem = str(local(awk_cmd, quiet = True, echo_off = True)).strip()
307+
if not pem:
308+
fail("no PEM private key block found in {}".format(path_to_read))
309+
return pem + "\n"
310+
311+
def shell_single_quote(s):
312+
return "'" + str(s).replace("'", "'\"'\"'") + "'"
313+
314+
def expand_path(path):
315+
if path == None:
316+
return ""
317+
path_str = str(path).strip()
318+
if path_str == "":
319+
return ""
320+
home = str(os.getenv("HOME", ""))
321+
if path_str[0:2] == "~/":
322+
return home + path_str[1:]
323+
return path_str.replace("$HOME", home).replace("${HOME}", home)
324+
325+
def read_oci_profile_value(profile, key):
326+
config_file = expand_path("~/.oci/config")
327+
if not config_file:
328+
return ""
329+
profile_name = str(profile).strip()
330+
key_name = str(key).strip()
331+
if not profile_name or not key_name:
332+
return ""
333+
awk_cmd = """awk '
334+
$0 == "[{profile}]" {{ in_profile=1; next }}
335+
/^\\[/ {{ in_profile=0 }}
336+
in_profile && $0 ~ /^[[:space:]]*{key}[[:space:]]*=/ {{
337+
line=$0
338+
sub(/^[[:space:]]*{key}[[:space:]]*=[[:space:]]*/, "", line)
339+
print line
340+
exit
341+
}}
342+
' {config_file}""".format(
343+
profile = profile_name,
344+
key = key_name,
345+
config_file = shell_single_quote(config_file),
346+
)
347+
return str(local(awk_cmd, quiet = True, echo_off = True)).strip()
348+
349+
def set_env_if_missing(name, value):
350+
if value and not str(os.environ.get(name, "")).strip():
351+
os.environ[name] = str(value).strip()
352+
353+
def ensure_env_matches_profile(name, profile_value, profile_name):
354+
env_val = str(os.environ.get(name, "")).strip()
355+
if not profile_value or not env_val:
356+
return
357+
if env_val != profile_value:
358+
fail("{} in kustomize_substitutions ({}) does not match {} profile {} value ({}).".format(name, env_val, profile_name, name.lower(), profile_value))
359+
360+
def apply_session_profile_defaults():
361+
profile = str(settings.get("oci_session_profile", "")).strip()
362+
if not profile:
363+
return
364+
365+
profile_tenancy = read_oci_profile_value(profile, "tenancy")
366+
profile_region = read_oci_profile_value(profile, "region")
367+
profile_fingerprint = read_oci_profile_value(profile, "fingerprint")
368+
profile_token_path = read_oci_profile_value(profile, "security_token_file")
369+
profile_key_path = read_oci_profile_value(profile, "key_file")
370+
371+
set_env_if_missing("OCI_TENANCY_ID", profile_tenancy)
372+
set_env_if_missing("OCI_REGION", profile_region)
373+
set_env_if_missing("OCI_CREDENTIALS_FINGERPRINT", profile_fingerprint)
374+
375+
ensure_env_matches_profile("OCI_TENANCY_ID", profile_tenancy, profile)
376+
ensure_env_matches_profile("OCI_REGION", profile_region, profile)
377+
ensure_env_matches_profile("OCI_CREDENTIALS_FINGERPRINT", profile_fingerprint, profile)
378+
379+
token_path = expand_path(settings.get("oci_session_token_path"))
380+
private_key_path = expand_path(settings.get("oci_session_private_key_path"))
381+
if not token_path:
382+
token_path = expand_path(profile_token_path)
383+
if not private_key_path:
384+
private_key_path = expand_path(profile_key_path)
385+
386+
if token_path and not os.environ.get("OCI_SESSION_TOKEN"):
387+
os.environ["OCI_SESSION_TOKEN"] = read_file_from_path(token_path)
388+
if os.environ.get("OCI_SESSION_TOKEN"):
389+
os.environ["OCI_SESSION_TOKEN_B64"] = base64_encode(os.environ["OCI_SESSION_TOKEN"])
390+
if private_key_path:
391+
os.environ["OCI_SESSION_PRIVATE_KEY"] = read_private_key_pem_from_path(private_key_path)
392+
os.environ["OCI_SESSION_PRIVATE_KEY_B64"] = base64_encode(os.environ["OCI_SESSION_PRIVATE_KEY"])
393+
194394
def base64_decode(to_decode):
195395
# Use -D for macOS (BSD), --decode - for Linux (GNU)
196396
# Detect OS using uname command
@@ -211,7 +411,8 @@ def kustomizesub(folder):
211411
# Actual work happens here
212412
##############################
213413

214-
validate_auth()
414+
auth_mode = validate_auth()
415+
add_session_token_refresh_resource(auth_mode)
215416

216417
include_user_tilt_files()
217418

0 commit comments

Comments
 (0)