From c92a46c49d6d73f349762760572e6eca714f64d4 Mon Sep 17 00:00:00 2001 From: Jason Swank Date: Mon, 11 May 2026 12:30:42 -0400 Subject: [PATCH] Initial revision --- Dockerfile | 25 ++++++++++++ Taskfile.yaml | 106 ++++++++++++++++++++++++++++++++++++++++++++++++++ go.mod | 5 +++ go.sum | 2 + main.go | 50 ++++++++++++++++++++++++ 5 files changed, 188 insertions(+) create mode 100644 Dockerfile create mode 100644 Taskfile.yaml create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..35be0d7 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,25 @@ +# Use the official Golang image to create a build artifact. +FROM golang:1.25.9 as builder + +# Create and change to the app directory. +WORKDIR /app + +# Retrieve application dependencies. +COPY go.* ./ +RUN go mod download + +# Copy local code to the container image. +COPY . ./ + +# Build the binary. +RUN CGO_ENABLED=0 GOOS=linux go build -v -o server + +# Use a Docker multi-stage build to create a lean production image. +FROM alpine:3.23 +RUN apk add --no-cache ca-certificates + +# Copy the binary to the production image from the builder stage. +COPY --from=builder /app/server /server + +# Run the web service on container startup. +CMD ["/server"] diff --git a/Taskfile.yaml b/Taskfile.yaml new file mode 100644 index 0000000..4bfe8a5 --- /dev/null +++ b/Taskfile.yaml @@ -0,0 +1,106 @@ +# This taskfile handles building and deploying the petname-service +version: '3' + +vars: + DEFAULT_PROJECT_ID: "infinite-deck-479516-g6" + REGION: "us-east4" + SERVICE_NAME: "petname" + DOMAIN: "proto-hype.net" + +tasks: + login: + desc: Login w/ gcloud + summary: | + Initializes gcloud and sets the project, region, and zone. You can optionally provide + a project ID as an argument, otherwise it defaults to the pre-existing project. + + Example: task init + cmds: + - | + PROJECT_ID="${1:-{{.DEFAULT_PROJECT_ID}}}" + gcloud auth login --update-adc + gcloud config set project "$PROJECT_ID" + gcloud config set compute/region {{.REGION}} + gcloud config set compute/zone {{.REGION}}-c + gcloud auth application-default set-quota-project "$PROJECT_ID" + + setup: + desc: Enable required Google Cloud APIs + summary: | + Enables the Cloud Run, Cloud Build, and Artifact Registry APIs required for deployment. + cmds: + - | + gcloud services enable run.googleapis.com cloudbuild.googleapis.com artifactregistry.googleapis.com + + deploy: + desc: Deploy the service to Google Cloud Run + summary: | + Deploys the source code directly to Cloud Run using Cloud Build (Option A). + + Example: task deploy + cmds: + - | + gcloud run deploy {{.SERVICE_NAME}} \ + --source . \ + --region {{.REGION}} \ + --allow-unauthenticated + + map-domain: + desc: Map a custom domain to the Cloud Run service + summary: | + Map . to the deployed service. This requires the service + to be deployed in a region which support domain-mappints (like us-east4). + + Example: task map-domain + cmds: + - | + gcloud beta run domain-mappings create \ + --service {{.SERVICE_NAME }} \ + --domain {{ .SERVICE_NAME }}.{{ .DOMAIN }} \ + --region {{.REGION}} + + logs: + desc: Tail the real-time logs for the service + summary: | + Streams the live logs from Cloud Run to your terminal. Useful for debugging + or watching traffic hit the service. Press Ctrl+C to stop. + + Example: task logs + cmds: + - | + gcloud beta run services logs tail {{.SERVICE_NAME}} \ + --region {{.REGION}} + + test: + desc: Test the deployed service + summary: | + Tests the deployed service by sending a request to the mapped domain. Make sure + to replace and with your actual service name and domain. + + Example: task test + silent: true + cmds: + - | + function test_service() { + local path="$1" + + # Use curl to capture both HTTP status code and response body + local response=$(curl -s -w "%{http_code}" https://{{.SERVICE_NAME}}.{{.DOMAIN}}/$path) + + # Extract status code (last 3 chars) and body (everything else) + local status_code="${response:${#response}-3}" + local body="${response:0:${#response}-3}" + + if [ "$status_code" -lt 399 ]; then + printf "GET /%s -> [SUCCESS] Status: %s, Body: %s\n" "$path" "$status_code" "$body" + else + printf "GET /%s -> [FAILURE] Status: %s, Body: %s\n" "$path" "$status_code" "$body" + fi + + # Explicitly return 0 so the task does not fail if a test fails + return 0 + } + + test_service "" + test_service "?words=4" + test_service "?separator=." diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..e50a465 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module petname-service + +go 1.25.9 + +require github.com/dustinkirkland/golang-petname v0.0.0-20231002161417-6a283f1aaaf2 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..73539e1 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/dustinkirkland/golang-petname v0.0.0-20231002161417-6a283f1aaaf2 h1:S6Dco8FtAhEI/qkg/00H6RdEGC+MCy5GPiQ+xweNRFE= +github.com/dustinkirkland/golang-petname v0.0.0-20231002161417-6a283f1aaaf2/go.mod h1:8AuBTZBRSFqEYBPYULd+NN474/zZBLP+6WeT5S9xlAc= diff --git a/main.go b/main.go new file mode 100644 index 0000000..4614ad4 --- /dev/null +++ b/main.go @@ -0,0 +1,50 @@ +package main + +import ( + "fmt" + "log" + "math/rand" + "net/http" + "os" + "strconv" + "time" + + "github.com/dustinkirkland/golang-petname" +) + +func main() { + // Seed the random number generator + rand.Seed(time.Now().UnixNano()) + + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + // Default to 2 words, can be overridden by query param "words" + words := 2 + if wParam := r.URL.Query().Get("words"); wParam != "" { + if parsed, err := strconv.Atoi(wParam); err == nil && parsed > 0 { + words = parsed + } + } + + // Default separator is hyphen, can be overridden by query param "separator" + separator := "-" + if sParam := r.URL.Query().Get("separator"); sParam != "" { + separator = sParam + } + + // Generate petname + name := petname.Generate(words, separator) + + w.Header().Set("Content-Type", "text/plain") + fmt.Fprint(w, name) + }) + + port := os.Getenv("PORT") + if port == "" { + port = "8080" + } + + log.Printf("Listening on port %s", port) + if err := http.ListenAndServe(":"+port, nil); err != nil { + log.Fatal(err) + } +}