diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..c61b102 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,66 @@ +name: Docker + +on: + push: + branches: [ "main" ] + paths-ignore: + - README.md + - score.yaml + +env: + # Use docker.io for Docker Hub if empty + REGISTRY: ghcr.io + # github.repository as / + IMAGE_NAME: ${{ github.repository }} + +jobs: + build: + + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + # Set up BuildKit Docker container builder to be able to build + # multi-platform images and export cache + # https://github.com/docker/setup-buildx-action + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0 + + # Login against a Docker registry except on PR + # https://github.com/docker/login-action + - name: Log into registry ${{ env.REGISTRY }} + uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + # Extract metadata (tags, labels) for Docker + # https://github.com/docker/metadata-action + - name: Extract Docker metadata + id: meta + uses: docker/metadata-action@96383f45573cb7f253c731d3b3ab81c87ef81934 # v5.0.0 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=sha + + # Build and push Docker image with Buildx (don't push on PR) + # https://github.com/docker/build-push-action + - name: Build and push Docker image + id: build-and-push + uses: docker/build-push-action@0565240e2d4ab88bba5387d719585280857ece09 # v5.0.0 + with: + context: . + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.gitignore b/.gitignore index 3b735ec..1c7f4dd 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,9 @@ # Go workspace file go.work + +# Score +.score-compose/ +compose.yaml +.score-k8s/ +manifests.yaml diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..998d4a3 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,15 @@ +FROM golang:1.22-alpine AS builder + +WORKDIR /app +ENV CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GOWORK=off +COPY go.mod go.sum ./ +RUN --mount=type=cache,target=/go/pkg/mod \ + go mod download +RUN --mount=target=. \ + --mount=type=cache,target=/go/pkg/mod \ + --mount=type=cache,target=/root/.cache/go-build \ + go build -o /sample . + +FROM gcr.io/distroless/static as final +COPY --from=builder /sample . +ENTRYPOINT ["/sample"] diff --git a/README.md b/README.md index f5232c7..ccd4d03 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,43 @@ # sample-app-gif -The sample app used for the gif and video embedded in the docs + +The sample app used for the gif and video embedded in the docs. The purpose is for it to be complex enough to show the core value propositions of Score: + +- Convert to more than one deployment format +- Provision ingress and database resources with dynamic credentials +- Launch locally or remotely +- Show evidence that the request was routed correctly and hit the target database + +All with as few lines of code as possible. We're going to for simple and short rather than _correct_. + +## How to record the sample gif + +Use . + +Preparation: + +``` +$ rm -rfv .score-compose .score-k8s compose.yaml manifests.yaml +$ docker pull ghcr.io/score-spec/sample-app-gif:main + +$ kind create cluster +$ kubectl use-context kind-kind +$ kubectl --context kind-kind apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.0.0/standard-install.yam +$ helm --kube-context kind-kind install ngf oci://ghcr.io/nginxinc/charts/nginx-gateway-fabric --create-namespace -n nginx-gateway --set service.type=ClusterIP +``` + +Instructions to record: + +``` +$ score-compose init +$ score-compose generate score.yaml +$ docker compose up -d +$ curl http://$(score-compose resources get-outputs 'dns.default#sample.dns' --format '{{ .host }}'):8080/ -i +$ docker logs sample-app-gif-sample-main-1 +$ docker compose down -v + +$ score-k8s init +$ score-k8s generate score.yaml +$ kubectl apply -f manifests.yaml +$ kubectl wait deployments/sample --for=condition=Ready +$ curl http://$(score-compose resources get-outputs 'dns.default#sample.dns' --format '{{ .host }}') +``` diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..dbd5485 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module github.com/score-spec/sample-app-gif + +go 1.22.0 + +require github.com/lib/pq v1.10.9 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..aeddeae --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= diff --git a/main.go b/main.go new file mode 100644 index 0000000..d0acc76 --- /dev/null +++ b/main.go @@ -0,0 +1,50 @@ +package main + +import ( + "context" + "database/sql" + "fmt" + "log" + "net/http" + "os" + + _ "github.com/lib/pq" +) + +const sqlConnectionStringVar = "PG_CONNECTION_STRING" +const listenAddress = ":8080" + +func main() { + // First, get the connection string and attempt a connection or fail + connStr := os.Getenv(sqlConnectionStringVar) + conn, err := sql.Open("postgres", connStr) + if err != nil { + log.Fatalf("failed to open the database %s=%s: %v", sqlConnectionStringVar, connStr, err) + } + defer conn.Close() + + // Second, wait until we get a successful connection to the db + if err := conn.PingContext(context.TODO()); err != nil { + log.Fatalf("failed to connect to database %s=%s: %v", sqlConnectionStringVar, connStr, err) + } + log.Printf("successfully connected to database %s=%s", sqlConnectionStringVar, connStr) + + // Third, link the handler function + http.HandleFunc("GET /{$}", func(writer http.ResponseWriter, request *http.Request) { + var versionOutput string + if err := conn.QueryRowContext(request.Context(), "SELECT version()").Scan(&versionOutput); err != nil { + writer.WriteHeader(http.StatusInternalServerError) + _, _ = fmt.Fprintf(writer, "Failed to get sql version: %v", err) + log.Printf("%s %s %s status=500: %v", request.Host, request.Method, request.RequestURI, err) + } else { + _, _ = fmt.Fprintf(writer, "SQL VERSION: %s", versionOutput) + log.Printf("%s %s %s status=200", request.Host, request.Method, request.RequestURI) + } + }) + + // Finally, run the http server + log.Print("starting server") + if err := http.ListenAndServe(listenAddress, http.DefaultServeMux); err != nil { + log.Fatal(err.Error()) + } +} diff --git a/score.yaml b/score.yaml new file mode 100644 index 0000000..4b211aa --- /dev/null +++ b/score.yaml @@ -0,0 +1,26 @@ +apiVersion: score.dev/v1b1 +metadata: + name: sample + +containers: + main: + image: ghcr.io/score-spec/sample-app-gif:main + variables: + PG_CONNECTION_STRING: "postgresql://${resources.db.username}:${resources.db.password}@${resources.db.host}:${resources.db.port}/${resources.db.database}?sslmode=disable" + +service: + ports: + web: + port: 8080 + +resources: + db: + type: postgres + dns: + type: dns + route: + type: route + params: + host: ${resources.dns.host} + path: / + port: 8080