diff --git a/labs/GSP1306/HASHNODE.md b/labs/GSP1306/HASHNODE.md new file mode 100644 index 0000000..d8f020e --- /dev/null +++ b/labs/GSP1306/HASHNODE.md @@ -0,0 +1,286 @@ +# Retrieving Credentials from Secret Manager with Spring Boot - GSP1306 + +## Overview + +This lab builds a Spring Boot REST application, stores a **demonstration greeting** in Google Cloud Secret Manager, reads the value through Spring Cloud GCP, and deploys the app to Cloud Run. The sample greeting is not a real credential. Never expose actual passwords or API keys through this public demo endpoint. + +The official lab has **four Tasks and four checkpoints** worth 30/20/25/25 points. Its [console screenshots and diagrams](https://www.skills.google/focuses/121708?parent=catalog) are linked rather than embedded because reuse rights have not been verified. + +### What you'll do + +- Generate and test a Java 21 Spring Boot REST service. +- Create a lab-only `greeting` secret. +- Configure Spring Cloud GCP Secret Manager and verify the updated app. +- Deploy the service to Cloud Run with a scoped service account. + +## Setup and requirements + +### Before you click the Start Lab button + +Open [the official GSP1306 lab](https://www.skills.google/focuses/121708?parent=catalog) and start it when ready. Use a private browser and the temporary student account from the lab panel. Do not use a personal Google Cloud project or publish any session credentials. + +The lab provides a Cloud Storage bucket and a service account named `hello-secret-manager-run-sa`. The region and bucket must come from the active lab, not an example. + +### Activate Cloud Shell + +Activate Cloud Shell and authorize the lab account. Confirm the active project and assigned region: + +```bash +gcloud auth list +export PROJECT_ID="$(gcloud config get-value project)" +export REGION="$(gcloud config get-value compute/region)" +gcloud storage buckets list --project="$PROJECT_ID" +``` + +If the region is unset, use the value shown by the lab panel. Set the existing bucket name: + +```bash +read -r -p "Lab bucket name (without gs://): " BUCKET_NAME +export BUCKET_NAME +gcloud storage buckets describe "gs://$BUCKET_NAME" +``` + +## Task 1. Create a new Spring Boot REST service + +### Generate the Maven project + +The official lab requests Spring Boot `4.0.1`, Java `21`, and the `web,cloud-gcp` dependencies from Spring Initializr. The same official page's explanation mentions Boot 3.3.5; this inconsistency must be reviewed, not silently ignored. + +```bash +cd "$HOME" +curl -fL --retry 3 https://start.spring.io/starter.zip \ + -d type=maven-project -d packaging=jar \ + -d bootVersion=4.0.1 -d dependencies=web,cloud-gcp \ + -d javaVersion=21 -d name=HelloSecretManager \ + -d artifactId=HelloSecretManager -o hello-secret-manager.zip +mkdir -p hello-secret-manager +unzip -q hello-secret-manager.zip -d hello-secret-manager +cd hello-secret-manager +chmod +x mvnw +``` + +Inspect `pom.xml` and the Maven wrapper. If Initializr rejects this dependency combination, stop and confirm a compatible version rather than inventing a result. + +### Add the REST controller + +Save this code as `src/main/java/com/example/HelloSecretManager/HelloSecretManagerController.java`: + +```java +package com.example.HelloSecretManager; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class HelloSecretManagerController { + String greeting = "Hi"; + + @GetMapping("/") + public String hello() { + return greeting + " World!"; + } +} +``` + +Use Java 21 and run the application: + +```bash +export JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64 +./mvnw -DskipTests spring-boot:run | tee verify_response.txt +``` + +In Cloud Shell select **Web Preview → Preview on port 8080**. The initial response should be `Hi World!`. Stop the local server after verifying it. Upload the actual Maven log and POM to the lab bucket: + +```bash +gcloud storage cp verify_response.txt "gs://$BUCKET_NAME/" +gcloud storage cp pom.xml "gs://$BUCKET_NAME/" +``` + +### Test completed task + +Click **Check my progress** for **Create a new Spring Boot REST service** (30 points). Do not fabricate a run log if the application fails. + +## Task 2. Configure a secret in Secret Manager + +Enable the API and create the lab's sample `greeting` secret with public demonstration text `Hello`: + +```bash +gcloud services enable secretmanager.googleapis.com +printf '%s' 'Hello' | gcloud secrets create greeting \ + --replication-policy=automatic --data-file=- +gcloud secrets list +gcloud secrets describe greeting +``` + +On reruns, inspect an existing `greeting` secret rather than creating a duplicate. This public demo value must never be replaced with a real password. + +### Test completed task + +Click **Check my progress** for **Configure a secret in Secret Manager** (20 points). + +## Task 3. Update the app to use the secret greeting + +### Add the Secret Manager dependency + +Within `pom.xml`, add the dependency below if it is not already present: + +```xml + + com.google.cloud + spring-cloud-gcp-starter-secretmanager + +``` + +Verify that the project's dependency management resolves a compatible Spring Cloud GCP version. + +### Configure application properties + +In `src/main/resources/application.properties` add: + +```properties +spring.config.import=sm:// +``` + +The lab uses `sm://` syntax. Newer Spring Cloud GCP releases also document `sm@`; test against the actual generated dependency version. + +### Replace the controller + +Update the Java controller to read the secret rather than use a hardcoded greeting: + +```java +package com.example.HelloSecretManager; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class HelloSecretManagerController { + @Value("${sm://greeting}") + String greeting; + + @GetMapping("/") + public String hello() { + return greeting + " World!"; + } +} +``` + +Restart the service and use Web Preview to check that it now returns `Hello World!`: + +```bash +cd "$HOME/hello-secret-manager" +./mvnw -DskipTests spring-boot:run | tee verify_test_response.txt +``` + +Stop the service after the check. Upload the new run log and the controller: + +```bash +gcloud storage cp verify_test_response.txt "gs://$BUCKET_NAME/" +gcloud storage cp src/main/java/com/example/HelloSecretManager/HelloSecretManagerController.java \ + "gs://$BUCKET_NAME/HelloSecretManagerController.java" +``` + +### Test completed task + +Click **Check my progress** for **Update the app to use the secret greeting** (25 points). A genuine Secret Manager-backed response is required. + +## Task 4. Deploy the app to Cloud Run + +### Enable Cloud Run and Cloud Build + +```bash +gcloud services enable run.googleapis.com cloudbuild.googleapis.com +``` + +Add the Cloud Run port property to `application.properties`: + +```properties +server.port=${PORT:8080} +``` + +Create `project.toml` in the app root to specify Java 21 for the buildpack: + +```toml +[[build.env]] +name = "GOOGLE_RUNTIME_VERSION" +value = "21" +``` + +### Grant least-privilege Secret Manager access + +The Cloud Run service account is pre-provisioned. Grant it access to the specific `greeting` secret rather than every secret in the project: + +```bash +export RUN_SA="hello-secret-manager-run-sa@$PROJECT_ID.iam.gserviceaccount.com" +gcloud iam service-accounts describe "$RUN_SA" +gcloud secrets add-iam-policy-binding greeting \ + --member="serviceAccount:$RUN_SA" \ + --role=roles/secretmanager.secretAccessor +``` + +### Add the final demo value and deploy + +Create a new version of the public demo greeting: + +```bash +printf '%s' 'Greetings' | gcloud secrets versions add greeting --data-file=- +``` + +Deploy the application from source to Cloud Run: + +```bash +cd "$HOME/hello-secret-manager" +gcloud run deploy hello-secret-manager \ + --region="$REGION" \ + --allow-unauthenticated \ + --service-account="$RUN_SA" \ + --source=. +``` + +The lab intentionally makes the demonstration endpoint public. **Never put real credentials in this greeting secret.** Retrieve the URL from the deployed service: + +```bash +export SERVICE_URL="$(gcloud run services describe hello-secret-manager \ + --region="$REGION" --format='value(status.url)')" +curl -fsS "$SERVICE_URL" +``` + +The expected response is `Greetings World!`. If it fails, inspect Cloud Run revision logs, Secret Manager IAM, and dependency compatibility rather than claiming completion. + +### Test completed task + +Click **Check my progress** for **Deploy the app to Cloud Run** (25 points). The checkpoints total 100 points; no grader result is asserted. + +--- + +## Solution of Lab + +### Quick + +Use the following two commands in the **authorized temporary lab Cloud Shell**. The URL always points to `main` and will only work **after the Draft PR has been merged**. + +```bash +curl -LO https://raw.githubusercontent.com/ePlus-DEV/storage/refs/heads/main/labs/GSP1306/lab.sh +source lab.sh +``` + +The script detects the project and region, uses the existing lab bucket, generates the Spring Boot app, tests both local responses, configures the sample secret, and deploys Cloud Run. It does not press Google Skills grader buttons. + +### Manual + +Chưa tìm thấy video YouTube hướng dẫn thủ công đã xác minh cho lab này. + +Follow Tasks 1–4 above and consult the [official Google Skills instructions](https://www.skills.google/focuses/121708?parent=catalog). + +## Sources and verification status + +- [Google Skills GSP1306 official lab](https://www.skills.google/focuses/121708?parent=catalog), last tested August 27, 2026. +- [Spring Initializr](https://start.spring.io/). +- [Spring Framework on Google Cloud — Secret Manager](https://googlecloudplatform.github.io/spring-cloud-gcp/reference/html/index.html). +- [Secret Manager documentation](https://cloud.google.com/secret-manager/docs). +- [Cloud Run source deployment](https://cloud.google.com/run/docs/deploying-source-code). + +**static validated only; runtime/grader unverified.** The official lab's Spring Boot version discrepancy (4.0.1 in the command versus 3.3.5 in its explanation) requires review. Maven compatibility, Secret Manager configuration, the pre-provisioned service account, and grader acceptance have not been tested with an authorized lab. Bash syntax and ShellCheck require latest-head GitHub CI. No real credentials are included. This Markdown has not been published to Hashnode. + +Copyright (c) 2026 ePlus.DEV | Unlicense — original automation and explanatory prose. diff --git a/labs/GSP1306/lab.sh b/labs/GSP1306/lab.sh new file mode 100644 index 0000000..cea23d7 --- /dev/null +++ b/labs/GSP1306/lab.sh @@ -0,0 +1,213 @@ +#!/usr/bin/env bash +# GSP1306 — Retrieving Credentials from Secret Manager with Spring Boot +# Copyright (c) 2026 ePlus.DEV | Unlicense +# static validated only; runtime/grader unverified +set -Eeuo pipefail +umask 077 +if [[ -n "${NO_COLOR+x}" ]]; then C='' G='' Y='' R=''; +elif [[ -t 1 || "${FORCE_COLOR:-0}" == 1 ]]; then + C=$'\033[36m'; G=$'\033[32m'; Y=$'\033[33m'; R=$'\033[0m' +else C='' G='' Y='' R=''; fi +info(){ printf '%s[INFO]%s %s\n' "$C" "$R" "$*"; } +ok(){ printf '%s[OK]%s %s\n' "$G" "$R" "$*"; } +warn(){ printf '%s[WARN]%s %s\n' "$Y" "$R" "$*" >&2; } +die(){ printf '[ERROR] %s\n' "$*" >&2; exit 1; } +trap 'warn "Stopped near line $LINENO; grader PASS has not been verified."' ERR +printf '%sePlus.DEV – Starting Execution%s\n' "$C" "$R" +printf 'Copyright (c) 2026 ePlus.DEV | Unlicense\n' +for cmd in gcloud curl unzip python3 grep; do command -v "$cmd" >/dev/null || die "Missing $cmd"; done +PROJECT_ID="${PROJECT_ID:-$(gcloud config get-value project 2>/dev/null)}" +[[ -n "$PROJECT_ID" && "$PROJECT_ID" != '(unset)' ]] || die 'Choose the temporary lab project.' +REGION="${REGION:-$(gcloud config get-value compute/region 2>/dev/null)}" +if [[ -z "$REGION" || "$REGION" == '(unset)' ]]; then + ZONE="$(gcloud config get-value compute/zone 2>/dev/null)" + [[ -n "$ZONE" && "$ZONE" != '(unset)' ]] || die 'Set REGION from the active lab panel.' + REGION="${ZONE%-*}" +fi +[[ "$REGION" =~ ^[a-z]+-[a-z0-9]+[0-9]$ ]] || die 'Invalid region.' +BUCKET_NAME="${BUCKET_NAME:-}" +if [[ -z "$BUCKET_NAME" ]]; then + mapfile -t buckets < <(gcloud storage buckets list --project="$PROJECT_ID" \ + --format='value(name)' | sed -E 's#^gs://##;s#/$##' | sed '/^$/d') + (( ${#buckets[@]} == 1 )) || + die 'Set BUCKET_NAME from the lab panel; bucket discovery is ambiguous.' + BUCKET_NAME="${buckets[0]}" +fi +BUCKET_NAME="${BUCKET_NAME#gs://}"; BUCKET_NAME="${BUCKET_NAME%/}" +gcloud storage buckets describe "gs://$BUCKET_NAME" --project="$PROJECT_ID" >/dev/null || + die 'Lab bucket is not accessible.' +APP_DIR="${APP_DIR:-$HOME/hello-secret-manager}" +RUN_SA="hello-secret-manager-run-sa@$PROJECT_ID.iam.gserviceaccount.com" +info "Project=$PROJECT_ID Region=$REGION Bucket=gs://$BUCKET_NAME" +retry(){ local count="$1" delay="$2" i; shift 2 + for ((i=1;i<=count;i++)); do + if "$@"; then return 0; fi + if ((i/dev/null || + die "Cannot verify $object" +} +test_local(){ local expected="$1" logfile="$2" pid response ready=0 i + if curl -fsS --max-time 2 http://127.0.0.1:8080/ >/dev/null 2>&1; then + die 'Port 8080 is already occupied; stop the other app first.' + fi + ( + cd "$APP_DIR" + timeout --signal=INT 180s ./mvnw -DskipTests spring-boot:run + ) > "$logfile" 2>&1 & + pid="$!" + for ((i=1;i<=36;i++)); do + response="$(curl -fsS --max-time 2 http://127.0.0.1:8080/ 2>/dev/null || true)" + if [[ "$response" == "$expected" ]]; then ready=1; break; fi + sleep 5 + done + kill -INT "$pid" 2>/dev/null || true + wait "$pid" 2>/dev/null || true + (( ready == 1 )) || die "Local Spring Boot did not return $expected; inspect $logfile" +} + +info 'Task 1: create the Spring Boot service and verify its initial response' +if [[ ! -f "$APP_DIR/pom.xml" ]]; then + [[ ! -e "$APP_DIR" ]] || die 'APP_DIR exists without pom.xml; refusing to overwrite it.' + zip_file="$HOME/hello-secret-manager-gsp1306.zip" + retry 3 5 curl -fL --retry 3 https://start.spring.io/starter.zip \ + -d type=maven-project -d packaging=jar -d bootVersion=4.0.1 \ + -d dependencies=web,cloud-gcp -d javaVersion=21 \ + -d name=HelloSecretManager -d artifactId=HelloSecretManager \ + -o "$zip_file" || die 'Spring Initializr could not generate the official starter.' + mkdir -p "$APP_DIR" + unzip -q "$zip_file" -d "$APP_DIR" +fi +[[ -s "$APP_DIR/pom.xml" && -s "$APP_DIR/mvnw" ]] || + die 'Spring starter is incomplete.' +chmod +x "$APP_DIR/mvnw" +controller="$APP_DIR/src/main/java/com/example/HelloSecretManager/HelloSecretManagerController.java" +mkdir -p "$(dirname "$controller")" +if [[ ! -e "$controller" ]]; then + cat > "$controller" <<'JAVA' +package com.example.HelloSecretManager; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class HelloSecretManagerController { + String greeting = "Hi"; + + @GetMapping("/") + public String hello() { + return greeting + " World!"; + } +} +JAVA +fi +if [[ -d /usr/lib/jvm/java-21-openjdk-amd64 ]]; then + export JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64 +fi +if [[ ! -s "$APP_DIR/verify_response.txt" ]]; then + test_local 'Hi World!' "$APP_DIR/verify_response.txt" +fi +upload "$APP_DIR/verify_response.txt" 'verify_response.txt' +upload "$APP_DIR/pom.xml" 'pom.xml' +ok 'Checkpoint: Create a new Spring Boot REST service (30 points)' + +info 'Task 2: create the lab-only sample greeting secret' +gcloud services enable secretmanager.googleapis.com --project="$PROJECT_ID" --quiet +if ! gcloud secrets describe greeting --project="$PROJECT_ID" >/dev/null 2>&1; then + printf '%s' 'Hello' | gcloud secrets create greeting \ + --project="$PROJECT_ID" --replication-policy=automatic --data-file=- +fi +gcloud secrets describe greeting --project="$PROJECT_ID" >/dev/null +ok 'Checkpoint: Configure a secret in Secret Manager (20 points)' + +info 'Task 3: configure Spring Cloud GCP Secret Manager' +python3 - "$APP_DIR" <<'PY' +from pathlib import Path +import sys +root = Path(sys.argv[1]) +pom = root / "pom.xml" +xml = pom.read_text() +starter = "spring-cloud-gcp-starter-secretmanager" +if starter not in xml: + dependency = """ + + com.google.cloud + spring-cloud-gcp-starter-secretmanager + +""" + if xml.count("") != 1: + raise SystemExit("Unexpected pom.xml dependency layout") + pom.write_text(xml.replace("", dependency + " ")) +props = root / "src/main/resources/application.properties" +text = props.read_text() if props.exists() else "" +for line in ("spring.config.import=sm://",): + if line not in text.splitlines(): + text += "\n" + line + "\n" +props.write_text(text) +PY +cat > "$controller" <<'JAVA' +package com.example.HelloSecretManager; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class HelloSecretManagerController { + @Value("${sm://greeting}") + String greeting; + + @GetMapping("/") + public String hello() { + return greeting + " World!"; + } +} +JAVA +if [[ ! -s "$APP_DIR/verify_test_response.txt" ]]; then + test_local 'Hello World!' "$APP_DIR/verify_test_response.txt" +fi +upload "$APP_DIR/verify_test_response.txt" 'verify_test_response.txt' +upload "$controller" 'HelloSecretManagerController.java' +ok 'Checkpoint: Update the app to use the secret greeting (25 points)' + +info 'Task 4: deploy the sample app to Cloud Run using the pre-provisioned service account' +gcloud services enable run.googleapis.com cloudbuild.googleapis.com --project="$PROJECT_ID" --quiet +gcloud iam service-accounts describe "$RUN_SA" --project="$PROJECT_ID" >/dev/null || + die 'Pre-provisioned Cloud Run service account not found.' +gcloud secrets add-iam-policy-binding greeting --project="$PROJECT_ID" \ + --member="serviceAccount:$RUN_SA" --role=roles/secretmanager.secretAccessor --quiet >/dev/null +props="$APP_DIR/src/main/resources/application.properties" +grep -Fxq 'server.port=${PORT:8080}' "$props" || + printf '%s\n' 'server.port=${PORT:8080}' >> "$props" +cat > "$APP_DIR/project.toml" <<'TOML' +[[build.env]] +name = "GOOGLE_RUNTIME_VERSION" +value = "21" +TOML +latest="$(gcloud secrets versions access latest --secret=greeting --project="$PROJECT_ID" 2>/dev/null || true)" +if [[ "$latest" != Greetings ]]; then + printf '%s' 'Greetings' | gcloud secrets versions add greeting --project="$PROJECT_ID" --data-file=- +fi +( + cd "$APP_DIR" + retry 3 15 gcloud run deploy hello-secret-manager --project="$PROJECT_ID" \ + --region="$REGION" --allow-unauthenticated --service-account="$RUN_SA" \ + --source=. --quiet +) +service_url="$(gcloud run services describe hello-secret-manager --project="$PROJECT_ID" \ + --region="$REGION" --format='value(status.url)')" +[[ "$service_url" == https://* ]] || die 'Cloud Run service URL is missing.' +ready=0 +for ((i=1;i<=18;i++)); do + response="$(curl -fsS --max-time 10 "$service_url" 2>/dev/null || true)" + if [[ "$response" == 'Greetings World!' ]]; then ready=1; break; fi + sleep 10 +done +(( ready == 1 )) || die 'Cloud Run did not return the expected lab-only greeting.' +ok 'Checkpoint: Deploy the app to Cloud Run (25 points)' +info 'Do not use real secrets with this intentionally public demo endpoint.' +info 'static validated only; runtime/grader unverified'