mirror of
https://github.com/hanzoai/deploy.git
synced 2026-10-07 08:44:50 +00:00
github.com/argoproj/pkg/v2 was one of two live argoproj dependencies. It is 11 files and 176K total, and we used five packages from it: grpc/http, kubeclientmetrics, stats, sync, time. Vendored into util/vendored/ and the dependency dropped from go.mod, so the gRPC surface (2 references, in grpc-http/forwarders.go) is now ours to remove on our own schedule rather than upstream's. Kubernetes client-go pulls gRPC transitively regardless, so this is isolation — a realistic first step — not a claim that gRPC is gone. Internalized rather than forked to a shared repo because the only other consumer, hanzo/gitops, has zero live deployments and zero dependents. A second repo would be a second thing to version for one caller. NOTICE carries the Apache-2.0 attribution. go build ./... exit 0. Co-authored-by: Hanzo Dev <[email protected]>
306 lines
7.8 KiB
Go
306 lines
7.8 KiB
Go
package http
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/felixge/httpsnoop"
|
|
//nolint:staticcheck // SA1019 Can't migrate due to grpc gateway using old package and major version bump needed
|
|
"github.com/golang/protobuf/proto"
|
|
"github.com/grpc-ecosystem/grpc-gateway/runtime"
|
|
log "github.com/sirupsen/logrus"
|
|
"golang.org/x/net/context"
|
|
)
|
|
|
|
type messageMarshaler struct {
|
|
fields map[string]interface{}
|
|
exclude bool
|
|
isSSE bool
|
|
fieldProcessor FieldProcessor
|
|
}
|
|
|
|
func (m *messageMarshaler) Unmarshal(data []byte, v interface{}) error {
|
|
return nil
|
|
}
|
|
|
|
func (m *messageMarshaler) NewDecoder(r io.Reader) runtime.Decoder {
|
|
return nil
|
|
}
|
|
|
|
func (m *messageMarshaler) NewEncoder(w io.Writer) runtime.Encoder {
|
|
return nil
|
|
}
|
|
|
|
func (m *messageMarshaler) ContentType() string {
|
|
if m.isSSE {
|
|
return "text/event-stream"
|
|
} else {
|
|
return "application/json"
|
|
}
|
|
}
|
|
|
|
// FieldProcessor is a function that handles included/excluded fields
|
|
type FieldProcessor func(val interface{}, fields map[string]interface{}, exclude bool) (interface{}, error)
|
|
|
|
func (m *messageMarshaler) Marshal(v interface{}) ([]byte, error) {
|
|
var dataBytes []byte
|
|
var err error
|
|
|
|
if len(m.fields) == 0 {
|
|
dataBytes, err = json.Marshal(v)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
} else if m.fieldProcessor != nil {
|
|
res, err := m.fieldProcessor(v, m.fields, m.exclude)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
dataBytes, err = json.Marshal(res)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
dataBytes, err = json.Marshal(v)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if _, ok := v.([]interface{}); ok {
|
|
data := make([]interface{}, 0)
|
|
err = json.Unmarshal(dataBytes, &data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for i := range data {
|
|
m.processItem([]string{}, data[i])
|
|
}
|
|
dataBytes, err = json.Marshal(data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
data := make(map[string]interface{})
|
|
err = json.Unmarshal(dataBytes, &data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
m.processItem([]string{}, data)
|
|
dataBytes, err = json.Marshal(data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
}
|
|
if m.isSSE {
|
|
dataBytes = []byte(fmt.Sprintf("data: %s \n\n", string(dataBytes)))
|
|
}
|
|
return dataBytes, nil
|
|
}
|
|
|
|
func (m *messageMarshaler) processItem(path []string, item interface{}) {
|
|
if mapItem, ok := item.(map[string]interface{}); ok {
|
|
for k, v := range mapItem {
|
|
fieldPath := strings.Join(append(path, k), ".")
|
|
_, pathIn := m.fields[fieldPath]
|
|
parentPathIn := pathIn
|
|
if !parentPathIn {
|
|
for k := range m.fields {
|
|
if strings.HasPrefix(k, fieldPath) {
|
|
parentPathIn = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
keep := m.exclude && !pathIn || !m.exclude && parentPathIn
|
|
|
|
if keep {
|
|
if !pathIn {
|
|
m.processItem(append(path, k), v)
|
|
}
|
|
} else {
|
|
delete(mapItem, k)
|
|
}
|
|
}
|
|
} else if arrayItem, ok := item.([]interface{}); ok {
|
|
for i := range arrayItem {
|
|
m.processItem(path, arrayItem[i])
|
|
}
|
|
}
|
|
}
|
|
|
|
func newMarshaler(req *http.Request, isSSE bool) *messageMarshaler {
|
|
fieldsQuery := req.URL.Query().Get("fields")
|
|
fields := make(map[string]interface{})
|
|
exclude := false
|
|
if fieldsQuery != "" {
|
|
if strings.HasPrefix(fieldsQuery, "-") {
|
|
fieldsQuery = fieldsQuery[1:]
|
|
exclude = true
|
|
}
|
|
for _, field := range strings.Split(fieldsQuery, ",") {
|
|
fields[field] = true
|
|
}
|
|
}
|
|
return &messageMarshaler{isSSE: isSSE, fields: fields, exclude: exclude}
|
|
}
|
|
|
|
type StreamForwarderFunc func(
|
|
ctx context.Context,
|
|
mux *runtime.ServeMux,
|
|
marshaler runtime.Marshaler,
|
|
w http.ResponseWriter,
|
|
req *http.Request,
|
|
recv func() (proto.Message, error),
|
|
opts ...func(context.Context, http.ResponseWriter, proto.Message) error,
|
|
)
|
|
|
|
func flush(flusher http.Flusher) {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
log.Warn("recovered in flush, issue with writer inside http.ResponseWriter")
|
|
}
|
|
}()
|
|
flusher.Flush()
|
|
}
|
|
|
|
func writeKeepalive(w http.ResponseWriter, mut *sync.Mutex) {
|
|
mut.Lock()
|
|
defer mut.Unlock()
|
|
|
|
// Per https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation,
|
|
// lines that start with a `:` must be ignored by the client.
|
|
_, err := w.Write([]byte(":\n"))
|
|
|
|
if err != nil {
|
|
log.Warnf("failed to write http keepalive response: %v", err)
|
|
} else if f, ok := w.(http.Flusher); ok {
|
|
flush(f)
|
|
}
|
|
}
|
|
|
|
func keepalive(ctx context.Context, w http.ResponseWriter, mut *sync.Mutex) {
|
|
keepaliveInterval := time.Duration(time.Second * 15)
|
|
keepaliveTicker := time.NewTicker(keepaliveInterval)
|
|
|
|
defer keepaliveTicker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-keepaliveTicker.C:
|
|
writeKeepalive(w, mut)
|
|
}
|
|
}
|
|
}
|
|
|
|
func withKeepalive(ctx context.Context, w http.ResponseWriter) http.ResponseWriter {
|
|
mut := sync.Mutex{}
|
|
|
|
go keepalive(ctx, w, &mut)
|
|
|
|
return httpsnoop.Wrap(w, httpsnoop.Hooks{
|
|
Write: func(next httpsnoop.WriteFunc) httpsnoop.WriteFunc {
|
|
return func(p []byte) (int, error) {
|
|
mut.Lock()
|
|
defer mut.Unlock()
|
|
return next(p)
|
|
}
|
|
},
|
|
})
|
|
}
|
|
|
|
func NewStreamForwarder(messageKey func(proto.Message) (string, error)) StreamForwarderFunc {
|
|
return func(
|
|
ctx context.Context,
|
|
mux *runtime.ServeMux,
|
|
marshaler runtime.Marshaler,
|
|
w http.ResponseWriter,
|
|
req *http.Request,
|
|
recv func() (proto.Message, error),
|
|
opts ...func(context.Context, http.ResponseWriter, proto.Message) error,
|
|
) {
|
|
isSSE := req.Header.Get("Accept") == "text/event-stream"
|
|
processCtx, cancel := context.WithCancel(ctx)
|
|
defer cancel()
|
|
if isSSE {
|
|
w.Header().Set("Content-Type", "text/event-stream")
|
|
w.Header().Set("Transfer-Encoding", "chunked")
|
|
w.Header().Set("X-Content-Type-Options", "nosniff")
|
|
w = withKeepalive(processCtx, w)
|
|
}
|
|
dataByKey := make(map[string][]byte)
|
|
m := newMarshaler(req, isSSE)
|
|
if messageKey != nil {
|
|
oldRecv := recv
|
|
recv = func() (proto.Message, error) {
|
|
for {
|
|
if ctx.Err() != nil {
|
|
return nil, ctx.Err()
|
|
}
|
|
|
|
msg, err := oldRecv()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
key, err := messageKey(msg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
data, err := m.Marshal(map[string]proto.Message{"result": msg})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
prevData, ok := dataByKey[key]
|
|
if !ok || string(prevData) != string(data) {
|
|
dataByKey[key] = data
|
|
return msg, nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
runtime.ForwardResponseStream(ctx, mux, m, w, req, recv, opts...)
|
|
}
|
|
}
|
|
func UnaryForwarderWithFieldProcessor(fieldProcessor FieldProcessor) func(
|
|
ctx context.Context,
|
|
mux *runtime.ServeMux,
|
|
marshaler runtime.Marshaler,
|
|
w http.ResponseWriter,
|
|
req *http.Request,
|
|
resp proto.Message,
|
|
opts ...func(context.Context, http.ResponseWriter, proto.Message) error,
|
|
) {
|
|
return func(ctx context.Context, mux *runtime.ServeMux, marshaler runtime.Marshaler, w http.ResponseWriter, req *http.Request, resp proto.Message, opts ...func(context.Context, http.ResponseWriter, proto.Message) error) {
|
|
m := newMarshaler(req, false)
|
|
m.fieldProcessor = fieldProcessor
|
|
runtime.ForwardResponseMessage(ctx, mux, m, w, req, resp, opts...)
|
|
}
|
|
}
|
|
|
|
var (
|
|
// UnaryForwarder serializes protobuf message to JSON and removes fields using query parameter `fields`.
|
|
// The `fields` parameter example:
|
|
// fields=items.metadata.name,items.spec - response should include only items.metadata.name and items.spec fields
|
|
// fields=-items.metadata.name - response should include all fields except items.metadata.name
|
|
UnaryForwarder = func(
|
|
ctx context.Context,
|
|
mux *runtime.ServeMux,
|
|
marshaler runtime.Marshaler,
|
|
w http.ResponseWriter,
|
|
req *http.Request,
|
|
resp proto.Message,
|
|
opts ...func(context.Context, http.ResponseWriter, proto.Message) error,
|
|
) {
|
|
runtime.ForwardResponseMessage(ctx, mux, newMarshaler(req, false), w, req, resp, opts...)
|
|
}
|
|
|
|
// StreamForwarder serializes protobuf message to JSON and removes fields using query parameter `fields`
|
|
StreamForwarder = NewStreamForwarder(nil)
|
|
)
|