diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..baffffc --- /dev/null +++ b/Dockerfile @@ -0,0 +1,14 @@ +FROM golang:1.23 AS build + +WORKDIR /usr/src/app + +COPY go.mod ./ +RUN go mod download && go mod verify + +COPY main.go index.html . +RUN CGO_ENABLED=0 go build -v -o /usr/local/bin/app ./... + +FROM debian:bookworm-slim +COPY --from=build /usr/local/bin/app /usr/local/bin/ + +CMD ["/usr/local/bin/app", "-addr", "0.0.0.0"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a2eedda --- /dev/null +++ b/LICENSE @@ -0,0 +1,13 @@ +Copyright (c) 2024 Cloudflare, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..6a6ff81 --- /dev/null +++ b/README.md @@ -0,0 +1,12 @@ +# Cloudflare Container Example + +This repo contains a simple example application which can be run in Cloudflare's +container runtime. + +The example app serves a single HTML page with an incrementing visit counter and a +toggleable health status. The health status can be modified by sending a POST +request to `/health` with the `status` query parameter set to either `healthy` +or `unhealthy`. The health status can be queried by sending a GET request to +`/health`: if the application is healthy it will return an empty response with +a 200 status code; otherwise, it will send an empty response with a 503 status +code. diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..256cb8d --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/cloudflare/hello-world-container-image + +go 1.23.2 diff --git a/index.html b/index.html new file mode 100644 index 0000000..f48ed34 --- /dev/null +++ b/index.html @@ -0,0 +1,24 @@ + + + + Cloudflare Container Example + + +

Cloudflare Container Example

+

+ This page is rendered and served by an application running on Cloudflare's container platform. + It has been visited {{ .Visits }} time(s). +

+ +
+

+ Health status: + {{ if .Healthy }} + HEALTHY + {{ else }} + UNHEALTHY + {{ end }} +

+ +
+ diff --git a/main.go b/main.go new file mode 100644 index 0000000..a1233ff --- /dev/null +++ b/main.go @@ -0,0 +1,84 @@ +package main + +import ( + _ "embed" + "flag" + "fmt" + "html/template" + "log" + "net/http" +) + +//go:embed index.html +var content string +var templates = template.Must(template.New("index.html").Parse(content)) + +type State struct { + Visits int + Healthy bool +} + +func index(s *State) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if err := templates.Execute(w, s); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + + s.Visits += 1 + } +} + +func health(s *State) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + if s.Healthy { + w.WriteHeader(http.StatusOK) + } else { + w.WriteHeader(http.StatusServiceUnavailable) + } + case http.MethodPost: + if err := r.ParseForm(); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + params := r.URL.Query() + h, ok := params["h"] + if !ok { + w.WriteHeader(http.StatusNotModified) + return + } + + if len(h) != 1 || (h[0] != "true" && h[0] != "false") { + w.WriteHeader(http.StatusBadRequest) + return + } + + healthy := h[0] == "true" + if healthy == s.Healthy { + w.WriteHeader(http.StatusNotModified) + return + } + + s.Healthy = healthy + http.Redirect(w, r, "/", http.StatusSeeOther) + default: + w.WriteHeader(http.StatusMethodNotAllowed) + } + } +} + +func main() { + addr := flag.String("addr", "127.0.0.1", "address to listen on") + port := flag.Int("port", 8000, "TCP port to listen on") + + flag.Parse() + + s := State{Healthy: true} + http.HandleFunc("/", index(&s)) + http.HandleFunc("/health", health(&s)) + + log.Printf("Listening on %s:%d...", *addr, *port) + log.Fatal(http.ListenAndServe(fmt.Sprintf("%s:%d", *addr, *port), nil)) +}