diff --git a/procfile/procfile_v1.go b/procfile/procfile_v1.go new file mode 100644 index 0000000..db3f749 --- /dev/null +++ b/procfile/procfile_v1.go @@ -0,0 +1,194 @@ +package procfile + +// ////////////////////////////////////////////////////////////////////////////////// // +// // +// Copyright (c) 2006-2017 FB GROUP LLC // +// // +// ////////////////////////////////////////////////////////////////////////////////// // + +import ( + "bufio" + "bytes" + "fmt" + "regexp" + "strings" + + "pkg.re/essentialkaos/ek.v7/log" +) + +// ////////////////////////////////////////////////////////////////////////////////// // + +// parseV1Procfile parse v1 procfile data +func parseV1Procfile(data []byte, config *Config) (*Application, error) { + if config == nil { + return nil, fmt.Errorf("Config is nil") + } + + log.Debug("Parsing procfile as v1") + + var services []*Service + + reader := bytes.NewReader(data) + scanner := bufio.NewScanner(reader) + + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + + switch { + case line == "": + // Skip empty line + case strings.HasPrefix(line, "#"): + // Skip comment + default: + service, err := parseV1Line(line) + + if err != nil { + return nil, err + } + + if service.Options.LimitFile == 0 && config.LimitFile != 0 { + service.Options.LimitFile = config.LimitFile + } + + if service.Options.LimitProc == 0 && config.LimitProc != 0 { + service.Options.LimitProc = config.LimitProc + } + + services = append(services, service) + } + } + + err := scanner.Err() + + if err != nil { + return nil, err + } + + app := &Application{ + ProcVersion: 1, + Name: config.Name, + User: config.User, + StartLevel: 3, + StopLevel: 3, + Group: config.Group, + WorkingDir: config.WorkingDir, + Services: services, + } + + addCrossLink(app) + + return app, nil +} + +// parseV1Line parse v1 procfile line +func parseV1Line(line string) (*Service, error) { + re := regexp.MustCompile(REGEXP_V1_LINE) + matches := re.FindStringSubmatch(line) + + if len(matches) != 3 { + return nil, fmt.Errorf("Procfile v1 should have format: 'some_label: command'") + } + + return parseV1Command(matches[1], matches[2]), nil +} + +// parseV1Command parse command and extract command and working dir +func parseV1Command(name, command string) *Service { + var service = &Service{Name: name, Options: &ServiceOptions{}} + + cmdSlice := splitV1Command(command) + + if strings.HasPrefix(cmdSlice[0], "cd") { + service.Options.WorkingDir = strings.Replace(cmdSlice[0], "cd ", "", -1) + cmdSlice = cmdSlice[1:] + } + + var ( + env map[string]string + cmd string + pre string + post string + log string + ) + + switch len(cmdSlice) { + case 3: + pre, _, _ = extractV1Data(cmdSlice[0]) + cmd, log, env = extractV1Data(cmdSlice[1]) + post, _, _ = extractV1Data(cmdSlice[2]) + case 2: + pre, _, _ = extractV1Data(cmdSlice[0]) + cmd, log, env = extractV1Data(cmdSlice[1]) + default: + cmd, log, env = extractV1Data(cmdSlice[0]) + } + + service.Cmd = cmd + service.PreCmd = pre + service.PostCmd = post + service.Options.Env = env + service.Options.LogPath = log + + return service +} + +// extractV1Data extract data from command +func extractV1Data(command string) (string, string, map[string]string) { + var ( + env map[string]string + cmd []string + log string + + isEnv bool + isLog bool + ) + + cmdSlice := strings.Fields(command) + + for _, cmdPart := range cmdSlice { + if strings.TrimSpace(cmdPart) == "" { + continue + } + + if strings.HasPrefix(cmdPart, "env") { + env = make(map[string]string) + isEnv = true + continue + } + + if isEnv { + if strings.Contains(cmdPart, "=") { + envSlice := strings.Split(cmdPart, "=") + env[envSlice[0]] = envSlice[1] + continue + } else { + isEnv = false + } + } + + if strings.Contains(cmdPart, ">>") { + isLog = true + continue + } + + if isLog { + log = cmdPart + break + } + + cmd = append(cmd, cmdPart) + } + + return strings.Join(cmd, " "), log, env +} + +// splitV1Cmd split command and format command +func splitV1Command(cmd string) []string { + var result []string + + for _, cmdPart := range strings.Split(cmd, "&&") { + result = append(result, strings.TrimSpace(cmdPart)) + } + + return result +} diff --git a/procfile/procfile_v2.go b/procfile/procfile_v2.go new file mode 100644 index 0000000..9a1c821 --- /dev/null +++ b/procfile/procfile_v2.go @@ -0,0 +1,223 @@ +package procfile + +// ////////////////////////////////////////////////////////////////////////////////// // +// // +// Copyright (c) 2006-2017 FB GROUP LLC // +// // +// ////////////////////////////////////////////////////////////////////////////////// // + +import ( + "fmt" + + "pkg.re/essentialkaos/ek.v7/log" + + "pkg.re/essentialkaos/go-simpleyaml.v1" +) + +// ////////////////////////////////////////////////////////////////////////////////// // + +// parseV2Procfile parse v2 procfile data +func parseV2Procfile(data []byte, config *Config) (*Application, error) { + var err error + + log.Debug("Parsing procfile as v2") + + yaml, err := simpleyaml.NewYaml(data) + + if err != nil { + return nil, err + } + + commands, err := yaml.Get("commands").Map() + + if err != nil { + return nil, fmt.Errorf("Commands missing in Procfile") + } + + services, err := parseV2Commands(yaml, commands, config) + + if err != nil { + return nil, err + } + + app := &Application{ + ProcVersion: 2, + Name: config.Name, + User: config.User, + Group: config.Group, + StartLevel: 3, + StopLevel: 3, + WorkingDir: config.WorkingDir, + Services: services, + } + + if yaml.IsExist("working_directory") { + app.WorkingDir, err = yaml.Get("working_directory").String() + + if err != nil { + return nil, fmt.Errorf("Can't parse working_directory value: %v", err) + } + } + + if yaml.IsExist("start_on_runlevel") { + app.StartLevel, err = yaml.Get("start_on_runlevel").Int() + + if err != nil { + return nil, fmt.Errorf("Can't parse start_on_runlevel value: %v", err) + } + } + + if yaml.IsExist("stop_on_runlevel") { + app.StopLevel, err = yaml.Get("stop_on_runlevel").Int() + + if err != nil { + return nil, fmt.Errorf("Can't parse stop_on_runlevel value: %v", err) + } + } + + addCrossLink(app) + + return app, nil +} + +// parseCommands parse command section in yaml based procfile +func parseV2Commands(yaml *simpleyaml.Yaml, commands map[interface{}]interface{}, config *Config) ([]*Service, error) { + var services []*Service + + commonOptions, err := parseV2Options(yaml) + + if err != nil { + return nil, err + } + + for key := range commands { + serviceName := fmt.Sprint(key) + commandYaml := yaml.GetPath("commands", serviceName) + serviceCmd, err := commandYaml.Get("command").String() + + if err != nil { + return nil, err + } + + servicePreCmd := commandYaml.Get("pre").MustString() + servicePostCmd := commandYaml.Get("post").MustString() + + serviceOptions, err := parseV2Options(commandYaml) + + if err != nil { + return nil, err + } + + mergeServiceOptions(serviceOptions, commonOptions) + configureDefaults(serviceOptions, config) + + service := &Service{ + Name: serviceName, + Cmd: serviceCmd, + PreCmd: servicePreCmd, + PostCmd: servicePostCmd, + Options: serviceOptions, + } + + services = append(services, service) + } + + return services, nil +} + +// parseV2Options parse service options in yaml based procfile +func parseV2Options(yaml *simpleyaml.Yaml) (*ServiceOptions, error) { + var err error + + options := &ServiceOptions{ + Env: make(map[string]string), + IsRespawnEnabled: true, + } + + if yaml.IsExist("working_directory") { + options.WorkingDir, err = yaml.Get("working_directory").String() + + if err != nil { + return nil, fmt.Errorf("Can't parse working_directory value: %v", err) + } + } + + if yaml.IsExist("log") { + options.LogPath, err = yaml.Get("log").String() + + if err != nil { + return nil, fmt.Errorf("Can't parse log value: %v", err) + } + } + + if yaml.IsExist("kill_timeout") { + options.KillTimeout, err = yaml.Get("kill_timeout").Int() + + if err != nil { + return nil, fmt.Errorf("Can't parse kill_timeout value: %v", err) + } + } + + if yaml.IsExist("count") { + options.Count, err = yaml.Get("count").Int() + + if err != nil { + return nil, fmt.Errorf("Can't parse count value: %v", err) + } + } + + if yaml.IsExist("env") { + env, err := yaml.Get("env").Map() + + if err != nil { + return nil, fmt.Errorf("Can't parse env value: %v", err) + } + + options.Env = convertMapType(env) + } + + if yaml.IsPathExist("respawn", "count") || yaml.IsPathExist("respawn", "interval") { + if yaml.IsPathExist("respawn", "count") { + options.RespawnCount, err = yaml.Get("respawn").Get("count").Int() + + if err != nil { + return nil, fmt.Errorf("Can't parse respawn.count value: %v", err) + } + } + + if yaml.IsPathExist("respawn", "interval") { + options.RespawnInterval, err = yaml.Get("respawn").Get("interval").Int() + + if err != nil { + return nil, fmt.Errorf("Can't parse respawn.interval value: %v", err) + } + } + + } else if yaml.IsExist("respawn") { + options.IsRespawnEnabled, err = yaml.Get("respawn").Bool() + + if err != nil { + return nil, fmt.Errorf("Can't parse respawn value: %v", err) + } + } + + if yaml.IsPathExist("limits", "nproc") || yaml.IsPathExist("limits", "nofile") { + if yaml.IsPathExist("limits", "nofile") { + options.LimitFile, err = yaml.Get("limits").Get("nofile").Int() + + if err != nil { + return nil, fmt.Errorf("Can't parse limits.nofile value: %v", err) + } + } + + if yaml.IsPathExist("limits", "nproc") { + options.LimitProc, err = yaml.Get("limits").Get("nproc").Int() + + if err != nil { + return nil, fmt.Errorf("Can't parse limits.nproc value: %v", err) + } + } + } + + return options, nil +}