From 322fec3b8d674227276942a65f880209cf0cbea7 Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Sun, 5 Jul 2026 09:59:12 +0200 Subject: [PATCH 1/2] Allow tokenFile instead of raw token --- .github/workflows/main.yml | 6 +++--- .github/workflows/npm-publish.yaml | 2 +- go.mod | 8 +++---- go.sum | 16 +++++++------- go/client/client-interceptors.go | 33 +++++++++++++++++++++++++++++ go/client/conn.go | 34 +++++++++++++++++++++++++----- go/client/ping.go | 9 ++++++++ 7 files changed, 87 insertions(+), 21 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0e04182f..7e77ae00 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -19,7 +19,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out code - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Set up Go uses: actions/setup-go@v6 @@ -39,7 +39,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out code - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Check spelling uses: crate-ci/typos@master @@ -60,7 +60,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out code - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Set up Python 3.13 uses: actions/setup-python@v6 diff --git a/.github/workflows/npm-publish.yaml b/.github/workflows/npm-publish.yaml index df7009c1..a547242a 100644 --- a/.github/workflows/npm-publish.yaml +++ b/.github/workflows/npm-publish.yaml @@ -19,7 +19,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out code - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: setup Node.js uses: actions/setup-node@v6 diff --git a/go.mod b/go.mod index 147544b8..c142ed5f 100644 --- a/go.mod +++ b/go.mod @@ -20,8 +20,8 @@ require ( cel.dev/expr v0.25.2 // indirect github.com/antlr4-go/antlr/v4 v4.13.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/google/cel-go v0.28.1 // indirect - github.com/klauspost/compress v1.18.6 // indirect + github.com/google/cel-go v0.29.1 // indirect + github.com/klauspost/compress v1.19.0 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/minio/minlz v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect @@ -29,8 +29,8 @@ require ( go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/exp v0.0.0-20260611194520-c48552f49976 // indirect golang.org/x/text v0.38.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20260610212136-7ab31c22f7ad // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260610212136-7ab31c22f7ad // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260630182238-925bb5da69e7 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260630182238-925bb5da69e7 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 35304b39..129d64be 100644 --- a/go.sum +++ b/go.sum @@ -19,14 +19,14 @@ github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1v github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= -github.com/google/cel-go v0.28.1 h1:YWIwi77J4xIsYUwAF/iIuS6haffzIHS8yWI8glSbLWM= -github.com/google/cel-go v0.28.1/go.mod h1:X0bD6iVNR8pkROSOoHVdgTkzmRcosof7WQqCD6wcMc8= +github.com/google/cel-go v0.29.1 h1:8Gx3S5HPop5XPOHtZI8vkaGk+DBW/nn9fkpNPbh5dBk= +github.com/google/cel-go v0.29.1/go.mod h1:X0bD6iVNR8pkROSOoHVdgTkzmRcosof7WQqCD6wcMc8= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao= -github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= +github.com/klauspost/compress v1.19.0 h1:sXLILfc9jV2QYWkzFOPWStmcUVH2RHEB1JCdY2oVvCQ= +github.com/klauspost/compress v1.19.0/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/klauspost/connect-compress/v2 v2.1.1 h1:ycZNp4rWOZBodVE2Ls5AzK4aHkyK+GteEfzRZgKNs+c= github.com/klauspost/connect-compress/v2 v2.1.1/go.mod h1:9oilsPHJMzGKkjafSBk9J7iVo4mO+dw0G0KSdVpnlVE= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= @@ -55,10 +55,10 @@ golang.org/x/exp v0.0.0-20260611194520-c48552f49976 h1:X8Hz2ImujgbmetVuW+w2YkyZC golang.org/x/exp v0.0.0-20260611194520-c48552f49976/go.mod h1:vnf4pv9iKZXY58sQE1L86zmNWJ4159e1RkcWiLCkeEY= golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= -google.golang.org/genproto/googleapis/api v0.0.0-20260610212136-7ab31c22f7ad h1:3iLyITS/sySRwbUKoC7ogfj2Yr1Cjs0pfaRKj5U5HEw= -google.golang.org/genproto/googleapis/api v0.0.0-20260610212136-7ab31c22f7ad/go.mod h1:KdNqO+rCIWgFumrNBSEDlDNrkrQnpkax7Tv1WxNY8V4= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260610212136-7ab31c22f7ad h1:45WmJvIV6C2+O/jjLkPUH+F3aOj/1miDoU2DD0+NWbg= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260610212136-7ab31c22f7ad/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/genproto/googleapis/api v0.0.0-20260630182238-925bb5da69e7 h1:jQ9p21COKWjP3VwuFrNRiiOTMh3mPpN45R7SLrH/HUU= +google.golang.org/genproto/googleapis/api v0.0.0-20260630182238-925bb5da69e7/go.mod h1:KqHwBx2upmfa1XSi1WuRvC+2VGCLtooKkfmyvRbUmqA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260630182238-925bb5da69e7 h1:eM/YSd5bBFagF51o1E745Ta7RwzpW0h+z+QDNZOgmQ8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260630182238-925bb5da69e7/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/go/client/client-interceptors.go b/go/client/client-interceptors.go index 71782fe1..8abfc8a0 100644 --- a/go/client/client-interceptors.go +++ b/go/client/client-interceptors.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "log/slog" + "os" "sync" "sync/atomic" "time" @@ -110,6 +111,38 @@ func (i *tokenRenewingInterceptor) renewTokenIfNeeded() error { i.config.Log = slog.Default() } + if !i.config.tokenFileLastRead.IsZero() { + // The token is refreshed by a sidecar, this means we should periodically read the tokenfile + // and store the token in config instead und update the tokenFileLastRead afterwards + if time.Since(i.config.tokenFileLastRead) < 5*time.Minute { + return nil + } + i.config.Log.Info("tokenfile specified, re-reading content") + + content, err := os.ReadFile(i.config.TokenFile) + if err != nil { + return fmt.Errorf("unable to read tokenfile %w", err) + } + newToken := string(content) + if i.config.Token == newToken { + return nil + } + + i.renewing.Store(true) + defer i.renewing.Store(false) + + i.Lock() + defer i.Unlock() + + i.config.Token = newToken + err = i.config.parse() + if err != nil { + return fmt.Errorf("unable to parse token %w", err) + } + i.config.tokenFileLastRead = time.Now() + return nil + } + replaceBefore := i.config.expiresAt.Sub(i.config.issuedAt) / tokenRenewChecksDuringLifetime if time.Until(i.config.expiresAt) > replaceBefore { diff --git a/go/client/conn.go b/go/client/conn.go index 30676829..f32024fa 100644 --- a/go/client/conn.go +++ b/go/client/conn.go @@ -16,14 +16,19 @@ import ( const ( tokenRenewChecksDuringLifetime = 4 TokenEnvName = "METAL_APIV2_TOKEN" + TokenFileEnvName = "METAL_APIV2_TOKEN_FILE" BaseURLEnvName = "METAL_APIV2_URL" ) type ( // DialConfig is the configuration to create a api-server connection DialConfig struct { + // BaseUrl points to the apiv2 url where the apiserver is reachable BaseURL string - Token string + // Token to be used to talk to the apiserver + Token string + // Tokenfile which contains the token, is only read if token is empty + TokenFile string // Optional client Interceptors Interceptors []connect.Interceptor @@ -36,8 +41,9 @@ type ( Log *slog.Logger - expiresAt time.Time - issuedAt time.Time + expiresAt time.Time + issuedAt time.Time + tokenFileLastRead time.Time } TokenRenewal struct { @@ -73,9 +79,27 @@ func (dc *DialConfig) parse() error { if dc.Token == "" { dc.Token = os.Getenv(TokenEnvName) - if dc.Token == "" { - return nil + } + + if dc.TokenFile == "" { + dc.TokenFile = os.Getenv(TokenFileEnvName) + } + + if dc.Token != "" && dc.TokenFile != "" { + return fmt.Errorf("either token or tokenfile must be specified, not both") + } + + if dc.Token == "" && dc.TokenFile != "" { + content, err := os.ReadFile(dc.TokenFile) + if err != nil { + return err } + dc.Token = string(content) + dc.tokenFileLastRead = time.Now() + } + + if dc.Token == "" && dc.TokenFile == "" { + return nil } parsed, err := jwt.Parse(dc.Token, nil) diff --git a/go/client/ping.go b/go/client/ping.go index 86586dee..ff6794a5 100644 --- a/go/client/ping.go +++ b/go/client/ping.go @@ -2,6 +2,7 @@ package client import ( "context" + "log/slog" "os" "time" @@ -47,6 +48,14 @@ func (c *client) Ping(ctx context.Context, config *PingConfig) { config.Interval = defaultInterval } + if config.StartedAt.IsZero() { + config.StartedAt = time.Now() + } + + if c.config.Log == nil { + c.config.Log = slog.Default() + } + var identifier string if config.Identifier == nil { hostname, err := os.Hostname() From c0e60b298403f30e3df105f7ebb637e2546d4ae9 Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Mon, 6 Jul 2026 15:44:50 +0200 Subject: [PATCH 2/2] Review findings --- go/client/client-interceptors.go | 5 ++++- go/client/conn.go | 6 ++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/go/client/client-interceptors.go b/go/client/client-interceptors.go index 8abfc8a0..033fc3cf 100644 --- a/go/client/client-interceptors.go +++ b/go/client/client-interceptors.go @@ -112,9 +112,12 @@ func (i *tokenRenewingInterceptor) renewTokenIfNeeded() error { } if !i.config.tokenFileLastRead.IsZero() { + if i.config.TokenFileRereadDuration == 0 { + i.config.TokenFileRereadDuration = tokenFileRereadDuration + } // The token is refreshed by a sidecar, this means we should periodically read the tokenfile // and store the token in config instead und update the tokenFileLastRead afterwards - if time.Since(i.config.tokenFileLastRead) < 5*time.Minute { + if time.Since(i.config.tokenFileLastRead) < i.config.TokenFileRereadDuration { return nil } i.config.Log.Info("tokenfile specified, re-reading content") diff --git a/go/client/conn.go b/go/client/conn.go index f32024fa..8a847281 100644 --- a/go/client/conn.go +++ b/go/client/conn.go @@ -15,6 +15,7 @@ import ( const ( tokenRenewChecksDuringLifetime = 4 + tokenFileRereadDuration = 5 * time.Minute TokenEnvName = "METAL_APIV2_TOKEN" TokenFileEnvName = "METAL_APIV2_TOKEN_FILE" BaseURLEnvName = "METAL_APIV2_URL" @@ -29,6 +30,8 @@ type ( Token string // Tokenfile which contains the token, is only read if token is empty TokenFile string + // Duration between token file re-reads + TokenFileRereadDuration time.Duration // Optional client Interceptors Interceptors []connect.Interceptor @@ -90,6 +93,9 @@ func (dc *DialConfig) parse() error { } if dc.Token == "" && dc.TokenFile != "" { + if dc.TokenFileRereadDuration < time.Minute { + return fmt.Errorf("token file re-read duration must be greater than 1min") + } content, err := os.ReadFile(dc.TokenFile) if err != nil { return err