Arcade Voyage: App Modernization | GSP761
Solution for Arcade Voyage: App Modernization | GSP761. 1 lab: GSP761. Fast copy-paste commands for Google Cloud.
GSP761 — Developing a REST API with Go and Cloud Run
Estimated time: 20 minutes
# 🚀 Build a Serverless App with Cloud Run and Firestore > ⚠️ **Disclaimer:** This is an independent, community-made walkthrough created for educational purposes, hands-on practice, and Google Cloud certification preparation. This guide is designed to help learners understand Google Cloud services and complete practical exercises. Always attempt the lab yourself first and follow Google Cloud Skills Boost / Qwiklabs Terms of Service. This walkthrough is not affiliated with or endorsed by Google,
clear
CYAN='\e[1;36m'
BLUE='\e[1;34m'
RESET='\e[0m'
BOLD='\e[1m'
GREEN='\e[1;32m'
YELLOW='\e[1;33m'
MAGENTA='\e[1;35m'
echo -e "${CYAN}${BOLD}"
cat << "EOF"
____ _ _ _ __ ___
/ __ \ | | (_) | / _| / _ \
| | | |_ __| |__ _| |_ ___ | |_ | | | |_ __ ___
| | | | '__| '_ \| | __| / _ \ | _|| | | | '_ \/ __|
| |__| | | | |_) | | |_ | (_) | | | | |_| | |_) \__ \
\____/|_| |_.__/|_|\__| \___/ |_| \___/| .__/|___/
| |
|_|
EOF
echo -e "${RESET}"
echo -e "${BLUE}${BOLD}╔════════════════════════════════════════════════════════════╗${RESET}"
echo -e "${BLUE}${BOLD}║ 🚀 BROUGHT TO YOU BY ORBIT OF OPS ║${RESET}"
echo -e "${BLUE}${BOLD}╚════════════════════════════════════════════════════════════╝${RESET}\n"
echo -e "${MAGENTA}${BOLD}Please check your lab instructions panel for your assigned Region.${RESET}"
read -p "Enter your Region (e.g., us-central1): " REGION
echo ""
export PROJECT_ID=$(gcloud config get-value core/project 2>/dev/null)
gcloud config set compute/region "${REGION}" --quiet >/dev/null 2>&1
echo -e "${CYAN}Task 1: Enabling Cloud Build & Cloud Run APIs...${RESET}"
gcloud services enable run.googleapis.com cloudbuild.googleapis.com --quiet
echo -e "${CYAN}Task 2a: Cloning repository and staging V1 Code...${RESET}"
cd ~
rm -rf pet-theory
git clone https://github.com/rosera/pet-theory.git >/dev/null 2>&1
cd pet-theory/lab08
cat << 'EOF' > main.go
package main
import (
"fmt"
"log"
"net/http"
"os"
)
func main() {
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
http.HandleFunc("/v1/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "{status: 'running'}")
})
log.Println("Pets REST API listening on port", port)
if err := http.ListenAndServe(":"+port, nil); err != nil {
log.Fatalf("Error launching Pets REST API server: %v", err)
}
}
EOF
cat << 'EOF' > Dockerfile
FROM gcr.io/distroless/base-debian12
WORKDIR /usr/src/app
COPY server .
CMD [ "/usr/src/app/server" ]
EOF
echo -e "${CYAN}Task 2b: Compiling Go binary...${RESET}"
go build -o server
echo -e "${CYAN}Task 2c: Creating Artifact Registry & Building V1 Container...${RESET}"
gcloud artifacts repositories create my-repo --repository-format=docker --location="${REGION}" --description="Docker repository for REST API" --quiet || true
gcloud builds submit --tag "${REGION}-docker.pkg.dev/${PROJECT_ID}/my-repo/rest-api:0.1" --quiet
echo -e "${CYAN}Task 2d: Deploying V1 to Cloud Run...${RESET}"
gcloud run deploy rest-api --image "${REGION}-docker.pkg.dev/${PROJECT_ID}/my-repo/rest-api:0.1" --region "${REGION}" --allow-unauthenticated --max-instances=2 --quiet
echo -e "${CYAN}Task 3: Provisioning Firestore Database and Importing Customer Data...${RESET}"
# Initialize Native Firestore in the assigned region
gcloud firestore databases create --location="${REGION}" --type=firestore-native --quiet || true
# Import the test dataset
gcloud storage buckets create gs://${PROJECT_ID}-customer --default-storage-class=standard --location="${REGION}" --quiet
gcloud storage cp -r gs://spls/gsp645/2019-10-06T20:10:37_43617 gs://${PROJECT_ID}-customer --quiet
gcloud beta firestore import gs://${PROJECT_ID}-customer/2019-10-06T20:10:37_43617/ --quiet
echo -e "\n${GREEN}${BOLD}✅ Phase 1 Complete! Click 'Check my progress' on the first 3 tasks, then proceed to Command 2.${RESET}"CYAN='\e[1;36m'
BLUE='\e[1;34m'
RESET='\e[0m'
BOLD='\e[1m'
GREEN='\e[1;32m'
YELLOW='\e[1;33m'
echo -e "${BLUE}${BOLD}╔════════════════════════════════════════════════════════════╗${RESET}"
echo -e "${BLUE}${BOLD}║ 🚀 COMMAND 2 OF 2: FIRESTORE INTEGRATION & V2 DEPLOY ║${RESET}"
echo -e "${BLUE}${BOLD}╚════════════════════════════════════════════════════════════╝${RESET}\n"
export PROJECT_ID=$(gcloud config get-value core/project 2>/dev/null)
export REGION=$(gcloud config get-value compute/region 2>/dev/null)
cd ~/pet-theory/lab08
echo -e "${CYAN}Task 4: Injecting V2 Go Code and linking to Firestore...${RESET}"
cat << 'EOF' > main.go
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"cloud.google.com/go/firestore"
"github.com/gorilla/handlers"
"github.com/gorilla/mux"
"google.golang.org/api/iterator"
)
var client *firestore.Client
func main() {
var err error
ctx := context.Background()
client, err = firestore.NewClient(ctx, "PROJECT_ID_PLACEHOLDER")
if err != nil {
log.Fatalf("Error initializing Cloud Firestore client: %v", err)
}
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
r := mux.NewRouter()
r.HandleFunc("/v1/", rootHandler)
r.HandleFunc("/v1/customer/{id}", customerHandler)
log.Println("Pets REST API listening on port", port)
cors := handlers.CORS(
handlers.AllowedHeaders([]string{"X-Requested-With", "Authorization", "Origin"}),
handlers.AllowedOrigins([]string{"https://storage.googleapis.com"}),
handlers.AllowedMethods([]string{"GET", "HEAD", "POST", "OPTIONS", "PATCH", "CONNECT"}),
)
if err := http.ListenAndServe(":"+port, cors(r)); err != nil {
log.Fatalf("Error launching Pets REST API server: %v", err)
}
}
func rootHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "{status: 'running'}")
}
func customerHandler(w http.ResponseWriter, r *http.Request) {
id := mux.Vars(r)["id"]
ctx := context.Background()
customer, err := getCustomer(ctx, id)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprintf(w, `{"status": "fail", "data": '%s'}`, err)
return
}
if customer == nil {
w.WriteHeader(http.StatusNotFound)
msg := fmt.Sprintf("`Customer \"%s\" not found`", id)
fmt.Fprintf(w, fmt.Sprintf(`{"status": "fail", "data": {"title": %s}}`, msg))
return
}
amount, err := getAmounts(ctx, customer)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprintf(w, `{"status": "fail", "data": "Unable to fetch amounts: %s"}`, err)
return
}
data, err := json.Marshal(amount)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprintf(w, `{"status": "fail", "data": "Unable to fetch amounts: %s"}`, err)
return
}
fmt.Fprintf(w, fmt.Sprintf(`{"status": "success", "data": %s}`, data))
}
type Customer struct {
Email string `firestore:"email"`
ID string `firestore:"id"`
Name string `firestore:"name"`
Phone string `firestore:"phone"`
}
func getCustomer(ctx context.Context, id string) (*Customer, error) {
query := client.Collection("customers").Where("id", "==", id)
iter := query.Documents(ctx)
var c Customer
for {
doc, err := iter.Next()
if err == iterator.Done {
break
}
if err != nil {
return nil, err
}
err = doc.DataTo(&c)
if err != nil {
return nil, err
}
}
return &c, nil
}
func getAmounts(ctx context.Context, c *Customer) (map[string]int64, error) {
if c == nil {
return map[string]int64{}, fmt.Errorf("Customer should be non-nil: %v", c)
}
result := map[string]int64{
"proposed": 0,
"approved": 0,
"rejected": 0,
}
query := client.Collection(fmt.Sprintf("customers/%s/treatments", c.Email))
if query == nil {
return map[string]int64{}, fmt.Errorf("Query is nil: %v", c)
}
iter := query.Documents(ctx)
for {
doc, err := iter.Next()
if err == iterator.Done {
break
}
if err != nil {
return nil, err
}
treatment := doc.Data()
result[treatment["status"].(string)] += treatment["cost"].(int64)
}
return result, nil
}
EOF
# Safely inject the active Project ID into the main.go code
sed -i "s/PROJECT_ID_PLACEHOLDER/${PROJECT_ID}/g" main.go
echo -e "${CYAN}Task 7a: Compiling updated Go binary...${RESET}"
go build -o server
echo -e "${CYAN}Task 7b: Building V2 Container Revision (0.2)...${RESET}"
gcloud builds submit --tag "${REGION}-docker.pkg.dev/${PROJECT_ID}/my-repo/rest-api:0.2" --quiet
echo -e "\n${GREEN}${BOLD}🎉 AUTOMATION COMPLETE! Click 'Check my progress' on the final task to collect your 100% score!${RESET}"
echo -e "${GREEN}${BOLD}# ─────────────────────────────────────────────────────────────${RESET}"
echo -e "${GREEN}${BOLD}# Lab cleared? Like the video and subscribe to Orbit of Ops 🚀${RESET}"
echo -e "${GREEN}${BOLD}# youtube.com/@orbitofops${RESET}"
echo -e "${GREEN}${BOLD}# ─────────────────────────────────────────────────────────────${RESET}"