diff --git a/.github/workflows/service-extensions-publish.yml b/.github/workflows/service-extensions-publish.yml new file mode 100644 index 0000000000..9e0c645be0 --- /dev/null +++ b/.github/workflows/service-extensions-publish.yml @@ -0,0 +1,99 @@ +name: Publish Service Extensions Callout images packages + +on: + push: + branches: + - 'flavien/service-extensions' + release: + types: + - published + workflow_dispatch: + inputs: + tag_name: + description: 'Docker image tag to use for the package' + required: true + default: 'dev' + commit_sha: + description: 'Commit SHA to checkout' + required: true + +permissions: + contents: read + packages: write + +jobs: + publish-service-extensions: + runs-on: ubuntu-latest + steps: + + - name: Get tag name + id: get_tag_name + run: | + if [ "${{ github.event_name }}" = "release" ]; then + echo "::set-output name=tag::${{ github.event.release.tag_name }}" + echo "Here1: tag=${{ github.event.release.tag_name }}" + else + if [ -z "${{ github.event.inputs.tag_name }}" ]; then + echo "::set-output name=tag::dev" + echo "Here2: tag=dev" + else + echo "::set-output name=tag::${{ github.event.inputs.tag_name }}" + echo "Here3: tag=${{ github.event.inputs.tag_name }}" + fi + fi + echo "Finally: ${{ steps.get_tag_name.outputs.tag }}" + + - name: Checkout + uses: actions/checkout@v4 + if: github.event_name == 'release' + with: + ref: ${{ steps.get_tag_name.outputs.tag }} + + - name: Checkout + uses: actions/checkout@v4 + if: github.event_name != 'release' + with: + ref: ${{ github.event.inputs.commit_sha || github.sha }} + + - name: Set up Go 1.22 + uses: actions/setup-go@v5 + with: + go-version: 1.22 + id: go + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Docker + shell: bash + run: docker login -u publisher -p ${{ secrets.GITHUB_TOKEN }} ghcr.io + + - name: Build and push [dev] + id: build-dev + if: github.event_name != 'release' + uses: docker/build-push-action@v6 + with: + context: . + file: ./contrib/envoyproxy/envoy/cmd/serviceextensions/Dockerfile + platforms: linux/amd64,linux/arm64 + push: true + tags: | # Use the commit SHA from the manual trigger or default to the SHA from the push event + ghcr.io/datadog/dd-trace-go/service-extensions-callout:${{ steps.get_tag_name.outputs.tag }} + ghcr.io/datadog/dd-trace-go/service-extensions-callout:${{ github.event.inputs.commit_sha || github.sha }} + + - name: Build and push [release] + id: build-release + if: github.event_name == 'release' + uses: docker/build-push-action@v6 + with: + context: . + file: ./contrib/envoyproxy/envoy/cmd/serviceextensions/Dockerfile + platforms: linux/amd64,linux/arm64 + push: true + tags: | + ghcr.io/datadog/dd-trace-go/service-extensions-callout:latest + ghcr.io/datadog/dd-trace-go/service-extensions-callout:${{ steps.get_tag_name.outputs.tag }} + ghcr.io/datadog/dd-trace-go/service-extensions-callout:${{ github.sha }} \ No newline at end of file diff --git a/contrib/envoyproxy/envoy/cmd/serviceextensions/.gitignore b/contrib/envoyproxy/envoy/cmd/serviceextensions/.gitignore new file mode 100644 index 0000000000..68295c4a55 --- /dev/null +++ b/contrib/envoyproxy/envoy/cmd/serviceextensions/.gitignore @@ -0,0 +1 @@ +serviceextensions \ No newline at end of file diff --git a/contrib/envoyproxy/envoy/cmd/serviceextensions/Dockerfile b/contrib/envoyproxy/envoy/cmd/serviceextensions/Dockerfile new file mode 100644 index 0000000000..87136d2cba --- /dev/null +++ b/contrib/envoyproxy/envoy/cmd/serviceextensions/Dockerfile @@ -0,0 +1,20 @@ +# Build stage +FROM golang:1.22-alpine AS builder +ENV CGO_ENABLED=1 +WORKDIR /app +COPY . . +RUN apk add --no-cache --update git build-base +RUN go build -o ./contrib/envoyproxy/envoy/cmd/serviceextensions/serviceextensions ./contrib/envoyproxy/envoy/cmd/serviceextensions + +# Runtime stage +FROM alpine:3.20.3 +RUN apk --no-cache add ca-certificates tzdata libc6-compat libgcc libstdc++ +WORKDIR /app +COPY --from=builder /app/contrib/envoyproxy/envoy/cmd/serviceextensions/serviceextensions /app/serviceextensions +COPY ./contrib/envoyproxy/envoy/cmd/serviceextensions/localhost.crt /app/localhost.crt +COPY ./contrib/envoyproxy/envoy/cmd/serviceextensions/localhost.key /app/localhost.key + +EXPOSE 80 +EXPOSE 443 + +CMD ["./serviceextensions"] diff --git a/contrib/envoyproxy/envoy/cmd/serviceextensions/localhost.crt b/contrib/envoyproxy/envoy/cmd/serviceextensions/localhost.crt new file mode 100644 index 0000000000..fc54fd492e --- /dev/null +++ b/contrib/envoyproxy/envoy/cmd/serviceextensions/localhost.crt @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDFjCCAf4CCQCzrLIhrWa55zANBgkqhkiG9w0BAQsFADBCMQswCQYDVQQGEwJV +UzETMBEGA1UECAwKQ2FsaWZvcm5pYTEPMA0GA1UECgwGR29vZ2xlMQ0wCwYDVQQL +DARnUlBDMCAXDTE5MDYyNDIyMjIzM1oYDzIxMTkwNTMxMjIyMjMzWjBWMQswCQYD +VQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEPMA0GA1UECgwGR29vZ2xlMQ0w +CwYDVQQLDARnUlBDMRIwEAYDVQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEB +AQUAA4IBDwAwggEKAoIBAQCtCW0TjugnIUu8BEVIYvdMP+/2GENQDjZhZ8eKR5C6 +toDGbgjsDtt/GxISAg4cg70fIvy0XolnGPZodvfHDM4lJ7yHBOdZD8TXQoE6okR7 +HZuLUJ20M0pXgWqtRewKRUjuYsSDXBnzLiZw1dcv9nGpo+Bqa8NonpiGRRpEkshF +D6T9KU9Ts/x+wMQBIra2Gj0UMh79jPhUuxcYAQA0JQGivnOtdwuPiumpnUT8j8h6 +tWg5l01EsCZWJecCF85KnGpJEVYPyPqBqGsy0nGS9plGotOWF87+jyUQt+KD63xA +aBmTro86mKDDKEK4JvzjVeMGz2UbVcLPiiZnErTFaiXJAgMBAAEwDQYJKoZIhvcN +AQELBQADggEBAKsDgOPCWp5WCy17vJbRlgfgk05sVNIHZtzrmdswjBmvSg8MUpep +XqcPNUpsljAXsf9UM5IFEMRdilUsFGWvHjBEtNAW8WUK9UV18WRuU//0w1Mp5HAN +xUEKb4BoyZr65vlCnTR+AR5c9FfPvLibhr5qHs2RA8Y3GyLOcGqBWed87jhdQLCc +P1bxB+96le5JeXq0tw215lxonI2/3ZYVK4/ok9gwXrQoWm8YieJqitk/ZQ4S17/4 +pynHtDfdxLn23EXeGx+UTxJGfpRmhEZdJ+MN7QGYoomzx5qS5XoYKxRNrDlirJpr +OqXIn8E1it+6d5gOZfuHawcNGhRLplE/pfA= +-----END CERTIFICATE----- diff --git a/contrib/envoyproxy/envoy/cmd/serviceextensions/localhost.key b/contrib/envoyproxy/envoy/cmd/serviceextensions/localhost.key new file mode 100644 index 0000000000..72e2463282 --- /dev/null +++ b/contrib/envoyproxy/envoy/cmd/serviceextensions/localhost.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEArQltE47oJyFLvARFSGL3TD/v9hhDUA42YWfHikeQuraAxm4I +7A7bfxsSEgIOHIO9HyL8tF6JZxj2aHb3xwzOJSe8hwTnWQ/E10KBOqJEex2bi1Cd +tDNKV4FqrUXsCkVI7mLEg1wZ8y4mcNXXL/ZxqaPgamvDaJ6YhkUaRJLIRQ+k/SlP +U7P8fsDEASK2tho9FDIe/Yz4VLsXGAEANCUBor5zrXcLj4rpqZ1E/I/IerVoOZdN +RLAmViXnAhfOSpxqSRFWD8j6gahrMtJxkvaZRqLTlhfO/o8lELfig+t8QGgZk66P +OpigwyhCuCb841XjBs9lG1XCz4omZxK0xWolyQIDAQABAoIBADeq/Kh6JT3RfGf0 +h8WN8TlaqHxnueAbcmtL0+oss+cdp7gu1jf7X6o4r0uT1a5ew40s2Fe+wj2kzkE1 +ZOlouTlC22gkr7j7Vbxa7PBMG/Pvxoa/XL0IczZLsGImSJXVTG1E4SvRiZeulTdf +1GbdxhtpWV1jZe5Wd4Na3+SHxF5S7m3PrHiZlYdz1ND+8XZs1NlL9+ej72qSFul9 +t/QjMWJ9pky/Wad5abnRLRyOsg+BsgnXbkUy2rD89ZxFMLda9pzXo3TPyAlBHonr +mkEsE4eRMWMpjBM79JbeyDdHn/cs/LjAZrzeDf7ugXr2CHQpKaM5O0PsNHezJII9 +L5kCfzECgYEA4M/rz1UP1/BJoSqigUlSs0tPAg8a5UlkVsh6Osuq72IPNo8qg/Fw +oV/IiIS+q+obRcFj1Od3PGdTpCJwW5dzd2fXBQGmGdj0HucnCrs13RtBh91JiF5i +y/YYI9KfgOG2ZT9gG68T0gTs6jRrS3Qd83npqjrkJqMOd7s00MK9tUcCgYEAxQq7 +T541oCYHSBRIIb0IrR25krZy9caxzCqPDwOcuuhaCqCiaq+ATvOWlSfgecm4eH0K +PCH0xlWxG0auPEwm4pA8+/WR/XJwscPZMuoht1EoKy1his4eKx/s7hHNeO6KOF0V +Y/zqIiuZnEwUoKbn7EqqNFSTT65PJKyGsICJFG8CgYAfaw9yl1myfQNdQb8aQGwN +YJ33FLNWje427qeeZe5KrDKiFloDvI9YDjHRWnPnRL1w/zj7fSm9yFb5HlMDieP6 +MQnsyjEzdY2QcA+VwVoiv3dmDHgFVeOKy6bOAtaFxYWfGr9MvygO9t9BT/gawGyb +JVORlc9i0vDnrMMR1dV7awKBgBpTWLtGc/u1mPt0Wj7HtsUKV6TWY32a0l5owTxM +S0BdksogtBJ06DukJ9Y9wawD23WdnyRxlPZ6tHLkeprrwbY7dypioOKvy4a0l+xJ +g7+uRCOgqIuXBkjUtx8HmeAyXp0xMo5tWArAsIFFWOwt4IadYygitJvMuh44PraO +NcJZAoGADEiV0dheXUCVr8DrtSom8DQMj92/G/FIYjXL8OUhh0+F+YlYP0+F8PEU +yYIWEqL/S5tVKYshimUXQa537JcRKsTVJBG/ZKD2kuqgOc72zQy3oplimXeJDCXY +h2eAQ0u8GN6tN9C4t8Kp4a3y6FGsxgu+UTxdnL3YQ+yHAVhtCzo= +-----END RSA PRIVATE KEY----- diff --git a/contrib/envoyproxy/envoy/cmd/serviceextensions/main.go b/contrib/envoyproxy/envoy/cmd/serviceextensions/main.go new file mode 100644 index 0000000000..fcd86b8fb3 --- /dev/null +++ b/contrib/envoyproxy/envoy/cmd/serviceextensions/main.go @@ -0,0 +1,128 @@ +package main + +import ( + "crypto/tls" + "gopkg.in/DataDog/dd-trace-go.v1/contrib/envoyproxy/envoy" + "gopkg.in/DataDog/dd-trace-go.v1/internal/log" + "gopkg.in/DataDog/dd-trace-go.v1/internal/version" + "net" + "net/http" + "os" + + extproc "github.com/envoyproxy/go-control-plane/envoy/service/ext_proc/v3" + "github.com/gorilla/mux" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/reflection" + "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer" +) + +// AppsecCalloutExtensionService defines the struct that follows the ExternalProcessorServer interface. +type AppsecCalloutExtensionService struct { + extproc.ExternalProcessorServer +} + +type serviceExtensionConfig struct { + extensionPort string + extensionHost string + healthcheckPort string +} + +func loadConfig() serviceExtensionConfig { + extensionPort := os.Getenv("DD_SERVICE_EXTENSION_PORT") + if extensionPort == "" { + extensionPort = "443" + } + + extensionHost := os.Getenv("DD_SERVICE_EXTENSION_HOST") + if extensionHost == "" { + extensionHost = "0.0.0.0" + } + + healthcheckPort := os.Getenv("DD_SERVICE_EXTENSION_HEALTHCHECK_PORT") + if healthcheckPort == "" { + healthcheckPort = "80" + } + + return serviceExtensionConfig{ + extensionPort: extensionPort, + extensionHost: extensionHost, + healthcheckPort: healthcheckPort, + } +} + +func main() { + var extensionService AppsecCalloutExtensionService + + // Force set ASM as enabled only if the environment variable is not set + // Note: If the environment variable is set to false, it should be disabled + if os.Getenv("DD_APPSEC_ENABLED") == "" { + if err := os.Setenv("DD_APPSEC_ENABLED", "1"); err != nil { + log.Error("service_extension: failed to set DD_APPSEC_ENABLED environment variable: %v\n", err) + } + } + + // TODO: Enable ASM standalone mode when it is developed (should be done for Q4 2024) + + // Set the DD_VERSION to the current tracer version if not set + if os.Getenv("DD_VERSION") == "" { + if err := os.Setenv("DD_VERSION", version.Tag); err != nil { + log.Error("service_extension: failed to set DD_VERSION environment variable: %v\n", err) + } + } + + config := loadConfig() + + tracer.Start() + + go StartGPRCSsl(&extensionService, config) + log.Info("service_extension: callout gRPC server started on %s:%s\n", config.extensionHost, config.extensionPort) + + go startHealthCheck(config) + log.Info("service_extension: health check server started on %s:%s\n", config.extensionHost, config.healthcheckPort) + + select {} +} + +func startHealthCheck(config serviceExtensionConfig) { + muxServer := mux.NewRouter() + muxServer.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + _, err := w.Write([]byte(`{"status": "ok", "library": {"language": "golang", "version": "` + version.Tag + `"}}`)) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) + }) + + server := &http.Server{ + Addr: config.extensionHost + ":" + config.healthcheckPort, + Handler: muxServer, + } + + println(server.ListenAndServe()) +} + +func StartGPRCSsl(service extproc.ExternalProcessorServer, config serviceExtensionConfig) { + cert, err := tls.LoadX509KeyPair("localhost.crt", "localhost.key") + if err != nil { + log.Error("Failed to load key pair: %v\n", err) + } + + lis, err := net.Listen("tcp", config.extensionHost+":"+config.extensionPort) + if err != nil { + log.Error("Failed to listen: %v\n", err) + } + + si := envoy.StreamServerInterceptor() + creds := credentials.NewServerTLSFromCert(&cert) + grpcServer := grpc.NewServer(grpc.StreamInterceptor(si), grpc.Creds(creds)) + + extproc.RegisterExternalProcessorServer(grpcServer, service) + reflection.Register(grpcServer) + if err := grpcServer.Serve(lis); err != nil { + log.Error("service_extension: failed to serve gRPC: %v\n", err) + } +}