mirror of https://github.com/grafana/grafana
K8s: Add Aggregation to Backend Service (#81591)
Co-authored-by: Charandas Batra <charandas.batra@grafana.com>pull/82215/head^2
parent
6d5211e172
commit
d6e6298103
@ -0,0 +1,14 @@ |
||||
apiVersion: apiregistration.k8s.io/v1 |
||||
kind: APIService |
||||
metadata: |
||||
name: v0alpha1.example.grafana.app |
||||
spec: |
||||
version: v0alpha1 |
||||
insecureSkipTLSVerify: true |
||||
group: example.grafana.app |
||||
groupPriorityMinimum: 1000 |
||||
versionPriority: 15 |
||||
service: |
||||
name: example-apiserver |
||||
namespace: grafana |
||||
port: 7443 |
||||
@ -0,0 +1,7 @@ |
||||
apiVersion: service.grafana.app/v0alpha1 |
||||
kind: ExternalName |
||||
metadata: |
||||
name: example-apiserver |
||||
namespace: grafana |
||||
spec: |
||||
host: localhost |
||||
@ -0,0 +1,132 @@ |
||||
package responsewriter |
||||
|
||||
import ( |
||||
"bufio" |
||||
"fmt" |
||||
"io" |
||||
"net/http" |
||||
|
||||
"k8s.io/apiserver/pkg/endpoints/responsewriter" |
||||
"k8s.io/klog/v2" |
||||
) |
||||
|
||||
var _ responsewriter.CloseNotifierFlusher = (*ResponseAdapter)(nil) |
||||
var _ http.ResponseWriter = (*ResponseAdapter)(nil) |
||||
var _ io.ReadCloser = (*ResponseAdapter)(nil) |
||||
|
||||
func WrapHandler(handler http.Handler) func(req *http.Request) (*http.Response, error) { |
||||
// ignore the lint error because the response is passed directly to the client,
|
||||
// so the client will be responsible for closing the response body.
|
||||
//nolint:bodyclose
|
||||
return func(req *http.Request) (*http.Response, error) { |
||||
w := NewAdapter(req) |
||||
resp := w.Response() |
||||
go func() { |
||||
handler.ServeHTTP(w, req) |
||||
if err := w.CloseWriter(); err != nil { |
||||
klog.Errorf("error closing writer: %v", err) |
||||
} |
||||
}() |
||||
return resp, nil |
||||
} |
||||
} |
||||
|
||||
// ResponseAdapter is an implementation of [http.ResponseWriter] that allows conversion to a [http.Response].
|
||||
type ResponseAdapter struct { |
||||
req *http.Request |
||||
res *http.Response |
||||
reader io.ReadCloser |
||||
writer io.WriteCloser |
||||
buffered *bufio.ReadWriter |
||||
} |
||||
|
||||
// NewAdapter returns an initialized [ResponseAdapter].
|
||||
func NewAdapter(req *http.Request) *ResponseAdapter { |
||||
r, w := io.Pipe() |
||||
writer := bufio.NewWriter(w) |
||||
reader := bufio.NewReader(r) |
||||
buffered := bufio.NewReadWriter(reader, writer) |
||||
return &ResponseAdapter{ |
||||
req: req, |
||||
res: &http.Response{ |
||||
Proto: req.Proto, |
||||
ProtoMajor: req.ProtoMajor, |
||||
ProtoMinor: req.ProtoMinor, |
||||
Header: make(http.Header), |
||||
}, |
||||
reader: r, |
||||
writer: w, |
||||
buffered: buffered, |
||||
} |
||||
} |
||||
|
||||
// Header implements [http.ResponseWriter].
|
||||
// It returns the response headers to mutate within a handler.
|
||||
func (ra *ResponseAdapter) Header() http.Header { |
||||
return ra.res.Header |
||||
} |
||||
|
||||
// Write implements [http.ResponseWriter].
|
||||
func (ra *ResponseAdapter) Write(buf []byte) (int, error) { |
||||
return ra.buffered.Write(buf) |
||||
} |
||||
|
||||
// Read implements [io.Reader].
|
||||
func (ra *ResponseAdapter) Read(buf []byte) (int, error) { |
||||
return ra.buffered.Read(buf) |
||||
} |
||||
|
||||
// WriteHeader implements [http.ResponseWriter].
|
||||
func (ra *ResponseAdapter) WriteHeader(code int) { |
||||
ra.res.StatusCode = code |
||||
ra.res.Status = fmt.Sprintf("%03d %s", code, http.StatusText(code)) |
||||
} |
||||
|
||||
// Flush implements [http.Flusher].
|
||||
func (ra *ResponseAdapter) Flush() { |
||||
if ra.buffered.Writer.Buffered() == 0 { |
||||
return |
||||
} |
||||
|
||||
if err := ra.buffered.Writer.Flush(); err != nil { |
||||
klog.Error("Error flushing response buffer: ", "error", err) |
||||
} |
||||
} |
||||
|
||||
// Response returns the [http.Response] generated by the [http.Handler].
|
||||
func (ra *ResponseAdapter) Response() *http.Response { |
||||
// make sure to set the status code to 200 if the request is a watch
|
||||
// this is to ensure that client-go uses a streamwatcher:
|
||||
// https://github.com/kubernetes/client-go/blob/76174b8af8cfd938018b04198595d65b48a69334/rest/request.go#L737
|
||||
if ra.res.StatusCode == 0 && ra.req.URL.Query().Get("watch") == "true" { |
||||
ra.WriteHeader(http.StatusOK) |
||||
} |
||||
ra.res.Body = ra |
||||
return ra.res |
||||
} |
||||
|
||||
// Decorate implements [responsewriter.UserProvidedDecorator].
|
||||
func (ra *ResponseAdapter) Unwrap() http.ResponseWriter { |
||||
return ra |
||||
} |
||||
|
||||
// CloseNotify implements [http.CloseNotifier].
|
||||
func (ra *ResponseAdapter) CloseNotify() <-chan bool { |
||||
ch := make(chan bool) |
||||
go func() { |
||||
<-ra.req.Context().Done() |
||||
ch <- true |
||||
}() |
||||
return ch |
||||
} |
||||
|
||||
// Close implements [io.Closer].
|
||||
func (ra *ResponseAdapter) Close() error { |
||||
return ra.reader.Close() |
||||
} |
||||
|
||||
// CloseWriter should be called after the http.Handler has returned.
|
||||
func (ra *ResponseAdapter) CloseWriter() error { |
||||
ra.Flush() |
||||
return ra.writer.Close() |
||||
} |
||||
@ -0,0 +1,136 @@ |
||||
package responsewriter_test |
||||
|
||||
import ( |
||||
"io" |
||||
"math/rand" |
||||
"net/http" |
||||
"testing" |
||||
"time" |
||||
|
||||
grafanaresponsewriter "github.com/grafana/grafana/pkg/services/apiserver/endpoints/responsewriter" |
||||
"github.com/stretchr/testify/require" |
||||
) |
||||
|
||||
func TestResponseAdapter(t *testing.T) { |
||||
t.Run("should handle synchronous write", func(t *testing.T) { |
||||
client := &http.Client{ |
||||
Transport: &roundTripperFunc{ |
||||
ready: make(chan struct{}), |
||||
// ignore the lint error because the response is passed directly to the client,
|
||||
// so the client will be responsible for closing the response body.
|
||||
//nolint:bodyclose
|
||||
fn: grafanaresponsewriter.WrapHandler(http.HandlerFunc(syncHandler)), |
||||
}, |
||||
} |
||||
close(client.Transport.(*roundTripperFunc).ready) |
||||
req, err := http.NewRequest("GET", "http://localhost/test", nil) |
||||
require.NoError(t, err) |
||||
|
||||
resp, err := client.Do(req) |
||||
require.NoError(t, err) |
||||
|
||||
defer func() { |
||||
err := resp.Body.Close() |
||||
require.NoError(t, err) |
||||
}() |
||||
|
||||
bodyBytes, err := io.ReadAll(resp.Body) |
||||
require.NoError(t, err) |
||||
require.Equal(t, "OK", string(bodyBytes)) |
||||
}) |
||||
|
||||
t.Run("should handle synchronous write", func(t *testing.T) { |
||||
generateRandomStrings(10) |
||||
client := &http.Client{ |
||||
Transport: &roundTripperFunc{ |
||||
ready: make(chan struct{}), |
||||
// ignore the lint error because the response is passed directly to the client,
|
||||
// so the client will be responsible for closing the response body.
|
||||
//nolint:bodyclose
|
||||
fn: grafanaresponsewriter.WrapHandler(http.HandlerFunc(asyncHandler)), |
||||
}, |
||||
} |
||||
close(client.Transport.(*roundTripperFunc).ready) |
||||
req, err := http.NewRequest("GET", "http://localhost/test?watch=true", nil) |
||||
require.NoError(t, err) |
||||
|
||||
resp, err := client.Do(req) |
||||
require.NoError(t, err) |
||||
|
||||
defer func() { |
||||
err := resp.Body.Close() |
||||
require.NoError(t, err) |
||||
}() |
||||
|
||||
// ensure that watch request is a 200
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode) |
||||
|
||||
// limit to 100 bytes to test the reader buffer
|
||||
buf := make([]byte, 100) |
||||
// holds the read bytes between iterations
|
||||
cache := []byte{} |
||||
|
||||
for i := 0; i < 10; { |
||||
n, err := resp.Body.Read(buf) |
||||
require.NoError(t, err) |
||||
if n == 0 { |
||||
continue |
||||
} |
||||
cache = append(cache, buf[:n]...) |
||||
|
||||
if len(cache) >= len(randomStrings[i]) { |
||||
str := cache[:len(randomStrings[i])] |
||||
require.Equal(t, randomStrings[i], string(str)) |
||||
cache = cache[len(randomStrings[i]):] |
||||
i++ |
||||
} |
||||
} |
||||
}) |
||||
} |
||||
|
||||
func syncHandler(w http.ResponseWriter, r *http.Request) { |
||||
w.WriteHeader(http.StatusOK) |
||||
_, _ = w.Write([]byte("OK")) |
||||
} |
||||
|
||||
func asyncHandler(w http.ResponseWriter, r *http.Request) { |
||||
w.WriteHeader(http.StatusOK) |
||||
for _, s := range randomStrings { |
||||
time.Sleep(100 * time.Millisecond) |
||||
// write the current iteration
|
||||
_, _ = w.Write([]byte(s)) |
||||
w.(http.Flusher).Flush() |
||||
} |
||||
} |
||||
|
||||
var randomStrings = []string{} |
||||
|
||||
func generateRandomStrings(n int) { |
||||
for i := 0; i < n; i++ { |
||||
randomString := generateRandomString(1000 * (i + 1)) |
||||
randomStrings = append(randomStrings, randomString) |
||||
} |
||||
} |
||||
|
||||
func generateRandomString(n int) string { |
||||
gen := rand.New(rand.NewSource(time.Now().UnixNano())) |
||||
var chars = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") |
||||
b := make([]rune, n) |
||||
for i := range b { |
||||
b[i] = chars[gen.Intn(len(chars))] |
||||
} |
||||
return string(b) |
||||
} |
||||
|
||||
type roundTripperFunc struct { |
||||
ready chan struct{} |
||||
fn func(req *http.Request) (*http.Response, error) |
||||
} |
||||
|
||||
func (f *roundTripperFunc) RoundTrip(req *http.Request) (*http.Response, error) { |
||||
if f.fn == nil { |
||||
<-f.ready |
||||
} |
||||
res, err := f.fn(req) |
||||
return res, err |
||||
} |
||||
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in new issue