diff --git a/.gitignore b/.gitignore index efee0c0..5dd10f5 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,7 @@ validation/* # .env files .env* -*Procfile +*.Procfile +!fixtures/**/*Procfile procfile-util diff --git a/Dockerfile.build b/Dockerfile.build index d1d5f18..8ac4667 100644 --- a/Dockerfile.build +++ b/Dockerfile.build @@ -1,7 +1,7 @@ FROM golang:1.12.0-stretch RUN apt-get update \ - && apt install apt-transport-https build-essential curl gnupg2 lintian rpm rsync rubygems-integration ruby-dev ruby -qy \ + && apt install apt-transport-https bats build-essential curl gnupg2 lintian rpm rsync rubygems-integration ruby-dev ruby -qy \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* diff --git a/Makefile b/Makefile index 9ee5b0d..bc3aff4 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ MAINTAINER_NAME = Jose Diaz-Gonzalez REPOSITORY = go-procfile-util HARDWARE = $(shell uname -m) SYSTEM_NAME = $(shell uname -s | tr '[:upper:]' '[:lower:]') -BASE_VERSION ?= 0.10.1 +BASE_VERSION ?= 0.11.0 IMAGE_NAME ?= $(MAINTAINER)/$(REPOSITORY) PACKAGECLOUD_REPOSITORY ?= dokku/dokku-betafish @@ -162,7 +162,8 @@ validate: ls -lah build/deb build/rpm validation sha1sum build/deb/$(NAME)_$(VERSION)_amd64.deb sha1sum build/rpm/$(NAME)-$(VERSION)-1.x86_64.rpm + bats test.bats prebuild: go get -u github.com/go-bindata/go-bindata/... - go-bindata templates/... + cd export && go-bindata -pkg export templates/... diff --git a/commands/check_command.go b/commands/check_command.go new file mode 100644 index 0000000..bc80d1f --- /dev/null +++ b/commands/check_command.go @@ -0,0 +1,26 @@ +package commands + +import ( + "fmt" + "os" + "strings" + + "procfile-util/procfile" +) + + +func CheckCommand(entries []procfile.ProcfileEntry) bool { + if len(entries) == 0 { + fmt.Fprintf(os.Stderr, "no processes defined\n") + return false + } + + names := []string{} + for _, entry := range entries { + names = append(names, entry.Name) + } + + processNames := strings.Join(names[:], ", ") + fmt.Printf("valid procfile detected %v\n", processNames) + return true +} diff --git a/commands/commands.go b/commands/commands.go new file mode 100644 index 0000000..033fcf1 --- /dev/null +++ b/commands/commands.go @@ -0,0 +1,72 @@ +package commands + +import ( + "io/ioutil" + "os" + "strconv" + "strings" + + "procfile-util/procfile" + + "github.com/joho/godotenv" +) + +const portEnvVar = "PORT" + +func expandEnv(e procfile.ProcfileEntry, envPath string, allowEnv bool, defaultPort int) (string, error) { + baseExpandFunc := func(key string) string { + if key == "PS" { + return os.Getenv("PS") + } + if key == portEnvVar { + return strconv.Itoa(defaultPort) + } + return "" + } + + expandFunc := func(key string) string { + return baseExpandFunc(key) + } + + if allowEnv { + expandFunc = func(key string) string { + value := os.Getenv(key) + if value == "" { + value = baseExpandFunc(key) + } + return value + } + } + + if envPath != "" { + b, err := ioutil.ReadFile(envPath) + if err != nil { + return "", err + } + + content := string(b) + env, err := godotenv.Unmarshal(content) + if err != nil { + return "", err + } + + expandFunc = func(key string) string { + if val, ok := env[key]; ok { + return val + } + value := "" + if allowEnv { + value = os.Getenv(key) + } + if value == "" { + value = baseExpandFunc(key) + } + return value + } + } + + os.Setenv("PS", e.Name) + os.Setenv("EXPENV_PARENTHESIS", "$(") + s := strings.Replace(e.Command, "$(", "${EXPENV_PARENTHESIS}", -1) + return os.Expand(s, expandFunc), nil +} diff --git a/commands/delete_command.go b/commands/delete_command.go new file mode 100644 index 0000000..9e6fe95 --- /dev/null +++ b/commands/delete_command.go @@ -0,0 +1,17 @@ +package commands + +import ( + "procfile-util/procfile" +) + +func DeleteCommand(entries []procfile.ProcfileEntry, processType string, writePath string, stdout bool, delimiter string, path string) bool { + var validEntries []procfile.ProcfileEntry + for _, entry := range entries { + if processType == entry.Name { + continue + } + validEntries = append(validEntries, entry) + } + + return procfile.OutputProcfile(path, writePath, delimiter, stdout, validEntries) +} diff --git a/commands/exists_command.go b/commands/exists_command.go new file mode 100644 index 0000000..faba651 --- /dev/null +++ b/commands/exists_command.go @@ -0,0 +1,19 @@ +package commands + +import ( + "fmt" + "os" + + "procfile-util/procfile" +) + +func ExistsCommand(entries []procfile.ProcfileEntry, processType string) bool { + for _, entry := range entries { + if processType == entry.Name { + return true + } + } + + fmt.Fprint(os.Stderr, "no matching process entry found\n") + return false +} diff --git a/commands/expand_command.go b/commands/expand_command.go new file mode 100644 index 0000000..d6706ce --- /dev/null +++ b/commands/expand_command.go @@ -0,0 +1,35 @@ +package commands + +import ( + "fmt" + "os" + + "procfile-util/procfile" +) + +func ExpandCommand(entries []procfile.ProcfileEntry, envPath string, allowGetenv bool, processType string, defaultPort int, delimiter string) bool { + hasErrors := false + var expandedEntries []procfile.ProcfileEntry + for _, entry := range entries { + command, err := expandEnv(entry, envPath, allowGetenv, defaultPort) + if err != nil { + fmt.Fprintf(os.Stderr, "error processing command: %s\n", err) + hasErrors = true + } + + entry.Command = command + expandedEntries = append(expandedEntries, entry) + } + + if hasErrors { + return false + } + + for _, entry := range expandedEntries { + if processType == "" || processType == entry.Name { + fmt.Printf("%v%v %v\n", entry.Name, delimiter, entry.Command) + } + } + + return true +} diff --git a/commands/export_command.go b/commands/export_command.go new file mode 100644 index 0000000..2e114b0 --- /dev/null +++ b/commands/export_command.go @@ -0,0 +1,143 @@ +package commands + +import ( + "fmt" + "io/ioutil" + "os" + "os/user" + "strconv" + "strings" + + "procfile-util/export" + "procfile-util/procfile" + + "github.com/joho/godotenv" +) + +func ExportCommand(entries []procfile.ProcfileEntry, app string, description string, envPath string, format string, formation string, group string, home string, limitCoredump string, limitCputime string, limitData string, limitFileSize string, limitLockedMemory string, limitOpenFiles string, limitUserProcesses string, limitPhysicalMemory string, limitStackSize string, location string, logPath string, nice string, prestart string, workingDirectoryPath string, runPath string, timeout int, processUser string, defaultPort int) bool { + if format == "" { + fmt.Fprintf(os.Stderr, "no format specified\n") + return false + } + if location == "" { + fmt.Fprintf(os.Stderr, "no output location specified\n") + return false + } + + formats := map[string]export.ExportFunc{ + "launchd": export.ExportLaunchd, + "runit": export.ExportRunit, + "systemd": export.ExportSystemd, + "systemd-user": export.ExportSystemdUser, + "sysv": export.ExportSysv, + "upstart": export.ExportUpstart, + } + + if _, ok := formats[format]; !ok { + fmt.Fprintf(os.Stderr, "invalid format type: %s\n", format) + return false + } + + formations, err := procfile.ParseFormation(formation) + if err != nil { + fmt.Fprintf(os.Stderr, "%s\n", err) + return false + } + + if processUser == "" { + processUser = app + } + + if group == "" { + group = app + } + + u, err := user.Current() + if err != nil { + fmt.Fprintf(os.Stderr, "%s\n", err) + return false + } + + if home == "" { + home = "/home/" + u.Username + } + + env := make(map[string]string) + if envPath != "" { + b, err := ioutil.ReadFile(envPath) + if err != nil { + fmt.Fprintf(os.Stderr, "error reading env file: %s\n", err) + return false + } + + content := string(b) + env, err = godotenv.Unmarshal(content) + if err != nil { + fmt.Fprintf(os.Stderr, "error parsing env file: %s\n", err) + return false + } + } + + vars := make(map[string]interface{}) + vars["app"] = app + vars["description"] = description + vars["env"] = env + vars["group"] = group + vars["home"] = home + vars["log"] = logPath + vars["location"] = location + vars["limit_coredump"] = limitCoredump + vars["limit_cputime"] = limitCputime + vars["limit_data"] = limitData + vars["limit_file_size"] = limitFileSize + vars["limit_locked_memory"] = limitLockedMemory + vars["limit_open_files"] = limitOpenFiles + vars["limit_user_processes"] = limitUserProcesses + vars["limit_physical_memory"] = limitPhysicalMemory + vars["limit_stack_size"] = limitStackSize + vars["nice"] = nice + vars["prestart"] = prestart + vars["working_directory"] = workingDirectoryPath + vars["timeout"] = strconv.Itoa(timeout) + vars["ulimit_shell"] = ulimitShell(limitCoredump, limitCputime, limitData, limitFileSize, limitLockedMemory, limitOpenFiles, limitUserProcesses, limitPhysicalMemory, limitStackSize) + vars["user"] = processUser + + if fn, ok := formats[format]; ok { + return fn(app, entries, formations, location, defaultPort, vars) + } + + return false +} + +func ulimitShell(limitCoredump string, limitCputime string, limitData string, limitFileSize string, limitLockedMemory string, limitOpenFiles string, limitUserProcesses string, limitPhysicalMemory string, limitStackSize string) string { + s := []string{} + if limitCoredump != "" { + s = append(s, "ulimit -c ${limit_coredump}") + } + if limitCputime != "" { + s = append(s, "ulimit -t ${limit_cputime}") + } + if limitData != "" { + s = append(s, "ulimit -d ${limit_data}") + } + if limitFileSize != "" { + s = append(s, "ulimit -f ${limit_file_size}") + } + if limitLockedMemory != "" { + s = append(s, "ulimit -l ${limit_locked_memory}") + } + if limitOpenFiles != "" { + s = append(s, "ulimit -n ${limit_open_files}") + } + if limitUserProcesses != "" { + s = append(s, "ulimit -u ${limit_user_processes}") + } + if limitPhysicalMemory != "" { + s = append(s, "ulimit -m ${limit_physical_memory}") + } + if limitStackSize != "" { + s = append(s, "ulimit -s ${limit_stack_size}") + } + + return strings.Join(s, "\n") +} diff --git a/commands/list_command.go b/commands/list_command.go new file mode 100644 index 0000000..f2fba53 --- /dev/null +++ b/commands/list_command.go @@ -0,0 +1,14 @@ +package commands + +import ( + "fmt" + + "procfile-util/procfile" +) + +func ListCommand(entries []procfile.ProcfileEntry) bool { + for _, entry := range entries { + fmt.Printf("%v\n", entry.Name) + } + return true +} diff --git a/commands/set_command.go b/commands/set_command.go new file mode 100644 index 0000000..9e3f694 --- /dev/null +++ b/commands/set_command.go @@ -0,0 +1,18 @@ +package commands + +import ( + "procfile-util/procfile" +) + +func SetCommand(entries []procfile.ProcfileEntry, processType string, command string, writePath string, stdout bool, delimiter string, path string) bool { + var validEntries []procfile.ProcfileEntry + validEntries = append(validEntries, procfile.ProcfileEntry{processType, command}) + for _, entry := range entries { + if processType == entry.Name { + continue + } + validEntries = append(validEntries, entry) + } + + return procfile.OutputProcfile(path, writePath, delimiter, stdout, validEntries) +} \ No newline at end of file diff --git a/commands/show_command.go b/commands/show_command.go new file mode 100644 index 0000000..53a59ae --- /dev/null +++ b/commands/show_command.go @@ -0,0 +1,32 @@ +package commands + +import ( + "fmt" + "os" + + "procfile-util/procfile" +) + +func ShowCommand(entries []procfile.ProcfileEntry, envPath string, allowGetenv bool, processType string, defaultPort int) bool { + var foundEntry procfile.ProcfileEntry + for _, entry := range entries { + if processType == entry.Name { + foundEntry = entry + break + } + } + + if foundEntry == (procfile.ProcfileEntry{}) { + fmt.Fprintf(os.Stderr, "no matching process entry found\n") + return false + } + + command, err := expandEnv(foundEntry, envPath, allowGetenv, defaultPort) + if err != nil { + fmt.Fprintf(os.Stderr, "error processing command: %s\n", err) + return false + } + + fmt.Printf("%v\n", command) + return true +} diff --git a/export.go b/export.go deleted file mode 100644 index 79db037..0000000 --- a/export.go +++ /dev/null @@ -1,348 +0,0 @@ -package main - -import ( - "fmt" - "os" - "strconv" - "text/template" -) - -func exportLaunchd(app string, entries []ProcfileEntry, formations map[string]FormationEntry, location string, defaultPort int, vars map[string]interface{}) bool { - l, err := loadTemplate("launchd", "templates/launchd/launchd.plist.tmpl") - if err != nil { - fmt.Fprintf(os.Stderr, "%s\n", err) - return false - } - - if _, err := os.Stat(location + "/Library/LaunchDaemons/"); os.IsNotExist(err) { - os.MkdirAll(location+"/Library/LaunchDaemons/", os.ModePerm) - } - - for i, entry := range entries { - num := 1 - count := processCount(entry, formations) - - for num <= count { - processName := fmt.Sprintf("%s-%d", entry.Name, num) - port := portFor(i, num, defaultPort) - config := templateVars(app, entry, processName, num, port, vars) - if !writeOutput(l, fmt.Sprintf("%s/Library/LaunchDaemons/%s-%s.plist", location, app, processName), config) { - return false - } - - num += 1 - } - } - - return true -} - -func exportRunit(app string, entries []ProcfileEntry, formations map[string]FormationEntry, location string, defaultPort int, vars map[string]interface{}) bool { - r, err := loadTemplate("run", "templates/runit/run.tmpl") - if err != nil { - fmt.Fprintf(os.Stderr, "%s\n", err) - return false - } - l, err := loadTemplate("log", "templates/runit/log/run.tmpl") - if err != nil { - fmt.Fprintf(os.Stderr, "%s\n", err) - return false - } - - if _, err := os.Stat(location + "/service"); os.IsNotExist(err) { - os.MkdirAll(location+"/service", os.ModePerm) - } - - for i, entry := range entries { - num := 1 - count := processCount(entry, formations) - - for num <= count { - processDirectory := fmt.Sprintf("%s-%s-%d", app, entry.Name, num) - folderPath := location + "/service/" + processDirectory - processName := fmt.Sprintf("%s-%d", entry.Name, num) - - fmt.Println("creating:", folderPath) - os.MkdirAll(folderPath, os.ModePerm) - - fmt.Println("creating:", folderPath+"/env") - os.MkdirAll(folderPath+"/env", os.ModePerm) - - fmt.Println("creating:", folderPath+"/log") - os.MkdirAll(folderPath+"/log", os.ModePerm) - - port := portFor(i, num, defaultPort) - config := templateVars(app, entry, processName, num, port, vars) - - if !writeOutput(r, fmt.Sprintf("%s/run", folderPath), config) { - return false - } - - env, ok := config["env"].(map[string]string) - if !ok { - fmt.Fprintf(os.Stderr, "invalid env map\n") - return false - } - - env["PORT"] = strconv.Itoa(port) - env["PS"] = app + "-" + processName - - for key, value := range env { - fmt.Println("writing:", folderPath+"/env/"+key) - f, err := os.Create(folderPath + "/env/" + key) - if err != nil { - fmt.Fprintf(os.Stderr, "error creating file: %s\n", err) - return false - } - defer f.Close() - - if _, err = f.WriteString(value); err != nil { - fmt.Fprintf(os.Stderr, "error writing output: %s\n", err) - return false - } - - if err = f.Sync(); err != nil { - fmt.Fprintf(os.Stderr, "error syncing output: %s\n", err) - return false - } - } - - if !writeOutput(l, fmt.Sprintf("%s/log/run", folderPath), config) { - return false - } - - num += 1 - } - } - - return true -} - -func exportSystemd(app string, entries []ProcfileEntry, formations map[string]FormationEntry, location string, defaultPort int, vars map[string]interface{}) bool { - t, err := loadTemplate("target", "templates/systemd/default/control.target.tmpl") - if err != nil { - fmt.Fprintf(os.Stderr, "%s\n", err) - return false - } - - s, err := loadTemplate("service", "templates/systemd/default/program.service.tmpl") - if err != nil { - fmt.Fprintf(os.Stderr, "%s\n", err) - return false - } - - if _, err := os.Stat(location + "/etc/systemd/system/"); os.IsNotExist(err) { - os.MkdirAll(location+"/etc/systemd/system/", os.ModePerm) - } - - processes := []string{} - for i, entry := range entries { - num := 1 - count := processCount(entry, formations) - - for num <= count { - processName := fmt.Sprintf("%s-%d", entry.Name, num) - fileName := fmt.Sprintf("%s.%d", entry.Name, num) - processes = append(processes, fmt.Sprintf(app+"-%s.service", fileName)) - - port := portFor(i, num, defaultPort) - config := templateVars(app, entry, processName, num, port, vars) - if !writeOutput(s, fmt.Sprintf("%s/etc/systemd/system/%s-%s.service", location, app, fileName), config) { - return false - } - - num += 1 - } - } - - config := vars - config["processes"] = processes - if writeOutput(t, fmt.Sprintf("%s/etc/systemd/system/%s.target", location, app), config) { - fmt.Println("You will want to run 'systemctl --system daemon-reload' to activate the service on the target host") - return true - } - - return true -} - -func exportSystemdUser(app string, entries []ProcfileEntry, formations map[string]FormationEntry, location string, defaultPort int, vars map[string]interface{}) bool { - s, err := loadTemplate("service", "templates/systemd-user/default/program.service.tmpl") - if err != nil { - fmt.Fprintf(os.Stderr, "%s\n", err) - return false - } - - path := vars["home"].(string) + "/.config/systemd/user/" - if _, err := os.Stat(location + path); os.IsNotExist(err) { - os.MkdirAll(location+path, os.ModePerm) - } - - for i, entry := range entries { - num := 1 - count := processCount(entry, formations) - - for num <= count { - processName := fmt.Sprintf("%s-%d", entry.Name, num) - port := portFor(i, num, defaultPort) - config := templateVars(app, entry, processName, num, port, vars) - if !writeOutput(s, fmt.Sprintf("%s%s%s-%s.service", location, path, app, processName), config) { - return false - } - - num += 1 - } - } - - fmt.Println("You will want to run 'systemctl --user daemon-reload' to activate the service on the target host") - return true -} - -func exportSysv(app string, entries []ProcfileEntry, formations map[string]FormationEntry, location string, defaultPort int, vars map[string]interface{}) bool { - l, err := loadTemplate("launchd", "templates/sysv/default/init.sh.tmpl") - if err != nil { - fmt.Fprintf(os.Stderr, "%s\n", err) - return false - } - - if _, err := os.Stat(location + "/etc/init.d/"); os.IsNotExist(err) { - os.MkdirAll(location+"/etc/init.d/", os.ModePerm) - } - - for i, entry := range entries { - num := 1 - count := processCount(entry, formations) - - for num <= count { - processName := fmt.Sprintf("%s-%d", entry.Name, num) - port := portFor(i, num, defaultPort) - config := templateVars(app, entry, processName, num, port, vars) - if !writeOutput(l, fmt.Sprintf("%s/etc/init.d/%s-%s", location, app, processName), config) { - return false - } - - num += 1 - } - } - - return true -} -func exportUpstart(app string, entries []ProcfileEntry, formations map[string]FormationEntry, location string, defaultPort int, vars map[string]interface{}) bool { - p, err := loadTemplate("program", "templates/upstart/default/program.conf.tmpl") - if err != nil { - fmt.Fprintf(os.Stderr, "%s\n", err) - return false - } - - c, err := loadTemplate("app", "templates/upstart/default/control.conf.tmpl") - if err != nil { - fmt.Fprintf(os.Stderr, "%s\n", err) - return false - } - - t, err := loadTemplate("process-type", "templates/upstart/default/process-type.conf.tmpl") - if err != nil { - fmt.Fprintf(os.Stderr, "%s\n", err) - return false - } - - if _, err := os.Stat(location + "/etc/init/"); os.IsNotExist(err) { - os.MkdirAll(location+"/etc/init/", os.ModePerm) - } - - for i, entry := range entries { - num := 1 - count := processCount(entry, formations) - - if count > 0 { - config := vars - config["process_type"] = entry.Name - if !writeOutput(t, fmt.Sprintf("%s/etc/init/%s-%s.conf", location, app, entry.Name), config) { - return false - } - } - - for num <= count { - processName := fmt.Sprintf("%s-%d", entry.Name, num) - fileName := fmt.Sprintf("%s-%d", entry.Name, num) - port := portFor(i, num, defaultPort) - config := templateVars(app, entry, processName, num, port, vars) - if !writeOutput(p, fmt.Sprintf("%s/etc/init/%s-%s.conf", location, app, fileName), config) { - return false - } - - num += 1 - } - } - - config := vars - return writeOutput(c, fmt.Sprintf("%s/etc/init/%s.conf", location, app), config) -} -func processCount(entry ProcfileEntry, formations map[string]FormationEntry) int { - count := 0 - if f, ok := formations["all"]; ok { - count = f.Count - } - if f, ok := formations[entry.Name]; ok { - count = f.Count - } - return count -} - -func portFor(processIndex int, instance int, base int) int { - return 5000 + (processIndex * 100) + (instance - 1) -} - -func templateVars(app string, entry ProcfileEntry, processName string, num int, port int, vars map[string]interface{}) map[string]interface{} { - config := vars - config["args"] = entry.args() - config["args_escaped"] = entry.argsEscaped() - config["command"] = entry.Command - config["command_list"] = entry.commandList() - config["num"] = num - config["port"] = port - config["process_name"] = processName - config["process_type"] = entry.Name - config["program"] = entry.program() - config["ps"] = app + "-" + entry.Name + "." + strconv.Itoa(num) - if config["description"] == "" { - config["description"] = fmt.Sprintf("%s.%s process for %s", entry.Name, strconv.Itoa(num), app) - } - - return config -} - -func writeOutput(t *template.Template, outputPath string, variables map[string]interface{}) bool { - fmt.Println("writing:", outputPath) - f, err := os.Create(outputPath) - if err != nil { - fmt.Fprintf(os.Stderr, "error creating file: %s\n", err) - return false - } - defer f.Close() - - if err = t.Execute(f, variables); err != nil { - fmt.Fprintf(os.Stderr, "error writing output: %s\n", err) - return false - } - - if err := os.Chmod(outputPath, 0755); err != nil { - fmt.Fprintf(os.Stderr, "error setting mode: %s\n", err) - return false - } - - return true -} - -func loadTemplate(name string, filename string) (*template.Template, error) { - asset, err := Asset(filename) - if err != nil { - return nil, err - } - - t, err := template.New(name).Parse(string(asset)) - if err != nil { - return nil, fmt.Errorf("error parsing template: %s", err) - } - - return t, nil -} diff --git a/bindata.go b/export/bindata.go similarity index 98% rename from bindata.go rename to export/bindata.go index 328249c..e0f039f 100644 --- a/bindata.go +++ b/export/bindata.go @@ -1,4 +1,4 @@ -// Code generated for package main by go-bindata DO NOT EDIT. (@generated) +// Code generated for package export by go-bindata DO NOT EDIT. (@generated) // sources: // templates/launchd/launchd.plist.tmpl // templates/runit/log/run.tmpl @@ -10,7 +10,7 @@ // templates/upstart/default/control.conf.tmpl // templates/upstart/default/process-type.conf.tmpl // templates/upstart/default/program.conf.tmpl -package main +package export import ( "bytes" @@ -101,7 +101,7 @@ func templatesLaunchdLaunchdPlistTmpl() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "templates/launchd/launchd.plist.tmpl", size: 1294, mode: os.FileMode(420), modTime: time.Unix(1584491772, 0)} + info := bindataFileInfo{name: "templates/launchd/launchd.plist.tmpl", size: 1294, mode: os.FileMode(420), modTime: time.Unix(1603738030, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -121,7 +121,7 @@ func templatesRunitLogRunTmpl() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "templates/runit/log/run.tmpl", size: 186, mode: os.FileMode(420), modTime: time.Unix(1584491772, 0)} + info := bindataFileInfo{name: "templates/runit/log/run.tmpl", size: 186, mode: os.FileMode(420), modTime: time.Unix(1603738030, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -141,7 +141,7 @@ func templatesRunitRunTmpl() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "templates/runit/run.tmpl", size: 154, mode: os.FileMode(420), modTime: time.Unix(1584472497, 0)} + info := bindataFileInfo{name: "templates/runit/run.tmpl", size: 154, mode: os.FileMode(420), modTime: time.Unix(1603738030, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -161,7 +161,7 @@ func templatesSystemdDefaultControlTargetTmpl() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "templates/systemd/default/control.target.tmpl", size: 106, mode: os.FileMode(420), modTime: time.Unix(1584472497, 0)} + info := bindataFileInfo{name: "templates/systemd/default/control.target.tmpl", size: 106, mode: os.FileMode(420), modTime: time.Unix(1603738030, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -181,7 +181,7 @@ func templatesSystemdDefaultProgramServiceTmpl() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "templates/systemd/default/program.service.tmpl", size: 876, mode: os.FileMode(420), modTime: time.Unix(1584491772, 0)} + info := bindataFileInfo{name: "templates/systemd/default/program.service.tmpl", size: 876, mode: os.FileMode(420), modTime: time.Unix(1603738030, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -201,7 +201,7 @@ func templatesSystemdUserDefaultProgramServiceTmpl() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "templates/systemd-user/default/program.service.tmpl", size: 551, mode: os.FileMode(420), modTime: time.Unix(1584491772, 0)} + info := bindataFileInfo{name: "templates/systemd-user/default/program.service.tmpl", size: 551, mode: os.FileMode(420), modTime: time.Unix(1603738030, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -221,7 +221,7 @@ func templatesSysvDefaultInitShTmpl() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "templates/sysv/default/init.sh.tmpl", size: 6176, mode: os.FileMode(420), modTime: time.Unix(1584491772, 0)} + info := bindataFileInfo{name: "templates/sysv/default/init.sh.tmpl", size: 6176, mode: os.FileMode(420), modTime: time.Unix(1603738030, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -241,7 +241,7 @@ func templatesUpstartDefaultControlConfTmpl() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "templates/upstart/default/control.conf.tmpl", size: 76, mode: os.FileMode(420), modTime: time.Unix(1584472497, 0)} + info := bindataFileInfo{name: "templates/upstart/default/control.conf.tmpl", size: 76, mode: os.FileMode(420), modTime: time.Unix(1603738030, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -261,7 +261,7 @@ func templatesUpstartDefaultProcessTypeConfTmpl() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "templates/upstart/default/process-type.conf.tmpl", size: 120, mode: os.FileMode(420), modTime: time.Unix(1584472497, 0)} + info := bindataFileInfo{name: "templates/upstart/default/process-type.conf.tmpl", size: 120, mode: os.FileMode(420), modTime: time.Unix(1603738030, 0)} a := &asset{bytes: bytes, info: info} return a, nil } @@ -281,7 +281,7 @@ func templatesUpstartDefaultProgramConfTmpl() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "templates/upstart/default/program.conf.tmpl", size: 983, mode: os.FileMode(420), modTime: time.Unix(1584491772, 0)} + info := bindataFileInfo{name: "templates/upstart/default/program.conf.tmpl", size: 983, mode: os.FileMode(420), modTime: time.Unix(1603738030, 0)} a := &asset{bytes: bytes, info: info} return a, nil } diff --git a/export/export.go b/export/export.go new file mode 100644 index 0000000..b64afac --- /dev/null +++ b/export/export.go @@ -0,0 +1,82 @@ +package export + +import ( + "fmt" + "os" + "strconv" + "text/template" + + "procfile-util/procfile" +) + +type ExportFunc func(string, []procfile.ProcfileEntry, map[string]procfile.FormationEntry, string, int, map[string]interface{}) bool + +func processCount(entry procfile.ProcfileEntry, formations map[string]procfile.FormationEntry) int { + count := 0 + if f, ok := formations["all"]; ok { + count = f.Count + } + if f, ok := formations[entry.Name]; ok { + count = f.Count + } + return count +} + +func portFor(processIndex int, instance int, base int) int { + return 5000 + (processIndex * 100) + (instance - 1) +} + +func templateVars(app string, entry procfile.ProcfileEntry, processName string, num int, port int, vars map[string]interface{}) map[string]interface{} { + config := vars + config["args"] = entry.Args() + config["args_escaped"] = entry.ArgsEscaped() + config["command"] = entry.Command + config["command_list"] = entry.CommandList() + config["num"] = num + config["port"] = port + config["process_name"] = processName + config["process_type"] = entry.Name + config["program"] = entry.Program() + config["ps"] = app + "-" + entry.Name + "." + strconv.Itoa(num) + if config["description"] == "" { + config["description"] = fmt.Sprintf("%s.%s process for %s", entry.Name, strconv.Itoa(num), app) + } + + return config +} + +func writeOutput(t *template.Template, outputPath string, variables map[string]interface{}) bool { + fmt.Println("writing:", outputPath) + f, err := os.Create(outputPath) + if err != nil { + fmt.Fprintf(os.Stderr, "error creating file: %s\n", err) + return false + } + defer f.Close() + + if err = t.Execute(f, variables); err != nil { + fmt.Fprintf(os.Stderr, "error writing output: %s\n", err) + return false + } + + if err := os.Chmod(outputPath, 0755); err != nil { + fmt.Fprintf(os.Stderr, "error setting mode: %s\n", err) + return false + } + + return true +} + +func loadTemplate(name string, filename string) (*template.Template, error) { + asset, err := Asset(filename) + if err != nil { + return nil, err + } + + t, err := template.New(name).Parse(string(asset)) + if err != nil { + return nil, fmt.Errorf("error parsing template: %s", err) + } + + return t, nil +} diff --git a/export/export_launchd.go b/export/export_launchd.go new file mode 100644 index 0000000..80ce106 --- /dev/null +++ b/export/export_launchd.go @@ -0,0 +1,38 @@ +package export + +import ( + "fmt" + "os" + + "procfile-util/procfile" +) + +func ExportLaunchd(app string, entries []procfile.ProcfileEntry, formations map[string]procfile.FormationEntry, location string, defaultPort int, vars map[string]interface{}) bool { + l, err := loadTemplate("launchd", "templates/launchd/launchd.plist.tmpl") + if err != nil { + fmt.Fprintf(os.Stderr, "%s\n", err) + return false + } + + if _, err := os.Stat(location + "/Library/LaunchDaemons/"); os.IsNotExist(err) { + os.MkdirAll(location+"/Library/LaunchDaemons/", os.ModePerm) + } + + for i, entry := range entries { + num := 1 + count := processCount(entry, formations) + + for num <= count { + processName := fmt.Sprintf("%s-%d", entry.Name, num) + port := portFor(i, num, defaultPort) + config := templateVars(app, entry, processName, num, port, vars) + if !writeOutput(l, fmt.Sprintf("%s/Library/LaunchDaemons/%s-%s.plist", location, app, processName), config) { + return false + } + + num += 1 + } + } + + return true +} diff --git a/export/export_runit.go b/export/export_runit.go new file mode 100644 index 0000000..445e4ba --- /dev/null +++ b/export/export_runit.go @@ -0,0 +1,90 @@ +package export + +import ( + "fmt" + "os" + "strconv" + + "procfile-util/procfile" +) + +func ExportRunit(app string, entries []procfile.ProcfileEntry, formations map[string]procfile.FormationEntry, location string, defaultPort int, vars map[string]interface{}) bool { + r, err := loadTemplate("run", "templates/runit/run.tmpl") + if err != nil { + fmt.Fprintf(os.Stderr, "%s\n", err) + return false + } + l, err := loadTemplate("log", "templates/runit/log/run.tmpl") + if err != nil { + fmt.Fprintf(os.Stderr, "%s\n", err) + return false + } + + if _, err := os.Stat(location + "/service"); os.IsNotExist(err) { + os.MkdirAll(location+"/service", os.ModePerm) + } + + for i, entry := range entries { + num := 1 + count := processCount(entry, formations) + + for num <= count { + processDirectory := fmt.Sprintf("%s-%s-%d", app, entry.Name, num) + folderPath := location + "/service/" + processDirectory + processName := fmt.Sprintf("%s-%d", entry.Name, num) + + fmt.Println("creating:", folderPath) + os.MkdirAll(folderPath, os.ModePerm) + + fmt.Println("creating:", folderPath+"/env") + os.MkdirAll(folderPath+"/env", os.ModePerm) + + fmt.Println("creating:", folderPath+"/log") + os.MkdirAll(folderPath+"/log", os.ModePerm) + + port := portFor(i, num, defaultPort) + config := templateVars(app, entry, processName, num, port, vars) + + if !writeOutput(r, fmt.Sprintf("%s/run", folderPath), config) { + return false + } + + env, ok := config["env"].(map[string]string) + if !ok { + fmt.Fprintf(os.Stderr, "invalid env map\n") + return false + } + + env["PORT"] = strconv.Itoa(port) + env["PS"] = app + "-" + processName + + for key, value := range env { + fmt.Println("writing:", folderPath+"/env/"+key) + f, err := os.Create(folderPath + "/env/" + key) + if err != nil { + fmt.Fprintf(os.Stderr, "error creating file: %s\n", err) + return false + } + defer f.Close() + + if _, err = f.WriteString(value); err != nil { + fmt.Fprintf(os.Stderr, "error writing output: %s\n", err) + return false + } + + if err = f.Sync(); err != nil { + fmt.Fprintf(os.Stderr, "error syncing output: %s\n", err) + return false + } + } + + if !writeOutput(l, fmt.Sprintf("%s/log/run", folderPath), config) { + return false + } + + num += 1 + } + } + + return true +} diff --git a/export/export_systemd.go b/export/export_systemd.go new file mode 100644 index 0000000..55231c2 --- /dev/null +++ b/export/export_systemd.go @@ -0,0 +1,55 @@ +package export + +import ( + "fmt" + "os" + + "procfile-util/procfile" +) + +func ExportSystemd(app string, entries []procfile.ProcfileEntry, formations map[string]procfile.FormationEntry, location string, defaultPort int, vars map[string]interface{}) bool { + t, err := loadTemplate("target", "templates/systemd/default/control.target.tmpl") + if err != nil { + fmt.Fprintf(os.Stderr, "%s\n", err) + return false + } + + s, err := loadTemplate("service", "templates/systemd/default/program.service.tmpl") + if err != nil { + fmt.Fprintf(os.Stderr, "%s\n", err) + return false + } + + if _, err := os.Stat(location + "/etc/systemd/system/"); os.IsNotExist(err) { + os.MkdirAll(location+"/etc/systemd/system/", os.ModePerm) + } + + processes := []string{} + for i, entry := range entries { + num := 1 + count := processCount(entry, formations) + + for num <= count { + processName := fmt.Sprintf("%s-%d", entry.Name, num) + fileName := fmt.Sprintf("%s.%d", entry.Name, num) + processes = append(processes, fmt.Sprintf(app+"-%s.service", fileName)) + + port := portFor(i, num, defaultPort) + config := templateVars(app, entry, processName, num, port, vars) + if !writeOutput(s, fmt.Sprintf("%s/etc/systemd/system/%s-%s.service", location, app, fileName), config) { + return false + } + + num += 1 + } + } + + config := vars + config["processes"] = processes + if writeOutput(t, fmt.Sprintf("%s/etc/systemd/system/%s.target", location, app), config) { + fmt.Println("You will want to run 'systemctl --system daemon-reload' to activate the service on the target host") + return true + } + + return true +} diff --git a/export/export_systemd_user.go b/export/export_systemd_user.go new file mode 100644 index 0000000..48c1458 --- /dev/null +++ b/export/export_systemd_user.go @@ -0,0 +1,40 @@ +package export + +import ( + "fmt" + "os" + + "procfile-util/procfile" +) + +func ExportSystemdUser(app string, entries []procfile.ProcfileEntry, formations map[string]procfile.FormationEntry, location string, defaultPort int, vars map[string]interface{}) bool { + s, err := loadTemplate("service", "templates/systemd-user/default/program.service.tmpl") + if err != nil { + fmt.Fprintf(os.Stderr, "%s\n", err) + return false + } + + path := vars["home"].(string) + "/.config/systemd/user/" + if _, err := os.Stat(location + path); os.IsNotExist(err) { + os.MkdirAll(location+path, os.ModePerm) + } + + for i, entry := range entries { + num := 1 + count := processCount(entry, formations) + + for num <= count { + processName := fmt.Sprintf("%s-%d", entry.Name, num) + port := portFor(i, num, defaultPort) + config := templateVars(app, entry, processName, num, port, vars) + if !writeOutput(s, fmt.Sprintf("%s%s%s-%s.service", location, path, app, processName), config) { + return false + } + + num += 1 + } + } + + fmt.Println("You will want to run 'systemctl --user daemon-reload' to activate the service on the target host") + return true +} diff --git a/export/export_sysv.go b/export/export_sysv.go new file mode 100644 index 0000000..bfc0905 --- /dev/null +++ b/export/export_sysv.go @@ -0,0 +1,38 @@ +package export + +import ( + "fmt" + "os" + + "procfile-util/procfile" +) + +func ExportSysv(app string, entries []procfile.ProcfileEntry, formations map[string]procfile.FormationEntry, location string, defaultPort int, vars map[string]interface{}) bool { + l, err := loadTemplate("launchd", "templates/sysv/default/init.sh.tmpl") + if err != nil { + fmt.Fprintf(os.Stderr, "%s\n", err) + return false + } + + if _, err := os.Stat(location + "/etc/init.d/"); os.IsNotExist(err) { + os.MkdirAll(location+"/etc/init.d/", os.ModePerm) + } + + for i, entry := range entries { + num := 1 + count := processCount(entry, formations) + + for num <= count { + processName := fmt.Sprintf("%s-%d", entry.Name, num) + port := portFor(i, num, defaultPort) + config := templateVars(app, entry, processName, num, port, vars) + if !writeOutput(l, fmt.Sprintf("%s/etc/init.d/%s-%s", location, app, processName), config) { + return false + } + + num += 1 + } + } + + return true +} diff --git a/export/export_upstart.go b/export/export_upstart.go new file mode 100644 index 0000000..b044210 --- /dev/null +++ b/export/export_upstart.go @@ -0,0 +1,60 @@ +package export + +import ( + "fmt" + "os" + + "procfile-util/procfile" +) + +func ExportUpstart(app string, entries []procfile.ProcfileEntry, formations map[string]procfile.FormationEntry, location string, defaultPort int, vars map[string]interface{}) bool { + p, err := loadTemplate("program", "templates/upstart/default/program.conf.tmpl") + if err != nil { + fmt.Fprintf(os.Stderr, "%s\n", err) + return false + } + + c, err := loadTemplate("app", "templates/upstart/default/control.conf.tmpl") + if err != nil { + fmt.Fprintf(os.Stderr, "%s\n", err) + return false + } + + t, err := loadTemplate("process-type", "templates/upstart/default/process-type.conf.tmpl") + if err != nil { + fmt.Fprintf(os.Stderr, "%s\n", err) + return false + } + + if _, err := os.Stat(location + "/etc/init/"); os.IsNotExist(err) { + os.MkdirAll(location+"/etc/init/", os.ModePerm) + } + + for i, entry := range entries { + num := 1 + count := processCount(entry, formations) + + if count > 0 { + config := vars + config["process_type"] = entry.Name + if !writeOutput(t, fmt.Sprintf("%s/etc/init/%s-%s.conf", location, app, entry.Name), config) { + return false + } + } + + for num <= count { + processName := fmt.Sprintf("%s-%d", entry.Name, num) + fileName := fmt.Sprintf("%s-%d", entry.Name, num) + port := portFor(i, num, defaultPort) + config := templateVars(app, entry, processName, num, port, vars) + if !writeOutput(p, fmt.Sprintf("%s/etc/init/%s-%s.conf", location, app, fileName), config) { + return false + } + + num += 1 + } + } + + config := vars + return writeOutput(c, fmt.Sprintf("%s/etc/init/%s.conf", location, app), config) +} diff --git a/templates/launchd/launchd.plist.tmpl b/export/templates/launchd/launchd.plist.tmpl similarity index 100% rename from templates/launchd/launchd.plist.tmpl rename to export/templates/launchd/launchd.plist.tmpl diff --git a/templates/runit/log/run.tmpl b/export/templates/runit/log/run.tmpl similarity index 100% rename from templates/runit/log/run.tmpl rename to export/templates/runit/log/run.tmpl diff --git a/templates/runit/run.tmpl b/export/templates/runit/run.tmpl similarity index 100% rename from templates/runit/run.tmpl rename to export/templates/runit/run.tmpl diff --git a/templates/systemd-user/default/program.service.tmpl b/export/templates/systemd-user/default/program.service.tmpl similarity index 100% rename from templates/systemd-user/default/program.service.tmpl rename to export/templates/systemd-user/default/program.service.tmpl diff --git a/templates/systemd/default/control.target.tmpl b/export/templates/systemd/default/control.target.tmpl similarity index 100% rename from templates/systemd/default/control.target.tmpl rename to export/templates/systemd/default/control.target.tmpl diff --git a/templates/systemd/default/program.service.tmpl b/export/templates/systemd/default/program.service.tmpl similarity index 100% rename from templates/systemd/default/program.service.tmpl rename to export/templates/systemd/default/program.service.tmpl diff --git a/templates/sysv/default/init.sh.tmpl b/export/templates/sysv/default/init.sh.tmpl similarity index 100% rename from templates/sysv/default/init.sh.tmpl rename to export/templates/sysv/default/init.sh.tmpl diff --git a/templates/upstart/default/control.conf.tmpl b/export/templates/upstart/default/control.conf.tmpl similarity index 100% rename from templates/upstart/default/control.conf.tmpl rename to export/templates/upstart/default/control.conf.tmpl diff --git a/templates/upstart/default/process-type.conf.tmpl b/export/templates/upstart/default/process-type.conf.tmpl similarity index 100% rename from templates/upstart/default/process-type.conf.tmpl rename to export/templates/upstart/default/process-type.conf.tmpl diff --git a/templates/upstart/default/program.conf.tmpl b/export/templates/upstart/default/program.conf.tmpl similarity index 100% rename from templates/upstart/default/program.conf.tmpl rename to export/templates/upstart/default/program.conf.tmpl diff --git a/fixtures/comments.Procfile b/fixtures/comments.Procfile new file mode 100644 index 0000000..af4deb0 --- /dev/null +++ b/fixtures/comments.Procfile @@ -0,0 +1,57 @@ +############################### +# DEVELOPMENT # +############################### + +# Procfile for development using the new threaded worker (scheduler, twitter stream and delayed job) +cron: node worker.js +web: node web.js # testing inline comment +wor-ker: node worker.js +# -wor-ker2: node worker.js +# -wor-ker_2: node worker.js +custom: echo -n +2custom: echo -n +# -3custom-: echo -n +release: touch /app/release.test + +# Old version with separate processes (use this if you have issues with the threaded version) +# web: bundle exec rails server +# schedule: bundle exec rails runner bin/schedule.rb +# twitter: bundle exec rails runner bin/twitter_stream.rb +# dj: bundle exec script/delayed_job run + +############################### +# PRODUCTION # +############################### + +# You need to copy or link config/unicorn.rb.example to config/unicorn.rb for both production versions. +# Have a look at the deployment guides, if you want to set up huginn on your server: +# https://github.com/cantino/huginn/doc + +# Using the threaded worker (consumes less RAM but can run slower) +# web: bundle exec unicorn -c config/unicorn.rb +# jobs: bundle exec rails runner bin/threaded.rb + +# Old version with separate processes (use this if you have issues with the threaded version) +# web: bundle exec unicorn -c config/unicorn.rb +# schedule: bundle exec rails runner bin/schedule.rb +# twitter: bundle exec rails runner bin/twitter_stream.rb +# dj: bundle exec script/delayed_job run + +############################### +# Multiple DelayedJob workers # +############################### +# Per default Huginn can just run one agent at a time. Using a lot of agents or calling slow +# external services frequently might require more DelayedJob workers (an indicator for this is +# a backlog in your 'Job Management' page). +# Every uncommented line starts an additional DelayedJob worker. This works for development, production +# and for the threaded and separate worker processes. Keep in mind one worker needs about 300MB of RAM. +# +#dj2: bundle exec script/delayed_job -i 2 run +#dj3: bundle exec script/delayed_job -i 3 run +#dj4: bundle exec script/delayed_job -i 4 run +#dj5: bundle exec script/delayed_job -i 5 run +#dj6: bundle exec script/delayed_job -i 6 run +#dj7: bundle exec script/delayed_job -i 7 run +#dj8: bundle exec script/delayed_job -i 8 run +#dj9: bundle exec script/delayed_job -i 9 run +#dj10: bundle exec script/delayed_job -i 10 run diff --git a/fixtures/multiple.Procfile b/fixtures/multiple.Procfile new file mode 100644 index 0000000..b0b92f8 --- /dev/null +++ b/fixtures/multiple.Procfile @@ -0,0 +1,4 @@ +web: bundle exec puma -C config/puma.rb +webpacker: bundle exec bin/webpack +worker: rake jobs:work +release: rails db:migrate diff --git a/fixtures/port.Procfile b/fixtures/port.Procfile new file mode 100644 index 0000000..aebdb51 --- /dev/null +++ b/fixtures/port.Procfile @@ -0,0 +1,2 @@ +web: node web.js --port $PORT +worker: node worker.js diff --git a/fixtures/strict/Procfile b/fixtures/strict/Procfile new file mode 100644 index 0000000..6e9c4fc --- /dev/null +++ b/fixtures/strict/Procfile @@ -0,0 +1,10 @@ +# comment +#comment +web: python app.py + +# comment +worker: python worker.py + +worker-2: python worker.py 2 + +worker-3: python worker.py 3 \ No newline at end of file diff --git a/fixtures/strict/invalid.Procfile b/fixtures/strict/invalid.Procfile new file mode 100644 index 0000000..0b7828f --- /dev/null +++ b/fixtures/strict/invalid.Procfile @@ -0,0 +1,10 @@ +# comment +#comment +web: python app.py + +# comment +worker: python worker.py + +worker_2: python worker.py 2 + +worker-3: python worker.py 3 \ No newline at end of file diff --git a/fixtures/strict/single-invalid.Procfile b/fixtures/strict/single-invalid.Procfile new file mode 100644 index 0000000..ced8e09 --- /dev/null +++ b/fixtures/strict/single-invalid.Procfile @@ -0,0 +1 @@ +we_b: worker.js diff --git a/fixtures/strict/single.Procfile b/fixtures/strict/single.Procfile new file mode 100644 index 0000000..1eaa069 --- /dev/null +++ b/fixtures/strict/single.Procfile @@ -0,0 +1 @@ +web: worker.js diff --git a/fixtures/v1/Procfile b/fixtures/v1/Procfile new file mode 100644 index 0000000..0b7828f --- /dev/null +++ b/fixtures/v1/Procfile @@ -0,0 +1,10 @@ +# comment +#comment +web: python app.py + +# comment +worker: python worker.py + +worker_2: python worker.py 2 + +worker-3: python worker.py 3 \ No newline at end of file diff --git a/fixtures/v1/single.Procfile b/fixtures/v1/single.Procfile new file mode 100644 index 0000000..b3be750 --- /dev/null +++ b/fixtures/v1/single.Procfile @@ -0,0 +1 @@ +command: avahi-register diff --git a/main.go b/main.go index d7864d5..2825494 100644 --- a/main.go +++ b/main.go @@ -1,570 +1,31 @@ package main import ( - "bufio" "fmt" - "io/ioutil" "os" - "os/user" - "regexp" - "sort" "strconv" - "strings" + + "procfile-util/procfile" + "procfile-util/commands" "github.com/akamensky/argparse" - "github.com/andrew-d/go-termutil" - "github.com/joho/godotenv" - "gopkg.in/alessio/shellescape.v1" ) -// ProcfileEntry is a struct containing a process type and the corresponding command -type ProcfileEntry struct { - Name string - Command string -} - -// FormationEntry is a struct containing a process type and the corresponding count -type FormationEntry struct { - Name string - Count int -} - -type exportFunc func(string, []ProcfileEntry, map[string]FormationEntry, string, int, map[string]interface{}) bool - -func (p *ProcfileEntry) commandList() []string { - return strings.Fields(p.Command) -} - -func (p *ProcfileEntry) program() string { - return strings.Fields(p.Command)[0] -} - -func (p *ProcfileEntry) args() string { - return strings.Join(strings.Fields(p.Command)[1:], " ") -} - -func (p *ProcfileEntry) argsEscaped() string { - return shellescape.Quote(p.args()) -} - -const portEnvVar = "PORT" - // Version contains the procfile-util version var Version string -// Loglevel stores the current app log level -var Loglevel = "info" - -func logMessage(message string, level string) { - if level == "info" { - fmt.Println(message) - return - } - - if Loglevel == "debug" { - fmt.Fprintf(os.Stderr, fmt.Sprintf("%v\n", message)) - } -} - -func debugMessage(message string) { - logMessage(message, "debug") -} - -func infoMessage(message string) { - logMessage(message, "info") -} - -// GetProcfileContent returns the content at a path as a string -func GetProcfileContent(path string) (string, error) { - debugMessage(fmt.Sprintf("Attempting to read input from file: %v", path)) - f, err := os.Open(path) - if err != nil { - if !termutil.Isatty(os.Stdin.Fd()) { - debugMessage("Reading input from stdin") - bytes, err := ioutil.ReadAll(os.Stdin) - if err != nil { - return "", err - } - return string(bytes), nil - } - return "", err - } - defer f.Close() - - lines := []string{} - scanner := bufio.NewScanner(f) - for scanner.Scan() { - lines = append(lines, scanner.Text()) - } - err = scanner.Err() - return strings.Join(lines, "\n"), err -} - -func outputProcfile(path string, writePath string, delimiter string, stdout bool, entries []ProcfileEntry) bool { - if writePath != "" && stdout { - fmt.Fprintf(os.Stderr, "cannot specify both --stdout and --write-path flags\n") - return false - } - - sort.Slice(entries, func(i, j int) bool { - return entries[i].Name < entries[j].Name - }) - - if stdout { - for _, entry := range entries { - fmt.Printf("%v%v %v\n", entry.Name, delimiter, entry.Command) - } - return true - } - - if writePath != "" { - path = writePath - } - - if err := writeProcfile(path, delimiter, entries); err != nil { - fmt.Fprintf(os.Stderr, "error writing procfile: %s\n", err) - return false - } - - return true -} - -func writeProcfile(path string, delimiter string, entries []ProcfileEntry) error { - debugMessage(fmt.Sprintf("Attempting to write output to file: %v", path)) - file, err := os.Create(path) - if err != nil { - return err - } - defer file.Close() - - w := bufio.NewWriter(file) - for _, entry := range entries { - fmt.Fprintln(w, fmt.Sprintf("%v%v %v", entry.Name, delimiter, entry.Command)) - } - return w.Flush() -} - -func parseProcfile(path string, delimiter string, strict bool) ([]ProcfileEntry, error) { - var entries []ProcfileEntry - text, err := GetProcfileContent(path) +func parseProcfile(path string, delimiter string, strict bool) ([]procfile.ProcfileEntry, error) { + var entries []procfile.ProcfileEntry + text, err := procfile.GetProcfileContent(path) if err != nil { return entries, err } - return ParseProcfile(text, delimiter, strict) -} - -// ParseProcfile parses text as a procfile and returns a list of procfile entries -func ParseProcfile(text string, delimiter string, strict bool) ([]ProcfileEntry, error) { - var entries []ProcfileEntry - reCmd, _ := regexp.Compile(`^([a-z0-9]([-a-z0-9]*[a-z0-9])?)` + delimiter + `\s*(.+)$`) - reOldCmd, _ := regexp.Compile(`^([A-Za-z0-9_-]+)` + delimiter + `\s*(.+)$`) - - reComment, _ := regexp.Compile(`^(.*)\s#.+$`) - - lineNumber := 0 - names := make(map[string]bool) - scanner := bufio.NewScanner(strings.NewReader(text)) - for scanner.Scan() { - lineNumber++ - line := scanner.Text() - line = strings.TrimSpace(line) - - if len(line) == 0 { - continue - } - - oldParams := reOldCmd.FindStringSubmatch(line) - params := reCmd.FindStringSubmatch(line) - isCommand := len(params) == 4 - isOldCommand := len(oldParams) == 3 - isComment := strings.HasPrefix(line, "#") - if isComment { - continue - } - - if !isCommand { - if !isOldCommand { - return entries, fmt.Errorf("invalid line in procfile, line %d", lineNumber) - } - - if strict { - return entries, fmt.Errorf("process name contains invalid characters, line %d", lineNumber) - } - } - - name := "" - cmd := "" - if strict { - name, cmd = params[1], params[3] - } else { - name, cmd = oldParams[1], oldParams[2] - } - - if len(name) > 63 { - return entries, fmt.Errorf("process name over 63 characters, line %d", lineNumber) - } - - if names[name] { - return entries, fmt.Errorf("process names must be unique, line %d", lineNumber) - } - names[name] = true - - commentParams := reComment.FindStringSubmatch(cmd) - if len(commentParams) == 2 { - cmd = commentParams[1] - } - - cmd = strings.TrimSpace(cmd) - if strings.HasPrefix(cmd, "#") { - return entries, fmt.Errorf("comment specified in place of command, line %d", lineNumber) - } - - if len(cmd) == 0 { - return entries, fmt.Errorf("no command specified, line %d", lineNumber) - } - - entries = append(entries, ProcfileEntry{name, cmd}) - } - - if scanner.Err() != nil { - return entries, scanner.Err() - } - - if len(entries) == 0 { - return entries, fmt.Errorf("no entries found in Procfile") - } - - sort.Slice(entries, func(i, j int) bool { - return entries[i].Name < entries[j].Name - }) - - return entries, nil -} - -func expandEnv(e ProcfileEntry, envPath string, allowEnv bool, defaultPort int) (string, error) { - baseExpandFunc := func(key string) string { - if key == "PS" { - return os.Getenv("PS") - } - if key == portEnvVar { - return strconv.Itoa(defaultPort) - } - return "" - } - - expandFunc := func(key string) string { - return baseExpandFunc(key) - } - - if allowEnv { - debugMessage("Allowing getenv variable expansion") - expandFunc = func(key string) string { - value := os.Getenv(key) - if value == "" { - value = baseExpandFunc(key) - } - return value - } - } - - if envPath != "" { - b, err := ioutil.ReadFile(envPath) - if err != nil { - return "", err - } - - content := string(b) - env, err := godotenv.Unmarshal(content) - if err != nil { - return "", err - } - - debugMessage("Allowing .env variable expansion") - expandFunc = func(key string) string { - if val, ok := env[key]; ok { - return val - } - value := "" - if allowEnv { - value = os.Getenv(key) - } - if value == "" { - value = baseExpandFunc(key) - } - return value - } - } - - os.Setenv("PS", e.Name) - os.Setenv("EXPENV_PARENTHESIS", "$(") - s := strings.Replace(e.Command, "$(", "${EXPENV_PARENTHESIS}", -1) - return os.Expand(s, expandFunc), nil -} - -func checkCommand(entries []ProcfileEntry) bool { - if len(entries) == 0 { - fmt.Fprintf(os.Stderr, "no processes defined\n") - return false - } - - names := []string{} - for _, entry := range entries { - names = append(names, entry.Name) - } - - processNames := strings.Join(names[:], ", ") - fmt.Printf("valid procfile detected %v\n", processNames) - return true -} - -func existsCommand(entries []ProcfileEntry, processType string) bool { - for _, entry := range entries { - if processType == entry.Name { - return true - } - } - - fmt.Fprint(os.Stderr, "no matching process entry found\n") - return false -} - -func expandCommand(entries []ProcfileEntry, envPath string, allowGetenv bool, processType string, defaultPort int, delimiter string) bool { - hasErrors := false - var expandedEntries []ProcfileEntry - for _, entry := range entries { - command, err := expandEnv(entry, envPath, allowGetenv, defaultPort) - if err != nil { - fmt.Fprintf(os.Stderr, "error processing command: %s\n", err) - hasErrors = true - } - - entry.Command = command - expandedEntries = append(expandedEntries, entry) - } - - if hasErrors { - return false - } - - for _, entry := range expandedEntries { - if processType == "" || processType == entry.Name { - fmt.Printf("%v%v %v\n", entry.Name, delimiter, entry.Command) - } - } - - return true -} - -func exportCommand(entries []ProcfileEntry, app string, description string, envPath string, format string, formation string, group string, home string, limitCoredump string, limitCputime string, limitData string, limitFileSize string, limitLockedMemory string, limitOpenFiles string, limitUserProcesses string, limitPhysicalMemory string, limitStackSize string, location string, logPath string, nice string, prestart string, workingDirectoryPath string, runPath string, timeout int, processUser string, defaultPort int) bool { - if format == "" { - fmt.Fprintf(os.Stderr, "no format specified\n") - return false - } - if location == "" { - fmt.Fprintf(os.Stderr, "no output location specified\n") - return false - } - - formats := map[string]exportFunc{ - "launchd": exportLaunchd, - "runit": exportRunit, - "systemd": exportSystemd, - "systemd-user": exportSystemdUser, - "sysv": exportSysv, - "upstart": exportUpstart, - } - - if _, ok := formats[format]; !ok { - fmt.Fprintf(os.Stderr, "invalid format type: %s\n", format) - return false - } - - formations, err := parseFormation(formation) - if err != nil { - fmt.Fprintf(os.Stderr, "%s\n", err) - return false - } - - if processUser == "" { - processUser = app - } - - if group == "" { - group = app - } - - u, err := user.Current() - if err != nil { - fmt.Fprintf(os.Stderr, "%s\n", err) - return false - } - - if home == "" { - home = "/home/" + u.Username - } - - env := make(map[string]string) - if envPath != "" { - b, err := ioutil.ReadFile(envPath) - if err != nil { - fmt.Fprintf(os.Stderr, "error reading env file: %s\n", err) - return false - } - - content := string(b) - env, err = godotenv.Unmarshal(content) - if err != nil { - fmt.Fprintf(os.Stderr, "error parsing env file: %s\n", err) - return false - } - } - - vars := make(map[string]interface{}) - vars["app"] = app - vars["description"] = description - vars["env"] = env - vars["group"] = group - vars["home"] = home - vars["log"] = logPath - vars["location"] = location - vars["limit_coredump"] = limitCoredump - vars["limit_cputime"] = limitCputime - vars["limit_data"] = limitData - vars["limit_file_size"] = limitFileSize - vars["limit_locked_memory"] = limitLockedMemory - vars["limit_open_files"] = limitOpenFiles - vars["limit_user_processes"] = limitUserProcesses - vars["limit_physical_memory"] = limitPhysicalMemory - vars["limit_stack_size"] = limitStackSize - vars["nice"] = nice - vars["prestart"] = prestart - vars["working_directory"] = workingDirectoryPath - vars["timeout"] = strconv.Itoa(timeout) - vars["ulimit_shell"] = ulimitShell(limitCoredump, limitCputime, limitData, limitFileSize, limitLockedMemory, limitOpenFiles, limitUserProcesses, limitPhysicalMemory, limitStackSize) - vars["user"] = processUser - - if fn, ok := formats[format]; ok { - return fn(app, entries, formations, location, defaultPort, vars) - } - - return false -} - -func ulimitShell(limitCoredump string, limitCputime string, limitData string, limitFileSize string, limitLockedMemory string, limitOpenFiles string, limitUserProcesses string, limitPhysicalMemory string, limitStackSize string) string { - s := []string{} - if limitCoredump != "" { - s = append(s, "ulimit -c ${limit_coredump}") - } - if limitCputime != "" { - s = append(s, "ulimit -t ${limit_cputime}") - } - if limitData != "" { - s = append(s, "ulimit -d ${limit_data}") - } - if limitFileSize != "" { - s = append(s, "ulimit -f ${limit_file_size}") - } - if limitLockedMemory != "" { - s = append(s, "ulimit -l ${limit_locked_memory}") - } - if limitOpenFiles != "" { - s = append(s, "ulimit -n ${limit_open_files}") - } - if limitUserProcesses != "" { - s = append(s, "ulimit -u ${limit_user_processes}") - } - if limitPhysicalMemory != "" { - s = append(s, "ulimit -m ${limit_physical_memory}") - } - if limitStackSize != "" { - s = append(s, "ulimit -s ${limit_stack_size}") - } - - return strings.Join(s, "\n") -} - -func parseFormation(formation string) (map[string]FormationEntry, error) { - entries := make(map[string]FormationEntry) - for _, formation := range strings.Split(formation, ",") { - parts := strings.Split(formation, "=") - if len(parts) != 2 { - return entries, fmt.Errorf("invalid formation: %s", formation) - } - - i, err := strconv.Atoi(parts[1]) - if err != nil { - return entries, fmt.Errorf("invalid formation: %s", err) - } - - entries[parts[0]] = FormationEntry{ - Name: parts[0], - Count: i, - } - } - - return entries, nil -} - -func deleteCommand(entries []ProcfileEntry, processType string, writePath string, stdout bool, delimiter string, path string) bool { - var validEntries []ProcfileEntry - for _, entry := range entries { - if processType == entry.Name { - continue - } - validEntries = append(validEntries, entry) - } - - return outputProcfile(path, writePath, delimiter, stdout, validEntries) -} - -func listCommand(entries []ProcfileEntry) bool { - for _, entry := range entries { - fmt.Printf("%v\n", entry.Name) - } - return true -} - -func setCommand(entries []ProcfileEntry, processType string, command string, writePath string, stdout bool, delimiter string, path string) bool { - var validEntries []ProcfileEntry - validEntries = append(validEntries, ProcfileEntry{processType, command}) - for _, entry := range entries { - if processType == entry.Name { - continue - } - validEntries = append(validEntries, entry) - } - - return outputProcfile(path, writePath, delimiter, stdout, validEntries) -} - -func showCommand(entries []ProcfileEntry, envPath string, allowGetenv bool, processType string, defaultPort int) bool { - var foundEntry ProcfileEntry - for _, entry := range entries { - if processType == entry.Name { - foundEntry = entry - break - } - } - - if foundEntry == (ProcfileEntry{}) { - fmt.Fprintf(os.Stderr, "no matching process entry found\n") - return false - } - - command, err := expandEnv(foundEntry, envPath, allowGetenv, defaultPort) - if err != nil { - fmt.Fprintf(os.Stderr, "error processing command: %s\n", err) - return false - } - - fmt.Printf("%v\n", command) - return true + return procfile.ParseProcfile(text, delimiter, strict) } func main() { parser := argparse.NewParser("procfile-util", "A procfile parsing tool") - loglevelFlag := parser.Selector("l", "loglevel", []string{"info", "debug"}, &argparse.Options{Default: "info", Help: "loglevel to use"}) procfileFlag := parser.String("P", "procfile", &argparse.Options{Default: "Procfile", Help: "path to a procfile"}) delimiterFlag := parser.String("D", "delimiter", &argparse.Options{Default: ":", Help: "delimiter in use within procfile"}) defaultPortFlag := parser.String("d", "default-port", &argparse.Options{Default: "5000", Help: "default port to use"}) @@ -637,7 +98,6 @@ func main() { return } - Loglevel = *loglevelFlag entries, err := parseProcfile(*procfileFlag, *delimiterFlag, *strictFlag) if err != nil { fmt.Fprintf(os.Stderr, "%s\n", err) @@ -658,21 +118,21 @@ func main() { success := false if checkCmd.Happened() { - success = checkCommand(entries) + success = commands.CheckCommand(entries) } else if deleteCmd.Happened() { - success = deleteCommand(entries, *processTypeDeleteFlag, *writePathDeleteFlag, *stdoutDeleteFlag, *delimiterFlag, *procfileFlag) + success = commands.DeleteCommand(entries, *processTypeDeleteFlag, *writePathDeleteFlag, *stdoutDeleteFlag, *delimiterFlag, *procfileFlag) } else if existsCmd.Happened() { - success = existsCommand(entries, *processTypeExistsFlag) + success = commands.ExistsCommand(entries, *processTypeExistsFlag) } else if expandCmd.Happened() { - success = expandCommand(entries, *envPathExpandFlag, *allowGetenvExpandFlag, *processTypeExpandFlag, defaultPort, *delimiterFlag) + success = commands.ExpandCommand(entries, *envPathExpandFlag, *allowGetenvExpandFlag, *processTypeExpandFlag, defaultPort, *delimiterFlag) } else if exportCmd.Happened() { - success = exportCommand(entries, *appExportFlag, *descriptionExportFlag, *envPathExportFlag, *formatExportFlag, *formationExportFlag, *groupExportFlag, *homeExportFlag, *limitCoredumpExportFlag, *limitCputimeExportFlag, *limitDataExportFlag, *limitFileSizeExportFlag, *limitLockedMemoryExportFlag, *limitOpenFilesExportFlag, *limitUserProcessesExportFlag, *limitPhysicalMemoryExportFlag, *limitStackSizeExportFlag, *locationExportFlag, *logPathExportFlag, *niceExportFlag, *prestartExportFlag, *workingDirectoryPathExportFlag, *runExportFlag, *timeoutExportFlag, *userExportFlag, defaultPort) + success = commands.ExportCommand(entries, *appExportFlag, *descriptionExportFlag, *envPathExportFlag, *formatExportFlag, *formationExportFlag, *groupExportFlag, *homeExportFlag, *limitCoredumpExportFlag, *limitCputimeExportFlag, *limitDataExportFlag, *limitFileSizeExportFlag, *limitLockedMemoryExportFlag, *limitOpenFilesExportFlag, *limitUserProcessesExportFlag, *limitPhysicalMemoryExportFlag, *limitStackSizeExportFlag, *locationExportFlag, *logPathExportFlag, *niceExportFlag, *prestartExportFlag, *workingDirectoryPathExportFlag, *runExportFlag, *timeoutExportFlag, *userExportFlag, defaultPort) } else if listCmd.Happened() { - success = listCommand(entries) + success = commands.ListCommand(entries) } else if setCmd.Happened() { - success = setCommand(entries, *processTypeSetFlag, *commandSetFlag, *writePathSetFlag, *stdoutSetFlag, *delimiterFlag, *procfileFlag) + success = commands.SetCommand(entries, *processTypeSetFlag, *commandSetFlag, *writePathSetFlag, *stdoutSetFlag, *delimiterFlag, *procfileFlag) } else if showCmd.Happened() { - success = showCommand(entries, *envPathShowFlag, *allowGetenvShowFlag, *processTypeShowFlag, defaultPort) + success = commands.ShowCommand(entries, *envPathShowFlag, *allowGetenvShowFlag, *processTypeShowFlag, defaultPort) } else { fmt.Print(parser.Usage(err)) } diff --git a/procfile/entry.go b/procfile/entry.go new file mode 100644 index 0000000..a1f849e --- /dev/null +++ b/procfile/entry.go @@ -0,0 +1,35 @@ +package procfile + +import ( + "strings" + + "gopkg.in/alessio/shellescape.v1" +) + +// ProcfileEntry is a struct containing a process type and the corresponding command +type ProcfileEntry struct { + Name string + Command string +} + +func (p *ProcfileEntry) CommandList() []string { + return strings.Fields(p.Command) +} + +func (p *ProcfileEntry) Program() string { + return strings.Fields(p.Command)[0] +} + +func (p *ProcfileEntry) Args() string { + return strings.Join(strings.Fields(p.Command)[1:], " ") +} + +func (p *ProcfileEntry) ArgsEscaped() string { + return shellescape.Quote(p.Args()) +} + +// FormationEntry is a struct containing a process type and the corresponding count +type FormationEntry struct { + Name string + Count int +} diff --git a/procfile/io.go b/procfile/io.go new file mode 100644 index 0000000..03d0dc6 --- /dev/null +++ b/procfile/io.go @@ -0,0 +1,79 @@ +package procfile + +import ( + "bufio" + "fmt" + "io/ioutil" + "os" + "sort" + "strings" + + "github.com/andrew-d/go-termutil" +) + +// GetProcfileContent returns the content at a path as a string +func GetProcfileContent(path string) (string, error) { + f, err := os.Open(path) + if err != nil { + if !termutil.Isatty(os.Stdin.Fd()) { + bytes, err := ioutil.ReadAll(os.Stdin) + if err != nil { + return "", err + } + return string(bytes), nil + } + return "", err + } + defer f.Close() + + lines := []string{} + scanner := bufio.NewScanner(f) + for scanner.Scan() { + lines = append(lines, scanner.Text()) + } + err = scanner.Err() + return strings.Join(lines, "\n"), err +} + +func OutputProcfile(path string, writePath string, delimiter string, stdout bool, entries []ProcfileEntry) bool { + if writePath != "" && stdout { + fmt.Fprintf(os.Stderr, "cannot specify both --stdout and --write-path flags\n") + return false + } + + sort.Slice(entries, func(i, j int) bool { + return entries[i].Name < entries[j].Name + }) + + if stdout { + for _, entry := range entries { + fmt.Printf("%v%v %v\n", entry.Name, delimiter, entry.Command) + } + return true + } + + if writePath != "" { + path = writePath + } + + if err := writeProcfile(path, delimiter, entries); err != nil { + fmt.Fprintf(os.Stderr, "error writing procfile: %s\n", err) + return false + } + + return true +} + +func writeProcfile(path string, delimiter string, entries []ProcfileEntry) error { + file, err := os.Create(path) + if err != nil { + return err + } + defer file.Close() + + w := bufio.NewWriter(file) + for _, entry := range entries { + fmt.Fprintln(w, fmt.Sprintf("%v%v %v", entry.Name, delimiter, entry.Command)) + } + return w.Flush() +} diff --git a/procfile/parse.go b/procfile/parse.go new file mode 100644 index 0000000..72f8c15 --- /dev/null +++ b/procfile/parse.go @@ -0,0 +1,120 @@ +package procfile + +import ( + "bufio" + "fmt" + "regexp" + "sort" + "strconv" + "strings" +) + +func ParseFormation(formation string) (map[string]FormationEntry, error) { + entries := make(map[string]FormationEntry) + for _, formation := range strings.Split(formation, ",") { + parts := strings.Split(formation, "=") + if len(parts) != 2 { + return entries, fmt.Errorf("invalid formation: %s", formation) + } + + i, err := strconv.Atoi(parts[1]) + if err != nil { + return entries, fmt.Errorf("invalid formation: %s", err) + } + + entries[parts[0]] = FormationEntry{ + Name: parts[0], + Count: i, + } + } + + return entries, nil +} + +// ParseProcfile parses text as a procfile and returns a list of procfile entries +func ParseProcfile(text string, delimiter string, strict bool) ([]ProcfileEntry, error) { + var entries []ProcfileEntry + reCmd, _ := regexp.Compile(`^([a-z0-9]([-a-z0-9]*[a-z0-9])?)` + delimiter + `\s*(.+)$`) + reOldCmd, _ := regexp.Compile(`^([A-Za-z0-9_-]+)` + delimiter + `\s*(.+)$`) + + reComment, _ := regexp.Compile(`^(.*)\s#.+$`) + + lineNumber := 0 + names := make(map[string]bool) + scanner := bufio.NewScanner(strings.NewReader(text)) + for scanner.Scan() { + lineNumber++ + line := scanner.Text() + line = strings.TrimSpace(line) + + if len(line) == 0 { + continue + } + + oldParams := reOldCmd.FindStringSubmatch(line) + params := reCmd.FindStringSubmatch(line) + isCommand := len(params) == 4 + isOldCommand := len(oldParams) == 3 + isComment := strings.HasPrefix(line, "#") + if isComment { + continue + } + + if !isCommand { + if !isOldCommand { + return entries, fmt.Errorf("invalid line in procfile, line %d", lineNumber) + } + + if strict { + return entries, fmt.Errorf("process name contains invalid characters, line %d", lineNumber) + } + } + + name := "" + cmd := "" + if strict { + name, cmd = params[1], params[3] + } else { + name, cmd = oldParams[1], oldParams[2] + } + + if len(name) > 63 { + return entries, fmt.Errorf("process name over 63 characters, line %d", lineNumber) + } + + if names[name] { + return entries, fmt.Errorf("process names must be unique, line %d", lineNumber) + } + names[name] = true + + commentParams := reComment.FindStringSubmatch(cmd) + if len(commentParams) == 2 { + cmd = commentParams[1] + } + + cmd = strings.TrimSpace(cmd) + if strings.HasPrefix(cmd, "#") { + return entries, fmt.Errorf("comment specified in place of command, line %d", lineNumber) + } + + if len(cmd) == 0 { + return entries, fmt.Errorf("no command specified, line %d", lineNumber) + } + + entries = append(entries, ProcfileEntry{name, cmd}) + } + + if scanner.Err() != nil { + return entries, scanner.Err() + } + + if len(entries) == 0 { + return entries, fmt.Errorf("no entries found in Procfile") + } + + sort.Slice(entries, func(i, j int) bool { + return entries[i].Name < entries[j].Name + }) + + return entries, nil +} diff --git a/test.bats b/test.bats new file mode 100644 index 0000000..6245fcd --- /dev/null +++ b/test.bats @@ -0,0 +1,56 @@ +#!/usr/bin/env bats + +export SYSTEM_NAME="$(uname -s | tr '[:upper:]' '[:lower:]')" +export PROCFILE_BIN="build/$SYSTEM_NAME/procfile-util" + +setup_file() { + make prebuild $PROCFILE_BIN +} + +teardown_file() { + make clean +} + +@test "[lax] comments" { + run $PROCFILE_BIN check -P fixtures/comments.Procfile + [[ "$status" -eq 0 ]] + [[ "$output" == "valid procfile detected 2custom, cron, custom, release, web, wor-ker" ]] +} + +@test "[lax] multiple" { + run $PROCFILE_BIN check -P fixtures/multiple.Procfile + [[ "$status" -eq 0 ]] + [[ "$output" == "valid procfile detected release, web, webpacker, worker" ]] +} + +@test "[lax] port" { + run $PROCFILE_BIN check -P fixtures/port.Procfile + [[ "$status" -eq 0 ]] + [[ "$output" == "valid procfile detected web, worker" ]] + + run $PROCFILE_BIN show -P fixtures/port.Procfile -p web + [[ "$status" -eq 0 ]] + [[ "$output" == "node web.js --port 5000" ]] +} + +@test "[strict] comments" { + run $PROCFILE_BIN check -S -P fixtures/comments.Procfile + [[ "$status" -eq 0 ]] + [[ "$output" == "valid procfile detected 2custom, cron, custom, release, web, wor-ker" ]] +} + +@test "[strict] multiple" { + run $PROCFILE_BIN check -S -P fixtures/multiple.Procfile + [[ "$status" -eq 0 ]] + [[ "$output" == "valid procfile detected release, web, webpacker, worker" ]] +} + +@test "[strict] port" { + run $PROCFILE_BIN check -S -P fixtures/port.Procfile + [[ "$status" -eq 0 ]] + [[ "$output" == "valid procfile detected web, worker" ]] + + run $PROCFILE_BIN show -S -P fixtures/port.Procfile -p web + [[ "$status" -eq 0 ]] + [[ "$output" == "node web.js --port 5000" ]] +}