diff --git a/.scripts/bk b/.scripts/bk index 94c016f..e25a530 100755 --- a/.scripts/bk +++ b/.scripts/bk @@ -1,195 +1,457 @@ #!/bin/bash -# This script initializes the Data & AI Bootkon environment in Cloud Shell. -# It clones the Bootkon repository (or uses an existing one), sets up necessary -# environment variables, installs required packages, renders the tutorial markdown file, -# and finally opens the tutorial in Cloud Shell. It also adds helpful aliases and -# configurations to the .bashrc file for easier future access and customization. +# This script initializes the Bootkon environment in Cloud Shell. +# It clones the Bootkon repository (or uses an existing one), sets up the +# environment variables, installs required packages, renders the tutorial +# markdown file, and finally opens the tutorial in Cloud Shell. +# +# All per-participant state lives in a single git-ignored file, +# $BK_DIR/vars.local.sh (created from vars.sh on first run). A minimal guarded +# block in ~/.bashrc sources that file so every new terminal picks up the +# environment automatically -- no need to re-run this script per terminal. +# +# `. bk` has two modes: +# * First run (not yet initialized): clone, create vars.local.sh, autodetect +# PROJECT_ID/GCP_USERNAME, render and open the tutorial. +# * Re-run (already initialized): a fast reload -- re-source vars.local.sh, +# fill still-empty config from the environment, refresh PROJECT_NUMBER and +# the active gcloud project. Handy after editing vars.local.sh. A reload +# never changes the caller's working directory. # # Author: Fabian Hirschmann # # Usage: # From GitHub (replace ): -# BK_REPO=; . <(wget -qO- https://raw.githubusercontent.com//main/.scripts/bk) +# BK_STREAM= BK_REPO=; . <(wget -qO- https://raw.githubusercontent.com//main/.scripts/bk) # Locally: -# BK_REPO= . bk +# BK_STREAM= BK_REPO= . bk # Defaults, repo as argument: -# . bk -# -# Skip workspace opening (add to ~/.bashrc above bk): +# BK_STREAM= . bk # # Environment variables: -# BK_REPO: GitHub repository (). +# BK_REPO: GitHub repository (). +# BK_STREAM: stream to start (a directory under content/). +# BK_DIR: checkout location (defaults to ~/bootkon). + +# The whole body lives in _bk_main, called once at the very bottom. The launch +# streams this file via `. <(wget ...)`: with a plain script body, a connection +# drop would execute everything up to the break (worst case inside the +# ~/.bashrc heredoc, appending a block without its end marker). Wrapped in a +# function, a truncated download is a parse error that executes NOTHING. It +# also lets internal working variables be `local` instead of leaking into the +# participant's shell on early returns. The body is deliberately NOT indented, +# to keep diffs and history readable. +_bk_main() { -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[0;33m' -BLUE='\033[0;34m' -MAGENTA='\033[0;35m' -CYAN='\033[0;36m' -NC='\033[0m' # No Color +local RED='\033[0;31m' +local GREEN='\033[0;32m' +local YELLOW='\033[0;33m' +local BLUE='\033[0;34m' +local MAGENTA='\033[0;35m' +local CYAN='\033[0;36m' +local NC='\033[0m' # No Color err() { echo -e "${RED}Error: $1${NC}" >&2 } -echo -e "${YELLOW}Running bootkon init script $(readlink -f ${BASH_SOURCE[0]})...${NC}" +# Internal narration (paths, sourcing, PATH bookkeeping) is hidden unless +# BK_DEBUG is set: a /proc/self/fd/... pipe path must not be the first thing +# a first-day participant ever sees in a terminal. +dbg() { + if [ -n "${BK_DEBUG:-}" ]; then echo -e "$1"; fi +} + +dbg "${YELLOW}Running bootkon init script $(readlink -f ${BASH_SOURCE[0]})...${NC}" -if [ -z $CLOUD_SHELL ]; then - err 'Please run this script in Cloud Shell.' +if [ -z "$CLOUD_SHELL" ]; then + err 'bootkon runs inside Google Cloud Shell.' + err 'Open https://shell.cloud.google.com and paste the setup command there.' return 1 fi if [ "${BASH_SOURCE[0]}" == "$0" ]; then - err 'Script is not sourced. Please source it.' - err 'Example: BK_REPO=GoogleCloudPlatform/bootkon . bk' + err 'This command must start with a dot and a space -- the dot loads bootkon' + err 'into your current terminal. Example:' + err ' . bk' exit 1 fi -if [ -z "$1" ]; then - if [ -z $BK_REPO ]; then - err 'Variable BK_REPO is not set. Please set it to a GitHub username and repository' - err 'Example: BK_REPO=GoogleCloudPlatform/bootkon . bk' - return 1 - fi -else - export BK_REPO=$1 +if [ -n "$1" ]; then + export BK_REPO="$1" echo -e "Setting BK_REPO to $BK_REPO based on first argument to this script." fi -if [ -z $BK_STREAM ]; then +if [ -z "$BK_REPO" ]; then + err 'Variable BK_REPO is not set. Please set it to a GitHub username and repository' + err 'Example: BK_STREAM=data BK_REPO=GoogleCloudPlatform/bootkon . bk' + return 1 +fi + +if [ -z "$BK_STREAM" ]; then err 'Variable BK_STREAM is not set. Please set it to the stream you want to start' + err 'Example: BK_STREAM=data BK_REPO=GoogleCloudPlatform/bootkon . bk' + return 1 fi -export BK_GITHUB_USERNAME=$(echo $BK_REPO | cut -d/ -f1) # first part of GoogleCloudPlatform/bootkon -export BK_GITHUB_REPOSITORY=$(echo $BK_REPO | cut -d/ -f2) # second part of GoogleCloudPlatform/bootkon +# Remember the explicitly requested repo/stream (env or argument) so they can +# win over a stale persisted value further below. +local BK_REQ_REPO="$BK_REPO" +local BK_REQ_STREAM="$BK_STREAM" +local BK_REQ_MY_NAME="${MY_NAME:-}" # optionally provided in the launch command; captured before sourcing overwrites it + +export BK_REPO="$BK_REPO" +export BK_STREAM="$BK_STREAM" export BK_REPO_URL="https://github.com/${BK_REPO}.git" -export BK_STREAM="${BK_STREAM}" -export BK_DIR=~/${BK_GITHUB_REPOSITORY} -export BK_INITIALIZED=1 +# Always clone to a fixed location so the ~/.bashrc block is static. Override +# BK_DIR (e.g. contributors juggling several checkouts) is honored. +export BK_DIR="${BK_DIR:-$HOME/bootkon}" + +# A Google Cloud project must be selected in the Cloud Shell project picker before +# we do anything -- every lab needs it and PROJECT_ID is derived live from it. +# Fail fast HERE, before cloning or opening the editor, instead of setting up a +# project-less dead end. (DEVSHELL_PROJECT_ID / GOOGLE_CLOUD_PROJECT are empty when +# no project is selected; Cloud Shell refreshes them on every prompt.) +export PROJECT_ID="${DEVSHELL_PROJECT_ID:-${GOOGLE_CLOUD_PROJECT:-$(gcloud config get-value project 2>/dev/null || true)}}" +[ "$PROJECT_ID" = "(unset)" ] && PROJECT_ID="" +if [ -z "$PROJECT_ID" ]; then + err 'No Google Cloud project is selected.' + err 'Pick your event project in the project selector at the top of the Cloud' + err 'Shell window, then paste the setup command again.' + return 1 +fi -cd ~/ +# ── Interactive first-run onboarding ────────────────────────────────────── +# On the very first launch (no vars.local.sh yet) the robot greets UP FRONT +# and asks for the participant's name, instead of silently running the whole +# setup and dropping a banner at the end. Because the name is collected here, +# the launch command needs no MY_NAME= edit anymore -- a single, edit-free +# paste, which kills the whole editing-inside-a-quoted-string trap (deleting +# a quote left novices at a bare dquote> prompt as their first terminal +# experience). Guards: +# * first run only -- never on `. bk` reloads or in new terminals; +# * interactive only ([ -t 0 ]: sourcing `. <(wget ...)` keeps stdin on +# the tty, unlike `wget | bash`, so read works on the launch path); +# * MY_NAME=... in the launch env still short-circuits the prompt, so +# organizer/scripted/headless flows are unaffected. +local BK_FIRST_RUN="" +if [ ! -f "$BK_DIR/vars.local.sh" ]; then + BK_FIRST_RUN=1 + echo + echo -e " __ ---------------------------------------" + echo -e " _(\ |${RED}@@${NC}| | |" + echo -e "(__/\__ \--/ __ | \e[34mW\e[31me\e[33ml\e[32mc\e[34mo\e[31mm\e[33me \e[32mt\e[34mo \e[32mB\e[34mo\e[31mo\e[33mt\e[32mk\e[34mo\e[31mn\e[0m! |" + echo -e " \___|----| | __ | |" + echo -e " \ }{ /\ )_ / _\ ---------------------------------------" + echo -e " /\__/\ \__O (__" + echo -e " (--/\--) \__/ Setting you up in project ${YELLOW}${PROJECT_ID}${NC}." + echo -e " _)( )(_ (Wrong project? Switch it in the selector at the" + echo -e ' `---''---`'" top of this window and paste the command again.)" + echo + if [ -z "$BK_REQ_MY_NAME" ] && [ -t 0 ]; then + read -r -p "What should I call you? (first name, or press Enter to skip) " BK_REQ_MY_NAME + fi +fi +# Whether prompted or passed via the launch env: strip characters that must +# never reach vars.local.sh or the greeting (bk-set-var rejects newlines +# outright; stray quotes would just look broken in the rendered markdown). +BK_REQ_MY_NAME="$(printf '%s' "$BK_REQ_MY_NAME" | tr -d "\r\n'\"")" +# Cloud Shell ships git; if it is ever missing something is deeply wrong with +# the environment -- do not greet a novice with a sudo prompt and apt noise. if ! command -v git &> /dev/null; then - sudo apt update - sudo apt install -y git + err 'git is not available in this environment. Please open a fresh Cloud Shell' + err 'at https://shell.cloud.google.com and paste the setup command again.' + return 1 fi -if [ -d $BK_GITHUB_REPOSITORY ]; then - echo -e "${GREEN}Not cloning $BK_REPO_URL because folder ~/$BK_GITHUB_REPOSITORY already exists.${NC}" +# Reuse a healthy checkout, reject a broken one (an interrupted first clone +# used to wedge the participant permanently: the .git test passed, the copy of +# vars.sh failed, and every re-paste printed a reassuring green message), or +# clone fresh -- after a one-second remote probe that catches repo/branch/ +# stream typos BEFORE any clone happens or any workspace opens. +BK_BRANCH="${BK_BRANCH:-main}" # defaults to main; can be overwritten +if [ -d "$BK_DIR/.git" ] && [ -f "$BK_DIR/vars.sh" ]; then + # Re-pasting the launch command is the documented way to pick up mid-event + # content fixes, so on that path -- and only there; plain `. bk` reloads + # stay offline, instant and quiet -- fast-forward a clean tree. + if [ "$(basename "${BASH_SOURCE[0]}")" != "bk" ]; then + echo -e "${GREEN}Using the existing checkout in $BK_DIR.${NC}" + if [ -n "$(git -C "$BK_DIR" status --porcelain 2>/dev/null)" ]; then + echo -e "${YELLOW}Not updating it: you have local changes (to update: git -C $BK_DIR pull).${NC}" + elif ! git -C "$BK_DIR" pull --ff-only --quiet 2>/dev/null; then + echo -e "${YELLOW}Could not update it; continuing with the current files.${NC}" + fi + fi +elif [ -e "$BK_DIR" ]; then + err "$BK_DIR exists but does not look like a working bootkon checkout" + err "(usually a previous setup was interrupted). To start over, run:" + err " rm -rf $BK_DIR" + err "and then paste the setup command again." + return 1 else - BK_BRANCH="${BK_BRANCH:-main}" # defaults to main; can be overwritten - echo -e "Cloning branch $BK_BRANCH from $BK_REPO_URL into $BK_GITHUB_REPOSITORY..." - git clone --branch "$BK_BRANCH" https://github.com/${BK_REPO}.git + # One HEAD request against the raw stream tutorial validates BK_REPO, + # BK_BRANCH and BK_STREAM at once. (--spider returns non-zero on a 404; + # curl -fsIL is the fallback in case the wget build GETs instead.) + local _probe="https://raw.githubusercontent.com/${BK_REPO}/${BK_BRANCH}/content/${BK_STREAM}/TUTORIAL.md" + if ! wget -q --spider "$_probe" 2>/dev/null && ! curl -fsIL "$_probe" >/dev/null 2>&1; then + err "Could not find stream '${BK_STREAM}' on ${BK_REPO}@${BK_BRANCH} (or the network is down)." + err "Check BK_STREAM / BK_REPO / BK_BRANCH in the command you pasted, then paste it again." + err "(Tried: $_probe)" + return 1 + fi + echo -e "Cloning branch $BK_BRANCH from $BK_REPO_URL into $BK_DIR..." + if ! git clone --branch "$BK_BRANCH" "$BK_REPO_URL" "$BK_DIR"; then + err "Cloning $BK_REPO_URL failed -- see the git message above." + err "Usually this is a network hiccup: paste the setup command again." + err "If $BK_DIR was partially created, remove it first with: rm -rf $BK_DIR" + return 1 + fi fi -cd $BK_GITHUB_REPOSITORY +# NOTE: this script never cd's the caller's shell mid-run -- it is sourced, so +# a cd here would teleport the participant out of their lab directory on every +# reload and every error path. Everything below works from absolute paths; only +# a successful FIRST run ends with a cd to the repo root (see the bottom). +if [ ! -d "$BK_DIR/content/${BK_STREAM}" ]; then + err "Stream '${BK_STREAM}' does not exist (no content/${BK_STREAM} directory)." + err "Available streams: $(cd "$BK_DIR/content" && ls -d */ | tr -d '/' | grep -vE '^(common|contributing|example)$' | tr '\n' ' ')" + err "To fix this, correct BK_STREAM in the command you pasted, then paste it again." + return 1 +fi -NEW_PATH=~/${BK_GITHUB_REPOSITORY}/.scripts -PATH_EXPORT_LINE="export PATH=\${HOME}/${BK_GITHUB_REPOSITORY}/.scripts:\$PATH" +# Add the scripts directory to the current session's PATH (idempotent). The +# permanent export lives in the ~/.bashrc block written further below. +local BK_SCRIPTS="$BK_DIR/.scripts" +case ":$PATH:" in + *":$BK_SCRIPTS:"*) ;; + *) dbg "${MAGENTA}Adding $BK_SCRIPTS to your current session's PATH${NC}" + export PATH="$BK_SCRIPTS:$PATH" ;; +esac -# 1. Add to current session's PATH if missing -if [[ ":$PATH:" != *":$NEW_PATH:"* ]]; then - echo -e "${MAGENTA}Adding $NEW_PATH to your current session's PATH${NC}" - export PATH=${NEW_PATH}:$PATH -else - echo -e "${GREEN}Your current session's PATH already contains $NEW_PATH. Not adding it again.${NC}" +local LOCAL="$BK_DIR/vars.local.sh" +local BK_SET_VAR="$BK_SCRIPTS/bk-set-var" +local BK_REMOVE_BLOCK="$BK_SCRIPTS/bk-remove-block" + +# vars.local.sh is the single per-participant config + state file. Seed it from +# the checked-in template on first run; it is git-ignored, never pushed, and +# holds generated secrets, so keep it private. +if [ ! -f "$LOCAL" ]; then + echo -e "Creating your personal config file: $LOCAL" + cp "$BK_DIR/vars.sh" "$LOCAL" + chmod 600 "$LOCAL" fi -# 2. Persist the PATH setting in ~/.bashrc if not already there -if ! grep -qF "$PATH_EXPORT_LINE" ~/.bashrc ; then - echo -e "${MAGENTA}Adding $NEW_PATH to ~/.bashrc for future sessions.${NC}" - # Use '>>' to append the line to the file - echo "$PATH_EXPORT_LINE" >> ~/.bashrc -else - echo -e "${GREEN}The permanent PATH export for $NEW_PATH is already in ~/.bashrc. Skipping.${NC}" +# The tutorial explicitly sends beginners into vars.local.sh; one lost quote +# would otherwise surface as a raw 'unexpected EOF' in every new terminal, +# with everything below the broken line (stream, secrets) silently unloaded +# and every follow-up error pointing away from the cause. Check the file and +# name the actual problem before sourcing it. (The ~/.bashrc block written +# below carries the same guard.) +if ! bash -n "$LOCAL" 2>/dev/null; then + err "Your configuration file has a syntax error (usually a missing quote):" + err " $(bash -n "$LOCAL" 2>&1 | head -n 1)" + err "Open $LOCAL in the editor, fix that line, save it, then run '. bk' again." + return 1 fi -unset NEW_PATH -unset PATH_EXPORT_LINE +# Migrate older vars.local.sh files: drop lines bk no longer persists -- +# BK_INITIALIZED (a non-empty PROJECT_NUMBER is the "environment is ready" +# signal now) and the three repo values derived fresh from BK_REPO on every +# run. Besides burying the two lines participants actually edit, a stale +# persisted copy would clobber the freshly derived value when the file is +# re-sourced later in this script. +if grep -qE '^export (BK_INITIALIZED|BK_GITHUB_USERNAME|BK_GITHUB_REPOSITORY|BK_REPO_URL)=' "$LOCAL" 2>/dev/null; then + sed -i -E -e '/^# Set to 1 once bk validated the project/d' \ + -e '/^# GitHub org\/user \(first half of BK_REPO\)/d' \ + -e '/^# GitHub repo name \(second half of BK_REPO\)/d' \ + -e '/^# Clone URL derived from BK_REPO/d' \ + -e '/^export (BK_INITIALIZED|BK_GITHUB_USERNAME|BK_GITHUB_REPOSITORY|BK_REPO_URL)=/d' "$LOCAL" +fi -echo -e "Sourcing $(readlink -f $BK_DIR/vars.sh)" -source $BK_DIR/vars.sh +dbg "Sourcing $(readlink -f "$BK_DIR/vars.sh")" +source "$BK_DIR/vars.sh" +dbg "Sourcing $(readlink -f "$LOCAL")" +source "$LOCAL" -if [ -f $BK_DIR/vars.local.sh ]; then - echo -e "Sourcing $(readlink -f $BK_DIR/vars.local.sh)" - source $BK_DIR/vars.local.sh +# GCP_USERNAME: autodetect once, filling only if still empty (a manual edit wins). +local detected_account="$(gcloud config get-value account 2>/dev/null || true)" +[ "$detected_account" = "(unset)" ] && detected_account="" +if [ -z "$GCP_USERNAME" ] && [ -n "$detected_account" ]; then + "$BK_SET_VAR" "$LOCAL" GCP_USERNAME "$detected_account" fi +# Fill MY_NAME from the launch command if it was provided and not already set +# (respects a later manual edit; the comment from vars.sh stays on replace). +if [ -z "$MY_NAME" ] && [ -n "$BK_REQ_MY_NAME" ]; then + "$BK_SET_VAR" "$LOCAL" MY_NAME "$BK_REQ_MY_NAME" +fi +# Reload so this shell sees any freshly detected values. +source "$LOCAL" -echo -e "Variables from $BK_DIR/vars.sh: PROJECT_ID=${YELLOW}$PROJECT_ID${NC} GCP_USERNAME=${YELLOW}$GCP_USERNAME${NC} REGION=${YELLOW}$REGION${NC}" - +# Re-assert the live PROJECT_ID after the source: an older vars.local.sh may still +# carry a persisted PROJECT_ID line that would otherwise clobber the value we +# derived (and validated) up front. It stays live from the picker -- never written +# to vars.local.sh, nothing to edit, nothing to go stale. +PROJECT_ID="${DEVSHELL_PROJECT_ID:-${GOOGLE_CLOUD_PROJECT:-$(gcloud config get-value project 2>/dev/null || true)}}" +[ "$PROJECT_ID" = "(unset)" ] && PROJECT_ID="" +export PROJECT_ID -if [ -z $PROJECT_ID ]; then - echo -e "The variable PROJECT_ID is empty. Not setting the default Google Cloud project." -else - gcloud projects get-iam-policy $PROJECT_ID --quiet 2>>/dev/null 1>>/dev/null - if [ $? -ne 0 ]; then - echo -en "${RED}ERROR: Unable to get iam policy for project $PROJECT_ID. " - echo -e "Are you sure you set your PROJECT_ID in vars.sh correctly?${NC}" - return 1 +# Derive the repo identity AFTER the last source (so the persisted values can't +# clobber it) and from the checkout's own origin remote, so BK_REPO and the +# rendered image links always match the files actually on disk -- even if +# vars.local.sh is stale or a fork was requested against an existing checkout. +local _origin="$(git -C "$BK_DIR" remote get-url origin 2>/dev/null || true)" +local _actual="$(printf '%s' "$_origin" | sed -E 's#^git@github\.com:##; s#^https?://github\.com/##; s#\.git$##')" +if [ -n "$_actual" ]; then + if [ "$_actual" != "$BK_REQ_REPO" ]; then + echo -e "${YELLOW}Note: $BK_DIR is a checkout of $_actual, not $BK_REQ_REPO.${NC}" + echo -e "${YELLOW}Using $_actual; set BK_DIR to another path (or remove $BK_DIR) to use $BK_REQ_REPO.${NC}" fi + BK_REPO="$_actual" +else + BK_REPO="$BK_REQ_REPO" +fi +export BK_REPO +export BK_GITHUB_USERNAME="$(echo "$BK_REPO" | cut -d/ -f1)" +export BK_GITHUB_REPOSITORY="$(echo "$BK_REPO" | cut -d/ -f2)" +export BK_REPO_URL="https://github.com/${BK_REPO}.git" +# The explicitly requested stream wins over any persisted value (stream switch). +export BK_STREAM="$BK_REQ_STREAM" - gcloud config set project $PROJECT_ID --quiet 2>>/dev/null - - export PROJECT_NUMBER=$(gcloud projects describe "$PROJECT_ID" --format="value(projectNumber)") - echo -e "Project for gcloud CLI has been set: PROJECT_ID=${YELLOW}${PROJECT_ID}${NC} PROJECT_NUMBER=${YELLOW}${PROJECT_NUMBER}${NC}." +# BK_BRANCH is derived from the checkout. Fall back cleanly for a detached HEAD +# ('HEAD') or a non-git directory (empty), which are not valid clone targets. +local _branch="$(git -C "$BK_DIR" rev-parse --abbrev-ref HEAD 2>/dev/null || true)" +if [ -z "$_branch" ] || [ "$_branch" = "HEAD" ]; then + _branch="${BK_BRANCH:-main}" fi +export BK_BRANCH="$_branch" + +# Persist the managed variables and install the ~/.bashrc block BEFORE the +# project is validated below: even if validation fails (expired auth, wrong +# project) a freshly opened terminal must still have PATH and the seed vars. +"$BK_SET_VAR" --comment "Selected stream; picks content//" "$LOCAL" BK_STREAM "$BK_STREAM" +"$BK_SET_VAR" --comment "GitHub repo the labs are served from (user/repo)" "$LOCAL" BK_REPO "$BK_REPO" +"$BK_SET_VAR" --comment "Checkout branch; used for rendered image links" "$LOCAL" BK_BRANCH "$BK_BRANCH" +# BK_GITHUB_USERNAME / BK_GITHUB_REPOSITORY / BK_REPO_URL are NOT persisted: +# they are recomputed from BK_REPO above on every run and have no consumer +# outside this session -- persisting them only buried the lines participants +# actually care about. -## Set or update $BK_REPO in ~/.bashrc -line="export BK_REPO=${BK_REPO}" -if grep -q '^export BK_REPO=' ~/.bashrc; then - # If the line exists but differs, update it - if ! grep -Fxq "$line" ~/.bashrc; then - sed -i "s|^export BK_REPO=.*|$line|" ~/.bashrc - echo "Updated the existing BK_REPO line in ~/.bashrc." +# Install/refresh a single guarded block in ~/.bashrc so every new terminal +# loads the environment by sourcing vars.local.sh. bk-remove-block strips any +# existing block robustly; then we append a fresh one. +local BASHRC=~/.bashrc +"$BK_REMOVE_BLOCK" "$BASHRC" 2>/dev/null || true +cat >> "$BASHRC" <>> bootkon >>> +# Managed by bk (.scripts/bk). Loads the bootkon environment in every new +# terminal. Edit values in \$BK_DIR/vars.local.sh, not here. +export BK_DIR="$BK_DIR" +case ":\$PATH:" in *":\$BK_DIR/.scripts:"*) ;; *) export PATH="\$BK_DIR/.scripts:\$PATH" ;; esac +if [ -f "\$BK_DIR/vars.local.sh" ]; then + if bash -n "\$BK_DIR/vars.local.sh" 2>/dev/null; then + source "\$BK_DIR/vars.local.sh" + else + echo "bootkon: vars.local.sh has a syntax error (usually a missing quote) --" >&2 + echo "bootkon: open it in the editor, fix the marked line, then run: . bk" >&2 fi -else - echo "Adding BK_REPO to .bashrc" - echo "$line" >> ~/.bashrc fi +export PROJECT_ID="\${DEVSHELL_PROJECT_ID:-\$GOOGLE_CLOUD_PROJECT}" # live from Cloud Shell +# <<< bootkon <<< +EOF -## Set or update $BK_STREAM in ~/.bashrc -line="export BK_STREAM=${BK_STREAM}" -if grep -q '^export BK_STREAM=' ~/.bashrc; then - # If the line exists but differs, update it - if ! grep -Fxq "$line" ~/.bashrc; then - sed -i "s|^export BK_STREAM=.*|$line|" ~/.bashrc - echo "Updated the existing BK_STREAM line in ~/.bashrc." +# The project is guaranteed selected (checked before the clone). Validate it now +# -- this also triggers the "Authorize Cloud Shell" popup at setup rather than +# mid-lab. gcloud already points at the picker's project, so no `config set`. +# On failure, disambiguate the cause (auth vs. wrong project) so the +# participant gets exactly ONE instruction -- and always say how to get the +# tutorial afterwards, because on this path it was never opened. +if ! gcloud projects get-iam-policy "$PROJECT_ID" --quiet >/dev/null 2>&1; then + if ! gcloud auth print-access-token >/dev/null 2>&1; then + err "Your Cloud Shell session is not authorized (this happens after clicking" + err "Reject on the authorization popup, or after a longer break)." + err "Run '. bk' again and click 'Authorize' this time." + else + err "Your account has no access to project $PROJECT_ID." + err "Pick your event project in the project selector at the top of the" + err "Cloud Shell window, then run '. bk' again." fi -else - echo "Adding BK_STREAM=$BK_STREAM to .bashrc" - echo "$line" >> ~/.bashrc + err "Afterwards, type bk-start to open the tutorial." + return 1 fi +# agy and the google-genai/Vertex SDKs read GOOGLE_CLOUD_PROJECT, and the +# agenticdata stream deliberately does not pin it. Cloud Shell refreshes the +# variable on every new prompt, but THIS shell still holds the pre-switch +# value right after a project change -- align it with the project we just +# validated so SDK calls in this very session hit the right project. +if [ -n "${GOOGLE_CLOUD_PROJECT:-}" ] && [ "$GOOGLE_CLOUD_PROJECT" != "$PROJECT_ID" ]; then + export GOOGLE_CLOUD_PROJECT="$PROJECT_ID" +fi -## Set or update $BK_BRANCH in ~/.bashrc -export BK_BRANCH=$(git rev-parse --abbrev-ref HEAD) -line="export BK_BRANCH=${BK_BRANCH}" -if grep -q '^export BK_BRANCH=' ~/.bashrc; then - # If the line exists but differs, update it - if ! grep -Fxq "$line" ~/.bashrc; then - sed -i "s|^export BK_BRANCH=.*|$line|" ~/.bashrc - echo "Updated the existing BK_BRANCH line in ~/.bashrc." +# PROJECT_NUMBER is the only project value NOT available from the environment, so +# cache it in vars.local.sh. Recompute only when missing or when the live project +# changed; every new terminal then reads it with no network call. +if [ -z "$PROJECT_NUMBER" ] || [ "$PROJECT_ID" != "${BK_PROJECT_NUMBER_OF:-}" ]; then + PROJECT_NUMBER="$(gcloud projects describe "$PROJECT_ID" --format='value(projectNumber)')" + if [ -z "$PROJECT_NUMBER" ]; then + err "Could not read the project number for $PROJECT_ID — please run '. bk' again." + return 1 fi -else - echo "Adding BK_BRANCH=$BK_BRANCH to .bashrc" - echo "$line" >> ~/.bashrc + "$BK_SET_VAR" --comment "Numeric project number, cached for the current project" "$LOCAL" PROJECT_NUMBER "$PROJECT_NUMBER" + "$BK_SET_VAR" --comment "Internal: the project the cached PROJECT_NUMBER belongs to" "$LOCAL" BK_PROJECT_NUMBER_OF "$PROJECT_ID" fi +export PROJECT_NUMBER -unset line +# There is deliberately no separate "initialized" flag: the PROJECT_NUMBER +# export/persist above happens only on this validated path, so bk-start and +# the stream bootstraps guard on a non-empty PROJECT_NUMBER -- the exact +# precondition they consume. A failed or aborted setup never sets it. -echo -echo -e " __ ---------------------------------------" -echo -e " _(\ |${RED}@@${NC}| | |" -echo -e "(__/\__ \--/ __ | \e[34mW\e[31me\e[33ml\e[32mc\e[34mo\e[31mm\e[33me \e[32mt\e[34mo \e[32mB\e[34mo\e[31mo\e[33mt\e[32mk\e[34mo\e[31mn\e[0m! |" -echo -e " \___|----| | __ | |" -echo -e " \ }{ /\ )_ / _\ ---------------------------------------" -echo -e " /\__/\ \__O (__" -echo -e " (--/\--) \__/ To start ${BK_STREAM} Bootkon, type ${GREEN}\e[1mbk-start\e[0m${NC}." -echo -e " _)( )(_ This will open the tutorial and IDE workspace." -echo -e ' `---''---`' -echo +# Optional per-stream init hook: content/$BK_STREAM/bk-init. Runs on every `. bk` +# once the project is validated -- and, crucially, BEFORE the first tutorial +# render, so stream secrets already exist when the labs are rendered. Contract: +# idempotent (write via `bk-set-var --if-empty`), local-only and instant (no +# gcloud calls); heavy, visible GCP work belongs in the participant-run +# bk-bootstrap instead. Streams without a hook simply skip this. +if [ -x "$BK_DIR/content/$BK_STREAM/bk-init" ]; then + if ! "$BK_DIR/content/$BK_STREAM/bk-init"; then + err "The $BK_STREAM stream's init script (content/$BK_STREAM/bk-init) failed." + err "Please run '. bk' again; if it keeps failing, ask the event staff." + return 1 + fi + # Load what the hook just wrote (e.g. generated passwords) into this shell, + # so the upcoming render and any lab command here already see them. + source "$LOCAL" +fi + +if [ -n "$BK_FIRST_RUN" ]; then + # The robot already greeted up front; close the first run with the facts + # and a path-aware call to action (on the wget path bk-start auto-runs). + echo -e "Project: ${YELLOW}$PROJECT_ID${NC} (number $PROJECT_NUMBER) · Account: ${YELLOW}$GCP_USERNAME${NC} · Stream: ${YELLOW}$BK_STREAM${NC}" + if [ "$(basename "${BASH_SOURCE[0]}")" != "bk" ]; then + echo -e "${GREEN}All set${MY_NAME:+, $MY_NAME}!${NC} Opening the ${BK_STREAM} tutorial and IDE workspace now." + echo -e "If the tutorial ever closes, type ${GREEN}bk-start${NC} to reopen it." + else + echo -e "${GREEN}All set${MY_NAME:+, $MY_NAME}!${NC} Type ${GREEN}bk-start${NC} to open the ${BK_STREAM} tutorial and IDE workspace." + fi + # A first run ends at the repo root: that is where the labs' relative + # commands are meant to run from. + cd "$BK_DIR" +else + echo -e "${GREEN}Reloaded bootkon${NC}: project ${YELLOW}$PROJECT_ID${NC} · account ${YELLOW}$GCP_USERNAME${NC} · stream ${YELLOW}$BK_STREAM${NC}@${BK_BRANCH} — tutorial closed? Type ${GREEN}bk-start${NC}." +fi if [ "$(basename ${BASH_SOURCE[0]})" != "bk" ]; then # This script is run the first time from GitHub bk-start -fi +fi + +} + +_bk_main "$@" +_bk_rc=$? +unset -f _bk_main err dbg +# Propagate the status: `return` works in the sourced case (the normal one); +# if someone executes the file instead, return fails silently and exit runs. +return $_bk_rc 2>/dev/null || exit $_bk_rc diff --git a/.scripts/bk-deactivate b/.scripts/bk-deactivate index 91cb7d6..c34556a 100755 --- a/.scripts/bk-deactivate +++ b/.scripts/bk-deactivate @@ -1,13 +1,25 @@ #!/bin/bash +# Removes the bootkon block from ~/.bashrc so new terminals no longer load the +# environment automatically. vars.local.sh and the checkout are left untouched; +# re-run `. bk` to reactivate. -line='if [ -f ${BK_INIT_SCRIPT} ]; then source ${BK_INIT_SCRIPT}; fi' +BASHRC=~/.bashrc +START='# >>> bootkon >>>' +REMOVE="${BK_DIR:-$HOME/bootkon}/.scripts/bk-remove-block" -# Check if the line exists in the file -if grep -qF "$line" ~/.bashrc; then - sed -i "/$(echo "$line" | sed 's/[]\\/.*^$[]/\\&/g')/d" ~/.bashrc - echo "SUCCESS: The following line has been removed from ~/.bashrc:" -else - echo "ERROR: The following line has not been found in ~/.bashrc:" +if [ ! -f "$BASHRC" ] || ! grep -qF "$START" "$BASHRC"; then + echo "No bootkon block found in ~/.bashrc; nothing to do." + exit 0 fi -echo $line \ No newline at end of file +"$REMOVE" "$BASHRC" + +if grep -qF "$START" "$BASHRC"; then + echo "ERROR: could not cleanly remove the bootkon block from ~/.bashrc" >&2 + echo "(the block may be missing its end marker). Please check the file by hand." >&2 + exit 1 +fi + +echo "Removed the bootkon block from ~/.bashrc." +echo "Open a new terminal for a clean environment (this one keeps the variables" +echo "until you 'unset PROJECT_NUMBER' or start a new shell)." diff --git a/.scripts/bk-info b/.scripts/bk-info index f6e9d65..374ae15 100755 --- a/.scripts/bk-info +++ b/.scripts/bk-info @@ -7,23 +7,32 @@ MAGENTA='\033[0;35m' CYAN='\033[0;36m' NC='\033[0m' # No Color +if [ -z "$BK_DIR" ] || [ -z "$BK_STREAM" ]; then + echo "The bootkon environment is not loaded. Please run: . bk" + exit 1 +fi + echo -e "${CYAN}Variables:${NC}" -echo -e "BK_GITHUB_USERNAME: ${GREEN}$BK_GITHUB_USERNAME${NC}" -echo -e "BK_GITHUB_REPOSITORY: ${GREEN}$BK_GITHUB_REPOSITORY${NC}" -echo -e "BK_REPO_URL: ${GREEN}$BK_REPO_URL${NC}" -echo -e "BK_TUTORIAL: ${GREEN}$BK_TUTORIAL${NC}" +echo -e "BK_REPO: ${GREEN}$BK_REPO${NC} (https://github.com/${BK_REPO}.git)" echo -e "BK_DIR: ${GREEN}$BK_DIR${NC}" -echo -e "BK_INIT_SCRIPT: ${GREEN}$BK_INIT_SCRIPT${NC}" -echo -e "BK_INITIALIZED: ${GREEN}$BK_INITIALIZED${NC}" +echo -e "BK_STREAM: ${GREEN}$BK_STREAM${NC}" +echo -e "BK_BRANCH: ${GREEN}$BK_BRANCH${NC}" +echo -e "config file: ${GREEN}$BK_DIR/vars.local.sh${NC}" +echo -e "PROJECT_ID: ${GREEN}$PROJECT_ID${NC}" +echo -e "PROJECT_NUMBER: ${GREEN}$PROJECT_NUMBER${NC}" +echo -e "REGION: ${GREEN}$REGION${NC}" echo echo -e "${CYAN}Info:${NC}" -if [ ! -d "data" ]; then - echo -e "data: ${RED}does not exist${NC}" -else - echo -e "data: ${GREEN}exists${NC}" - du -sh data +# The bundled dataset exists only for the data stream; other streams should +# not see an alarming red line about it. +if [ "$BK_STREAM" = "data" ]; then + if [ -d "${BK_DIR}/data" ]; then + echo -e "data: ${GREEN}exists${NC} ($(du -sh "${BK_DIR}/data" 2>/dev/null | cut -f1))" + else + echo -e "data: not downloaded yet (the data-stream labs fetch it when needed)" + fi fi -echo -e "git origin: ${GREEN}$(git config --get remote.origin.url)${NC}" \ No newline at end of file +echo -e "git origin: ${GREEN}$(git -C "${BK_DIR}" config --get remote.origin.url 2>/dev/null)${NC}" diff --git a/.scripts/bk-remove-block b/.scripts/bk-remove-block new file mode 100755 index 0000000..9d2f6a9 --- /dev/null +++ b/.scripts/bk-remove-block @@ -0,0 +1,65 @@ +#!/bin/bash +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# bk-remove-block FILE +# +# Remove the bootkon-managed block(s) from FILE. Shared by `bk` (which strips +# then re-appends a fresh block) and `bk-deactivate`. Robust by design: +# * markers are matched ignoring trailing whitespace / CR (CRLF-safe); +# * a start marker with no matching end (truncated/corrupted file) is KEPT, +# not deleted to EOF -- no data loss; +# * trailing blank lines are stripped so repeated `. bk` reloads do not +# accumulate whitespace; +# * the rewrite is atomic (temp file in the SAME directory, then mv) and +# preserves the original file's permissions. + +set -eu + +file="${1:?bk-remove-block: FILE required}" +[ -f "$file" ] || exit 0 + +# Operate on the real file: replacing a symlinked ~/.bashrc with a regular +# file would silently detach dotfile setups. +file="$(readlink -f "$file")" + +start='# >>> bootkon >>>' +end='# <<< bootkon <<<' + +dir="$(dirname "$file")" +tmp="$(mktemp "$dir/.bootkon.XXXXXX")" +chmod --reference="$file" "$tmp" 2>/dev/null || true + +awk -v s="$start" -v e="$end" ' + function trim(x) { sub(/[[:space:]]+$/, "", x); return x } + { + if (!inblk) { + if (trim($0) == s) { inblk = 1; buf = $0; next } + out[++n] = $0; next + } + buf = buf "\n" $0 + if (trim($0) == e) { inblk = 0; buf = ""; next } + next + } + END { + if (inblk) { # unterminated block: keep it verbatim + m = split(buf, a, "\n") + for (i = 1; i <= m; i++) out[++n] = a[i] + } + while (n > 0 && trim(out[n]) == "") n-- # strip trailing blank lines + for (i = 1; i <= n; i++) print out[i] + } +' "$file" > "$tmp" + +mv "$tmp" "$file" diff --git a/.scripts/bk-render-jinja2 b/.scripts/bk-render-jinja2 index 8ac8bd7..dca0895 100755 --- a/.scripts/bk-render-jinja2 +++ b/.scripts/bk-render-jinja2 @@ -30,16 +30,27 @@ def render_tutorial(content): # Pass all variables starting with BK_ to the template context context = {k: v for k, v in os.environ.items() if k.startswith("BK_")} + # Unset scalars must render as '' (like undefined variables do), never as + # the literal string 'None' inside copy-paste commands. context.update( ON_ARGOLIS=(os.getenv("GCP_USERNAME") or "").endswith("altostrat.com"), - PROJECT_ID=os.getenv("PROJECT_ID"), - PROJECT_NUMBER=os.getenv("PROJECT_NUMBER"), - REGION=os.getenv("REGION"), - MY_NAME=os.getenv("MY_NAME"), - GCP_USERNAME=os.getenv("GCP_USERNAME"), + PROJECT_ID=os.getenv("PROJECT_ID") or "", + PROJECT_NUMBER=os.getenv("PROJECT_NUMBER") or "", + REGION=os.getenv("REGION") or "", + MY_NAME=os.getenv("MY_NAME") or "", + GCP_USERNAME=os.getenv("GCP_USERNAME") or "", author=partial(render_author), ) + # A secret that renders empty produces labs with blank passwords in the + # copy-paste commands -- warn loudly instead of failing an hour later. + for name in sorted(set(re.findall( + r"{{\s*(BK_\w*(?:PASSWORD|SECRET|TOKEN|KEY)\w*)\s*}}", content))): + if not os.getenv(name): + print(f"WARNING: {name} is referenced by this tutorial but is " + f"empty -- commands using it will not work. Run '. bk' and " + f"then bk-start to refresh.", file=sys.stderr) + rendered = template.render(**context) rendered = re.sub( diff --git a/.scripts/bk-set-var b/.scripts/bk-set-var new file mode 100755 index 0000000..dc72f80 --- /dev/null +++ b/.scripts/bk-set-var @@ -0,0 +1,140 @@ +#!/bin/bash +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# bk-set-var — idempotently upsert an `export NAME='VALUE'` line into a file. +# +# Usage: +# bk-set-var [--if-empty] [--comment "text"] FILE NAME VALUE +# +# --if-empty Only write if NAME is currently unset or empty in FILE. +# Existing non-empty values are left untouched. Used for +# autodetected config (respect manual edits) and for +# once-generated secrets (never regenerate). +# --comment One-line description, written as a `# text` comment directly +# above the export when the line is first appended (so a fresh +# vars.local.sh is self-documenting). Not re-added on later +# in-place replacements, and never duplicated. +# +# The file is created if missing. An existing `export NAME=...` line is +# replaced in place (order preserved, duplicates collapsed); otherwise the +# line is appended. This one helper replaces the repetitive grep/sed/echo +# blocks that used to litter `bk` and the stream bootstraps. + +set -eu + +if_empty=0 +comment="" +while [ $# -gt 0 ]; do + case "$1" in + --if-empty) if_empty=1; shift ;; + --comment) # one-line description, written above the export + if [ $# -lt 2 ]; then + echo "bk-set-var: --comment requires an argument" >&2 + exit 1 + fi + comment="$2"; shift 2 ;; + *) break ;; + esac +done + +# A multi-line comment would put its second line outside the '# ' prefix -- +# executed by every new terminal that sources the file. Reject it like a +# newline in the value. +case "$comment" in + *" +"*) + echo "bk-set-var: comment may not contain a newline" >&2 + exit 1 + ;; +esac + +file="${1:?bk-set-var: FILE required}" +name="${2:?bk-set-var: NAME required}" +value="${3-}" + +# Guard: NAME must be a plain shell identifier. +case "$name" in + [!A-Za-z_]* | *[!A-Za-z0-9_]*) + echo "bk-set-var: invalid variable name '$name'" >&2 + exit 1 + ;; +esac + +# A newline would be split across physical lines and corrupt the file on the +# next upsert; no caller needs it, so reject it outright. +case "$value" in + *" +"*) + echo "bk-set-var: value for '$name' may not contain a newline" >&2 + exit 1 + ;; +esac + +# If we create the file it may receive secrets later, so make it private +# from the start (a file created via the default umask would be 644). +if [ ! -f "$file" ]; then + touch "$file" + chmod 600 "$file" +fi + +match="^[[:space:]]*export[[:space:]]+${name}=" + +if [ "$if_empty" -eq 1 ]; then + # Skip if a non-empty value is already present. Parse robustly: strip the + # `export NAME=` prefix, a trailing ` # comment`, surrounding whitespace and + # quotes. (Values in this project never contain a literal '#'.) + current_line="$(grep -E "$match" "$file" | tail -n1 || true)" + if [ -n "$current_line" ]; then + current_value="$(printf '%s' "$current_line" | sed -E \ + 's/^[^=]*=//; s/[[:space:]]+#.*$//; s/^[[:space:]]+//; s/[[:space:]]+$//; s/^"(.*)"$/\1/; s/^'\''(.*)'\''$/\1/')" + [ -n "$current_value" ] && exit 0 + fi + # Nothing to gain from writing an empty value. + [ -z "$value" ] && exit 0 +fi + +# Single-quote the value, escaping any embedded single quotes. +escaped="$(printf '%s' "$value" | sed "s/'/'\\\\''/g")" +new_line="export ${name}='${escaped}'" + +# No-op if the exact line is already present -- avoids rewriting the file (and +# its mtime) on every `. bk` reload for the many deterministic values. +grep -qxF "$new_line" "$file" && exit 0 + +# Write atomically: a temp file in the SAME directory (so the final mv is a +# same-filesystem rename, not a cross-device copy -- $HOME and /tmp are +# different filesystems in Cloud Shell), preserving the target's permissions. +# This protects the once-generated secrets in vars.local.sh from a truncated +# write if the process is interrupted mid-update. +dir="$(dirname "$file")" +tmp="$(mktemp "$dir/.bkvar.XXXXXX")" +chmod --reference="$file" "$tmp" 2>/dev/null || true +if grep -qE "$match" "$file"; then + # Replace the first matching line, drop any later duplicates. + NEW_LINE="$new_line" MATCH="$match" awk ' + $0 ~ ENVIRON["MATCH"] { if (!seen) { print ENVIRON["NEW_LINE"]; seen=1 } next } + { print } + ' "$file" > "$tmp" +else + cat "$file" > "$tmp" + # A hand-edited file may lack a trailing newline; appending directly would + # concatenate two exports on one line. + if [ -s "$tmp" ] && [ -n "$(tail -c1 "$tmp")" ]; then + echo >> "$tmp" + fi + [ -n "$comment" ] && printf '# %s\n' "$comment" >> "$tmp" + printf '%s\n' "$new_line" >> "$tmp" +fi +mv "$tmp" "$file" diff --git a/.scripts/bk-start b/.scripts/bk-start index cdc5d6a..f6acba8 100755 --- a/.scripts/bk-start +++ b/.scripts/bk-start @@ -1,15 +1,19 @@ #!/bin/sh -cd $BK_DIR - -if [ -z "$BK_INITIALIZED" ]; then +if [ -z "$PROJECT_NUMBER" ]; then echo "Bootkon has not been initialized." echo "Please execute: " echo " . bk (including the dot)" exit 1 fi -bk-tutorial content/${BK_STREAM}/TUTORIAL.md +cd "$BK_DIR" || { + echo "ERROR: the bootkon folder $BK_DIR is missing." >&2 + echo "Paste the setup command from the workshop instructions again to reinstall." >&2 + exit 1 +} + +bk-tutorial "content/${BK_STREAM}/TUTORIAL.md" if [ "${BK_STREAM}" != "devex" ]; then cloudshell open-workspace . fi diff --git a/.scripts/bk-tutorial b/.scripts/bk-tutorial index 022bbf3..fad8630 100755 --- a/.scripts/bk-tutorial +++ b/.scripts/bk-tutorial @@ -8,21 +8,44 @@ # # Usage: bk-tutorial -MEMORY="${BK_DIR}/.tutorial.memory" +if [[ -z "${BK_DIR}" ]]; then + echo "The bootkon environment is not loaded. Please run: . bk" >&2 + exit 1 +fi + +# Tutorial paths (argument and memory) are relative to the checkout, and the +# renderer resolves includes relative to ./content -- so always work from +# $BK_DIR, no matter which lab directory the participant is currently in. +cd "$BK_DIR" || { + echo "ERROR: the bootkon folder $BK_DIR is missing." >&2 + echo "Paste the setup command from the workshop instructions again to reinstall." >&2 + exit 1 +} + +MEMORY="$BK_DIR/.tutorial.memory" RENDERED=/tmp/bootkon.md FILE=$1 -if [[ -z "${FILE}" ]]; then - if [[ -f "${MEMORY}" ]]; then - FILE=$(cat $MEMORY) - echo "No tutorial specified. Reloading last opened file ${FILE}" - fi +if [[ -z "$FILE" && -f "$MEMORY" ]]; then + FILE="$(cat "$MEMORY")" + echo "No tutorial specified. Reloading last opened file $FILE" fi -echo "$FILE" > $MEMORY +if [[ -z "$FILE" || ! -f "$FILE" ]]; then + echo "No tutorial to open. Type bk-start to open your stream's tutorial." >&2 + exit 1 +fi + +bk-render-jinja2 "$FILE" "$RENDERED" || exit 1 + +# Remember the file only after a successful render, so a bad invocation can +# never poison the reload memory. +echo "$FILE" > "$MEMORY" -bk-render-jinja2 $FILE $RENDERED -cloudshell launch-tutorial $RENDERED +# Launching twice with a pause works around the tutorial pane sometimes not +# picking up the new content on the first call (observed in Cloud Shell). +# If a future Cloud Shell release makes the second call unnecessary, drop it. +cloudshell launch-tutorial "$RENDERED" sleep 1.5 -cloudshell launch-tutorial $RENDERED +cloudshell launch-tutorial "$RENDERED" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b16bd94..c27c780 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -31,3 +31,18 @@ All submissions, including submissions by project members, require review. We use GitHub pull requests for this purpose. Consult [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more information on using pull requests. + +## Running bootkon from a fork + +Forks work out of the box -- `bk` derives the repo identity (`BK_REPO`) from +the checkout's own git origin, so rendered image links automatically follow +the fork. Three caveats: + +1. Set `BK_REPO=/` in the launch command you hand out; the launch + fetches `.scripts/bk` from whatever repo that line names (only after the + clone does bk self-correct from the checkout's origin). +2. The fork must be **public**: the tutorial pane fetches images through + unauthenticated `?raw=true` blob URLs, which 404 on a private fork. +3. A few upstream references are hardcoded and are NOT redirected by forking, + e.g. `.scripts/bk-legacy-download` always fetches the data-stream dataset + from `fhirschmann/bootkon-data`. diff --git a/content/agenticdata/README.md b/content/agenticdata/README.md index 6b4c442..df45dc0 100644 --- a/content/agenticdata/README.md +++ b/content/agenticdata/README.md @@ -171,7 +171,8 @@ Click into the terminal that has opened at the bottom of your screen. And copy & paste the following command and press return: ```bash -BK_STREAM=agenticdata BK_REPO=GoogleCloudPlatform/bootkon; . <(wget -qO- https://raw.githubusercontent.com/${BK_REPO}/main/.scripts/bk) +BK_STREAM=agenticdata BK_REPO=GoogleCloudPlatform/bootkon; . <(wget -qO- https://raw.githubusercontent.com/${BK_REPO}/main/.scripts/bk \ + || echo 'echo "ERROR: could not download bootkon — check your network and paste the command again, or ask the event staff." >&2') ``` Now, please go back to Cloud Shell and continue with the tutorial that has been opened on the right hand side of your screen! diff --git a/content/agenticdata/TUTORIAL.md b/content/agenticdata/TUTORIAL.md index b4ffbfd..6cc89ba 100644 --- a/content/agenticdata/TUTORIAL.md +++ b/content/agenticdata/TUTORIAL.md @@ -31,36 +31,31 @@ echo "Let's go, Cymbal!" Execute by pressing the return key in the terminal that has been opened in the lower part of your screen. -### Setting environment variables -You can open files directly from this tutorial. -Open `vars.sh` by clicking here -and set `GCP_USERNAME`, `PROJECT_ID` according to the information you received. Also let us know your (first?) name in `MY_NAME`. Don't forget to save it. +### Check your environment -❗ Please do not include any whitespaces when setting these variables. +The setup command you pasted has already configured everything. Let's verify it: -Please initialize bootkon. The next command will set environment variables in your current terminal. +* Your `PROJECT_ID` is `{% if PROJECT_ID == "" %}None{% else %}{{ PROJECT_ID }}{% endif %}` -```bash -. bk -``` - -Reload the tutorial window on the right-hand side of your screen. +* Your `GCP_USERNAME` is `{% if GCP_USERNAME == "" %}None{% else %}{{ GCP_USERNAME }}{% endif %}` -```bash -bk-start -``` +If both are correct, you are done — press the `START` button below to get started! +(Tip: if you ever close this tutorial or the editor, type `bk-start` in the +terminal to reopen them.) -In case you accidentally close the tutorial or the editor, you can execute `bk-start` to start it again. Please make sure that you execute `. bk` in every terminal -you open so that the environment variables are set. +Only if a value is wrong or shows `None`: -Now, your +* Wrong `PROJECT_ID`: switch to your event project in the Cloud Shell project + selector at the top of the window. +* Wrong or missing `GCP_USERNAME` or name: open `vars.local.sh` + by clicking here, + fix the value (no spaces), and save. -* `PROJECT_ID` is `{% if PROJECT_ID == "" %}None{% else %}{{ PROJECT_ID }}{% endif %}` +Then reload the environment and refresh the values shown on this page: -* `GCP_USERNAME` is `{% if GCP_USERNAME == "" %}None{% else %}{{ GCP_USERNAME }}{% endif %}`. - - -If neither is `None`, press the `START` button below to get started! +```bash +. bk && bk-start +``` {% include 'agenticdata/labs/1_environment_setup.md' %} diff --git a/content/agenticdata/bk-bootstrap b/content/agenticdata/bk-bootstrap index d45c7dd..a047d9a 100755 --- a/content/agenticdata/bk-bootstrap +++ b/content/agenticdata/bk-bootstrap @@ -1,26 +1,50 @@ #!/bin/bash # Bootstraps the Agentic Data Bootkon stream: Python deps, APIs, IAM, -# service accounts, and the secrets used across the labs. +# service accounts, and the agy pre-configuration. # # It deliberately does NOT create the Cloud SQL instance or any networking: # those commands live inline in the labs, so the connectivity variant can be # swapped by applying option1-vpc-peering-proxy.diff to the lab markdown. +# +# Error handling: every step is checked and counted. The script keeps going +# so one flaky IAM call does not abort the remaining setup, but it only +# claims success when NOTHING failed -- a silent failure here used to +# resurface an hour later as a cryptic PERMISSION_DENIED in Lab 1. + +# PROJECT_NUMBER is set by `. bk` only after successful project validation -- +# and it is exactly what the service-agent emails below are built from. +[[ -z "$PROJECT_NUMBER" ]] && echo "Bootkon has not been initialized. Please follow the tutorial and run '. bk'" && exit 1 -[[ "$BK_INITIALIZED" != "1" ]] && echo "Bootkon has not been initialized. Please follow the tutorial and run '. bk'" && exit 1 +# The lab invokes this script by relative path from the repo root, but a fresh +# tab starts in $HOME -- pin the cwd so the relative paths below always work. +cd "$BK_DIR" || { echo "ERROR: could not enter $BK_DIR -- please run '. bk' first." >&2; exit 1; } + +FAILURES=0 +step_failed() { + FAILURES=$((FAILURES + 1)) + echo "ERROR: $1 -- see the message above." >&2 +} # The Datastream service agent must exist and hold its service-agent role # BEFORE Lab 1 creates the private connection: on a fresh project the agent # is created lazily, and `private-connections create` then fails with # PERMISSION_DENIED on the network attachment (verified live). The IAM grant # also needs a few minutes to propagate -- doing this FIRST, before the -# pip install and the role loop below, buys that time for free. -gcloud services enable datastream.googleapis.com >/dev/null 2>&1 -gcloud beta services identity create --service=datastream.googleapis.com --project=$PROJECT_ID >/dev/null 2>&1 +# pip install and the role loop below, buys that time for free. This block +# is the reason this script exists: keep its errors loud. +echo "Preparing the Datastream service agent (Lab 1 depends on it) ..." +gcloud services enable datastream.googleapis.com \ + || step_failed "enabling the Datastream API failed" +gcloud beta services identity create --service=datastream.googleapis.com \ + --project="$PROJECT_ID" >/dev/null \ + || step_failed "creating the Datastream service identity failed" gcloud projects add-iam-policy-binding "$PROJECT_ID" \ --member="serviceAccount:service-${PROJECT_NUMBER}@gcp-sa-datastream.iam.gserviceaccount.com" \ - --role="roles/datastream.serviceAgent" --condition=None >>/dev/null + --role="roles/datastream.serviceAgent" --condition=None >/dev/null \ + || step_failed "granting the Datastream service-agent role failed" -pip install -r content/agenticdata/requirements.txt +pip install -r content/agenticdata/requirements.txt \ + || step_failed "installing the Python dependencies (pip) failed" echo "Enabling APIs (idempotent; the tutorial enables these too) ..." gcloud services enable \ @@ -42,13 +66,20 @@ gcloud services enable \ serviceusage.googleapis.com \ cloudresourcemanager.googleapis.com \ iam.googleapis.com \ - artifactregistry.googleapis.com + artifactregistry.googleapis.com \ + || step_failed "enabling the APIs failed" # Knowledge Catalog data-quality service account: profile/quality scans in -# Lab 4 run as this SA (custom execution identity). +# Lab 4 run as this SA (custom execution identity). Creation is idempotent: +# re-running the bootstrap is the natural novice recovery and must not greet +# them with an unexplained 'already exists' error. DQ_SCAN_SERVICE_ACCOUNT="dataquality-service-account" DQ_SCAN_SA_EMAIL="${DQ_SCAN_SERVICE_ACCOUNT}@${PROJECT_ID}.iam.gserviceaccount.com" -gcloud iam service-accounts create $DQ_SCAN_SERVICE_ACCOUNT --display-name="Knowledge Catalog DQ Service Account" +if ! gcloud iam service-accounts describe "$DQ_SCAN_SA_EMAIL" >/dev/null 2>&1; then + gcloud iam service-accounts create "$DQ_SCAN_SERVICE_ACCOUNT" \ + --display-name="Knowledge Catalog DQ Service Account" \ + || step_failed "creating the data-quality service account failed" +fi # A freshly created SA can take a few seconds to become visible to IAM; # granting against it too early fails with "does not exist". for i in $(seq 1 15); do @@ -58,6 +89,9 @@ done # Roles for the participant. Sandbox users are typically project owners # already; the explicit list keeps the stream portable to other setups. +# --condition=None: a conditional binding anywhere in the project's policy +# would otherwise make gcloud stop and prompt interactively -- mid-bootstrap, +# once per role, with the actual question hidden by the stdout redirect. declare -a user_roles=( "roles/cloudsql.admin" # create/manage the Cymbal instance "roles/datastream.admin" # profiles, private connection, stream @@ -77,7 +111,8 @@ declare -a user_roles=( for role in "${user_roles[@]}"; do echo "Assigning role $role to $GCP_USERNAME in project $PROJECT_ID..." gcloud projects add-iam-policy-binding "$PROJECT_ID" \ - --member="user:$GCP_USERNAME" --role="$role" >>/dev/null + --member="user:$GCP_USERNAME" --role="$role" --condition=None >>/dev/null \ + || step_failed "assigning $role to $GCP_USERNAME failed" done # Roles for the data-quality service account. @@ -92,37 +127,20 @@ declare -a dataquality_roles=( for role in "${dataquality_roles[@]}"; do echo "Assigning role $role to $DQ_SCAN_SA_EMAIL in project $PROJECT_ID..." gcloud projects add-iam-policy-binding "$PROJECT_ID" \ - --member="serviceAccount:$DQ_SCAN_SA_EMAIL" --role="$role" >>/dev/null + --member="serviceAccount:$DQ_SCAN_SA_EMAIL" --role="$role" --condition=None >>/dev/null \ + || step_failed "assigning $role to $DQ_SCAN_SA_EMAIL failed" done -# Per-participant configuration, appended to ~/.bashrc so every new terminal -# picks it up automatically (same pattern .scripts/bk uses for BK_* vars). -if ! grep -q '# agenticdata-bootkon' ~/.bashrc; then - echo "Generating database passwords and adding stream exports to ~/.bashrc ..." - cat >> ~/.bashrc <&2 + echo "then run this script again (it is safe to re-run). If it keeps failing," >&2 + echo "ask the event staff." >&2 + exit 1 +fi diff --git a/content/agenticdata/bk-init b/content/agenticdata/bk-init new file mode 100755 index 0000000..b9dfbfd --- /dev/null +++ b/content/agenticdata/bk-init @@ -0,0 +1,29 @@ +#!/bin/bash +# Stream init hook for agenticdata, run by `. bk` on every initialization and +# reload -- AFTER project validation and BEFORE the first tutorial render, so +# everything written here is already available when the labs are rendered. +# +# Contract (see .scripts/bk): idempotent, local-only, instant. Secrets are +# written once (--if-empty); deterministic runtime config is upserted. Heavy, +# visible GCP work (APIs, IAM, service accounts, pip) lives in bk-bootstrap, +# which participants run in Lab 1. + +set -eu + +LOCAL="$BK_DIR/vars.local.sh" +SET_VAR="$BK_DIR/.scripts/bk-set-var" + +# Generated once: Lab 1 creates the Cloud SQL instance with BK_DB_PASSWORD and +# Lab 2 the Datastream connection profile with BK_DS_PASSWORD. +"$SET_VAR" --if-empty --comment "Cloud SQL (Postgres) root password -- generated once" "$LOCAL" BK_DB_PASSWORD "$(openssl rand -hex 16)" +"$SET_VAR" --if-empty --comment "Datastream connection password -- generated once" "$LOCAL" BK_DS_PASSWORD "$(openssl rand -hex 16)" + +# Deterministic runtime config for the ADK / Antigravity (agy) tooling. +# GOOGLE_CLOUD_PROJECT is intentionally NOT set: Cloud Shell already exports it +# (= the active project = PROJECT_ID); bootkon's own code uses PROJECT_ID. +"$SET_VAR" --comment "Route google-genai through Vertex AI" "$LOCAL" GOOGLE_GENAI_USE_VERTEXAI TRUE +"$SET_VAR" --comment "Location for the GenAI/Vertex client" "$LOCAL" GOOGLE_CLOUD_LOCATION global +"$SET_VAR" --comment "Gemini model id used by the Cymbal agents" "$LOCAL" BK_CYMBAL_MODEL gemini-2.5-flash +"$SET_VAR" --comment "Conversational Analytics data agent id" "$LOCAL" BK_DATA_AGENT_ID cymbal-data-agent +"$SET_VAR" --comment "Cymbal DB host (reached via the bk-tunnel)" "$LOCAL" BK_CYMBAL_DB_HOST localhost +"$SET_VAR" --comment "Cymbal DB port (reached via the bk-tunnel)" "$LOCAL" BK_CYMBAL_DB_PORT 5432 diff --git a/content/agenticdata/labs/1_environment_setup.md b/content/agenticdata/labs/1_environment_setup.md index 2df1358..f0a79af 100644 --- a/content/agenticdata/labs/1_environment_setup.md +++ b/content/agenticdata/labs/1_environment_setup.md @@ -37,23 +37,13 @@ One rule for today: **you** run the infrastructure commands, **agy** writes code ### Assign permissions -Execute the following script. It installs Python dependencies, grants IAM roles, creates a service account for data-quality scans, and generates the database passwords for your project. It runs for about two minutes — you can inspect bk-bootstrap while it works: +Execute the following script. It installs Python dependencies, grants IAM roles, creates a service account for data-quality scans, and pre-configures your AI co-engineer. It runs for about two minutes — you can inspect bk-bootstrap while it works: ```bash content/agenticdata/bk-bootstrap ``` -The script appended your generated database passwords and agent settings to `~/.bashrc`, so **every new terminal picks them up automatically**. Load them into this already-open one: - -```bash -source ~/.bashrc -``` - -Then reload the tutorial once — this renders your freshly generated passwords directly into the commands of the next labs: - -```bash -bk-start -``` +Your database passwords were already generated during setup and live in `vars.local.sh` — the commands below use them as `$BK_DB_PASSWORD`, and **every terminal picks them up automatically**. ### Create the network path diff --git a/content/agenticdata/labs/6_a2a.md b/content/agenticdata/labs/6_a2a.md index 6fda64c..5da7866 100644 --- a/content/agenticdata/labs/6_a2a.md +++ b/content/agenticdata/labs/6_a2a.md @@ -25,7 +25,7 @@ First, verify the foundation: open [BigQuery](https://console.cloud.google.com/b ### Set up the project -The agent project ships with the repository — you work directly in `content/agenticdata/src/adk`. There is no configuration file to create: everything the agents need (`GOOGLE_CLOUD_PROJECT`, the pinned Gemini model, the data agent ID, the database password) was exported to your `~/.bashrc` by `bk-bootstrap` in Lab 1, and the agents read it from the environment. They authenticate to Gemini via Vertex AI with your logged-in credentials — no API keys anywhere. +The agent project ships with the repository — you work directly in `content/agenticdata/src/adk`. There is no configuration file to create: everything the agents need (your `PROJECT_ID`, the pinned Gemini model, the data agent ID, the database password) lives in `vars.local.sh` from Lab 1, and the agents read it from the environment. They authenticate to Gemini via Vertex AI with your logged-in credentials — Vertex picks up your project from Cloud Shell — no API keys anywhere. All Python dependencies were installed by the bootstrap too, so just confirm the ADK CLI is there and check your configuration: diff --git a/content/agenticdata/src/adk/AGENTS.md b/content/agenticdata/src/adk/AGENTS.md index e3834fc..ae3e0b4 100644 --- a/content/agenticdata/src/adk/AGENTS.md +++ b/content/agenticdata/src/adk/AGENTS.md @@ -46,9 +46,11 @@ tunnel on localhost:5432. `from google.adk.a2a.utils.agent_to_a2a import to_a2a`, `from google.cloud import geminidataanalytics`. - All configuration comes from environment variables: - `GOOGLE_CLOUD_PROJECT`, `GOOGLE_GENAI_USE_VERTEXAI`, `GOOGLE_CLOUD_LOCATION`, + `PROJECT_ID` (bootkon's canonical project var — use this, not + `GOOGLE_CLOUD_PROJECT`), `GOOGLE_GENAI_USE_VERTEXAI`, `GOOGLE_CLOUD_LOCATION`, `BK_CYMBAL_MODEL` (default `gemini-2.5-flash`), `BK_DATA_AGENT_ID`, - `BK_CYMBAL_DB_HOST`/`BK_CYMBAL_DB_PORT`, `BK_DB_PASSWORD`. + `BK_CYMBAL_DB_HOST`/`BK_CYMBAL_DB_PORT`, `BK_DB_PASSWORD`. (The Vertex SDK and + agy read `GOOGLE_CLOUD_PROJECT`, which Cloud Shell provides = `PROJECT_ID`.) Never create config files and never hardcode credentials. - Read the model as `os.environ.get("BK_CYMBAL_MODEL", "gemini-2.5-flash")`. - Keep tools small and readable; docstrings are the tool documentation the diff --git a/content/agenticdata/src/adk/cymbal_analyst/a2a_server.py b/content/agenticdata/src/adk/cymbal_analyst/a2a_server.py index 2e723c6..e9f33d0 100644 --- a/content/agenticdata/src/adk/cymbal_analyst/a2a_server.py +++ b/content/agenticdata/src/adk/cymbal_analyst/a2a_server.py @@ -4,7 +4,7 @@ uvicorn cymbal_analyst.a2a_server:a2a_app --host 127.0.0.1 --port 8001 -Configuration comes from the environment (~/.bashrc, set by bk-bootstrap). +Configuration comes from the environment (vars.local.sh, set by bk-bootstrap). to_a2a() auto-generates the agent card, served at http://localhost:8001/.well-known/agent-card.json (A2A spec >= 0.3/1.0 -- diff --git a/content/agenticdata/src/adk/cymbal_analyst/agent.py b/content/agenticdata/src/adk/cymbal_analyst/agent.py index 27a3d95..3c96f15 100644 --- a/content/agenticdata/src/adk/cymbal_analyst/agent.py +++ b/content/agenticdata/src/adk/cymbal_analyst/agent.py @@ -4,9 +4,10 @@ agent (Conversational Analytics) and is exposed to other agents over the A2A protocol by a2a_server.py. -All configuration (GOOGLE_CLOUD_PROJECT, GOOGLE_GENAI_USE_VERTEXAI, -BK_CYMBAL_MODEL, BK_DATA_AGENT_ID) comes from environment variables exported to -~/.bashrc by bk-bootstrap in Lab 1. +All configuration (PROJECT_ID, GOOGLE_GENAI_USE_VERTEXAI, BK_CYMBAL_MODEL, +BK_DATA_AGENT_ID) comes from environment variables in vars.local.sh (written by +bk / bk-bootstrap in Lab 1). Vertex AI reads GOOGLE_CLOUD_PROJECT, which Cloud +Shell provides (= PROJECT_ID). """ import os diff --git a/content/agenticdata/src/adk/cymbal_analyst/ca_tool.py b/content/agenticdata/src/adk/cymbal_analyst/ca_tool.py index 6f32b18..21e1cc9 100644 --- a/content/agenticdata/src/adk/cymbal_analyst/ca_tool.py +++ b/content/agenticdata/src/adk/cymbal_analyst/ca_tool.py @@ -24,7 +24,7 @@ def ask_cymbal_data_agent(question: str) -> str: The data agent's final textual answer (it runs SQL on BigQuery behind the scenes). """ - project = os.environ["GOOGLE_CLOUD_PROJECT"] + project = os.environ["PROJECT_ID"] agent_id = os.environ.get("BK_DATA_AGENT_ID", "cymbal-data-agent") client = geminidataanalytics.DataChatServiceClient() diff --git a/content/agenticdata/src/adk/cymbal_concierge/agent.py b/content/agenticdata/src/adk/cymbal_concierge/agent.py index 48cd0ea..e23fe7b 100644 --- a/content/agenticdata/src/adk/cymbal_concierge/agent.py +++ b/content/agenticdata/src/adk/cymbal_concierge/agent.py @@ -7,7 +7,7 @@ Postgres through the IAP tunnel (localhost:5432). All configuration (model, database password, hosts) comes from environment -variables exported to ~/.bashrc by bk-bootstrap in Lab 1. +variables written to vars.local.sh by bk-bootstrap in Lab 1. """ import os diff --git a/content/agenticdata/src/datagen/simulate.py b/content/agenticdata/src/datagen/simulate.py index b240817..ea62868 100755 --- a/content/agenticdata/src/datagen/simulate.py +++ b/content/agenticdata/src/datagen/simulate.py @@ -39,7 +39,7 @@ def log(action, detail): def main(): password = os.environ.get("BK_DB_PASSWORD") if not password: - sys.exit("BK_DB_PASSWORD is not set. Run: source ~/.bashrc (set by bk-bootstrap)") + sys.exit("BK_DB_PASSWORD is not set. Run '. bk' to reload it (written to vars.local.sh at setup by bk-init).") host = os.environ.get("BK_CYMBAL_DB_HOST", "localhost") port = int(os.environ.get("BK_CYMBAL_DB_PORT", "5432")) diff --git a/content/agents/README.md b/content/agents/README.md index ad7cb94..fd12ddb 100644 --- a/content/agents/README.md +++ b/content/agents/README.md @@ -79,7 +79,8 @@ Click into the terminal that has opened at the bottom of your screen. And copy & paste the following command and press return: ```bash -BK_STREAM=agents BK_REPO=fhirschmann/bootkon; . <(wget -qO- https://raw.githubusercontent.com/${BK_REPO}/${BK_BRANCH}/.scripts/bk) +BK_STREAM=agents BK_REPO=GoogleCloudPlatform/bootkon; . <(wget -qO- https://raw.githubusercontent.com/${BK_REPO}/main/.scripts/bk \ + || echo 'echo "ERROR: could not download bootkon — check your network and paste the command again, or ask the event staff." >&2') ``` Now, please go back to Cloud Shell and continue with the tutorial that has been opened on the right hand side of your screen! diff --git a/content/agents/TUTORIAL.md b/content/agents/TUTORIAL.md index 3d040ca..4b48e71 100644 --- a/content/agents/TUTORIAL.md +++ b/content/agents/TUTORIAL.md @@ -30,34 +30,31 @@ echo "I'm ready to get started." Execute by pressing the return key in the terminal that has been opened in the lower part of your screen. -### Setting environment variables -You can open files directly from this tutorial. -Open `vars.sh` by clicking here -and set `GCP_USERNAME`, `PROJECT_ID` according to the information you received. Also let us know your (first?) name in `MY_NAME`. Don't forget to save it. +### Check your environment -❗ Please do not include any whitespaces when setting these variablers. +The setup command you pasted has already configured everything. Let's verify it: -Please reload bootkon and make sure there are no errors printed: - -```bash -. bk -``` +* Your `PROJECT_ID` is `{% if PROJECT_ID == "" %}None{% else %}{{ PROJECT_ID }}{% endif %}` +* Your `GCP_USERNAME` is `{% if GCP_USERNAME == "" %}None{% else %}{{ GCP_USERNAME }}{% endif %}` -And restart the tutorial using the next command. You can also use the next command to continue bootkon in case you accidentally close the tutorial or the editor: +If both are correct, you are done — press the `START` button below to get started! +(Tip: if you ever close this tutorial or the editor, type `bk-start` in the +terminal to reopen them.) -```bash -bk-start -``` +Only if a value is wrong or shows `None`: -Now, your +* Wrong `PROJECT_ID`: switch to your event project in the Cloud Shell project + selector at the top of the window. +* Wrong or missing `GCP_USERNAME` or name: open `vars.local.sh` + by clicking here, + fix the value (no spaces), and save. -* `PROJECT_ID` is `{% if PROJECT_ID == "" %}None{% else %}{{ PROJECT_ID }}{% endif %}` +Then reload the environment and refresh the values shown on this page: -* `GCP_USERNAME` is `{% if GCP_USERNAME == "" %}None{% else %}{{ GCP_USERNAME }}{% endif %}`. - - -If neither is `None`, press the `START` button below to get started! +```bash +. bk && bk-start +``` {% include 'agents/labs/1_environment_setup.md' %} {% include 'agents/labs/2_adk.md' %} diff --git a/content/contributing/TUTORIAL.md b/content/contributing/TUTORIAL.md index 1c0fcf3..6c5f9c0 100644 --- a/content/contributing/TUTORIAL.md +++ b/content/contributing/TUTORIAL.md @@ -52,21 +52,35 @@ git status ## Set up your development environment -During the first lab, participants are asked to edit `vars.sh`. It is suggested to make a copy of this file and not touch the original in order not to accidently commit it to git. +`. bk` creates `vars.local.sh` from `vars.sh` on first run and auto-detects your +`PROJECT_ID`/`GCP_USERNAME` from Cloud Shell. `vars.local.sh` is git-ignored, so +you never risk committing your project — never edit `vars.sh` itself. -First, make a copy: +To override a detected value (or set `MY_NAME`), edit `vars.local.sh`, then reload: ```bash -cp vars.sh vars.local.sh +. bk ``` -And edit it. It also runs on Argolis (for Google employees). +It also runs on Argolis (for Google employees). `vars.local.sh` takes precedence +over `vars.sh`, and every new terminal picks it up automatically via the block +`bk` adds to `~/.bashrc`. + +## Test a branch end-to-end + +To try your branch the way a participant would — a clean clone and the full +`bk` bootstrap — run the one-liner with your branch in **both** `BK_BRANCH` and +the URL. The stream READMEs hard-code `main`, so you must swap it out: -Next, source it: ```bash -. vars.local.sh +BK_BRANCH= BK_STREAM= BK_REPO=/bootkon; . <(wget -qO- https://raw.githubusercontent.com/${BK_REPO}/${BK_BRANCH}/.scripts/bk) ``` -Note that the init script (`bk`) automatically loads `vars.local.sh` the next time and `vars.local.sh` takes presendence over `vars.sh`. +`bk` clones the branch into `~/bootkon`. If `~/bootkon` already exists (your dev +checkout), `bk` reuses it instead of cloning — so for a truly clean test, remove +it first (`rm -rf ~/bootkon`). In your own dev checkout you don't need the +one-liner at all: `git checkout ` and run `. bk`. `BK_BRANCH` — and +therefore the rendered image links (`.../blob//...`) — follow whatever +the checkout is on, so push image changes before expecting them to render. ## Reloading the tutorial diff --git a/content/data/README.md b/content/data/README.md index a059038..a095999 100644 --- a/content/data/README.md +++ b/content/data/README.md @@ -133,7 +133,8 @@ Click into the terminal that has opened at the bottom of your screen. And copy & paste the following command and press return: ```bash -BK_STREAM=data BK_REPO=GoogleCloudPlatform/bootkon; . <(wget -qO- https://raw.githubusercontent.com/${BK_REPO}/main/.scripts/bk) +BK_STREAM=data BK_REPO=GoogleCloudPlatform/bootkon; . <(wget -qO- https://raw.githubusercontent.com/${BK_REPO}/main/.scripts/bk \ + || echo 'echo "ERROR: could not download bootkon — check your network and paste the command again, or ask the event staff." >&2') ``` Now, please go back to Cloud Shell and continue with the tutorial that has been opened on the right hand side of your screen! diff --git a/content/data/TUTORIAL.md b/content/data/TUTORIAL.md index f320715..8af7255 100644 --- a/content/data/TUTORIAL.md +++ b/content/data/TUTORIAL.md @@ -31,36 +31,31 @@ echo "I'm ready to get started." Execute by pressing the return key in the terminal that has been opened in the lower part of your screen. -### Setting environment variables -You can open files directly from this tutorial. -Open `vars.sh` by clicking here -and set `GCP_USERNAME`, `PROJECT_ID` according to the information you received. Also let us know your (first?) name in `MY_NAME`. Don't forget to save it. +### Check your environment -❗ Please do not include any whitespaces when setting these variablers. +The setup command you pasted has already configured everything. Let's verify it: -Please initialize bootkon. The next command will set environment variables in your current terminal. +* Your `PROJECT_ID` is `{% if PROJECT_ID == "" %}None{% else %}{{ PROJECT_ID }}{% endif %}` -```bash -. bk -``` - -Reload the tutorial window on the right-hand side of your screen. +* Your `GCP_USERNAME` is `{% if GCP_USERNAME == "" %}None{% else %}{{ GCP_USERNAME }}{% endif %}` -```bash -bk-start -``` +If both are correct, you are done — press the `START` button below to get started! +(Tip: if you ever close this tutorial or the editor, type `bk-start` in the +terminal to reopen them.) -In case you accidently close the tutorial or the editor, you can execute `bk-start` to start it again. Please make sure that you execute `. bk` in every terminal -you open so that the environment variables are set. +Only if a value is wrong or shows `None`: -Now, your +* Wrong `PROJECT_ID`: switch to your event project in the Cloud Shell project + selector at the top of the window. +* Wrong or missing `GCP_USERNAME` or name: open `vars.local.sh` + by clicking here, + fix the value (no spaces), and save. -* `PROJECT_ID` is `{% if PROJECT_ID == "" %}None{% else %}{{ PROJECT_ID }}{% endif %}` +Then reload the environment and refresh the values shown on this page: -* `GCP_USERNAME` is `{% if GCP_USERNAME == "" %}None{% else %}{{ GCP_USERNAME }}{% endif %}`. - - -If neither is `None`, press the `START` button below to get started! +```bash +. bk && bk-start +``` {% include 'data/labs/1_environment_setup.md' %} diff --git a/content/data/bk-bootstrap b/content/data/bk-bootstrap index 712e711..a97ccef 100755 --- a/content/data/bk-bootstrap +++ b/content/data/bk-bootstrap @@ -1,9 +1,12 @@ #!/bin/bash # author: Fabian Hirschmann -pip install -r content/data/requirements.txt +# PROJECT_NUMBER is set by `. bk` only after successful project validation -- +# and the service-account emails below are built from it. Guard before doing +# any work (the pip install used to run before this check). +[[ -z "$PROJECT_NUMBER" ]] && echo "Bootkon has not been initialized. Please follow the tutorial and run '. bk'" && exit 1 -[[ "$BK_INITIALIZED" != "1" ]] && echo "Bootkon has not been initialized. Please follow the tutorial and run '. bk'" && exit 1 +pip install -r content/data/requirements.txt # Create Knowledge Catalog DQ Service Account first here to avoid potential timing issues during role assignment later DQ_SCAN_SERVICE_ACCOUNT="dataquality-service-account" diff --git a/content/devex/README.md b/content/devex/README.md index eda3236..d613ba0 100644 --- a/content/devex/README.md +++ b/content/devex/README.md @@ -79,7 +79,8 @@ Click into the terminal that has opened at the bottom of your screen. And copy & paste the following command and press return: ```bash -BK_STREAM=devex BK_REPO=GoogleCloudPlatform/bootkon; . <(wget -qO- https://raw.githubusercontent.com/${BK_REPO}/main/.scripts/bk) +BK_STREAM=devex BK_REPO=GoogleCloudPlatform/bootkon; . <(wget -qO- https://raw.githubusercontent.com/${BK_REPO}/main/.scripts/bk \ + || echo 'echo "ERROR: could not download bootkon — check your network and paste the command again, or ask the event staff." >&2') ``` Now, please go back to Cloud Shell and continue with the tutorial that has been opened on the right hand side of your screen! diff --git a/content/devex/TUTORIAL.md b/content/devex/TUTORIAL.md index 5172d47..fd877a1 100644 --- a/content/devex/TUTORIAL.md +++ b/content/devex/TUTORIAL.md @@ -30,34 +30,31 @@ echo "I'm ready to get started." Execute by pressing the return key in the terminal that has been opened in the lower part of your screen. -### Setting environment variables -You can open files directly from this tutorial. -Open `vars.sh` by clicking here -and set `GCP_USERNAME`, `PROJECT_ID` according to the information you received. Also let us know your (first?) name in `MY_NAME`. Don't forget to save it. +### Check your environment -❗ Please do not include any whitespaces when setting these variablers. +The setup command you pasted has already configured everything. Let's verify it: -Please reload bootkon and make sure there are no errors printed: - -```bash -. bk -``` +* Your `PROJECT_ID` is `{% if PROJECT_ID == "" %}None{% else %}{{ PROJECT_ID }}{% endif %}` +* Your `GCP_USERNAME` is `{% if GCP_USERNAME == "" %}None{% else %}{{ GCP_USERNAME }}{% endif %}` -And restart the tutorial using the next command. You can also use the next command to continue bootkon in case you accidentally close the tutorial or the editor: +If both are correct, you are done — press the `START` button below to get started! +(Tip: if you ever close this tutorial or the editor, type `bk-start` in the +terminal to reopen them.) -```bash -bk-start -``` +Only if a value is wrong or shows `None`: -Now, your +* Wrong `PROJECT_ID`: switch to your event project in the Cloud Shell project + selector at the top of the window. +* Wrong or missing `GCP_USERNAME` or name: open `vars.local.sh` + by clicking here, + fix the value (no spaces), and save. -* `PROJECT_ID` is `{% if PROJECT_ID == "" %}None{% else %}{{ PROJECT_ID }}{% endif %}` +Then reload the environment and refresh the values shown on this page: -* `GCP_USERNAME` is `{% if GCP_USERNAME == "" %}None{% else %}{{ GCP_USERNAME }}{% endif %}`. - - -If neither is `None`, press the `START` button below to get started! +```bash +. bk && bk-start +``` {% include 'devex/labs/2_devex_agy_cli.md' %} diff --git a/content/example/README.md b/content/example/README.md index 805f5d4..ac4ac92 100644 --- a/content/example/README.md +++ b/content/example/README.md @@ -1,3 +1,4 @@ ```bash -BK_STREAM=example BK_REPO=GoogleCloudPlatform/bootkon; . <(wget -qO- https://raw.githubusercontent.com/${BK_REPO}/main/.scripts/bk) +BK_STREAM=example BK_REPO=GoogleCloudPlatform/bootkon; . <(wget -qO- https://raw.githubusercontent.com/${BK_REPO}/main/.scripts/bk \ + || echo 'echo "ERROR: could not download bootkon — check your network and paste the command again, or ask the event staff." >&2') ``` \ No newline at end of file diff --git a/vars.sh b/vars.sh index c19ef33..6ffddbb 100644 --- a/vars.sh +++ b/vars.sh @@ -12,7 +12,17 @@ # See the License for the specific language governing permissions and # limitations under the License. -export MY_NAME="" # example: Ada -export PROJECT_ID="" # example: bootkon-data-3472 -export GCP_USERNAME="" # example: devstar3110@gcplab.me -export REGION="us-central1" # do not change this value +# This file holds the defaults. `. bk` copies it to vars.local.sh (git-ignored) +# on first run and works from there -- edit vars.local.sh, not this file. + +# Your (first) name, shown in the tutorial greeting. Optional. Example: Ada +export MY_NAME="" + +# Your Google Cloud account. Leave empty to auto-detect from Cloud Shell. +# (PROJECT_ID is NOT set here: bk derives it live from the Cloud Shell project +# picker. To use a different project, switch it in the picker.) +# Example: devstar3110@gcplab.me +export GCP_USERNAME="" + +# Deployment region. Do not change this value. +export REGION="us-central1"