diff --git a/cmd/gerritbot/Dockerfile b/cmd/gerritbot/Dockerfile index 5acad1c4ad..74a2432d7f 100644 --- a/cmd/gerritbot/Dockerfile +++ b/cmd/gerritbot/Dockerfile @@ -37,7 +37,7 @@ COPY . /go/src/golang.org/x/build/ RUN go install golang.org/x/build/cmd/gerritbot FROM alpine -LABEL maintainer "golang-dev@googlegroups.com" +LABEL maintainer="golang-dev@googlegroups.com" # See https://github.com/golang/go/issues/23705 for why tini is needed RUN apk add --no-cache git tini RUN git config --global user.email "letsusegerrit@gmail.com" diff --git a/cmd/makemac/Dockerfile b/cmd/makemac/Dockerfile new file mode 100644 index 0000000000..c3fc298c45 --- /dev/null +++ b/cmd/makemac/Dockerfile @@ -0,0 +1,38 @@ +# Copyright 2023 The Go Authors. All rights reserved. +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file. + +FROM golang:1.21-bookworm AS build +LABEL maintainer="golang-dev@googlegroups.com" + +COPY go.mod /go/src/golang.org/x/build/go.mod +COPY go.sum /go/src/golang.org/x/build/go.sum + +WORKDIR /go/src/golang.org/x/build + +# Download module dependencies to improve speed of re-building the +# Docker image during minor code changes. +RUN go mod download + +COPY . /go/src/golang.org/x/build/ + +RUN go install golang.org/x/build/cmd/makemac + +FROM debian:bookworm +LABEL maintainer="golang-dev@googlegroups.com" + +# netbase and ca-certificates are needed for dialing TLS. +# The rest are useful for debugging if somebody needs to exec into the container. +RUN apt-get update && apt-get install -y \ + --no-install-recommends \ + netbase \ + ca-certificates \ + curl \ + strace \ + procps \ + lsof \ + psmisc \ + && rm -rf /var/lib/apt/lists/* + +COPY --from=build /go/bin/makemac / +ENTRYPOINT ["/makemac"] diff --git a/cmd/makemac/Makefile b/cmd/makemac/Makefile new file mode 100644 index 0000000000..711401c35f --- /dev/null +++ b/cmd/makemac/Makefile @@ -0,0 +1,20 @@ +# Copyright 2023 The Go Authors. All rights reserved. +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file. + +MUTABLE_VERSION ?= latest +VERSION ?= $(shell git rev-parse --short HEAD) + +IMAGE_PROD := gcr.io/symbolic-datum-552/makemac + +docker-prod: + docker build -f Dockerfile --force-rm --tag=$(IMAGE_PROD):$(VERSION) ../.. + docker tag $(IMAGE_PROD):$(VERSION) $(IMAGE_PROD):$(MUTABLE_VERSION) + +push-prod: docker-prod + docker push $(IMAGE_PROD):$(MUTABLE_VERSION) + docker push $(IMAGE_PROD):$(VERSION) + +deploy-prod: push-prod + go install golang.org/x/build/cmd/xb + xb --prod kubectl --namespace prod set image deployment/makemac-deployment makemac=$(IMAGE_PROD):$(VERSION) diff --git a/cmd/makemac/deployment-prod.yaml b/cmd/makemac/deployment-prod.yaml new file mode 100644 index 0000000000..55dc07488d --- /dev/null +++ b/cmd/makemac/deployment-prod.yaml @@ -0,0 +1,39 @@ +# Copyright 2023 The Go Authors. All rights reserved. +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file. + +apiVersion: apps/v1 +kind: Deployment +metadata: + namespace: prod + name: makemac-deployment +spec: + selector: + matchLabels: + app: makemac + template: + metadata: + labels: + app: makemac + spec: + serviceAccountName: makemac + containers: + - name: makemac + image: gcr.io/symbolic-datum-552/makemac:latest + imagePullPolicy: Always + command: ["/makemac", "-api-key=secret:macservice-api-key"] + resources: + requests: + cpu: "1" + memory: "1Gi" + limits: + cpu: "2" + memory: "2Gi" +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + namespace: prod + name: makemac + annotations: + iam.gke.io/gcp-service-account: makemac@symbolic-datum-552.iam.gserviceaccount.com diff --git a/cmd/makemac/main.go b/cmd/makemac/main.go new file mode 100644 index 0000000000..a61e1220fb --- /dev/null +++ b/cmd/makemac/main.go @@ -0,0 +1,78 @@ +// Copyright 2023 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Command makemac ensures that MacService instances continue running. +// Currently, it simply renews any existing leases. +package main + +import ( + "context" + "flag" + "log" + "time" + + "golang.org/x/build/internal/macservice" + "golang.org/x/build/internal/secret" +) + +var ( + apiKey = secret.Flag("api-key", "MacService API key") + period = flag.Duration("period", 2*time.Hour, "How often to check leases. As a special case, -period=0 checks exactly once and then exits") +) + +const renewDuration = "86400s" // 24h + +func main() { + secret.InitFlagSupport(context.Background()) + flag.Parse() + + c := macservice.NewClient(*apiKey) + + // Always check once at startup. + checkAndRenewLeases(c) + + if *period == 0 { + // User only wants a single check. We're done. + return + } + + t := time.NewTicker(*period) + for range t.C { + checkAndRenewLeases(c) + } +} + +func checkAndRenewLeases(c *macservice.Client) { + log.Printf("Renewing leases...") + + resp, err := c.Find(macservice.FindRequest{ + VMResourceNamespace: macservice.Namespace{ + CustomerName: "golang", + }, + }) + if err != nil { + log.Printf("Error finding leases: %v", err) + return + } + + if len(resp.Instances) == 0 { + log.Printf("No leases found") + return + } + + for _, i := range resp.Instances { + log.Printf("Renewing lease ID: %s; currently expires: %v...", i.Lease.LeaseID, i.Lease.Expires) + + rr, err := c.Renew(macservice.RenewRequest{ + LeaseID: i.Lease.LeaseID, + Duration: renewDuration, + }) + if err == nil { + // Extra spaces to make fields line up with the message above. + log.Printf("Renewed lease ID: %s; now expires: %v", i.Lease.LeaseID, rr.Expires) + } else { + log.Printf("Error renewing lease ID: %s: %v", i.Lease.LeaseID, err) + } + } +} diff --git a/cmd/pubsubhelper/Dockerfile b/cmd/pubsubhelper/Dockerfile index cefc2f9bf3..02205d0780 100644 --- a/cmd/pubsubhelper/Dockerfile +++ b/cmd/pubsubhelper/Dockerfile @@ -3,7 +3,7 @@ # license that can be found in the LICENSE file. FROM golang:1.20-bookworm AS build -LABEL maintainer "golang-dev@googlegroups.com" +LABEL maintainer="golang-dev@googlegroups.com" RUN mkdir /gocache ENV GOCACHE /gocache @@ -27,7 +27,7 @@ COPY . /go/src/golang.org/x/build/ RUN go install golang.org/x/build/cmd/pubsubhelper FROM debian:bookworm -LABEL maintainer "golang-dev@googlegroups.com" +LABEL maintainer="golang-dev@googlegroups.com" # netbase and ca-certificates are needed for dialing TLS. # The rest are useful for debugging if somebody needs to exec into the container. diff --git a/internal/macservice/client.go b/internal/macservice/client.go new file mode 100644 index 0000000000..3d4694cf02 --- /dev/null +++ b/internal/macservice/client.go @@ -0,0 +1,88 @@ +// Copyright 2023 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package macservice defines the client API for MacService. +package macservice + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" +) + +const baseURL = "https://macservice-pa.googleapis.com/v1alpha1/" + +// Client is a MacService client. +type Client struct { + apiKey string + + client *http.Client +} + +// NewClient creates a MacService client, authenticated with the provided API +// key. +func NewClient(apiKey string) *Client { + return &Client{ + apiKey: apiKey, + client: http.DefaultClient, + } +} + +func (c *Client) do(method, endpoint string, input, output any) error { + var buf bytes.Buffer + enc := json.NewEncoder(&buf) + if err := enc.Encode(input); err != nil { + return fmt.Errorf("error encoding request: %w", err) + } + + req, err := http.NewRequest(method, baseURL+endpoint, &buf) + if err != nil { + return fmt.Errorf("error building request: %w", err) + } + req.Header.Add("Content-Type", "application/json") + req.Header.Add("x-goog-api-key", c.apiKey) + + resp, err := c.client.Do(req) + if err != nil { + return fmt.Errorf("error sending request: %w", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("error reading response body: %w", err) + } + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("response error %s: %s", resp.Status, body) + } + + if json.Unmarshal(body, output); err != nil { + return fmt.Errorf("error decoding response: %w; body: %s", err, body) + } + + return nil +} + +// Renew updates the expiration time of a lease. Note that +// RenewRequest.Duration is the lease duration from now, not from the current +// lease expiration time. +func (c *Client) Renew(req RenewRequest) (RenewResponse, error) { + var resp RenewResponse + if err := c.do("POST", "leases:renew", req, &resp); err != nil { + return RenewResponse{}, fmt.Errorf("error sending request: %w", err) + } + return resp, nil +} + +// Find searches for leases. +func (c *Client) Find(req FindRequest) (FindResponse, error) { + var resp FindResponse + if err := c.do("POST", "leases:find", req, &resp); err != nil { + return FindResponse{}, fmt.Errorf("error sending request: %w", err) + } + return resp, nil +} diff --git a/internal/macservice/leases.go b/internal/macservice/leases.go new file mode 100644 index 0000000000..59e91c13c0 --- /dev/null +++ b/internal/macservice/leases.go @@ -0,0 +1,53 @@ +// Copyright 2023 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package macservice + +import ( + "time" +) + +// These are minimal definitions. Many fields have been omitted since we don't +// need them yet. + +type RenewRequest struct { + LeaseID string `json:"leaseId"` + + // Duration is ultimately a Duration protobuf message. + // + // https://pkg.go.dev/google.golang.org/protobuf@v1.31.0/types/known/durationpb#hdr-JSON_Mapping: + // "In JSON format, the Duration type is encoded as a string rather + // than an object, where the string ends in the suffix "s" (indicating + // seconds) and is preceded by the number of seconds, with nanoseconds + // expressed as fractional seconds." + Duration string `json:"duration"` +} + +type RenewResponse struct { + Expires time.Time `json:"expires"` +} + +type FindRequest struct { + VMResourceNamespace Namespace `json:"vmResourceNamespace"` +} + +type FindResponse struct { + Instances []Instance `json:"instances"` +} + +type Namespace struct { + CustomerName string `json:"customerName"` + ProjectName string `json:"projectName"` + SubCustomerName string `json:"subCustomerName"` +} + +type Instance struct { + Lease Lease `json:"lease"` +} + +type Lease struct { + LeaseID string `json:"leaseId"` + + Expires time.Time `json:"expires"` +} diff --git a/internal/secret/gcp_secret_manager.go b/internal/secret/gcp_secret_manager.go index 70b16b3964..28aa550293 100644 --- a/internal/secret/gcp_secret_manager.go +++ b/internal/secret/gcp_secret_manager.go @@ -90,6 +90,9 @@ const ( // The secret value encodes relevant keys and their secrets as // a JSON object that can be unmarshaled into TwitterCredentials. NameStagingTwitterAPISecret = "staging-" + NameTwitterAPISecret + + // NameMacServiceAPIKey is the secret name for the MacService API key. + NameMacServiceAPIKey = "macservice-api-key" ) // TwitterCredentials holds Twitter API credentials.