|
| 1 | +"""Initialize Docker secrets with Earthdata credentials for local development. |
| 2 | +
|
| 3 | +Run this once prior to running "docker compose up --build". There is no need |
| 4 | +to run it again unless your Earthdata credentials change. |
| 5 | +
|
| 6 | +Creates the following secret files within the ./secrets directory (git-ignored) |
| 7 | +for use with Docker: |
| 8 | +
|
| 9 | +- earthdata-username.txt |
| 10 | +- earthdata-password.txt |
| 11 | +
|
| 12 | +If necessary, creates the ./secrets directory with permissions 700, and creates |
| 13 | +files within it with permissions 600. |
| 14 | +
|
| 15 | +Attempts to populate each secret as follows, in descending order of precedence: |
| 16 | +
|
| 17 | +1. Looks for corresponding environment variable (EARTHDATA_USERNAME and |
| 18 | + EARTHDATA_PASSWORD). |
| 19 | +2. Loads from .env.secrets file, if exists. |
| 20 | +3. Loads from .env file, if exists. |
| 21 | +4. Loads from netrc file, if exists (defaults to ~/.netrc, but can set NETRC env var) |
| 22 | +5. Prompts user as last resort. |
| 23 | +
|
| 24 | +This will ALWAYS overwrite existing secret files. |
| 25 | +""" |
| 26 | + |
| 27 | +# /// script |
| 28 | +# requires-python = ">=3.12" |
| 29 | +# dependencies = [ |
| 30 | +# "python-dotenv>=1.2.1", |
| 31 | +# ] |
| 32 | +# /// |
| 33 | +import logging |
| 34 | +import netrc |
| 35 | +import os |
| 36 | +from getpass import getpass |
| 37 | +from pathlib import Path |
| 38 | +from typing import Mapping |
| 39 | + |
| 40 | +from dotenv import dotenv_values |
| 41 | + |
| 42 | +logging.basicConfig(level=logging.INFO) |
| 43 | + |
| 44 | + |
| 45 | +def read_env() -> Mapping[str, str]: |
| 46 | + """Read environment variables, env files, and netrc as unified 'environment'. |
| 47 | +
|
| 48 | + Returns a mapping of "environment variables", obtained from the following |
| 49 | + sources in descending order of precedence: |
| 50 | +
|
| 51 | + - Environment variables (highest precedence) |
| 52 | + - Variables from .env.secrets, if it exists |
| 53 | + - Variables from .env, if it exists |
| 54 | + - netrc file, if it exists (default location can be overridden via NETRC |
| 55 | + environment variable): if an entry is found for host urs.earthdata.nasa.gov, |
| 56 | + sets default values on returned mapping for EARTHDATA_USERNAME and |
| 57 | + EARTHDATA_PASSWORD (i.e., only visible if these are not already set by |
| 58 | + a source listed above, with higher precedence) |
| 59 | + """ |
| 60 | + env = { |
| 61 | + **dotenv_values(".env"), |
| 62 | + **dotenv_values(".env.secrets"), |
| 63 | + **os.environ, |
| 64 | + } |
| 65 | + |
| 66 | + if auth := netrc.netrc(env.get("NETRC")).authenticators("urs.earthdata.nasa.gov"): |
| 67 | + username, _, password = auth |
| 68 | + env.setdefault("EARTHDATA_USERNAME", username) |
| 69 | + env.setdefault("EARTHDATA_PASSWORD", password) |
| 70 | + |
| 71 | + return env |
| 72 | + |
| 73 | + |
| 74 | +def ensure_secrets_dir() -> Path: |
| 75 | + """Make sure the secrets directory exists with mode 0o700 (rwx for user only).""" |
| 76 | + path = Path("./secrets") |
| 77 | + path.mkdir(mode=0o700, parents=True, exist_ok=True) |
| 78 | + |
| 79 | + return path |
| 80 | + |
| 81 | + |
| 82 | +def to_kebab_case(name: str) -> str: |
| 83 | + """Convert variable name to kebab case. |
| 84 | +
|
| 85 | + Example: EARTHDATA_USERNAME becomes earthdata-username |
| 86 | + """ |
| 87 | + return name.lower().replace("_", "-") |
| 88 | + |
| 89 | + |
| 90 | +def ask_user(prompt: str, sensitive: bool) -> str: |
| 91 | + """Prompt user for a possibly sensitive value.""" |
| 92 | + return getpass(prompt) if sensitive else input(prompt) |
| 93 | + |
| 94 | + |
| 95 | +def write_secret(secrets_dir: Path, name: str, value: str) -> None: |
| 96 | + """Write a secret (with only rw perms for user) to the secrets directory.""" |
| 97 | + file = (secrets_dir / name).with_suffix(".txt") |
| 98 | + file.write_text(value) |
| 99 | + file.chmod(0o600) |
| 100 | + |
| 101 | + |
| 102 | +def main(): |
| 103 | + """Write Earthdata credentials as secrets for Docker. |
| 104 | +
|
| 105 | + Looks for EDL credentials in the environment, and if not found, prompts |
| 106 | + user to enter values, then writes them as secret files for Docker to use. |
| 107 | + """ |
| 108 | + env = read_env() |
| 109 | + secrets_dir = ensure_secrets_dir() |
| 110 | + |
| 111 | + descriptors = ( |
| 112 | + ("EARTHDATA_USERNAME", "Earthdata username: ", False), |
| 113 | + ("EARTHDATA_PASSWORD", "Earthdata password: ", True), |
| 114 | + ) |
| 115 | + |
| 116 | + for name, prompt, sensitive in descriptors: |
| 117 | + value = env.get(name) or ask_user(prompt, sensitive) |
| 118 | + write_secret(secrets_dir, to_kebab_case(name), value) |
| 119 | + |
| 120 | + |
| 121 | +if __name__ == "__main__": |
| 122 | + main() |
0 commit comments