diff --git a/generate/go_client.tpl b/generate/go_client.tpl index 1ae99979..16edcbf9 100644 --- a/generate/go_client.tpl +++ b/generate/go_client.tpl @@ -40,35 +40,6 @@ type ( {{ end }} ) -func New(config *DialConfig) (Client, error) { - err := config.parse() - if err != nil { - return nil, err - } - - c := &client{ - config: config, - interceptors: []connect.Interceptor{}, - } - - if config.Token != "" { - authInterceptor := &authInterceptor{config: config} - c.interceptors = append(c.interceptors, authInterceptor) - - if config.TokenRenewal != nil { - tokenRenewingInterceptor := &tokenRenewingInterceptor{config: config, client: c} - c.interceptors = append(c.interceptors, tokenRenewingInterceptor) - } - } - if config.Log != nil { - loggingInterceptor := &loggingInterceptor{config: config} - c.interceptors = append(c.interceptors, loggingInterceptor) - } - c.interceptors = append(c.interceptors, config.Interceptors...) - - return c, nil -} - {{ range $name, $api := . -}} func (c *client) {{ $name | title }}() {{ $name | title }} { a := &{{ $name }}{ diff --git a/go.mod b/go.mod index 7bd09035..37e8dc63 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,8 @@ module github.com/metal-stack/api go 1.26 require ( - buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260709200747-435963d16310.1 - buf.build/go/protovalidate v1.2.0 + buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.12-20260709200747-435963d16310.1 + buf.build/go/protovalidate v1.3.0 connectrpc.com/connect v1.20.0 github.com/bufbuild/protocompile v0.14.1 github.com/go-task/slim-sprig/v3 v3.0.0 @@ -13,25 +13,25 @@ require ( github.com/google/uuid v1.6.0 github.com/klauspost/connect-compress/v2 v2.1.1 github.com/stretchr/testify v1.11.1 - google.golang.org/grpc v1.82.1 - google.golang.org/protobuf v1.36.11 + google.golang.org/grpc v1.83.0 + google.golang.org/protobuf v1.36.12 ) require ( - cel.dev/expr v0.25.2 // indirect + cel.dev/expr v0.25.3 // 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.30.0 // indirect - github.com/klauspost/compress v1.19.1 // indirect + github.com/google/cel-go v0.31.0 // indirect + github.com/klauspost/compress v1.19.2 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/minio/minlz v1.2.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect go.yaml.in/yaml/v3 v3.0.5 // indirect - golang.org/x/exp v0.0.0-20260718201538-764159d718ef // indirect + golang.org/x/exp v0.0.0-20260813180055-c1d0aacb2297 // indirect golang.org/x/sys v0.47.0 // indirect - golang.org/x/text v0.40.0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20260724162435-b2f20204f0df // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260724162435-b2f20204f0df // indirect + golang.org/x/text v0.41.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260810153831-ec0a7760b754 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260810153831-ec0a7760b754 // 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 ff3fee83..ac22a09f 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,9 @@ -buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260709200747-435963d16310.1 h1:fXh8CsdNpjRr8R5vFdqtIxPt/Lno2IIJlYOdZBIZn0w= -buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260709200747-435963d16310.1/go.mod h1:tvtbpgaVXZX4g6Pn+AnzFycuRK3MOz5HJfEGeEllXYM= -buf.build/go/protovalidate v1.2.0 h1:DQVrUWkmGTBij+kOYv/x2LLxwcLaGKMdzShj1/6/3H0= -buf.build/go/protovalidate v1.2.0/go.mod h1:7rYiQEhqvAipoazpVNBBH2S2f8bjG4huMVy1V2Yofn4= -cel.dev/expr v0.25.2 h1:K6j46C81hXtZQfuX60cVWQFBJahKSE2gfRbNuvr5bFs= -cel.dev/expr v0.25.2/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.12-20260709200747-435963d16310.1 h1:6nlcxMOui23ZRVAfJM451duu79P1npA5JRdZqMilrrQ= +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.12-20260709200747-435963d16310.1/go.mod h1:TCt1lluMFnctISJXvkIQ4x3ABrPuUKCWKyjKdkJNBpw= +buf.build/go/protovalidate v1.3.0 h1:8ITcnZGkAHx6TyhZvro+iET/AyqU8gEWQJK2WsT62ms= +buf.build/go/protovalidate v1.3.0/go.mod h1:82s5g+rFRj1CZPiLv6OTA31jBu2fpq7mLXHwa9mZfEs= +cel.dev/expr v0.25.3 h1:A2jO8jwOugrrovveCWfj0KEZOfqiLgAcwjpHPhzIGw0= +cel.dev/expr v0.25.3/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= connectrpc.com/connect v1.20.0 h1:6TNDAB+WeNd2uolWNlYczB5E0KNNaVMNUEx8JEUsPmQ= connectrpc.com/connect v1.20.0/go.mod h1:A2ygJrukXwWy32vkCAAHNVguZrqZ+jeZ9rGRnGR4dN4= github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= @@ -21,14 +21,14 @@ github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63Y github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/google/cel-go v0.30.0 h1:ll54AkzKunWkBn9wSoiUXbFZXYZTkdJGNXTBXUoolGo= -github.com/google/cel-go v0.30.0/go.mod h1:X0bD6iVNR8pkROSOoHVdgTkzmRcosof7WQqCD6wcMc8= +github.com/google/cel-go v0.31.0 h1:H0bhpFTqOvmHrBGrWKp7ZlhBm5Hh8PYUEXnwxT1LL7A= +github.com/google/cel-go v0.31.0/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.19.1 h1:VsB4HPswih7mmZ8WleSFQ75c/Ui1M4trX5oAsJnhSlk= -github.com/klauspost/compress v1.19.1/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= +github.com/klauspost/compress v1.19.2 h1:hMRETovs/pu/dVWN7zIT1PGG8t509MwT6bO7XSi26R8= +github.com/klauspost/compress v1.19.2/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= @@ -51,22 +51,22 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= -golang.org/x/exp v0.0.0-20260718201538-764159d718ef h1:LkZ48HFgy/TvhTI0bcWkjgFkgLyKUwcTbDjS0DUjw+A= -golang.org/x/exp v0.0.0-20260718201538-764159d718ef/go.mod h1:EdfpwwqSu+0Li0mzskwHU6FWDV3t9Q+RZDo3QMUtL3Q= +golang.org/x/exp v0.0.0-20260813180055-c1d0aacb2297 h1:YXnL44eJ77R+ji4/ooy8UsXIhz+lbi2Qgdlc8iRN0gY= +golang.org/x/exp v0.0.0-20260813180055-c1d0aacb2297/go.mod h1:Mkmymgv+uMpSQ/XxJ/7GpdrdYoqm3u72jEbpCLiJmNk= golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= -golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= -google.golang.org/genproto/googleapis/api v0.0.0-20260724162435-b2f20204f0df h1:NsJx+hCSwIBI6+C4BuJIkb8xOG1M+nfQDsqIrQHT92k= -google.golang.org/genproto/googleapis/api v0.0.0-20260724162435-b2f20204f0df/go.mod h1:1brfde68Npq6+WA75c1EHWPijZEG1kMus61ygPZfn4A= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260724162435-b2f20204f0df h1:O3ig1i5WDDzsVzRp+cCdgelT9vXnlnOFdlEeFtL4HCc= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260724162435-b2f20204f0df/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= -google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE= -google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= -google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= -google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= +golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= +google.golang.org/genproto/googleapis/api v0.0.0-20260810153831-ec0a7760b754 h1:dWeMvEJ3JhYgqSCAHUZZJgMUyfniiiCvDc72x5EqJP0= +google.golang.org/genproto/googleapis/api v0.0.0-20260810153831-ec0a7760b754/go.mod h1:q/3oV3jAi5vwelxsVAprMBC8BcM2zmNe+IjRGd+9/ks= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260810153831-ec0a7760b754 h1:k5CJw9e5ONCcA/u0webKt092npXuY+KeGh3Q8NAVf0g= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260810153831-ec0a7760b754/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.83.0 h1:JeNZEKJFbQxArAMl+hiytHauacDNqJUllNfmIMmpqnQ= +google.golang.org/grpc v1.83.0/go.mod h1:kDyl6SKsiHKt0uylY5gtn5cEjkrIOhQOGDgIc4JGwzQ= +google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc= +google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/go/client/client-interceptors.go b/go/client/client-interceptors.go index d2fd9cb5..e85f14b8 100644 --- a/go/client/client-interceptors.go +++ b/go/client/client-interceptors.go @@ -101,9 +101,6 @@ func (i *tokenRenewingInterceptor) WrapStreamingHandler(next connect.StreamingHa } func (i *tokenRenewingInterceptor) renewTokenIfNeeded() error { - if i.config.expiresAt.IsZero() { - return nil - } if i.renewing.Load() { return nil } @@ -111,35 +108,47 @@ 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) < i.config.TokenFileRereadDuration { - return nil - } - i.config.Log.Info("tokenfile specified, re-reading content") + if i.config.TokenFile != "" { + return i.rereadTokenFile() + } - 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 - } + return i.renewToken() +} + +func (i *tokenRenewingInterceptor) rereadTokenFile() error { + // 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) < i.config.TokenFileRereadDuration { + 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.renewing.Store(true) + defer i.renewing.Store(false) - i.Lock() - defer i.Unlock() + 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() + i.config.Token = newToken + err = i.config.parseTokenClaims() + if err != nil { + return fmt.Errorf("unable to parse token %w", err) + } + i.config.tokenFileLastRead = time.Now() + return nil +} + +func (i *tokenRenewingInterceptor) renewToken() error { + if i.config.expiresAt.IsZero() { return nil } diff --git a/go/client/client.go b/go/client/client.go index 9c7577b2..40b546e1 100755 --- a/go/client/client.go +++ b/go/client/client.go @@ -122,35 +122,6 @@ type ( } ) -func New(config *DialConfig) (Client, error) { - err := config.parse() - if err != nil { - return nil, err - } - - c := &client{ - config: config, - interceptors: []connect.Interceptor{}, - } - - if config.Token != "" { - authInterceptor := &authInterceptor{config: config} - c.interceptors = append(c.interceptors, authInterceptor) - - if config.TokenRenewal != nil { - tokenRenewingInterceptor := &tokenRenewingInterceptor{config: config, client: c} - c.interceptors = append(c.interceptors, tokenRenewingInterceptor) - } - } - if config.Log != nil { - loggingInterceptor := &loggingInterceptor{config: config} - c.interceptors = append(c.interceptors, loggingInterceptor) - } - c.interceptors = append(c.interceptors, config.Interceptors...) - - return c, nil -} - func (c *client) Adminv2() Adminv2 { a := &adminv2{ auditservice: adminv2connect.NewAuditServiceClient( diff --git a/go/client/conn.go b/go/client/conn.go index 7ad9c75d..a415bc83 100644 --- a/go/client/conn.go +++ b/go/client/conn.go @@ -26,11 +26,15 @@ type ( DialConfig struct { // BaseUrl points to the apiv2 url where the apiserver is reachable BaseURL string - // Token to be used to talk to the apiserver + // Token to be used to talk to the apiserver, the string representation of the token. + // If Token is specified, TokenFile cannot be specified. + // It is possible to renew this token automatically, see TokenRenewal. Token string - // Tokenfile which contains the token, is only read if token is empty + // Tokenfile path to a file containing the string representation of the token. + // If Tokenfile is specified, Token cannot be specified. + // Token renewal must be done from outside TokenFile string - // Duration between token file re-reads + // Duration between token file re-reads, optional, defaults to 5min if not specified. TokenFileRereadDuration time.Duration // Optional client Interceptors @@ -40,6 +44,7 @@ type ( // TokenRenewal defines if and how the token should be renewed TokenRenewal *TokenRenewal + // Transport optional, can be used to configure how the http transport works. Transport http.RoundTripper Log *slog.Logger @@ -58,6 +63,44 @@ type ( PersistTokenFn func(token string) error ) +func New(config *DialConfig) (Client, error) { + err := config.parse() + if err != nil { + return nil, err + } + + c := &client{ + config: config, + interceptors: []connect.Interceptor{}, + } + + if config.Token != "" { + authInterceptor := &authInterceptor{config: config} + c.interceptors = append(c.interceptors, authInterceptor) + + if config.TokenRenewal != nil { + tokenRenewingInterceptor := &tokenRenewingInterceptor{config: config, client: c} + c.interceptors = append(c.interceptors, tokenRenewingInterceptor) + } + } + + if config.TokenFile != "" { + authInterceptor := &authInterceptor{config: config} + c.interceptors = append(c.interceptors, authInterceptor) + + tokenRenewingInterceptor := &tokenRenewingInterceptor{config: config, client: c} + c.interceptors = append(c.interceptors, tokenRenewingInterceptor) + } + + if config.Log != nil { + loggingInterceptor := &loggingInterceptor{config: config} + c.interceptors = append(c.interceptors, loggingInterceptor) + } + c.interceptors = append(c.interceptors, config.Interceptors...) + + return c, nil +} + func (d *DialConfig) HttpClient() *http.Client { transport := http.DefaultTransport if d.Transport != nil { @@ -111,6 +154,13 @@ func (dc *DialConfig) parse() error { return nil } + return dc.parseTokenClaims() +} + +// parseTokenClaims extracts expiresAt and issuedAt from the current Token. +// It is called both when initially parsing the config and when a tokenfile +// token is re-read, in which case only the claims need to be refreshed. +func (dc *DialConfig) parseTokenClaims() error { parsed, err := jwt.Parse(dc.Token, nil) if err != nil && !errors.Is(err, jwt.ErrTokenUnverifiable) { return fmt.Errorf("unable to parse token:%w", err)