Skip to content

Commit 122b92f

Browse files
committed
more progres
1 parent e244bdb commit 122b92f

File tree

10 files changed

+474
-47
lines changed

10 files changed

+474
-47
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
spoa
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Build stage
2+
FROM golang:1.24-alpine AS builder
3+
ENV CGO_ENABLED=1
4+
5+
WORKDIR /app
6+
COPY . .
7+
8+
RUN apk add --no-cache --update git build-base openssl
9+
10+
# Build the spoa binary
11+
RUN go build -tags=appsec -o ./contrib/haproxy/stream-processing-offload/cmd/spoa/spoa ./contrib/haproxy/stream-processing-offload/cmd/spoa
12+
13+
# Runtime stage
14+
FROM alpine:3.20.3
15+
16+
# Set opencontainers labels for Github container registry
17+
LABEL org.opencontainers.image.source=https://github.com/DataDog/dd-trace-go/tree/main/contrib/haproxy/stream-processing-offload/cmd/spoa
18+
LABEL org.opencontainers.image.description="An HAProxy Stream Processing Offload Agent service with Datadog App & API Protection support"
19+
LABEL org.opencontainers.image.licenses=Apache-2.0
20+
21+
ARG COMMIT_SHA=""
22+
LABEL org.opencontainers.image.revision=${COMMIT_SHA}
23+
24+
RUN apk --no-cache add ca-certificates tzdata libc6-compat libgcc libstdc++
25+
WORKDIR /app
26+
27+
ARG DD_VERSION=""
28+
ENV DD_VERSION=${DD_VERSION}
29+
30+
COPY --from=builder /app/contrib/haproxy/stream-processing-offload/cmd/spoa/spoa /app/spoa
31+
32+
EXPOSE 3000
33+
EXPOSE 3080
34+
35+
CMD ["./spoa"]
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# HAProxy Stream Processing Offload (SPOA) with Datadog App & API Protection
2+
3+
[GCP Services Extensions](https://cloud.google.com/service-extensions/docs/overview) enable Google Cloud users to provide programmability and extensibility on Cloud Load Balancing data paths and at the edge.
4+
5+
## Installation
6+
7+
### From Release
8+
9+
This package provides a docker image to be used with Google Cloud Service Extensions.
10+
The images are published at each release of the tracer and can be found in [the repo registry](https://github.com/DataDog/dd-trace-go/pkgs/container/dd-trace-go%2Fservice-extensions-callout).
11+
12+
### Build image
13+
14+
The docker image can be build locally using docker. Start by cloning the `dd-trace-go` repo, `cd` inside it and run that command:
15+
```sh
16+
docker build -f contrib/haproxy/stream-processing-offload/cmd/spoa/Dockerfile -t datadog/dd-trace-go/haproxy-spoa:local .
17+
```
18+
19+
## Configuration
20+
21+
The ASM Service Extension expose some configuration. The configuration can be tweaked if the Service Extension is only used as an External Processor for Envoy that is not operated by GCP.
22+
23+
>**GCP requires that the default configuration for the Service Extension should not change.**
24+
25+
| Environment variable | Default value | Description |
26+
|---|---------------|---------------------------------------------------------------------------------------------------------------|
27+
| `DD_SERVICE_EXTENSION_HOST` | `0.0.0.0` | Host on where the gRPC and HTTP server should listen to. |
28+
| `DD_SERVICE_EXTENSION_PORT` | `443` | Port used by the gRPC Server.<br>Envoy Google backend’s is only using secure connection to Service Extension. |
29+
| `DD_SERVICE_EXTENSION_HEALTHCHECK_PORT` | `80` | Port used for the HTTP server for the health check. |
30+
| `DD_SERVICE_EXTENSION_OBSERVABILITY_MODE` | `false` | Enable observability mode. This will process a request asynchronously (blocking would be disabled). |
31+
32+
> The Service Extension need to be connected to a deployed [Datadog agent](https://docs.datadoghq.com/agent).
33+
34+
| Environment variable | Default value | Description |
35+
|---|---|---|
36+
| `DD_AGENT_HOST` | `N/A` | Host of a running Datadog Agent. |
37+
| `DD_TRACE_AGENT_PORT` | `8126` | Port of a running Datadog Agent. |
38+
39+
### SSL Configuration
40+
41+
The Envoy of GCP is configured to communicate to the Service Extension with TLS.
42+
43+
`localhost` self signed certificates are generated and bundled into the ASM Service Extension docker image and loaded at the start of the gRPC server.
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Unless explicitly stated otherwise all files in this repository are licensed
2+
// under the Apache License Version 2.0.
3+
// This product includes software developed at Datadog (https://www.datadoghq.com/).
4+
// Copyright 2016 Datadog, Inc.
5+
6+
package main
7+
8+
import (
9+
"net"
10+
"os"
11+
"strconv"
12+
)
13+
14+
// IntEnv returns the parsed int value of an environment variable, or
15+
// def otherwise.
16+
func intEnv(key string, def int) int {
17+
vv, ok := os.LookupEnv(key)
18+
if !ok {
19+
return def
20+
}
21+
v, err := strconv.Atoi(vv)
22+
if err != nil {
23+
log.Warn("Non-integer value for env var %s, defaulting to %d. Parse failed with error: %v", key, def, err)
24+
return def
25+
}
26+
return v
27+
}
28+
29+
// IpEnv returns the valid IP value of an environment variable, or def otherwise.
30+
func ipEnv(key string, def net.IP) net.IP {
31+
vv, ok := os.LookupEnv(key)
32+
if !ok {
33+
return def
34+
}
35+
36+
ip := net.ParseIP(vv)
37+
if ip == nil {
38+
log.Warn("Non-IP value for env var %s, defaulting to %s", key, def.String())
39+
return def
40+
}
41+
42+
return ip
43+
}

contrib/haproxy/stream-processing-offload/cmd/spoa/main.go

Lines changed: 127 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,37 +7,155 @@ package main
77

88
import (
99
"context"
10-
"github.com/DataDog/dd-trace-go/v2/ddtrace/tracer"
10+
"errors"
11+
"fmt"
1112
"net"
13+
"net/http"
1214
"os"
15+
"os/signal"
16+
"strconv"
17+
"syscall"
18+
"time"
1319

14-
"github.com/DataDog/dd-trace-go/contrib/haproxy/stream-processing-offload/v2"
1520
"github.com/negasus/haproxy-spoe-go/agent"
21+
"golang.org/x/sync/errgroup"
22+
23+
"github.com/DataDog/dd-trace-go/contrib/haproxy/stream-processing-offload/v2"
24+
"github.com/DataDog/dd-trace-go/v2/ddtrace/tracer"
25+
"github.com/DataDog/dd-trace-go/v2/instrumentation"
1626
)
1727

28+
type haProxySpoaConfig struct {
29+
extensionPort string
30+
healthcheckPort string
31+
extensionHost string
32+
bodyParsingSizeLimit int
33+
}
34+
1835
var log = NewLogger()
1936

37+
func getDefaultEnvVars() map[string]string {
38+
return map[string]string{
39+
"DD_VERSION": instrumentation.Version(), // Version of the tracer
40+
"DD_APM_TRACING_ENABLED": "false", // Appsec Standalone
41+
"DD_APPSEC_WAF_TIMEOUT": "10ms", // Proxy specific WAF timeout
42+
"_DD_APPSEC_PROXY_ENVIRONMENT": "true", // Internal config: Enable API Security proxy sampler
43+
}
44+
}
45+
46+
// initializeEnvironment sets up required environment variables with their defaults
47+
func initializeEnvironment() {
48+
for k, v := range getDefaultEnvVars() {
49+
if os.Getenv(k) == "" {
50+
if err := os.Setenv(k, v); err != nil {
51+
log.Error("haproxy_spoa: failed to set %s environment variable: %s\n", k, err.Error())
52+
}
53+
}
54+
}
55+
}
56+
57+
// loadConfig loads the configuration from the environment variables
58+
func loadConfig() haProxySpoaConfig {
59+
extensionHostStr := ipEnv("DD_HAPROXY_SPOA_HOST", net.IP{0, 0, 0, 0}).String()
60+
extensionPortInt := intEnv("DD_HAPROXY_SPOA_PORT", 3000)
61+
healthcheckPortInt := intEnv("DD_HAPROXY_SPOA_HEALTHCHECK_PORT", 3080)
62+
bodyParsingSizeLimit := intEnv("DD_APPSEC_BODY_PARSING_SIZE_LIMIT", 0)
63+
64+
extensionPortStr := strconv.FormatInt(int64(extensionPortInt), 10)
65+
healthcheckPortStr := strconv.FormatInt(int64(healthcheckPortInt), 10)
66+
67+
return haProxySpoaConfig{
68+
extensionPort: extensionPortStr,
69+
extensionHost: extensionHostStr,
70+
healthcheckPort: healthcheckPortStr,
71+
bodyParsingSizeLimit: bodyParsingSizeLimit,
72+
}
73+
}
74+
2075
func main() {
21-
listener, err := net.Listen("tcp4", "127.0.0.1:3000")
22-
if err != nil {
23-
log.Error("haproxy_spoa: error create listener, %v", err)
76+
initializeEnvironment()
77+
config := loadConfig()
78+
79+
if err := startService(config); err != nil {
80+
log.Error("haproxy_spoa: %s\n", err.Error())
2481
os.Exit(1)
2582
}
83+
84+
log.Info("haproxy_spoa: shutting down\n")
85+
}
86+
87+
func startService(config haProxySpoaConfig) error {
88+
ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
89+
defer cancel()
90+
g, ctx := errgroup.WithContext(ctx)
91+
92+
g.Go(func() error {
93+
return startSpoa(ctx, config)
94+
})
95+
96+
g.Go(func() error {
97+
return startHealthCheck(ctx, config)
98+
})
99+
100+
if err := g.Wait(); err != nil {
101+
return err
102+
}
103+
104+
return nil
105+
}
106+
107+
func startHealthCheck(ctx context.Context, config haProxySpoaConfig) error {
108+
muxServer := http.NewServeMux()
109+
muxServer.HandleFunc("/", func(w http.ResponseWriter, _ *http.Request) {
110+
w.Header().Set("Content-Type", "application/json")
111+
w.WriteHeader(http.StatusOK)
112+
w.Write([]byte(`{"status": "ok", "library": {"language": "golang", "version": "` + instrumentation.Version() + `"}}`))
113+
})
114+
115+
server := &http.Server{
116+
Addr: config.extensionHost + ":" + config.healthcheckPort,
117+
Handler: muxServer,
118+
}
119+
120+
go func() {
121+
<-ctx.Done()
122+
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
123+
defer cancel()
124+
if err := server.Shutdown(shutdownCtx); err != nil {
125+
log.Error("haproxy_spoa: health check server shutdown: %s\n", err.Error())
126+
}
127+
}()
128+
129+
log.Info("haproxy_spoa: health check server started on %s:%s\n", config.extensionHost, config.healthcheckPort)
130+
if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
131+
return fmt.Errorf("health check http server: %s", err.Error())
132+
}
133+
134+
return nil
135+
}
136+
137+
func startSpoa(ctx context.Context, config haProxySpoaConfig) error {
138+
listener, err := net.Listen("tcp4", config.extensionHost+":"+config.extensionPort)
139+
if err != nil {
140+
return fmt.Errorf("error creating listener: %w", err)
141+
}
26142
defer listener.Close()
27143

28144
_ = tracer.Start(tracer.WithAppSecEnabled(true))
29145
defer tracer.Stop()
30146

31147
appsecHAProxy := streamprocessingoffload.NewHAProxySPOA(streamprocessingoffload.AppsecHAProxyConfig{
32148
BlockingUnavailable: false,
33-
BodyParsingSizeLimit: 1000000, // 1MB
34-
Context: context.Background(),
149+
BodyParsingSizeLimit: config.bodyParsingSizeLimit,
150+
Context: ctx,
35151
})
36152

37153
a := agent.New(appsecHAProxy.Handler, log)
38154

39-
log.Info("haproxy_spoa: started\n")
155+
log.Info("haproxy_spoa: datadog agent server started on %s:%s\n", config.extensionHost, config.extensionPort)
40156
if err := a.Serve(listener); err != nil {
41-
log.Error("haproxy_spoa: error agent serve: %+v\n", err)
157+
return fmt.Errorf("error starting agent server: %w", err)
42158
}
159+
160+
return nil
43161
}

0 commit comments

Comments
 (0)