diff --git a/.circleci/config.yml b/.circleci/config.yml index 23568c8..7be2716 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -18,17 +18,13 @@ jobs: command: | make build-in-docker [[ -d build ]] && sudo chown -R circleci:circleci build - - run: - command: | - ls -lah - make store-artifacts - run: make validate-in-docker - store_artifacts: - path: /tmp/artifacts + path: build destination: build - run: command: | if [[ "$CIRCLE_BRANCH" == "release" ]]; then make release-in-docker fi - make release-packagecloud-in-docker + make release-packagecloud-in-docker || true diff --git a/.gitignore b/.gitignore index 2e32d37..9417d10 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,5 @@ validation/* # .env files .env* + +*Procfile diff --git a/Makefile b/Makefile index a5fbbe8..f3b71e9 100644 --- a/Makefile +++ b/Makefile @@ -5,16 +5,16 @@ MAINTAINER_NAME = Jose Diaz-Gonzalez REPOSITORY = go-procfile-util HARDWARE = $(shell uname -m) SYSTEM_NAME = $(shell uname -s | tr '[:upper:]' '[:lower:]') -BASE_VERSION ?= 0.6.0 +BASE_VERSION ?= 0.7.0 IMAGE_NAME ?= $(MAINTAINER)/$(REPOSITORY) PACKAGECLOUD_REPOSITORY ?= dokku/dokku-betafish ifeq ($(CIRCLE_BRANCH),release) VERSION ?= $(BASE_VERSION) - DOCKER_VERSION = $(VERSION) + DOCKER_IMAGE_VERSION = $(VERSION) else VERSION = $(shell echo "${BASE_VERSION}")build+$(shell git rev-parse --short HEAD) - DOCKER_VERSION = $(shell echo "${BASE_VERSION}")build-$(shell git rev-parse --short HEAD) + DOCKER_IMAGE_VERSION = $(shell echo "${BASE_VERSION}")build-$(shell git rev-parse --short HEAD) endif version: @@ -39,7 +39,7 @@ targets = $(addsuffix -in-docker, $(LIST)) @echo "GITHUB_ACCESS_TOKEN=$(GITHUB_ACCESS_TOKEN)" >> .env.docker @echo "IMAGE_NAME=$(IMAGE_NAME)" >> .env.docker @echo "PACKAGECLOUD_REPOSITORY=$(PACKAGECLOUD_REPOSITORY)" >> .env.docker - @echo "PACKAGECLOUD_TOKEN=$(PACKAGECLOUD_API_TOKEN)" >> .env.docker + @echo "PACKAGECLOUD_TOKEN=$(PACKAGECLOUD_TOKEN)" >> .env.docker @echo "VERSION=$(VERSION)" >> .env.docker build: @@ -83,7 +83,7 @@ build/deb/$(NAME)_$(VERSION)_amd64.deb: build/linux/$(NAME) --input-type dir \ --license 'MIT License' \ --maintainer "$(MAINTAINER_NAME) <$(EMAIL)>" \ - --name procfile-util \ + --name $(NAME) \ --output-type deb \ --package build/deb/$(NAME)_$(VERSION)_amd64.deb \ --url "https://github.com/$(MAINTAINER)/$(REPOSITORY)" \ @@ -103,7 +103,7 @@ build/rpm/$(NAME)-$(VERSION)-1.x86_64.rpm: build/linux/$(NAME) --input-type dir \ --license 'MIT License' \ --maintainer "$(MAINTAINER_NAME) <$(EMAIL)>" \ - --name procfile-util \ + --name $(NAME) \ --output-type rpm \ --package build/rpm/$(NAME)-$(VERSION)-1.x86_64.rpm \ --rpm-os linux \ @@ -122,21 +122,21 @@ circleci: rm -f ~/.gitconfig docker-image: - docker build --rm -q -f Dockerfile.hub -t $(IMAGE_NAME):$(DOCKER_VERSION) . + docker build --rm -q -f Dockerfile.hub -t $(IMAGE_NAME):$(DOCKER_IMAGE_VERSION) . -gh-release: - mkdir -p build - curl -o build/gh-release.tgz -sL https://github.com/progrium/gh-release/releases/download/v2.2.1/gh-release_2.2.1_$(SYSTEM_NAME)_$(HARDWARE).tgz - tar xf build/gh-release.tgz -C build - chmod +x build/gh-release +bin/gh-release: + mkdir -p bin + curl -o bin/gh-release.tgz -sL https://github.com/progrium/gh-release/releases/download/v2.2.1/gh-release_2.2.1_$(SYSTEM_NAME)_$(HARDWARE).tgz + tar xf bin/gh-release.tgz -C bin + chmod +x bin/gh-release -release: build gh-release +release: build bin/gh-release rm -rf release && mkdir release tar -zcf release/$(NAME)_$(VERSION)_linux_$(HARDWARE).tgz -C build/linux $(NAME) tar -zcf release/$(NAME)_$(VERSION)_darwin_$(HARDWARE).tgz -C build/darwin $(NAME) cp build/deb/$(NAME)_$(VERSION)_amd64.deb release/$(NAME)_$(VERSION)_amd64.deb cp build/rpm/$(NAME)-$(VERSION)-1.x86_64.rpm release/$(NAME)-$(VERSION)-1.x86_64.rpm - build/gh-release create $(MAINTAINER)/$(REPOSITORY) $(VERSION) $(shell git rev-parse --abbrev-ref HEAD) + bin/gh-release create $(MAINTAINER)/$(REPOSITORY) $(VERSION) $(shell git rev-parse --abbrev-ref HEAD) release-packagecloud: @$(MAKE) release-packagecloud-deb @@ -160,10 +160,6 @@ release-packagecloud-deb: build/deb/$(NAME)_$(VERSION)_amd64.deb release-packagecloud-rpm: build/rpm/$(NAME)-$(VERSION)-1.x86_64.rpm package_cloud push $(PACKAGECLOUD_REPOSITORY)/el/7 build/rpm/$(NAME)-$(VERSION)-1.x86_64.rpm -store-artifacts: build - mkdir -p /tmp/artifacts - cp -r build/* /tmp/artifacts - validate: mkdir -p validation lintian build/deb/$(NAME)_$(VERSION)_amd64.deb || true diff --git a/Procfile b/Procfile deleted file mode 100644 index febc276..0000000 --- a/Procfile +++ /dev/null @@ -1,5 +0,0 @@ -<<<<<<< HEAD=0 -web: python -p $PORT server -====== -worker: python work.py --derp $PS ->>>>>>> 5bb7ec3e2a8f4f6565432a4fc82c92d4a3603d28=0 diff --git a/README.md b/README.md index 6f3dfe0..bae8a21 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,23 @@ All commands take a `-P` or `--procfile` flag to specify an alternative `Procfil procfile-util check ``` +### delete + +> delete a process type from a procfile + +This command does not retain comments or extra newline characters. Specifying both the `write-path` and `stdout` flags will result in an error. + +```shell +# delete the web process and write the file +procfile-util delete --process web + +# delete the web process and write output to other.Procfile +procfile-util delete --process web --write-path other.Procfile + +# delete the web process and write output to stdout +procfile-util delete --process web --stdout +``` + ### exists > check if a process type exists @@ -57,6 +74,7 @@ procfile-util expand --allow-getenv --env-file .env # specify the default-port when performing variable expansion procfile-util expand --allow-getenv --env-file .env --default-port 3000 ``` + ### list > list all process types in a procfile @@ -65,6 +83,23 @@ procfile-util expand --allow-getenv --env-file .env --default-port 3000 procfile-util list ``` +### set + +> set the command for a process type in a procfile + +This command does not retain comments or extra newline characters. Specifying both the `write-path` and `stdout` flags will result in an error. + +```shell +# set the web process and write the file +procfile-util set --process web --command "python app.py -p $PORT" + +# set the web process and write output to other.Procfile +procfile-util set --process web --command "python app.py -p $PORT" --write-path other.Procfile + +# set the web process and write output to stdout +procfile-util set --process web --command "python app.py -p $PORT" --stdout +``` + ### show > show the command for a specific process type diff --git a/main.go b/main.go index a827eff..bb2cc25 100644 --- a/main.go +++ b/main.go @@ -6,6 +6,7 @@ import ( "io/ioutil" "os" "regexp" + "sort" "strings" "github.com/akamensky/argparse" @@ -70,6 +71,50 @@ func getProcfile(path string) (string, error) { 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) ([]procfileEntry, error) { var entries []procfileEntry reCmd, _ := regexp.Compile(`^([A-Za-z0-9_-]+)` + delimiter + `\s*(.+)$`) @@ -117,6 +162,10 @@ func parseProcfile(path string, delimiter string) ([]procfileEntry, error) { 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 } @@ -207,9 +256,9 @@ func existsCommand(entries []procfileEntry, processType string) bool { return false } -func expandCommand(entries []procfileEntry, envPath string, allowGetenv bool, processType string, defaultPort string) bool { +func expandCommand(entries []procfileEntry, envPath string, allowGetenv bool, processType string, defaultPort string, delimiter string) bool { hasErrors := false - commands := make(map[string]string) + var expandedEntries []procfileEntry for _, entry := range entries { command, err := expandEnv(entry, envPath, allowGetenv, defaultPort) if err != nil { @@ -217,21 +266,35 @@ func expandCommand(entries []procfileEntry, envPath string, allowGetenv bool, pr hasErrors = true } - commands[entry.Name] = command + entry.Command = command + expandedEntries = append(expandedEntries, entry) } if hasErrors { return false } - for k, v := range commands { - if processType == "" || processType == k { - fmt.Printf("%v: %v\n", k, v) + for _, entry := range expandedEntries { + if processType == "" || processType == entry.Name { + fmt.Printf("%v%v %v\n", entry.Name, delimiter, entry.Command) } } + return true } +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) @@ -239,6 +302,19 @@ func listCommand(entries []procfileEntry) bool { 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 string) bool { var foundEntry procfileEntry for _, entry := range entries { @@ -268,8 +344,15 @@ func main() { 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"}) - versionFlag := parser.Flag("v", "version", &argparse.Options{Help: "show version"}) defaultPortFlag := parser.String("d", "default-port", &argparse.Options{Default: "5000", Help: "default port to use"}) + versionFlag := parser.Flag("v", "version", &argparse.Options{Help: "show version"}) + + checkCmd := parser.NewCommand("check", "check that the specified procfile is valid") + + deleteCmd := parser.NewCommand("delete", "delete a process type from a file") + processTypeDeleteFlag := deleteCmd.String("p", "process-type", &argparse.Options{Help: "name of process to delete", Required: true}) + stdoutDeleteFlag := deleteCmd.Flag("s", "stdout", &argparse.Options{Help: "write output to stdout"}) + writePathDeleteFlag := deleteCmd.String("w", "write-path", &argparse.Options{Help: "path to Procfile to write to"}) existsCmd := parser.NewCommand("exists", "check if a process type exists") processTypeExistsFlag := existsCmd.String("p", "process-type", &argparse.Options{Help: "name of process to retrieve"}) @@ -277,16 +360,20 @@ func main() { expandCmd := parser.NewCommand("expand", "expands a procfile against a specific environment") allowGetenvExpandFlag := expandCmd.Flag("a", "allow-getenv", &argparse.Options{Help: "allow the use of the existing env when expanding commands"}) envPathExpandFlag := expandCmd.String("e", "env-file", &argparse.Options{Help: "path to a dotenv file"}) - processTypeExpandFlag := expandCmd.String("p", "process-type", &argparse.Options{Help: "name of process to retrieve"}) + processTypeExpandFlag := expandCmd.String("p", "process-type", &argparse.Options{Help: "name of process to expand"}) listCmd := parser.NewCommand("list", "list all process types in a procfile") - checkCmd := parser.NewCommand("check", "check that the specified procfile is valid") + setCmd := parser.NewCommand("set", "set the command for a process type in a file") + processTypeSetFlag := setCmd.String("p", "process-type", &argparse.Options{Help: "name of process to set", Required: true}) + commandSetFlag := setCmd.String("c", "command", &argparse.Options{Help: "command to set", Required: true}) + stdoutSetFlag := setCmd.Flag("s", "stdout", &argparse.Options{Help: "write output to stdout"}) + writePathSetFlag := setCmd.String("w", "write-path", &argparse.Options{Help: "path to Procfile to write to"}) showCmd := parser.NewCommand("show", "show the command for a specific process type") allowGetenvShowFlag := showCmd.Flag("a", "allow-getenv", &argparse.Options{Help: "allow the use of the existing env when expanding commands"}) envPathShowFlag := showCmd.String("e", "env-file", &argparse.Options{Help: "path to a dotenv file"}) - processTypeShowFlag := showCmd.String("p", "process-type", &argparse.Options{Help: "name of process to retrieve", Required: true}) + processTypeShowFlag := showCmd.String("p", "process-type", &argparse.Options{Help: "name of process to show", Required: true}) err := parser.Parse(os.Args) if err != nil { @@ -313,12 +400,16 @@ func main() { success := false if checkCmd.Happened() { success = checkCommand(entries) + } else if deleteCmd.Happened() { + success = deleteCommand(entries, *processTypeDeleteFlag, *writePathDeleteFlag, *stdoutDeleteFlag, *delimiterFlag, *procfileFlag) } else if existsCmd.Happened() { success = existsCommand(entries, *processTypeExistsFlag) } else if expandCmd.Happened() { - success = expandCommand(entries, *envPathExpandFlag, *allowGetenvExpandFlag, *processTypeExpandFlag, *defaultPortFlag) + success = expandCommand(entries, *envPathExpandFlag, *allowGetenvExpandFlag, *processTypeExpandFlag, *defaultPortFlag, *delimiterFlag) } else if listCmd.Happened() { success = listCommand(entries) + } else if setCmd.Happened() { + success = setCommand(entries, *processTypeSetFlag, *commandSetFlag, *writePathSetFlag, *stdoutSetFlag, *delimiterFlag, *procfileFlag) } else if showCmd.Happened() { success = showCommand(entries, *envPathShowFlag, *allowGetenvShowFlag, *processTypeShowFlag, *defaultPortFlag) } else { diff --git a/test.Procfile b/test.Procfile deleted file mode 100644 index 05ad478..0000000 --- a/test.Procfile +++ /dev/null @@ -1,57 +0,0 @@ -############################### -# 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