diff --git a/CHANGELOG b/CHANGELOG index 70a5c755..75cde1f7 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,9 @@ +unreleased +===================== + +* Add Dynatrace OneAgent injection support (via github.com/Dynatrace/libbuildpack-dynatrace). When a Dynatrace service is bound, the OneAgent is downloaded during staging and LD_PRELOADed into the app at launch. Defaults to the generic `process` code module; use the `addtechnologies` binding field to add language modules (e.g. `go`). + + v2.0.0 Jun 15, 2026 ===================== diff --git a/README.md b/README.md index 4231136b..6a2bb4e2 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,38 @@ To test this buildpack, run the following command from the buildpack's directory ./scripts/integration.sh ``` +### Dynatrace Integration + +This buildpack can automatically inject the [Dynatrace OneAgent](https://www.dynatrace.com/support/help/technology-support/cloud-platforms/cloud-foundry/) into your application. When a Dynatrace service is bound to the app, the buildpack downloads the OneAgent during staging and configures `LD_PRELOAD` (via `profile.d/dynatrace-env.sh`) so the agent is loaded into your binary at launch. + +Bind a Dynatrace user-provided service (the service name must contain `dynatrace`): + +```bash +cf create-user-provided-service dynatrace -p '{"environmentid":"","apitoken":""}' +cf bind-service my_app dynatrace +cf restage my_app +``` + +Supported credential fields: + +| Key | Type | Description | Required | Default | +| --------------- | ------- | ------------------------------------------------------------------------------------------------------- | -------- | --------------- | +| environmentid | string | The ID for the Dynatrace environment. | Yes | N/A | +| apitoken | string | The API Token for the Dynatrace environment. | Yes | N/A | +| apiurl | string | Overrides the default Dynatrace API URL to connect to. | No | Default API URL | +| skiperrors | boolean | If `true`, staging does not fail when the OneAgent download fails. | No | false | +| networkzone | string | If set, the agent is configured to use communication endpoints located in this network zone. | No | empty | +| enablefips | boolean | If `true`, FIPS 140-2 mode is enabled. | No | false | +| addtechnologies | string | Comma-separated list of additional OneAgent code modules to download (e.g. `go`, `java`, `nodejs`). | No | empty | + +By default the buildpack downloads the generic `process` code module. Since the +binary buildpack runs an opaque binary, it cannot detect the application's +language. For language-specific code-level insights, set `addtechnologies` +accordingly — e.g. `"addtechnologies":"go"` for a Go binary. Note that OneAgent +injection relies on `LD_PRELOAD`, so the binary must be **dynamically linked** +(for Go, built with `CGO_ENABLED=1`); a fully statically-linked binary ignores +`LD_PRELOAD` and will not be instrumented. + ### Contributing Find our guidelines [here](./CONTRIBUTING.md). diff --git a/fixtures/util/dynatrace/dynatrace-env.sh b/fixtures/util/dynatrace/dynatrace-env.sh new file mode 100755 index 00000000..b2258d78 --- /dev/null +++ b/fixtures/util/dynatrace/dynatrace-env.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +export DT_HELLO=some-value diff --git a/fixtures/util/dynatrace/fake_config.json b/fixtures/util/dynatrace/fake_config.json new file mode 100644 index 00000000..d79069df --- /dev/null +++ b/fixtures/util/dynatrace/fake_config.json @@ -0,0 +1,11 @@ +{ + "revision":1234567890, + "properties": + [ + { + "section":"fakesection", + "key":"fakekey", + "value":"fakevalue" + } + ] +} diff --git a/fixtures/util/dynatrace/go.mod b/fixtures/util/dynatrace/go.mod new file mode 100644 index 00000000..7055b281 --- /dev/null +++ b/fixtures/util/dynatrace/go.mod @@ -0,0 +1,3 @@ +module myapp + +go 1.26 diff --git a/fixtures/util/dynatrace/install.sh b/fixtures/util/dynatrace/install.sh new file mode 100755 index 00000000..5d3b8248 --- /dev/null +++ b/fixtures/util/dynatrace/install.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +function main() { + set -e + + local dir + dir="${1}" + echo "dir -> ${dir}" + + mkdir -p "${dir}/dynatrace/oneagent/agent/lib64" + mkdir -p "${dir}/dynatrace/oneagent/agent/conf" + + curl -s --fail "http://{{.URI}}/manifest.json" > "${dir}/dynatrace/oneagent/manifest.json" + curl -s --fail "http://{{.URI}}/dynatrace-env.sh" > "${dir}/dynatrace/oneagent/dynatrace-env.sh" + curl -s --fail "http://{{.URI}}/liboneagentproc.so" > "${dir}/dynatrace/oneagent/agent/lib64/liboneagentproc.so" + curl -s --fail "http://{{.URI}}/ruxitagentproc.conf" > "${dir}/dynatrace/oneagent/agent/conf/ruxitagentproc.conf" +} + +main "${@}" diff --git a/fixtures/util/dynatrace/liboneagentproc.so b/fixtures/util/dynatrace/liboneagentproc.so new file mode 100755 index 00000000..dd73ae82 Binary files /dev/null and b/fixtures/util/dynatrace/liboneagentproc.so differ diff --git a/fixtures/util/dynatrace/main.go b/fixtures/util/dynatrace/main.go new file mode 100644 index 00000000..35761abf --- /dev/null +++ b/fixtures/util/dynatrace/main.go @@ -0,0 +1,85 @@ +package main + +import ( + "encoding/json" + "fmt" + "log" + "net/http" + "os" + "strings" + "text/template" +) + +func main() { + http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) { + var withoutAgentPath bool + path := req.URL.Path + + if strings.HasPrefix(path, "/without-agent-path") { + path = strings.TrimPrefix(path, "/without-agent-path") + withoutAgentPath = true + } + + switch path { + case "/v1/deployment/installer/agent/unix/paas-sh/latest": + context := struct{ URI string }{URI: req.Host} + t := template.Must(template.New("install.sh").ParseFiles("install.sh")) + err := t.Execute(w, context) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + fmt.Fprint(w, err.Error()) + return + } + + case "/dynatrace-env.sh", "/liboneagentproc.so", "/ruxitagentproc.conf": + contents, err := os.ReadFile(strings.TrimPrefix(req.URL.Path, "/")) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + fmt.Fprint(w, err.Error()) + return + } + + fmt.Fprintf(w, "%s", contents) + + case "/manifest.json": + var payload map[string]interface{} + file, err := os.Open("manifest.json") + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(err.Error())) + return + } + + err = json.NewDecoder(file).Decode(&payload) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(err.Error())) + return + } + + if withoutAgentPath { + payload["technologies"] = map[string]interface{}{ + "process": map[string]interface{}{ + "linux-x86-64": []struct{}{}, + }, + } + } + + json.NewEncoder(w).Encode(payload) + + case "/v1/deployment/installer/agent/processmoduleconfig": + fakeConfig, err := os.ReadFile("fake_config.json") + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(err.Error())) + return + } + w.Write(fakeConfig) + + default: + w.WriteHeader(http.StatusNotFound) + } + }) + + log.Fatal(http.ListenAndServe(fmt.Sprintf(":%s", os.Getenv("PORT")), nil)) +} diff --git a/fixtures/util/dynatrace/manifest.json b/fixtures/util/dynatrace/manifest.json new file mode 100644 index 00000000..24fffbc2 --- /dev/null +++ b/fixtures/util/dynatrace/manifest.json @@ -0,0 +1,12 @@ +{ + "technologies": { + "process": { + "linux-x86-64": [ + { + "path": "agent/lib64/liboneagentproc.so", + "binarytype": "primary" + } + ] + } + } +} diff --git a/fixtures/util/dynatrace/ruxitagentproc.conf b/fixtures/util/dynatrace/ruxitagentproc.conf new file mode 100644 index 00000000..d3b6df53 --- /dev/null +++ b/fixtures/util/dynatrace/ruxitagentproc.conf @@ -0,0 +1,3 @@ +# some comment +[testsection] +testkey testvalue diff --git a/go.mod b/go.mod index cd331dc6..d645fe1f 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/cloudfoundry/binary-buildpack go 1.24.12 require ( + github.com/Dynatrace/libbuildpack-dynatrace v1.9.0 github.com/cloudfoundry/libbuildpack v0.0.0-20260306125332-dcaf55eb6f33 github.com/cloudfoundry/switchblade v0.9.5 github.com/onsi/ginkgo/v2 v2.28.1 diff --git a/go.sum b/go.sum index 28e6d4af..18cfa5c9 100644 --- a/go.sum +++ b/go.sum @@ -560,6 +560,8 @@ github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym github.com/CycloneDX/cyclonedx-go v0.7.1/go.mod h1:N/nrdWQI2SIjaACyyDs/u7+ddCkyl/zkNs8xFsHF2Ps= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= +github.com/Dynatrace/libbuildpack-dynatrace v1.9.0 h1:3tJzXt7VVTsvPPS9Q7+e/KAk0ccC2eAbgHib5C/XRhA= +github.com/Dynatrace/libbuildpack-dynatrace v1.9.0/go.mod h1:Uu9aa5UFAk1Ua+zZXnvzo+avDXuEi+GtegeOyja9xg4= github.com/GoogleCloudPlatform/docker-credential-gcr v2.0.5+incompatible/go.mod h1:BB1eHdMLYEFuFdBlRMb0N7YGVdM5s6Pt0njxgvfbGGs= github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c/go.mod h1:X0CRv0ky0k6m906ixxpzmDRLvX58TFUKS2eePweuyxk= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= diff --git a/src/binary/finalize/cli/main.go b/src/binary/finalize/cli/main.go index f1e72618..c8466a39 100644 --- a/src/binary/finalize/cli/main.go +++ b/src/binary/finalize/cli/main.go @@ -4,6 +4,8 @@ import ( "os" "time" + _ "github.com/cloudfoundry/binary-buildpack/src/binary/hooks" + "github.com/cloudfoundry/binary-buildpack/src/binary/finalize" "github.com/cloudfoundry/libbuildpack" diff --git a/src/binary/hooks/dynatrace.go b/src/binary/hooks/dynatrace.go new file mode 100644 index 00000000..35ec63dc --- /dev/null +++ b/src/binary/hooks/dynatrace.go @@ -0,0 +1,10 @@ +package hooks + +import ( + "github.com/cloudfoundry/libbuildpack" + "github.com/Dynatrace/libbuildpack-dynatrace" +) + +func init() { + libbuildpack.AddHook(dynatrace.NewHook("process")) +} diff --git a/src/binary/integration/dynatrace_test.go b/src/binary/integration/dynatrace_test.go new file mode 100644 index 00000000..71420e68 --- /dev/null +++ b/src/binary/integration/dynatrace_test.go @@ -0,0 +1,208 @@ +package integration_test + +import ( + "fmt" + "path/filepath" + "testing" + + "github.com/cloudfoundry/switchblade" + "github.com/sclevine/spec" + + . "github.com/cloudfoundry/switchblade/matchers" + . "github.com/onsi/gomega" +) + +func testDynatrace(platform switchblade.Platform, fixtures, uri string) func(*testing.T, spec.G, spec.S) { + return func(t *testing.T, context spec.G, it spec.S) { + var ( + Expect = NewWithT(t).Expect + + name string + ) + + it.Before(func() { + var err error + name, err = switchblade.RandomName() + Expect(err).NotTo(HaveOccurred()) + }) + + it.After(func() { + Expect(platform.Delete.Execute(name)).To(Succeed()) + }) + + context("deploying a binary app with Dynatrace agent with configured network zone", func() { + it("checks if networkzone setting was successful", func() { + _, logs, err := platform.Deploy. + WithBuildpacks("binary_buildpack"). + WithEnv(map[string]string{ + "BP_DEBUG": "true", + }). + WithServices(map[string]switchblade.Service{ + "some-dynatrace": { + "apitoken": "secretpaastoken", + "apiurl": uri, + "environmentid": "envid", + "networkzone": "testzone", + }, + }). + Execute(name, filepath.Join(fixtures, "default")) + Expect(err).NotTo(HaveOccurred()) + + Expect(logs).To(ContainLines(ContainSubstring("Dynatrace service credentials found. Setting up Dynatrace OneAgent."))) + Expect(logs).To(ContainLines(ContainSubstring("Starting Dynatrace OneAgent installer"))) + Expect(logs).To(ContainLines(ContainSubstring("Copy dynatrace-env.sh"))) + Expect(logs).To(ContainLines(ContainSubstring("Setting DT_NETWORK_ZONE..."))) + Expect(logs).To(ContainLines(ContainSubstring("Dynatrace OneAgent installed."))) + Expect(logs).To(ContainLines(ContainSubstring("Dynatrace OneAgent injection is set up."))) + }) + }) + + context("when deploying with Dynatrace agent with single credentials service", func() { + it("checks if Dynatrace injection was successful", func() { + _, logs, err := platform.Deploy. + WithBuildpacks("binary_buildpack"). + WithEnv(map[string]string{ + "BP_DEBUG": "true", + }). + WithServices(map[string]switchblade.Service{ + "some-dynatrace": { + "apitoken": "secretpaastoken", + "apiurl": uri, + "environmentid": "envid", + }, + }). + Execute(name, filepath.Join(fixtures, "default")) + Expect(err).NotTo(HaveOccurred()) + + Expect(logs).To(ContainLines(ContainSubstring("Dynatrace service credentials found. Setting up Dynatrace OneAgent."))) + Expect(logs).To(ContainLines(ContainSubstring("Starting Dynatrace OneAgent installer"))) + Expect(logs).To(ContainLines(ContainSubstring("Copy dynatrace-env.sh"))) + Expect(logs).To(ContainLines(ContainSubstring("Dynatrace OneAgent installed."))) + Expect(logs).To(ContainLines(ContainSubstring("Dynatrace OneAgent injection is set up."))) + }) + }) + + context("when deploying with Dynatrace agent with two credentials services", func() { + it("checks if detection of second service with credentials works", func() { + _, logs, err := platform.Deploy. + WithBuildpacks("binary_buildpack"). + WithEnv(map[string]string{ + "BP_DEBUG": "true", + }). + WithServices(map[string]switchblade.Service{ + "some-dynatrace": { + "apitoken": "secretpaastoken", + "apiurl": uri, + "environmentid": "envid", + }, + "other-dynatrace": { + "apitoken": "secretpaastoken", + "apiurl": uri, + "environmentid": "envid", + }, + }). + Execute(name, filepath.Join(fixtures, "default")) + Expect(err).NotTo(HaveOccurred()) + + Expect(logs).To(ContainLines(ContainSubstring("More than one matching service found!"))) + }) + }) + + context("when deploying with Dynatrace agent with failing agent download and ignoring errors", func() { + it("checks if skipping download errors works", func() { + _, logs, err := platform.Deploy. + WithBuildpacks("binary_buildpack"). + WithEnv(map[string]string{ + "BP_DEBUG": "true", + }). + WithServices(map[string]switchblade.Service{ + "some-dynatrace": { + "apitoken": "secretpaastoken", + "apiurl": fmt.Sprintf("%s/no-such-endpoint", uri), + "environmentid": "envid", + "skiperrors": "true", + }, + }). + Execute(name, filepath.Join(fixtures, "default")) + Expect(err).NotTo(HaveOccurred()) + + Expect(logs).To(ContainLines(ContainSubstring("Download returned with status 404"))) + Expect(logs).To(ContainLines(ContainSubstring("Error during installer download, skipping installation"))) + }) + }) + + context("deploying with Dynatrace agent with a service that has tags", func() { + it("check if service detection isn't disturbed by a service with tags", func() { + _, logs, err := platform.Deploy. + WithBuildpacks("binary_buildpack"). + WithEnv(map[string]string{ + "BP_DEBUG": "true", + }). + WithServices(map[string]switchblade.Service{ + "some-dynatrace": { + "apitoken": "secretpaastoken", + "apiurl": uri, + "environmentid": "envid", + }, + "dynatrace-tags": { + "tag:dttest": "dynatrace_test", + }, + }). + Execute(name, filepath.Join(fixtures, "default")) + Expect(err).NotTo(HaveOccurred()) + + Expect(logs).To(ContainLines(ContainSubstring("Dynatrace service credentials found. Setting up Dynatrace OneAgent."))) + Expect(logs).To(ContainLines(ContainSubstring("Starting Dynatrace OneAgent installer"))) + Expect(logs).To(ContainLines(ContainSubstring("Copy dynatrace-env.sh"))) + Expect(logs).To(ContainLines(ContainSubstring("Dynatrace OneAgent installed."))) + Expect(logs).To(ContainLines(ContainSubstring("Dynatrace OneAgent injection is set up."))) + }) + }) + + context("deploying Dynatrace agent with failing agent download and checking retry", func() { + it("checks if retrying downloads works", func() { + _, logs, err := platform.Deploy. + WithBuildpacks("binary_buildpack"). + WithEnv(map[string]string{ + "BP_DEBUG": "true", + }). + WithServices(map[string]switchblade.Service{ + "some-dynatrace": { + "apitoken": "secretpaastoken", + "apiurl": fmt.Sprintf("%s/no-such-endpoint", uri), + "environmentid": "envid", + }, + }). + Execute(name, filepath.Join(fixtures, "default")) + Expect(err).To(MatchError(ContainSubstring("App staging failed"))) + + Expect(logs).To(ContainLines(ContainSubstring("Error during installer download, retrying in 4s"))) + Expect(logs).To(ContainLines(ContainSubstring("Error during installer download, retrying in 5s"))) + Expect(logs).To(ContainLines(ContainSubstring("Error during installer download, retrying in 7s"))) + Expect(logs).To(ContainLines(ContainSubstring("Download returned with status 404"))) + }) + }) + + context("deploying Dynatrace agent with single credentials service", func() { + it("checks if agent config update via API was successful", func() { + _, logs, err := platform.Deploy. + WithBuildpacks("binary_buildpack"). + WithEnv(map[string]string{ + "BP_DEBUG": "true", + }). + WithServices(map[string]switchblade.Service{ + "some-dynatrace": { + "apitoken": "secretpaastoken", + "apiurl": uri, + "environmentid": "envid", + }, + }). + Execute(name, filepath.Join(fixtures, "default")) + Expect(err).NotTo(HaveOccurred()) + + Expect(logs).To(ContainLines(ContainSubstring("Fetching updated OneAgent configuration from tenant..."))) + Expect(logs).To(ContainLines(ContainSubstring("Finished writing updated OneAgent config back to"))) + }) + }) + } +} diff --git a/src/binary/integration/init_test.go b/src/binary/integration/init_test.go index 26ec3acd..9f1e600b 100644 --- a/src/binary/integration/init_test.go +++ b/src/binary/integration/init_test.go @@ -2,6 +2,9 @@ package integration_test import ( "flag" + "fmt" + "io" + "net/http" "os" "path/filepath" "testing" @@ -55,6 +58,9 @@ func TestIntegration(t *testing.T) { fakeSupply, err := packager.Package(filepath.Join(fixtures, "util", "fake_supply"), packager.CacheDir, "0.0.0", settings.Stack, true) Expect(err).NotTo(HaveOccurred()) + goBuildpackFile, err := downloadBuildpack("go") + Expect(err).NotTo(HaveOccurred()) + err = platform.Initialize( switchblade.Buildpack{ Name: "binary_buildpack", @@ -64,16 +70,51 @@ func TestIntegration(t *testing.T) { Name: "fake_supply", URI: fakeSupply, }, + switchblade.Buildpack{ + Name: "go_buildpack", + URI: goBuildpackFile, + }, ) Expect(err).NotTo(HaveOccurred()) + dynatraceName, err := switchblade.RandomName() + Expect(err).NotTo(HaveOccurred()) + + dynatraceDeployment, _, err := platform.Deploy. + WithBuildpacks("go_buildpack"). + WithEnv(map[string]string{"BP_DEBUG": "true"}). + Execute(dynatraceName, filepath.Join(fixtures, "util", "dynatrace")) + Expect(err).NotTo(HaveOccurred()) + suite := spec.New("integration", spec.Report(report.Terminal{}), spec.Parallel()) suite("Default", testDefault(platform, fixtures)) + suite("Dynatrace", testDynatrace(platform, fixtures, dynatraceDeployment.InternalURL)) suite("Fake Supply", testFakeSupply(platform, fixtures)) suite.Run(t) + Expect(platform.Delete.Execute(dynatraceName)).To(Succeed()) Expect(os.Remove(os.Getenv("BUILDPACK_FILE"))).To(Succeed()) Expect(os.Remove(fakeSupply)).To(Succeed()) + Expect(os.Remove(goBuildpackFile)).To(Succeed()) Expect(platform.Deinitialize()).To(Succeed()) } + +func downloadBuildpack(name string) (string, error) { + uri := fmt.Sprintf("https://github.com/cloudfoundry/%s-buildpack/archive/master.zip", name) + + file, err := os.CreateTemp("", fmt.Sprintf("%s-buildpack-*.zip", name)) + if err != nil { + return "", err + } + defer file.Close() + + resp, err := http.Get(uri) + if err != nil { + return "", err + } + defer resp.Body.Close() + + _, err = io.Copy(file, resp.Body) + return file.Name(), err +} diff --git a/src/binary/supply/cli/main.go b/src/binary/supply/cli/main.go index d87e8a8c..642d8a9d 100644 --- a/src/binary/supply/cli/main.go +++ b/src/binary/supply/cli/main.go @@ -4,6 +4,8 @@ import ( "os" "time" + _ "github.com/cloudfoundry/binary-buildpack/src/binary/hooks" + "github.com/cloudfoundry/binary-buildpack/src/binary/supply" "github.com/cloudfoundry/libbuildpack" diff --git a/vendor/github.com/Dynatrace/libbuildpack-dynatrace/.gitignore b/vendor/github.com/Dynatrace/libbuildpack-dynatrace/.gitignore new file mode 100644 index 00000000..31c5c6ff --- /dev/null +++ b/vendor/github.com/Dynatrace/libbuildpack-dynatrace/.gitignore @@ -0,0 +1,15 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, build with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# vim session file +Session.vim diff --git a/vendor/github.com/Dynatrace/libbuildpack-dynatrace/LICENSE b/vendor/github.com/Dynatrace/libbuildpack-dynatrace/LICENSE new file mode 100644 index 00000000..989e2c59 --- /dev/null +++ b/vendor/github.com/Dynatrace/libbuildpack-dynatrace/LICENSE @@ -0,0 +1,201 @@ +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/vendor/github.com/Dynatrace/libbuildpack-dynatrace/README.md b/vendor/github.com/Dynatrace/libbuildpack-dynatrace/README.md new file mode 100644 index 00000000..72432895 --- /dev/null +++ b/vendor/github.com/Dynatrace/libbuildpack-dynatrace/README.md @@ -0,0 +1,85 @@ +# libbuildpack-dynatrace + +Base Library for Go-based Cloud Foundry Buildpack integrations with Dynatrace. + +## Summary + +The library provides the `Hook` struct that implements the `libbuildpack.Hook` interface that it's requested by the CF Buildpacks. + +On the buildpacks, you're expected to provide a hook through a `init()` function where you can register such hook implementation. For example, a simple implementation could be, + +```go +import ( + "github.com/cloudfoundry/libbuildpack" + "github.com/Dynatrace/libbuildpack-dynatrace" +) + +func init() { + libbuildpack.AddHook(dynatrace.NewHook("nodejs", "process")) +} +``` + +## Configuration + +The Hook will look for credentials in the configurations for existing services. It searches for service credentials depending on the binding type: + +1. **File-based**: If the `VCAP_SERVICES_FILE_PATH` environment variable is set, the hook reads the VCAP_SERVICES JSON from the file at that path. +2. **Environment variable**: If the env var `VCAP_SERVICES` is set, it reads the JSON from the `VCAP_SERVICES` environment variable directly. + +In both cases, we look for service names having the 'dynatrace' substring. + +We support the following configuration fields, + +| Key | Type | Description | Required | Default | +| ------------- | ------- | ------------------------------------------------------------------------------------------- | -------- | --------------- | +| environmentid | string | The ID for the Dynatrace environment. | Yes | N/A | +| apitoken | string | The API Token for the Dynatrace environment. | Yes | N/A | +| apiurl | string | Overrides the default Dynatrace API URL to connect to. | No | Default API URL | +| skiperrors | boolean | If true, the deployment doesn't fail if the Dynatrace agent download fails. | No | false | +| networkzone | string | If set, agent is configured to choose communication endpoints located at the field's value. | No | empty | +| enablefips | boolean | If true, the [FIPS 140-2 mode](https://www.dynatrace.com/news/blog/dynatrace-achieves-fips-140-2-certification/) is enabled | No | false | +| addtechnologies| string | Adds additional OneAgent code-modules via a comma-separated list. See [supported values](https://docs.dynatrace.com/docs/dynatrace-api/environment-api/deployment/oneagent/download-oneagent-version#parameters) in the "included" row | No | empty | + +For example, + +```bash +cf create-user-provided-service dynatrace -p '{"environmentid":"...","apitoken":"..."}' +``` + +See more at the documentation for [`cf create-user-provided-service`](http://cli.cloudfoundry.org/en-US/cf/create-user-provided-service.html). + +We also support standard Dynatrace environment variables. + +## Requirements + +- Go 1.19 or higher. +- Deployment targets: Linux and Windows. +- Development and testing: Linux, Mac OS, and Windows. + +## Development + +You can download or clone the repository. + +You can run tests through, + +``` +go test ./... +``` + +By default, tests simulate the Linux platform. To test against a different target OS, use the `-os` flag: + +``` +go test ./... -os=windows +go test ./... -os=linux +``` + +If you modify/add interfaces, you may need to regenerate the mocks. For this you need [gomock](https://github.com/golang/mock): + +``` +# To download Gomock +go get github.com/golang/mock/gomock +go install github.com/golang/mock/mockgen + +# To generate the mocks +go generate +``` diff --git a/vendor/github.com/Dynatrace/libbuildpack-dynatrace/hook.go b/vendor/github.com/Dynatrace/libbuildpack-dynatrace/hook.go new file mode 100644 index 00000000..2a3c99ee --- /dev/null +++ b/vendor/github.com/Dynatrace/libbuildpack-dynatrace/hook.go @@ -0,0 +1,579 @@ +package dynatrace + +import ( + "bufio" + "encoding/json" + "errors" + "fmt" + "io" + "math" + "net/http" + "net/url" + "os" + "path/filepath" + "regexp" + "runtime" + "strings" + "time" + + "github.com/cloudfoundry/libbuildpack" +) + +// Command is an interface around libbuildpack.Command. Represents an executor for external command calls. We have it +// as an interface so that we can mock it and use in the unit tests. +type Command interface { + Execute(string, io.Writer, io.Writer, string, ...string) error +} + +// credentials represent the user settings extracted from the environment. +type credentials struct { + ServiceName string + EnvironmentID string + CustomOneAgentURL string + APIToken string + APIURL string + SkipErrors bool + NetworkZone string + EnableFIPS bool + AddTechnologies string +} + +// Hook implements libbuildpack.Hook. It downloads and install the Dynatrace OneAgent. +type Hook struct { + libbuildpack.DefaultHook + Log *libbuildpack.Logger + Command Command + + // IncludeTechnologies is used to indicate the technologies we want to download agents for. + IncludeTechnologies []string + + // MaxDownloadRetries is the maximum number of retries the hook will try to download the agent if they fail. + MaxDownloadRetries int +} + +// NewHook returns a libbuildpack.Hook instance for integrating monitoring with Dynatrace. The technology names for the +// agents to download can be set as parameters. +func NewHook(technologies ...string) libbuildpack.Hook { + return &Hook{ + Log: libbuildpack.NewLogger(os.Stdout), + Command: &libbuildpack.Command{}, + IncludeTechnologies: technologies, + MaxDownloadRetries: 3, + } +} + +// AfterCompile downloads and installs the Dynatrace agent. +func (h *Hook) AfterCompile(stager *libbuildpack.Stager) error { + // All other methods in this package are called from here, which + // makes it the main entry-point. + return h.injectDynatrace(stager, runtime.GOOS) + +} + +// injectDynatrace is an indirection to get rid of the tight coupling to the underlying operating system +func (h *Hook) injectDynatrace(stager *libbuildpack.Stager, operatingSystem string) error { + var err error + + h.Log.Debug("Checking for enabled dynatrace service...") + + // Get credentials... + creds := h.getCredentials() + if creds == nil { + h.Log.Debug("Dynatrace service credentials not found!") + return nil + } + + h.Log.Info("Dynatrace service credentials found. Setting up Dynatrace OneAgent.") + + installDir := filepath.Join("dynatrace", "oneagent") + + // download installer + var installerFilename string + if operatingSystem == "linux" { + installerFilename = "paasInstaller.sh" + } else if operatingSystem == "windows" { + installerFilename = "paasInstaller.zip" + } else { + // This is the only place where we need to return an error. + // All following operating system checks are just to determine installation specifics. + return errors.New("libbuildpack-dynatrace: Unsupported operating system: " + operatingSystem) + } + + installerFilePath := filepath.Join(os.TempDir(), installerFilename) + url := h.getDownloadURL(creds, operatingSystem) + err = h.download(url, installerFilePath, stager, creds) + if err != nil && creds.SkipErrors { + h.Log.Warning("Error during installer download, skipping installation") + return nil + } else if err != nil { + return err + } + + // run installer + if operatingSystem == "linux" { + err = h.runInstallerUnix(installerFilePath, installDir, creds, stager) + } else if operatingSystem == "windows" { + err = h.runInstallerWindows(installerFilePath, installDir, creds, stager) + } + + // update agent config + h.Log.Debug("Fetching updated OneAgent configuration from tenant... ") + configDir := filepath.Join(stager.BuildDir(), installDir) + if err := h.updateAgentConfig(creds, configDir, stager); err != nil { + if creds.SkipErrors { + h.Log.Warning("Error during agent config update, skipping it") + return nil + } + h.Log.Error("Error during agent config update: %s", err) + return err + + } + + if h.getCredentials().EnableFIPS { + h.Log.Debug("Removing file 'dt_fips_disabled.flag' to enable FIPS mode...") + flagFilePath := filepath.Join(stager.BuildDir(), installDir, "agent", "dt_fips_disabled.flag") + if err := os.Remove(flagFilePath); err != nil { + h.Log.Error("Error during fips flag file deletion: %s", err) + return err + } + } + + h.Log.Info("Dynatrace OneAgent injection is set up.") + return nil +} + +// loadVCAPServicesData returns the raw VCAP_SERVICES JSON data from the appropriate source. +func (h *Hook) loadVCAPServicesData() []byte { + filePath, filePathSet := os.LookupEnv("VCAP_SERVICES_FILE_PATH") + + if filePathSet { + if filePath == "" { + h.Log.Debug("VCAP_SERVICES_FILE_PATH is set but empty") + return nil + } + + h.Log.Debug("Loading VCAP services from file: %s", filePath) + fileContent, err := os.ReadFile(filePath) + if err != nil { + h.Log.Error("Failed to read VCAP services file %s: %s", filePath, err) + return nil + } + h.Log.Debug("Successfully read VCAP Service data.") + return fileContent + + } + + h.Log.Debug("Loading VCAP services from environment variable VCAP_SERVICES") + envData := os.Getenv("VCAP_SERVICES") + if envData == "" { + h.Log.Debug("Environment variable VCAP_SERVICES is not set or empty") + return nil + } + h.Log.Debug("Successfully read VCAP Service data from environment variable.") + return []byte(envData) +} + +// getCredentials returns the configuration from the environment, or nil if not found. The credentials are represented +// as a JSON object loaded via loadVCAPServicesData. +func (h *Hook) getCredentials() *credentials { + data := h.loadVCAPServicesData() + if data == nil { + return nil + } + + // Represent the structure of the JSON object in VCAP_SERVICES for parsing. + + var vcapServices map[string][]struct { + Name string `json:"name"` + Credentials map[string]interface{} `json:"credentials"` + } + + if err := json.Unmarshal(data, &vcapServices); err != nil { + h.Log.Debug("Failed to unmarshal VCAP_SERVICES: %s", err) + return nil + } + + var found []*credentials + + for _, services := range vcapServices { + for _, service := range services { + if !strings.Contains(strings.ToLower(service.Name), "dynatrace") { + continue + } + + queryString := func(key string) string { + if value, ok := service.Credentials[key].(string); ok { + return value + } + return "" + } + + creds := &credentials{ + ServiceName: service.Name, + EnvironmentID: queryString("environmentid"), + APIToken: queryString("apitoken"), + APIURL: queryString("apiurl"), + CustomOneAgentURL: queryString("customoneagenturl"), + SkipErrors: queryString("skiperrors") == "true", + NetworkZone: queryString("networkzone"), + EnableFIPS: queryString("enablefips") == "true", + AddTechnologies: queryString("addtechnologies"), + } + + if (creds.EnvironmentID != "" && creds.APIToken != "") || creds.CustomOneAgentURL != "" { + found = append(found, creds) + } else if !(creds.EnvironmentID == "" && creds.APIToken == "") { // One of the fields is empty. + h.Log.Error("Incomplete credentials for service: %s, environment ID: %s, API token: %s", creds.ServiceName, + creds.EnvironmentID, creds.APIToken) + } + } + } + + if len(found) == 1 { + h.Log.Debug("Found one matching service: %s", found[0].ServiceName) + return found[0] + } + + if len(found) > 1 { + h.Log.Error("More than one matching service found!") + } + + return nil +} + +// download gets url, and stores it as filePath, retrying a few more times if the downloads fail. +func (h *Hook) download(url, filePath string, stager *libbuildpack.Stager, creds *credentials) error { + client := &http.Client{} + req, _ := http.NewRequest("GET", url, nil) + if creds.CustomOneAgentURL == "" { + ver, err := stager.BuildpackVersion() + if err != nil { + h.Log.Warning("Failed to get buildpack version: %v", err) + ver = "unknown" + } + req.Header.Set("User-Agent", fmt.Sprintf("cf-%s-buildpack/%s", stager.BuildpackLanguage(), ver)) + req.Header.Set("Authorization", fmt.Sprintf("Api-Token %s", creds.APIToken)) + } + + out, err := os.Create(filePath) + if err != nil { + return err + } + defer out.Close() + + const baseWaitTime = 3 * time.Second + for i := 0; ; i++ { + resp, err := client.Do(req) + if err == nil { + // We truncate the file to make it empty, we also need to move the offset to the beginning. For errors + // here, these would be unexpected so we just fail the function without retrying. + + if err = out.Truncate(0); err != nil { + resp.Body.Close() + return err + } + + if _, err = out.Seek(0, io.SeekStart); err != nil { + resp.Body.Close() + return err + } + + // Now we copy the response content into the file. + _, err = io.Copy(out, resp.Body) + + resp.Body.Close() // Ignore error, nothing worth doing if it fails. + + if resp.StatusCode < 400 && err == nil { + return nil + } + + h.Log.Debug("Download returned with status %s, error: %v", resp.Status, err) + + if i == h.MaxDownloadRetries { + h.Log.Warning("Maximum number of retries attempted: %d", h.MaxDownloadRetries) + return fmt.Errorf("download returned with status %s, error: %v", resp.Status, err) + } + } else { + h.Log.Debug("Download failed: %v", err) + + if i == h.MaxDownloadRetries { + h.Log.Warning("Maximum number of retries attempted: %d", h.MaxDownloadRetries) + return err + } + } + + waitTime := baseWaitTime + time.Duration(math.Pow(2, float64(i)))*time.Second + h.Log.Warning("Error during installer download, retrying in %v", waitTime) + time.Sleep(waitTime) + } + +} + +func (h *Hook) getDownloadURL(c *credentials, operatingSystem string) string { + var osType, installerType string + switch operatingSystem { + case "linux": + osType = "unix" + installerType = "paas-sh" + case "windows": + osType = "windows" + installerType = "paas" + } + + if c.CustomOneAgentURL != "" { + return c.CustomOneAgentURL + } + + apiURL, err := h.ensureApiURL(c) + if err != nil { + return "" + } + + u, err := url.ParseRequestURI(fmt.Sprintf("%s/v1/deployment/installer/agent/%s/%s/latest", apiURL, osType, installerType)) + if err != nil { + return "" + } + + qv := make(url.Values) + qv.Add("bitness", "64") + // only set the networkzone property when it is configured + if c.NetworkZone != "" { + qv.Add("networkZone", c.NetworkZone) + } + for _, t := range h.IncludeTechnologies { + qv.Add("include", t) + } + if c.AddTechnologies != "" { + // add optionally configured OneAgent code modules + for _, t := range strings.Split(c.AddTechnologies, ",") { + h.Log.Debug("Adding additional code module to download: %s", t) + qv.Add("include", t) + } + } + u.RawQuery = qv.Encode() // Parameters will be sorted by key. + + return u.String() +} + +// ensureApiURL makes sure that a valid URL was provided via the cf service. +// If the c.APIURL property is empty, we assume this is a PaaS setting and generate +// a proper API URL for a PaaS tenant. +func (h *Hook) ensureApiURL(creds *credentials) (string, error) { + apiURL := creds.APIURL + if apiURL == "" { + apiURL = fmt.Sprintf("https://%s.live.dynatrace.com/api", creds.EnvironmentID) + h.Log.Debug("No apiurl configured, assuming PaaS tenant and setting apiurl to %s", apiURL) + } else { + h.Log.Debug("apiurl parameter configured is set to %s, no need to apply PaaS fallback", apiURL) + } + + url, err := url.ParseRequestURI(apiURL) + if err != nil { + h.Log.Error("Failed to verify the configured API URL: %s", err) + return "", err + } + + return url.String(), nil +} + +// findAgentPath reads the manifest file included in the OneAgent package, and looks +// for the process agent file path. +func (h *Hook) findAgentPath(installDir string, technology string, binaryType string, libraryFilename string, platformName string) (string, error) { + // With these classes, we try to replicate the structure for the manifest.json file, so that we can parse it. + + type Binary struct { + Path string `json:"path"` + BinaryType string `json:"binarytype"` + } + + type Architecture map[string][]Binary + type Technologies map[string]Architecture + + type Manifest struct { + Technologies Technologies `json:"technologies"` + } + + fallbackPath := filepath.Join("agent", "lib64", libraryFilename) + + manifestPath := filepath.Join(installDir, "manifest.json") + if _, err := os.Stat(manifestPath); os.IsNotExist(err) { + h.Log.Info("manifest.json not found, using fallback!") + return fallbackPath, nil + } + + var manifest Manifest + + if raw, err := os.ReadFile(manifestPath); err != nil { + return "", err + } else if err = json.Unmarshal(raw, &manifest); err != nil { + return "", err + } + + for _, binary := range manifest.Technologies[technology][platformName] { + if binary.BinaryType == binaryType { + return binary.Path, nil + } + } + + // Using fallback path if we don't find the 'primary' process agent. + h.Log.Warning("Agent path not found in manifest.json, using fallback!") + return fallbackPath, nil +} + +// Downloads most recent agent config from configuration API of the tenant +// and merges it with the local version the standalone installer package brings along. +func (h *Hook) updateAgentConfig(creds *credentials, installDir string, stager *libbuildpack.Stager) error { + // agentConfigProperty represents a line of raw data we get from the config api + type agentConfigProperty struct { + Section string + Key string + Value string + } + + // Container type for agentConfigProperty. + // Used for easy unmarshalling. + type properties struct { + Properties []agentConfigProperty + } + + // Fetch most recent OneAgent config from API, which we get back in JSON format + // According to the API spec it always returns at least some sort of Header Info. + // So, we do not need to handle the case that the request succeeds and the content is empty. + client := &http.Client{Timeout: 3 * time.Second} + apiURL, err := h.ensureApiURL(creds) + if err != nil { + return err + } + agentConfigUrl := apiURL + "/v1/deployment/installer/agent/processmoduleconfig" + + lang := stager.BuildpackLanguage() + ver, err := stager.BuildpackVersion() + if err != nil { + h.Log.Warning("Failed to get buildpack version: %v", err) + ver = "unknown" + } + + h.Log.Debug("Downloading updated OneAgent config from %s", agentConfigUrl) + req, _ := http.NewRequest("GET", agentConfigUrl, nil) + req.Header.Set("User-Agent", fmt.Sprintf("cf-%s-buildpack/%s", lang, ver)) + req.Header.Set("Authorization", fmt.Sprintf("Api-Token %s", creds.APIToken)) + client.Do(req) + resp, err := client.Do(req) + + configComment := "" + configFromAPI := make(map[string]map[string]string) + if err != nil || resp.StatusCode != 200 { + h.Log.Warning("Failed to fetch updated OneAgent config from the API") + configComment = "# Warning: Failed to fetch updated OneAgent config from the API. This config only includes settings provided by the installer.\n" + } else { + h.Log.Debug("Successfully fetched updated OneAgent config from the API") + configComment = "# This config is a merge between the installer and the Cluster config\n" + var jsonConfig properties + json.NewDecoder(resp.Body).Decode(&jsonConfig) + + for _, v := range jsonConfig.Properties { + // you gotta check if the required map is already there + // if not: initialize it with a nice make :-) + _, ok := configFromAPI[v.Section] + if !ok { + configFromAPI[v.Section] = make(map[string]string) + } + configFromAPI[v.Section][v.Key] = v.Value + } + } + + // read data from ruxitagentproc.conf file + agentConfigPath := filepath.Join(installDir, "agent", "conf", "ruxitagentproc.conf") + agentConfigFile, err := os.Open(agentConfigPath) + if err != nil { + h.Log.Error("Failure while reading OneAgent config file %s: %s", agentConfigPath, err) + return err + } + h.Log.Debug("Successfully read OneAgent config from %s", agentConfigPath) + defer agentConfigFile.Close() + + configFromAgent := make(map[string]map[string]string) + currentSection := "" + var configSection string + var sectionRegexp, _ = regexp.Compile(`\[(.*)\]`) + configScanner := bufio.NewScanner(agentConfigFile) + + h.Log.Debug("Starting to parse OneAgent config...") + for configScanner.Scan() { + // This parses the data we retrieved from ruxitagentproc.conf and stores + // it into the configFromAgent map of maps that was created above, for easy + // merging with configFromAPI later on. + currentLine := configScanner.Text() + + // Check if current line is a section header + if sectionHeader := sectionRegexp.FindStringSubmatch(currentLine); len(sectionHeader) != 0 { + configSection = sectionHeader[1] + } else { + configSection = "" + } + + if configSection != "" { + currentSection = configSection + } else if strings.HasPrefix(currentLine, "#") { //it's a comment line + // skipping over lines that are purely comments + continue + } else if currentLine == "" { + // skipping over empty lines + continue + } else { + // you gotta check if the required map is already there + // if not: initialize it with a nice make :-) + _, ok := configFromAgent[currentSection] + if !ok { + configFromAgent[currentSection] = make(map[string]string) + } + configLineKey := strings.Fields(currentLine)[0] + configLineValue := strings.Join(strings.Fields(currentLine)[1:], " ") + configFromAgent[currentSection][configLineKey] = configLineValue + } + } + h.Log.Debug("Successfully parsed OneAgent config...") + + // Merge the two configs to get an updated version. + // Just writes all of configFromAPI over eventually existing values in + // configFromAgent, since the ones from the API are supposed to be the recent ones. + // This includes adding possibly new sections and/or property keys. + h.Log.Debug("Starting with OneAgent configuration merging...") + for section := range configFromAPI { + for property := range configFromAPI[section] { + _, ok := configFromAgent[section] + if !ok { + configFromAgent[section] = make(map[string]string) + } + configFromAgent[section][property] = configFromAPI[section][property] + } + } + h.Log.Debug("Finished OneAgent configuration merging") + + // open ruxitagentproc.conf to overwrite its content + overwriteAgentConfigFile, err := os.Create(agentConfigPath) + if err != nil { + h.Log.Error("Error opening OneAgent config file %s: %s", agentConfigPath, err) + return err + } + h.Log.Debug("Successfully opened OneAgent config file %s for writing", agentConfigPath) + defer overwriteAgentConfigFile.Close() + + // Write additional comments to the config + fmt.Fprintf(overwriteAgentConfigFile, configComment) + + // write merged data to ruxitagentproc.conf + for section := range configFromAgent { + fmt.Fprintf(overwriteAgentConfigFile, "[%s]\n", section) + for k, v := range configFromAgent[section] { + fmt.Fprintf(overwriteAgentConfigFile, "%s %s\n", k, v) + } + + // Trailing empty newline at the end of each section for better human readability + fmt.Fprintf(overwriteAgentConfigFile, "\n") + } + + h.Log.Debug("Finished writing updated OneAgent config back to %s", agentConfigPath) + + return nil +} diff --git a/vendor/github.com/Dynatrace/libbuildpack-dynatrace/unix.go b/vendor/github.com/Dynatrace/libbuildpack-dynatrace/unix.go new file mode 100644 index 00000000..08bd9174 --- /dev/null +++ b/vendor/github.com/Dynatrace/libbuildpack-dynatrace/unix.go @@ -0,0 +1,97 @@ +package dynatrace + +import ( + "fmt" + "io" + "os" + "path/filepath" + + "github.com/cloudfoundry/libbuildpack" +) + +func (h *Hook) runInstallerUnix(installerFilePath, installDir string, creds *credentials, stager *libbuildpack.Stager) error { + h.Log.Debug("Making %s executable...", installerFilePath) + err := os.Chmod(installerFilePath, 0755) + if err != nil { + h.Log.Error("Error while setting installer file %s executable", installerFilePath) + return err + } + + + h.Log.BeginStep("Starting Dynatrace OneAgent installer") + + if os.Getenv("BP_DEBUG") != "" { + err = h.Command.Execute("", os.Stdout, os.Stderr, installerFilePath, stager.BuildDir()) + } else { + err = h.Command.Execute("", io.Discard, io.Discard, installerFilePath, stager.BuildDir()) + } + if err != nil { + return err + } + + h.Log.Info("Dynatrace OneAgent installed.") + + // Post-installation setup... + + dynatraceEnvName := "dynatrace-env.sh" + dynatraceEnvPath := filepath.Join(stager.DepDir(), "profile.d", dynatraceEnvName) + agentLibPath, err := h.findAgentPath(filepath.Join(stager.BuildDir(), installDir), "process", "primary", "liboneagentproc.so", "linux-x86-64") + if err != nil { + h.Log.Error("Manifest handling failed!") + return err + } + + agentLibPath = filepath.Join(installDir, agentLibPath) + agentBuilderLibPath := filepath.Join(stager.BuildDir(), agentLibPath) + + if _, err = os.Stat(agentBuilderLibPath); os.IsNotExist(err) { + h.Log.Error("Agent library (%s) not found!", agentBuilderLibPath) + return err + } + + h.Log.BeginStep("Setting up Dynatrace OneAgent injection...") + h.Log.Debug("Copy %s to %s", dynatraceEnvName, dynatraceEnvPath) + if err = libbuildpack.CopyFile(filepath.Join(stager.BuildDir(), installDir, dynatraceEnvName), dynatraceEnvPath); err != nil { + return err + } + + h.Log.Debug("Open %s for modification...", dynatraceEnvPath) + f, err := os.OpenFile(dynatraceEnvPath, os.O_APPEND|os.O_WRONLY, os.ModeAppend) + if err != nil { + return err + } + + defer f.Close() + + extra := "" + + h.Log.Debug("Setting LD_PRELOAD...") + extra += fmt.Sprintf("\nexport LD_PRELOAD=${HOME}/%s", agentLibPath) + + if creds.NetworkZone != "" { + h.Log.Debug("Setting DT_NETWORK_ZONE...") + extra += fmt.Sprintf("\nexport DT_NETWORK_ZONE=${DT_NETWORK_ZONE:-%s}", creds.NetworkZone) + } + + // By default, OneAgent logs are printed to stderr. If the customer doesn't override this behavior through an + // environment variable, then we change the default output to stdout. + if os.Getenv("DT_LOGSTREAM") == "" { + h.Log.Debug("Setting DT_LOGSTREAM to stdout...") + extra += "\nexport DT_LOGSTREAM=stdout" + } + + ver, err := stager.BuildpackVersion() + if err != nil { + h.Log.Warning("Failed to get buildpack version: %v", err) + ver = "unknown" + } + h.Log.Debug("Preparing custom properties...") + extra += fmt.Sprintf( + "\nexport DT_CUSTOM_PROP=\"${DT_CUSTOM_PROP} CloudFoundryBuildpackLanguage=%s CloudFoundryBuildpackVersion=%s\"", stager.BuildpackLanguage(), ver) + + if _, err = f.WriteString(extra); err != nil { + return err + } + + return nil +} diff --git a/vendor/github.com/Dynatrace/libbuildpack-dynatrace/windows.go b/vendor/github.com/Dynatrace/libbuildpack-dynatrace/windows.go new file mode 100644 index 00000000..2301e591 --- /dev/null +++ b/vendor/github.com/Dynatrace/libbuildpack-dynatrace/windows.go @@ -0,0 +1,99 @@ +package dynatrace + +import ( + "fmt" + "os" + "path/filepath" + "slices" + "strings" + + "github.com/cloudfoundry/libbuildpack" +) + +func (h *Hook) runInstallerWindows(installerFilePath, installDir string, creds *credentials, stager *libbuildpack.Stager) error { + h.Log.BeginStep("Starting Dynatrace OneAgent installation") + + h.Log.Info("Unzipping archive '%s' to '%s'", installerFilePath, filepath.Join(stager.BuildDir(), installDir)) + err := libbuildpack.ExtractZip(installerFilePath, filepath.Join(stager.BuildDir(), installDir)) + if err != nil { + h.Log.Error("Error during unzipping paas archive") + return err + } + + h.Log.Info("Dynatrace OneAgent installed.") + + // Post-installation setup... + + h.Log.BeginStep("Setting up Dynatrace OneAgent injection...") + if slices.Contains(h.IncludeTechnologies, "dotnet") { + err = h.setUpDotNetCorProfilerInjection(creds, installDir, stager) + } else { + h.Log.Warning("No injection method available for technology stack") + return nil + } + if err != nil { + return err + } + + return nil +} + +func (h *Hook) setUpDotNetCorProfilerInjection(creds *credentials, installDir string, stager *libbuildpack.Stager) error { + agentPath, err := h.findAbsoluteAgentPath(stager, installDir) + if err != nil { + return fmt.Errorf("cannot find oneagentdotnet.dll: %s", err) + } + + scriptContent := "set COR_ENABLE_PROFILING=1\n" + scriptContent += "set COR_PROFILER={B7038F67-52FC-4DA2-AB02-969B3C1EDA03}\n" + scriptContent += "set DT_AGENTACTIVE=true\n" + scriptContent += "set DT_BLOCKLIST=powershell*\n" + scriptContent += fmt.Sprintf("set COR_PROFILER_PATH_64=%s\n", agentPath) + + if creds.NetworkZone != "" { + h.Log.Debug("Setting DT_NETWORK_ZONE...") + scriptContent += "set DT_NETWORK_ZONE=" + creds.NetworkZone + "\n" + } + + ver, err := stager.BuildpackVersion() + if err != nil { + h.Log.Warning("Failed to get buildpack version: %v", err) + ver = "unknown" + } + h.Log.Debug("Preparing custom properties...") + scriptContent += fmt.Sprintf("set DT_CUSTOM_PROP=\"%%DT_CUSTOM_PROP%% CloudFoundryBuildpackLanguage=%s CloudFoundryBuildpackVersion=%s\"\n", stager.BuildpackLanguage(), ver) + + stager.WriteProfileD("dynatrace-env.cmd", scriptContent) + + return nil +} + +func (h *Hook) findAbsoluteAgentPath(stager *libbuildpack.Stager, installDir string) (string, error) { + + // look for dotnet agent DLL file relative to the root of the downloaded zip archive + // and get the path from the manifest e.g. agent/bin/windows-x86-64/oneagentdotnet .dll + agentDllPath, err := h.findAgentPath(filepath.Join(stager.BuildDir(), installDir), "dotnet", "primary", "oneagentdotnet.dll", "windows-x86-64") + if err != nil { + h.Log.Error("Manifest handling failed!") + return "", err + } + + // windows path separator is "\" instead of "/" + agentDllPath = strings.ReplaceAll(agentDllPath, "/", "\\") + + // build the agent DLL path relative to the app directory + // e.g. dynatrace/oneagent/agent/bin/windows-x86-64/oneagentdotnet.dll + agentDllPathInAppDir := filepath.Join(installDir, agentDllPath) + + // check that the agent dll is present in the build dir + // e.g. at \tmp\app\dynatrace\oneagent\agent\bin\1.303.0.20240930-081133\windows-x86-32\oneagentdotnet.dll + agentDllPathInBuildDir := filepath.Join(stager.BuildDir(), agentDllPathInAppDir) + + if _, err = os.Stat(agentDllPathInBuildDir); os.IsNotExist(err) { + h.Log.Error("Agent library (%s) not found!", agentDllPathInBuildDir) + return "", err + } + + // build the absolute path of the agent DLL as it will be available at runtime + return filepath.Join("C:\\users\\vcap\\app", agentDllPathInAppDir), nil +} diff --git a/vendor/modules.txt b/vendor/modules.txt index a8092726..6d587ea3 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1,6 +1,9 @@ # code.cloudfoundry.org/lager v2.0.0+incompatible ## explicit code.cloudfoundry.org/lager +# github.com/Dynatrace/libbuildpack-dynatrace v1.9.0 +## explicit; go 1.19 +github.com/Dynatrace/libbuildpack-dynatrace # github.com/Masterminds/semver v1.5.0 ## explicit github.com/Masterminds/semver