Browse Source

.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 <[email protected]>
Tom Proctor 2 months ago
parent
commit
d0d993f5d6

+ 9 - 48
.github/actions/go-cache/action.sh

@@ -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}"

+ 4 - 0
.github/actions/go-cache/action.yml

@@ -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

+ 5 - 2
.github/workflows/test.yml

@@ -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

+ 81 - 20
cmd/cigocacher/cigocacher.go

@@ -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)

+ 0 - 6
cmd/cigocacher/http.go

@@ -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 != "" {