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}"