diff --git a/.github/actions/go-cache/action.sh b/.github/actions/go-cache/action.sh index 58ceabc86..bd584f6f1 100755 --- a/.github/actions/go-cache/action.sh +++ b/.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}" diff --git a/.github/actions/go-cache/action.yml b/.github/actions/go-cache/action.yml index a671530f8..38bb15b37 100644 --- a/.github/actions/go-cache/action.yml +++ b/.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 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fd193401d..27862567f 100644 --- a/.github/workflows/test.yml +++ b/.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 diff --git a/cmd/cigocacher/cigocacher.go b/cmd/cigocacher/cigocacher.go index 1ada62b6a..872cb1953 100644 --- a/cmd/cigocacher/cigocacher.go +++ b/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) diff --git a/cmd/cigocacher/http.go b/cmd/cigocacher/http.go index 57d3bfb45..55735f089 100644 --- a/cmd/cigocacher/http.go +++ b/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 != "" {