.github,cmd/cigocacher: add flags --version --stats --cigocached-host

Add flags:

* --cigocached-host to support alternative host resolution in other
  environments, like the corp repo.
* --stats to reduce the amount of bash script we need.
* --version to support a caching tool/cigocacher script that will
  download from GitHub releases.

Updates tailscale/corp#10808

Change-Id: Ib2447bc5f79058669a70f2c49cef6aedd7afc049
Signed-off-by: Tom Proctor <tomhjp@users.noreply.github.com>
pull/15878/merge cmd/cigocacher/d0d993f5d6576b5d97d0242c64bbe2de049d6486
Tom Proctor 6 days ago
parent d7a5624841
commit d0d993f5d6

@ -7,6 +7,7 @@
# Usage: ./action.sh
# Inputs:
# URL: The cigocached server URL.
# HOST: The cigocached server host to dial.
# Outputs:
# success: Whether cigocacher was set up successfully.
@ -22,57 +23,17 @@ if [ -z "${URL:-}" ]; then
exit 0
fi
curl_and_parse() {
local jq_filter="$1"
local step="$2"
shift 2
local response
local curl_exit
response="$(curl -sSL "$@" 2>&1)" || curl_exit="$?"
if [ "${curl_exit:-0}" -ne "0" ]; then
echo "${step}: ${response}" >&2
return 1
fi
local parsed
local jq_exit
parsed=$(echo "${response}" | jq -e -r "${jq_filter}" 2>&1) || jq_exit=$?
if [ "${jq_exit:-0}" -ne "0" ]; then
echo "${step}: Failed to parse JSON response:" >&2
echo "${response}" >&2
return 1
fi
echo "${parsed}"
return 0
}
JWT="$(curl_and_parse ".value" "Fetching GitHub identity JWT" \
-H "Authorization: Bearer ${ACTIONS_ID_TOKEN_REQUEST_TOKEN}" \
"${ACTIONS_ID_TOKEN_REQUEST_URL}&audience=gocached")" || exit 0
BIN_PATH="${RUNNER_TEMP:-/tmp}/cigocacher$(go env GOEXE)"
go build -o "${BIN_PATH}" ./cmd/cigocacher
# cigocached serves a TLS cert with an FQDN, but DNS is based on VM name.
HOST_AND_PORT="${URL#http*://}"
FIRST_LABEL="${HOST_AND_PORT/.*/}"
# Save CONNECT_TO for later steps to use.
echo "CONNECT_TO=${HOST_AND_PORT}:${FIRST_LABEL}:" >> "${GITHUB_ENV}"
BODY="$(jq -n --arg jwt "$JWT" '{"jwt": $jwt}')"
CIGOCACHER_TOKEN="$(curl_and_parse ".access_token" "Exchanging token with cigocached" \
--connect-to "${HOST_AND_PORT}:${FIRST_LABEL}:" \
-H "Content-Type: application/json" \
"$URL/auth/exchange-token" \
-d "$BODY")" || exit 0
CIGOCACHER_TOKEN="$("${BIN_PATH}" --auth --cigocached-url "${URL}" --cigocached-host "${HOST}" )"
if [ -z "${CIGOCACHER_TOKEN:-}" ]; then
echo "Failed to fetch cigocacher token, skipping cigocacher setup"
exit 0
fi
# Wait until we successfully auth before building cigocacher to ensure we know
# it's worth building.
# TODO(tomhjp): bake cigocacher into runner image and use it for auth.
echo "Fetched cigocacher token successfully"
echo "::add-mask::${CIGOCACHER_TOKEN}"
echo "CIGOCACHER_TOKEN=${CIGOCACHER_TOKEN}" >> "${GITHUB_ENV}"
BIN_PATH="${RUNNER_TEMP:-/tmp}/cigocacher$(go env GOEXE)"
go build -o "${BIN_PATH}" ./cmd/cigocacher
echo "GOCACHEPROG=${BIN_PATH} --cache-dir ${CACHE_DIR} --cigocached-url ${URL} --token ${CIGOCACHER_TOKEN}" >> "${GITHUB_ENV}"
echo "GOCACHEPROG=${BIN_PATH} --cache-dir ${CACHE_DIR} --cigocached-url ${URL} --cigocached-host ${HOST} --token ${CIGOCACHER_TOKEN}" >> "${GITHUB_ENV}"
echo "success=true" >> "${GITHUB_OUTPUT}"

@ -5,6 +5,9 @@ inputs:
cigocached-url:
description: URL of the cigocached server
required: true
cigocached-host:
description: Host to dial for the cigocached server
required: true
checkout-path:
description: Path to cloned repository
required: true
@ -25,6 +28,7 @@ runs:
shell: bash
env:
URL: ${{ inputs.cigocached-url }}
HOST: ${{ inputs.cigocached-host }}
CACHE_DIR: ${{ inputs.cache-dir }}
working-directory: ${{ inputs.checkout-path }}
run: .github/actions/go-cache/action.sh

@ -263,6 +263,7 @@ jobs:
checkout-path: ${{ github.workspace }}/src
cache-dir: ${{ github.workspace }}/cigocacher
cigocached-url: ${{ vars.CIGOCACHED_AZURE_URL }}
cigocached-host: ${{ vars.CIGOCACHED_AZURE_HOST }}
- name: test
if: matrix.key != 'win-bench' # skip on bench builder
@ -278,10 +279,12 @@ jobs:
run: go test ./... -bench . -benchtime 1x -run "^$"
- name: Print stats
shell: bash
shell: pwsh
if: steps.cigocacher-setup.outputs.success == 'true'
env:
GOCACHEPROG: ${{ env.GOCACHEPROG }}
run: |
curl -sSL --connect-to "${CONNECT_TO}" -H "Authorization: Bearer ${CIGOCACHER_TOKEN}" "${{ vars.CIGOCACHED_AZURE_URL }}/session/stats" | jq .
Invoke-Expression "$env:GOCACHEPROG --stats" | jq .
win-tool-go:
runs-on: windows-latest

@ -22,8 +22,11 @@ import (
"log"
"net"
"net/http"
"net/url"
"os"
"path/filepath"
"runtime/debug"
"strconv"
"strings"
"sync/atomic"
"time"
@ -34,20 +37,56 @@ import (
func main() {
var (
auth = flag.Bool("auth", false, "auth with cigocached and exit, printing the access token as output")
token = flag.String("token", "", "the cigocached access token to use, as created using --auth")
cigocachedURL = flag.String("cigocached-url", "", "optional cigocached URL (scheme, host, and port). empty means to not use one.")
dir = flag.String("cache-dir", "", "cache directory; empty means automatic")
verbose = flag.Bool("verbose", false, "enable verbose logging")
version = flag.Bool("version", false, "print version and exit")
auth = flag.Bool("auth", false, "auth with cigocached and exit, printing the access token as output")
stats = flag.Bool("stats", false, "fetch and print cigocached stats and exit")
token = flag.String("token", "", "the cigocached access token to use, as created using --auth")
srvURL = flag.String("cigocached-url", "", "optional cigocached URL (scheme, host, and port). Empty means to not use one.")
srvHostDial = flag.String("cigocached-host", "", "optional cigocached host to dial instead of the host in the provided --cigocached-url. Useful for public TLS certs on private addresses.")
dir = flag.String("cache-dir", "", "cache directory; empty means automatic")
verbose = flag.Bool("verbose", false, "enable verbose logging")
)
flag.Parse()
if *version {
info, ok := debug.ReadBuildInfo()
if !ok {
log.Fatal("no build info")
}
var (
rev string
dirty bool
)
for _, s := range info.Settings {
switch s.Key {
case "vcs.revision":
rev = s.Value
case "vcs.modified":
dirty, _ = strconv.ParseBool(s.Value)
}
}
if dirty {
rev += "-dirty"
}
fmt.Println(rev)
return
}
var srvHost string
if *srvHostDial != "" && *srvURL != "" {
u, err := url.Parse(*srvURL)
if err != nil {
log.Fatal(err)
}
srvHost = u.Hostname()
}
if *auth {
if *cigocachedURL == "" {
if *srvURL == "" {
log.Print("--cigocached-url is empty, skipping auth")
return
}
tk, err := fetchAccessToken(httpClient(), os.Getenv("ACTIONS_ID_TOKEN_REQUEST_URL"), os.Getenv("ACTIONS_ID_TOKEN_REQUEST_TOKEN"), *cigocachedURL)
tk, err := fetchAccessToken(httpClient(srvHost, *srvHostDial), os.Getenv("ACTIONS_ID_TOKEN_REQUEST_URL"), os.Getenv("ACTIONS_ID_TOKEN_REQUEST_TOKEN"), *srvURL)
if err != nil {
log.Printf("error fetching access token, skipping auth: %v", err)
return
@ -56,6 +95,28 @@ func main() {
return
}
if *stats {
if *srvURL == "" {
log.Fatal("--cigocached-url is empty; cannot fetch stats")
}
tk := *token
if tk == "" {
log.Fatal("--token is empty; cannot fetch stats")
}
c := &gocachedClient{
baseURL: *srvURL,
cl: httpClient(srvHost, *srvHostDial),
accessToken: tk,
verbose: *verbose,
}
stats, err := c.fetchStats()
if err != nil {
log.Fatalf("error fetching gocached stats: %v", err)
}
fmt.Println(stats)
return
}
if *dir == "" {
d, err := os.UserCacheDir()
if err != nil {
@ -75,13 +136,13 @@ func main() {
},
verbose: *verbose,
}
if *cigocachedURL != "" {
if *srvURL != "" {
if *verbose {
log.Printf("Using cigocached at %s", *cigocachedURL)
log.Printf("Using cigocached at %s", *srvURL)
}
c.gocached = &gocachedClient{
baseURL: *cigocachedURL,
cl: httpClient(),
baseURL: *srvURL,
cl: httpClient(srvHost, *srvHostDial),
accessToken: *token,
verbose: *verbose,
}
@ -104,18 +165,18 @@ func main() {
}
}
func httpClient() *http.Client {
func httpClient(srvHost, srvHostDial string) *http.Client {
if srvHost == "" || srvHostDial == "" {
return http.DefaultClient
}
return &http.Client{
Transport: &http.Transport{
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
host, port, err := net.SplitHostPort(addr)
if err == nil {
// This does not run in a tailnet. We serve corp.ts.net
// TLS certs, and override DNS resolution to lookup the
// private IP for the VM by its hostname.
if vm, ok := strings.CutSuffix(host, ".corp.ts.net"); ok {
addr = net.JoinHostPort(vm, port)
}
if host, port, err := net.SplitHostPort(addr); err == nil && host == srvHost {
// This allows us to serve a publicly trusted TLS cert
// while also minimising latency by explicitly using a
// private network address.
addr = net.JoinHostPort(srvHostDial, port)
}
var d net.Dialer
return d.DialContext(ctx, network, addr)

@ -32,12 +32,6 @@ func tryReadErrorMessage(res *http.Response) []byte {
}
func (c *gocachedClient) get(ctx context.Context, actionID string) (outputID string, resp *http.Response, err error) {
// TODO(tomhjp): make sure we timeout if cigocached disappears, but for some
// reason, this seemed to tank network performance.
// // Set a generous upper limit on the time we'll wait for a response. We'll
// // shorten this deadline later once we know the content length.
// ctx, cancel := context.WithTimeout(ctx, time.Minute)
// defer cancel()
req, _ := http.NewRequestWithContext(ctx, "GET", c.baseURL+"/action/"+actionID, nil)
req.Header.Set("Want-Object", "1") // opt in to single roundtrip protocol
if c.accessToken != "" {

Loading…
Cancel
Save