forked from flipt-io/flipt
-
Notifications
You must be signed in to change notification settings - Fork 0
/
main.go
424 lines (354 loc) · 13.5 KB
/
main.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
package main
import (
"flag"
"fmt"
"os"
"slices"
"strings"
"google.golang.org/genproto/googleapis/api/visibility"
"google.golang.org/protobuf/compiler/protogen"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/pluginpb"
)
const (
importPath = "go.flipt.io/flipt/sdk/go"
emptyImport = "google.golang.org/protobuf/types/known/emptypb"
ignoreDecl = "flipt:sdk:ignore"
)
func main() {
var flags flag.FlagSet
grpcAPIConfig := flags.String("grpc_api_configuration", "", "path to GRPC API configuration")
protogen.Options{ParamFunc: flags.Set}.Run(func(gen *protogen.Plugin) error {
if *grpcAPIConfig == "" {
fmt.Fprintln(os.Stderr, "path to grpc API configuration required")
os.Exit(1)
}
// We have some use of the optional feature in our proto3 definitions.
// This broadcasts that our plugin supports it and hides the generated
// warning.
gen.SupportedFeatures |= uint64(pluginpb.CodeGeneratorResponse_FEATURE_PROTO3_OPTIONAL)
for _, f := range gen.Files {
f.Services = slices.DeleteFunc(f.Services, shouldIgnoreService)
f.Generate = f.Generate && len(f.Services) > 0
if !f.Generate {
continue
}
generateSubSDK(gen, f)
}
generateSDK(gen)
generateGRPC(gen)
generateHTTP(gen, *grpcAPIConfig)
return nil
})
}
func generateSDK(gen *protogen.Plugin) {
g := gen.NewGeneratedFile("sdk.gen.go", importPath)
g.P("// Code generated by protoc-gen-go-flipt-sdk. DO NOT EDIT.")
g.P()
g.P("package sdk")
g.P()
g.P("var _ *", importPackage(g, "time")("Time"))
g.P("var _ *", importPackage(g, "os")("File"))
g.P("var _ *", importPackage(g, "sync")("Mutex"))
g.P("var _ ", importPackage(g, "go.flipt.io/flipt/rpc/flipt/auth")("Method"))
g.P()
g.P("const (")
g.P(`defaultServiceAccountTokenPath = "/var/run/secrets/kubernetes.io/serviceaccount/token"`)
g.P(`defaultKubernetesExpiryLeeway = 10 * time.Second`)
g.P(")")
g.P()
g.P("type Transport interface {")
var types [][2]string
for _, file := range gen.Files {
if !file.Generate {
continue
}
var (
typ = strings.Title(string(file.GoPackageName))
method = typ + "Client"
returns = method
)
if len(file.Services) == 1 {
returns = relativeImport(g, file, file.Services[0].GoName+"Client")
}
types = append(types, [...]string{typ, method})
g.P(method, "() ", returns)
}
g.P("}")
g.P()
g.P(sdkBase)
g.P()
for _, t := range types {
g.P("func (s SDK) ", t[0], "() *", t[0], "{")
g.P("return &", t[0], "{")
g.P("transport: s.transport.", t[1], "(),")
g.P("authenticationProvider: s.authenticationProvider,")
g.P("}")
g.P("}\n")
}
authenticateFunction(g)
}
// generateSubSDK generates a .pb.sdk.go file containing a single SDK structure
// which represents an entire package from within the entire Flipt SDK API.
func generateSubSDK(gen *protogen.Plugin, file *protogen.File) (typ, client string) {
filename := string(file.GoPackageName) + ".sdk.gen.go"
g := gen.NewGeneratedFile(filename, importPath)
g.P("// Code generated by protoc-gen-go-flipt-sdk. DO NOT EDIT.")
g.P()
g.P("package sdk")
g.P()
context := importPackage(g, "context")
oneServicePackage := len(file.Services) == 1
// define client structure
typ = strings.Title(string(file.GoPackageName))
relativeImport(g, file, typ+"Client")
// We generate an interface which conjoins all the client interfaces
// generated by the gRPC protoc generator.
// Our gRPC and HTTP wrapper generators will take care of
// bundling these clients appropriately for the SDK to consume.
if !oneServicePackage {
client = typ + "Client"
g.P("type ", typ, "Client interface {")
for _, srv := range file.Services {
g.P(srv.GoName+"Client", "()", relativeImport(g, file, srv.GoName+"Client"))
}
g.P("}\n")
g.P("type ", typ, " struct {")
g.P("transport ", typ, "Client")
g.P("authenticationProvider ", "ClientAuthenticationProvider")
g.P("}\n")
}
for _, srv := range file.Services {
serviceName := srv.GoName
if oneServicePackage {
serviceName = typ
}
g.P("type ", serviceName, " struct {")
g.P("transport ", relativeImport(g, file, srv.GoName+"Client"))
g.P("authenticationProvider ", "ClientAuthenticationProvider")
g.P("}\n")
if !oneServicePackage {
g.P("func (s ", typ, ") ", srv.GoName, "() *", srv.GoName, "{")
g.P("return &", srv.GoName, "{")
g.P("transport: s.transport.", srv.GoName+"Client", "(),")
g.P("authenticationProvider: ", "s.authenticationProvider,")
g.P("}")
g.P("}")
}
for _, method := range srv.Methods {
var (
signature = []any{"func (x *", serviceName, ") ", method.GoName, "(ctx ", context("Context")}
returnStatement = []any{"x.transport.", method.GoName, "(ctx, "}
)
if method.Input.GoIdent.GoImportPath != emptyImport {
signature = append(signature, ", v *", method.Input.GoIdent)
returnStatement = append(returnStatement, "v)")
} else {
returnStatement = append(returnStatement, "&", method.Input.GoIdent, "{})")
}
if method.Output.GoIdent.GoImportPath != emptyImport {
g.P(append(signature, ") (*", method.Output.GoIdent, ", error) {")...)
g.P("ctx, err := authenticate(ctx, x.authenticationProvider)")
g.P("if err != nil { return nil, err }")
g.P(append([]any{"return "}, returnStatement...)...)
} else {
g.P(append(signature, ") error {")...)
g.P("ctx, err := authenticate(ctx, x.authenticationProvider)")
g.P("if err != nil { return err }")
g.P(append([]any{"_, err = "}, returnStatement...)...)
g.P("return err")
}
g.P("}\n")
}
}
return
}
func authenticateFunction(g *protogen.GeneratedFile) {
context := importPackage(g, "context")
g.P("func authenticate(ctx ", context("Context"), ", p ClientAuthenticationProvider) (", context("Context"), ", error) {")
metadata := importPackage(g, "google.golang.org/grpc/metadata")
g.P("if p != nil {")
g.P("authentication, err := p.Authentication(ctx)")
g.P("if err != nil { return ctx, err }")
g.P()
g.P("ctx = ", metadata("AppendToOutgoingContext"), `(ctx, "authorization", authentication)`)
g.P("}\n")
g.P("return ctx, nil")
g.P("}")
g.P()
}
func unexport(v string) string {
return strings.ToLower(v[:1]) + v[1:]
}
func importPackage(g *protogen.GeneratedFile, pkg string) func(string) string {
return func(name string) string {
return g.QualifiedGoIdent(protogen.GoIdent{
GoImportPath: protogen.GoImportPath(pkg),
GoName: name,
})
}
}
func relativeImport(g *protogen.GeneratedFile, file *protogen.File, name string) string {
return g.QualifiedGoIdent(protogen.GoIdent{
GoImportPath: file.GoImportPath,
GoName: name,
})
}
const sdkBase = `// ClientTokenProvider is a type which when requested provides a
// client token which can be used to authenticate RPC/API calls
// invoked through the SDK.
// Deprecated: Use ClientAuthenticationProvider instead.
type ClientTokenProvider interface {
ClientToken() (string, error)
}
// WithClientTokenProviders returns an Option which configures
// any supplied SDK with the provided ClientTokenProvider.
// Deprecated: Use WithAuthenticationProvider instead.
func WithClientTokenProvider(p ClientTokenProvider) Option {
return func(s *SDK) {
s.authenticationProvider = authenticationProviderFunc(func(context.Context) (string, error) {
clientToken, err := p.ClientToken()
if err != nil {
return "", err
}
return "Bearer " + string(clientToken), nil
})
}
}
type authenticationProviderFunc func(context.Context) (string, error)
func (f authenticationProviderFunc) Authentication(ctx context.Context) (string, error) {
return f(ctx)
}
// StaticClientTokenProvider is a string which is supplied as a static client token
// on each RPC which requires authentication.
// Deprecated: Use StaticTokenAuthenticationProvider instead.
type StaticClientTokenProvider string
// ClientToken returns the underlying string that is the StaticClientTokenProvider.
// Deprecated: Use StaticTokenAuthenticationProvider instead.
func (p StaticClientTokenProvider) ClientToken() (string, error) {
return string(p), nil
}
// ClientAuthenticationProvider is a type which when requested provides a
// client authentication which can be used to authenticate RPC/API calls
// invoked through the SDK.
type ClientAuthenticationProvider interface {
Authentication(context.Context) (string, error)
}
// SDK is the definition of Flipt's Go SDK.
// It depends on a pluggable transport implementation and exposes
// a consistent API surface area across both transport implementations.
// It also provides consistent client-side instrumentation and authentication
// lifecycle support.
type SDK struct {
transport Transport
authenticationProvider ClientAuthenticationProvider
}
// Option is a functional option which configures the Flipt SDK.
type Option func(*SDK)
// WithAuthenticationProviders returns an Option which configures
// any supplied SDK with the provided ClientAuthenticationProvider.
func WithAuthenticationProvider(p ClientAuthenticationProvider) Option {
return func(s *SDK) {
s.authenticationProvider = p
}
}
// StaticTokenAuthenticationProvider is a string which is supplied as a static client authentication
// on each RPC which requires authentication.
type StaticTokenAuthenticationProvider string
// Authentication returns the underlying string that is the StaticTokenAuthenticationProvider.
func (p StaticTokenAuthenticationProvider) Authentication(context.Context) (string, error) {
return "Bearer " + string(p), nil
}
// JWTAuthenticationProvider is a string which is supplied as a JWT client authentication
// on each RPC which requires authentication.
type JWTAuthenticationProvider string
// Authentication returns the underlying string that is the JWTAuthenticationProvider.
func (p JWTAuthenticationProvider) Authentication(context.Context) (string, error) {
return "JWT " + string(p), nil
}
// KubernetesAuthenticationProvider is an implementation of ClientAuthenticationProvider
// which automatically uses the service account token from the environment and exchanges
// it with Flipt for a client token.
// This provider keeps the client token up to date and refreshes it for a new client
// token before expiry. It re-reads the service account token as Kubernetes can and will refresh
// this token, as it also has its own expiry.
type KubernetesAuthenticationProvider struct {
transport Transport
serviceAccountTokenPath string
leeway time.Duration
mu sync.RWMutex
resp *auth.VerifyServiceAccountResponse
}
// KubernetesAuthenticationProviderOption is a functional option for configuring KubernetesAuthenticationProvider.
type KubernetesAuthenticationProviderOption func(*KubernetesAuthenticationProvider)
// WithKubernetesServiceAccountTokenPath sets the path on the host to locate the kubernetes service account.
// The KubernetesAuthenticationProvider uses the default location set by Kubernetes.
// This option lets you override that if your path happens to differ.
func WithKubernetesServiceAccountTokenPath(p string) KubernetesAuthenticationProviderOption {
return func(kctp *KubernetesAuthenticationProvider) {
kctp.serviceAccountTokenPath = p
}
}
// WithKubernetesExpiryLeeway configures the duration leeway for deciding when to refresh
// the client token. The default is 10 seconds, which ensures that tokens are automatically refreshed
// when their is less that 10 seconds of lifetime left on the previously fetched client token.
func WithKubernetesExpiryLeeway(d time.Duration) KubernetesAuthenticationProviderOption {
return func(kctp *KubernetesAuthenticationProvider) {
kctp.leeway = d
}
}
// NewKubernetesAuthenticationProvider constructs and configures a new KubernetesAuthenticationProvider
// using the provided transport.
func NewKubernetesAuthenticationProvider(transport Transport, opts ...KubernetesAuthenticationProviderOption) *KubernetesAuthenticationProvider {
k := &KubernetesAuthenticationProvider{
transport: transport,
serviceAccountTokenPath: defaultServiceAccountTokenPath,
leeway: defaultKubernetesExpiryLeeway,
}
for _, opt := range opts {
opt(k)
}
return k
}
// Authentication returns the authentication header string to be used for a request
// by the client SDK. It is generated via exchanging the local service account token
// with Flipt for a client token. The token is then formatted appropriately for use
// in the Authentication header as a bearer token.
func (k *KubernetesAuthenticationProvider) Authentication(ctx context.Context) (string, error) {
k.mu.RLock()
resp := k.resp
k.mu.RUnlock()
if resp != nil && time.Now().UTC().Add(k.leeway).Before(resp.Authentication.ExpiresAt.AsTime()) {
return StaticTokenAuthenticationProvider(k.resp.ClientToken).Authentication(ctx)
}
k.mu.Lock()
defer k.mu.Unlock()
saToken, err := os.ReadFile(k.serviceAccountTokenPath)
if err != nil {
return "", err
}
resp, err = k.transport.
AuthClient().
AuthenticationMethodKubernetesServiceClient().
VerifyServiceAccount(ctx, &auth.VerifyServiceAccountRequest{
ServiceAccountToken: string(saToken),
})
if err != nil {
return "", err
}
k.resp = resp
return StaticTokenAuthenticationProvider(k.resp.ClientToken).Authentication(ctx)
}
// New constructs and configures a Flipt SDK instance from
// the provided Transport implementation and options.
func New(t Transport, opts ...Option) SDK {
sdk := SDK{transport: t}
for _, opt := range opts { opt(&sdk) }
return sdk
}`
func shouldIgnoreService(srv *protogen.Service) bool {
if v := proto.GetExtension(srv.Desc.Options(), visibility.E_ApiVisibility).(*visibility.VisibilityRule); v != nil {
return v.Restriction == ignoreDecl
}
return false
}