From 92159ff89ea18c249acbaad6364f0fa8bd692b54 Mon Sep 17 00:00:00 2001 From: Silvestre Zabala Date: Fri, 4 Sep 2026 10:49:40 +0200 Subject: [PATCH] feat: inject Dynatrace OneAgent for bound dynatrace service # Issue: The binary-buildpack cannot instrument arbitrary binaries with Dynatrace OneAgent the way the go-buildpack does. # Fix: Register the `libbuildpack-dynatrace` hook (default `process` module) so a bound Dynatrace service triggers OneAgent download at stage time and `LD_PRELOAD` at launch. Wired into both CLIs, added an integration suite plus mock tenant fixture, and documented the binding and dynamic-linking caveat. --- CHANGELOG | 6 + README.md | 32 + fixtures/util/dynatrace/dynatrace-env.sh | 3 + fixtures/util/dynatrace/fake_config.json | 11 + fixtures/util/dynatrace/go.mod | 3 + fixtures/util/dynatrace/install.sh | 19 + fixtures/util/dynatrace/liboneagentproc.so | Bin 0 -> 7968 bytes fixtures/util/dynatrace/main.go | 85 +++ fixtures/util/dynatrace/manifest.json | 12 + fixtures/util/dynatrace/ruxitagentproc.conf | 3 + go.mod | 1 + go.sum | 2 + src/binary/finalize/cli/main.go | 2 + src/binary/hooks/dynatrace.go | 10 + src/binary/integration/dynatrace_test.go | 208 +++++++ src/binary/integration/init_test.go | 41 ++ src/binary/supply/cli/main.go | 2 + .../libbuildpack-dynatrace/.gitignore | 15 + .../Dynatrace/libbuildpack-dynatrace/LICENSE | 201 ++++++ .../libbuildpack-dynatrace/README.md | 85 +++ .../Dynatrace/libbuildpack-dynatrace/hook.go | 579 ++++++++++++++++++ .../Dynatrace/libbuildpack-dynatrace/unix.go | 97 +++ .../libbuildpack-dynatrace/windows.go | 99 +++ vendor/modules.txt | 3 + 24 files changed, 1519 insertions(+) create mode 100755 fixtures/util/dynatrace/dynatrace-env.sh create mode 100644 fixtures/util/dynatrace/fake_config.json create mode 100644 fixtures/util/dynatrace/go.mod create mode 100755 fixtures/util/dynatrace/install.sh create mode 100755 fixtures/util/dynatrace/liboneagentproc.so create mode 100644 fixtures/util/dynatrace/main.go create mode 100644 fixtures/util/dynatrace/manifest.json create mode 100644 fixtures/util/dynatrace/ruxitagentproc.conf create mode 100644 src/binary/hooks/dynatrace.go create mode 100644 src/binary/integration/dynatrace_test.go create mode 100644 vendor/github.com/Dynatrace/libbuildpack-dynatrace/.gitignore create mode 100644 vendor/github.com/Dynatrace/libbuildpack-dynatrace/LICENSE create mode 100644 vendor/github.com/Dynatrace/libbuildpack-dynatrace/README.md create mode 100644 vendor/github.com/Dynatrace/libbuildpack-dynatrace/hook.go create mode 100644 vendor/github.com/Dynatrace/libbuildpack-dynatrace/unix.go create mode 100644 vendor/github.com/Dynatrace/libbuildpack-dynatrace/windows.go 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 0000000000000000000000000000000000000000..dd73ae82e5c2be93d542082b8be6500f0e57f003 GIT binary patch literal 7968 zcmeHMYit}>6~61WV<(O6ZGgO`PNGDL8)dN5q;Vf^{q_XeX>e^}+D5~AXY5_E4|jJK z?5GM(!=qN?62c#<{t5pAssyONig=_ov=Im?h=PI;l0^w}L?j$ll=6_5bMBmPAG2%q z2Z%rTT07^S@1Dnf&7E_Pzc4U7SQiQ@l(2eCX>OB`bW4M=6B0^wt4`HO&%J7omTf3C zRn=}b`Vgp6h6dONyI5}4EKx&OKu_2C_=HgCG0S?ZqPHq~MJhvC724#RQT0UmcU|xx zJs@(REX(EeEzjZlF&RgYLLL-)pCmn*LT%^69uW~36@Tl^iy`v?D#OuZ`zYB6({Je1 zxXRsW{OKz%z5msh|Mt)AhhKQ_@}{fdU$6^q=b^A3_wR*kMvtoR-OSi#`fa7(_uswz zT;IU2?|gU1^H1M=c4F`Ai+5Z8{HwpTP}91F?HM&%Q-p1+f$t^EsDXE`gO3uvzlMB{ zuv+$iNO(l;P@NH@LhIWa->%vNc$DyMH2P&(v~JP*o|fNHCC}@%jb`jhVDoR+c(XEO zzla&7YsUSadS!`fasa z?AUCS4MdZPv#xSHdphg7ULrlM?5EPv6nV_eWD{v`%8tc9tCESyoae+2A2xFt^Fd~3 zym;1)I?T5-AM;ym*NJ*j(w@xa{O&5-O*<;;IGMDW&N!|b8a~cQYSWzoj}C);~{_+qigb zm+956Q@4D!cz)o0zai_mBIg6}OM)Mg^#=}qxz*4XSu7;BnXHuI{0^b4mf?8!x$T28 zynKu76u-PZ7pU5SoCQHSWOWal}ed=_&F zogG!7*+^*5*2bnwWd8tReD+x+&--4rLuNDpllTfTm;UyWEWXnS*LR5ij5r2KGT|q-&NtClJ>s~U+kxE?tk{gy*cRcH!}V|2}OK7)Ytd0 zacF!ppZ4;`3GxsG!koo4$nv!l(&gBGj#Fzr6v!^>O46d7avEAgw@$ z3j3oHZ%~-$N_>;T`c&eL3hP{nH!19gO1%31$L(RYMP0+jEmH{7?p&>j(_4j&T_IoN z9;xlOs5WfO3i;Mzo}vt<{zTM7P5FC^_ohnuZ3^#aCH@i0yBOt=u7~L@!o8`yF|0mD z7q(jR+ZFaxrF`{0o7=<1x2q)0=TyDATPPuZjnAh#&SC`Slg}~6y94+>!ErwNR+KtH zl@R3*kSD;oIH>XJ`QbbV+sAneJ9ApTdY->Qc&+}gH?D7LI}!CxjdiSL=R3Ls)%$_Z z@lcI*xvBB$yvQE~53U!Vw;@%%AO1nOLBC+xOarTxr`fCV>V3YSa3g4h_{Y}4CkWqP zL*662R$gbGaLx~5eW7|$@L=BPYuXN^O7vANUp;THY55xGLoDm%y!_OZ8B?W{kL{)G z7$<#lly`A5_H;5c8BN-bm&xYrXnt13GO3xQ>$#43;=zYb)E3goBs-eTM&~GD<7Ma6 zR5qG&Z6}{f&5=qOr?XELqH(_7nz0zk4UYDV4A=vw`Z-NyQ!>#WumrUFN0mJ^{6ufh zu>Hi~;FAMm_E=Bv@Bj-^+N!AJJJtHUkNTMjQ&Fobe!}#rUZ|;1@yFd;{LM?`Y*6@ zsDd`e5Z{1a6+!riKE|IG{a%qn90ZCun)T^xU-S?CS;BZu@Y%(^0Tl5ziRi+8Eiatk zFV{z01q%PriAV+XTtFZ37bwOHeej@P3FsFGA_Fo43l6`qw;0gJ{{=vCe}H^&{9hOS zqauK~4;1$w=%YKJuZTYO&wBsAf_~8dw*vZzBSGh(C=~Ms_%*6dm;Y66_f`L z`eV{*&jLz?@p48e{E6 z#IHdAZ~%vXImO;97yXR*kH~$9&%yiYhyLUI;P(T4FV~;p!-mT1aOAVPuTs_IUV7Hj MZ 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