Skip to content

Commit

Permalink
cmd/makemac: keep-alive service for MacService leases
Browse files Browse the repository at this point in the history
makemac is a tiny service which renews existing MacService leases on a
periodic basis to ensure that they stay alive. This is a temporary
holdover until we get proper support for MacService somewhere like
Robocrop.

For golang/go#60440.

Change-Id: Id52a39dda6bd3858e243a7ae0469ec8a85356309
Reviewed-on: https://go-review.googlesource.com/c/build/+/546495
Auto-Submit: Michael Pratt <mpratt@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Carlos Amedee <carlos@golang.org>
Reviewed-by: Dmitri Shuralyov <dmitshur@golang.org>
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
  • Loading branch information
prattmic authored and gopherbot committed Dec 1, 2023
1 parent f85f669 commit 2d1c9af
Show file tree
Hide file tree
Showing 9 changed files with 322 additions and 3 deletions.
2 changes: 1 addition & 1 deletion cmd/gerritbot/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
38 changes: 38 additions & 0 deletions cmd/makemac/Dockerfile
Original file line number Diff line number Diff line change
@@ -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"]
20 changes: 20 additions & 0 deletions cmd/makemac/Makefile
Original file line number Diff line number Diff line change
@@ -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)
39 changes: 39 additions & 0 deletions cmd/makemac/deployment-prod.yaml
Original file line number Diff line number Diff line change
@@ -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
78 changes: 78 additions & 0 deletions cmd/makemac/main.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
4 changes: 2 additions & 2 deletions cmd/pubsubhelper/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand Down
88 changes: 88 additions & 0 deletions internal/macservice/client.go
Original file line number Diff line number Diff line change
@@ -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
}
53 changes: 53 additions & 0 deletions internal/macservice/leases.go
Original file line number Diff line number Diff line change
@@ -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"`
}
3 changes: 3 additions & 0 deletions internal/secret/gcp_secret_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down

0 comments on commit 2d1c9af

Please sign in to comment.