package hooks import ( "encoding/json" "errors" "fmt" "io" "os" "path/filepath" "sort" "strings" "regexp" "github.com/cloudfoundry/libbuildpack" ) const ( appDynamicsServiceNameRegex = "app(-)?dynamics" ) type Command interface { Execute(string, io.Writer, io.Writer, string, ...string) error } type AppdynamicsHook struct { libbuildpack.DefaultHook Log *libbuildpack.Logger Command Command } type Plan struct { Credentials Credential `json:"credentials"` Name string `json:"name,omitempty"` } type Credential struct { ControllerHost string `json:"host-name"` ControllerPort string `json:"port"` SslEnabled bool `json:"ssl-enabled"` AccountAccessKey string `json:"account-access-key"` AccountName string `json:"account-name"` } type VcapApplication struct { ApplicationName string `json:"application_name"` Name string `json:"name"` ProcessType string `json:"process_type"` Limits struct { Mem int `json:"mem"` } `json:"limits"` } func (h AppdynamicsHook) getEnv(key, fallback string) string { if value, ok := os.LookupEnv(key); ok { return value } return fallback } func (h AppdynamicsHook) GenerateAppdynamicsScript(envVars map[string]string) string { var envKeys []string for k := range envVars { envKeys = append(envKeys, k) } sort.Strings(envKeys) // unnecessary but just to be deterministic for tests scriptContents := "# Autogenerated Appdynamics Script\n" for _, envKey := range envKeys { envStr := fmt.Sprintf("export %s=%s", envKey, envVars[envKey]) scriptContents += "\n" + envStr } return scriptContents } func (h AppdynamicsHook) GenerateStartUpCommand(startCommand string) (string, error) { webCommands := strings.SplitN(startCommand, ":", 2) if len(webCommands) != 2 { return "", errors.New("improper format found in Procfile") } return fmt.Sprintf("web: pyagent run -- %s", webCommands[1]), nil } func (h AppdynamicsHook) RewriteProcFile(procFilePath string) error { startCommand, err := os.ReadFile(procFilePath) if err != nil { return fmt.Errorf("Error reading file %s: %v", procFilePath, err) } newCommand, err := h.GenerateStartUpCommand(string(startCommand)) if err != nil { return err } if err := os.WriteFile(procFilePath, []byte(newCommand), 0666); err != nil { return fmt.Errorf("Error writing file %s: %v", procFilePath, err) } return nil } func (h AppdynamicsHook) RewriteRequirementsFile(stager *libbuildpack.Stager) error { h.Log.BeginStep("Rewriting Requirements file with appdynamics package") reqFile := filepath.Join(stager.BuildDir(), "requirements.txt") writeFlag := os.O_APPEND | os.O_WRONLY packageName := "\n" + "appdynamics" if exists, err := libbuildpack.FileExists(reqFile); err != nil { return err } else if !exists { h.Log.Info("Requirements file not found creating one with appdynamics packages") writeFlag = os.O_CREATE | os.O_WRONLY packageName = "appdynamics" } f, err := os.OpenFile(reqFile, writeFlag, 0666) if err != nil { panic(err) } defer f.Close() if _, err = f.WriteString(packageName); err != nil { panic(err) } fileContents, _ := os.ReadFile(f.Name()) h.Log.Info("%s", string(fileContents)) return nil } func (h AppdynamicsHook) RewriteProcFileWithAppdynamics(stager *libbuildpack.Stager) error { h.Log.BeginStep("Rewriting ProcFile to start with Appdynamics") file := filepath.Join(stager.BuildDir(), "Procfile") if exists, _ := libbuildpack.FileExists(file); exists { if err := h.RewriteProcFile(file); err != nil { return err } fileContents, _ := os.ReadFile(file) h.Log.Info("%s", string(fileContents)) } else { h.Log.Info("Cannot find Procfile, skipping this step!") } return nil } func (h AppdynamicsHook) CreateAppDynamicsEnv(stager *libbuildpack.Stager, environmentVars map[string]string) error { scriptContents := h.GenerateAppdynamicsScript(environmentVars) h.Log.BeginStep("Writing Appdynamics Environment") h.Log.Debug("%s", scriptContents) return stager.WriteProfileD("appdynamics.sh", scriptContents) } func (h AppdynamicsHook) BeforeCompile(stager *libbuildpack.Stager) error { if os.Getenv("APPD_AGENT") != "" { // APPD_AGENT is set => multibuildpack is used to configure appdynamics agent. Do nothing. return nil } // Some env var or something that lets us know that we are using app dynamics? vcapServices := os.Getenv("VCAP_SERVICES") services := make(map[string][]Plan) err := json.Unmarshal([]byte(vcapServices), &services) if err != nil { h.Log.Debug("Could not unmarshall VCAP_SERVICES JSON exiting: %v", err) return nil } appdServiceName, appdynamicsPlan, err := getAppDynamicsServiceName(services, h.Log) if appdServiceName == "" { return nil } h.Log.BeginStep("Setting up Appdynamics") vcapApplication := os.Getenv("VCAP_APPLICATION") application := VcapApplication{} err = json.Unmarshal([]byte(vcapApplication), &application) if err != nil { h.Log.Debug("Could not unmarshall VCAP_APPLICATION JSON %v", err) h.Log.Debug("VCAP_APPLICATION: %s", vcapApplication) } sslFlag := "off" credentials := appdynamicsPlan.Credentials if credentials.SslEnabled { sslFlag = "on" } appdEnv := map[string]string{ "APPD_APP_NAME": h.getEnv("APPD_APP_NAME", application.ApplicationName), "APPD_TIER_NAME": h.getEnv("APPD_TIER_NAME", application.ApplicationName), "APPD_NODE_NAME": h.getEnv("APPD_NODE_NAME", application.ApplicationName), "APPD_CONTROLLER_HOST": credentials.ControllerHost, "APPD_CONTROLLER_PORT": credentials.ControllerPort, "APPD_ACCOUNT_ACCESS_KEY": credentials.AccountAccessKey, "APPD_ACCOUNT_NAME": credentials.AccountName, "APPD_SSL_ENABLED": sslFlag, } if err := h.RewriteRequirementsFile(stager); err != nil { h.Log.Error("Could not write requirements file with Appdynamics packages: %v", err) return err } if err := h.CreateAppDynamicsEnv(stager, appdEnv); err != nil { h.Log.Error("Could not create Appdynamics environment: %v", err) return err } if err := h.RewriteProcFileWithAppdynamics(stager); err != nil { h.Log.Error("Could not rewrite procfile with Appdynamics start command: %v", err) return err } return nil } func getAppDynamicsServiceName(services map[string][]Plan, log *libbuildpack.Logger) (string, Plan, error) { // Check if there is a service with name appdynamics or app-dynamics for serviceName, servicePlans := range services { if isAppDynamicsServiceName(serviceName) { appdServiceName := serviceName logDeprecationWarning(log) return appdServiceName, servicePlans[0], nil } } // If this line is reached, no service with name appdynamics or app-dynamics was found. Check for user-provided services userProvidedServices, keyExists := services["user-provided"] if !keyExists { return "", Plan{}, nil } for _, plan := range userProvidedServices { if isAppDynamicsServiceName(plan.Name) { appdServiceName := plan.Name logDeprecationWarning(log) return appdServiceName, plan, nil } } // If this line is reached, no service with name appdynamics or app-dynamics was found in either the services and user-provided services. Return empty string, empty plan and nil error return "", Plan{}, nil } func isAppDynamicsServiceName(serviceName string) bool { match, err := regexp.MatchString(appDynamicsServiceNameRegex, serviceName) if err != nil { return false } return match } func logDeprecationWarning(log *libbuildpack.Logger) { log.Warning("[DEPRECATION WARNING]:") log.Warning("Please use AppDynamics extension buildpack for Python Application instrumentation") log.Warning("for more details: https://docs.pivotal.io/partners/appdynamics/multibuildpack.html") } func init() { logger := libbuildpack.NewLogger(os.Stdout) command := &libbuildpack.Command{} libbuildpack.AddHook(AppdynamicsHook{ Log: logger, Command: command, }) }