diff --git a/.travis.yml b/.travis.yml index 0732d7c..82d869c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -26,5 +26,6 @@ before_install: script: - make test - - make bin + - make all - ./init-exporter --version + - ./init-exporter-converter --version diff --git a/Makefile b/Makefile index 723defa..7b6cc43 100644 --- a/Makefile +++ b/Makefile @@ -5,41 +5,47 @@ PREFIX?=/usr ######################################################################################## -.PHONY = all clean install uninstall deps deps-glide test +.PHONY = all clean install uninstall deps deps-glide test upstart-playground systemd-playground ######################################################################################## -all: bin +all: init-exporter init-exporter-converter + +init-exporter: + go build init-exporter.go + +init-exporter-converter: + go build init-exporter-converter.go deps: - go get -v pkg.re/check.v1 - go get -v pkg.re/essentialkaos/ek.v7 - go get -v pkg.re/essentialkaos/go-simpleyaml.v1 - go get -v pkg.re/yaml.v2 + go get -d -v pkg.re/check.v1 + go get -d -v pkg.re/essentialkaos/ek.v7 + go get -d -v pkg.re/essentialkaos/go-simpleyaml.v1 + go get -d -v pkg.re/yaml.v2 deps-glide: glide install -bin: - go build init-exporter.go - fmt: find . -name "*.go" -exec gofmt -s -w {} \; test: - go test ./... -covermode=count + go test ./procfile ./export -covermode=count install: mkdir -p $(DESTDIR)$(PREFIX)/bin - cp init-exporter $(DESTDIR)$(PREFIX)/sbin/ + cp init-exporter $(DESTDIR)$(PREFIX)/bin/ + cp init-exporter-converter $(DESTDIR)$(PREFIX)/bin/ cp common/init-exporter.conf $(DESTDIR)/etc/ uninstall: - rm -f $(DESTDIR)$(PREFIX)/sbin/init-exporter + rm -f $(DESTDIR)$(PREFIX)/bin/init-exporter + rm -f $(DESTDIR)$(PREFIX)/bin/init-exporter-converter rm -rf $(DESTDIR)/etc/init-exporter.conf clean: rm -f init-exporter + rm -f init-exporter-converter upstart-playground: docker build -f ./Dockerfile.upstart -t upstart-playground . && docker run -ti --rm=true upstart-playground /bin/bash diff --git a/cli/cli.go b/cli/cli.go index 7fe67b7..e5192f1 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -30,11 +30,11 @@ import ( // App props const ( APP = "init-exporter" - VER = "0.8.0" + VER = "0.9.0" DESC = "Utility for exporting services described by Procfile to init system" ) -// Supported arguments list +// Supported arguments const ( ARG_PROCFILE = "p:procfile" ARG_APP_NAME = "n:appname" @@ -46,11 +46,13 @@ const ( ARG_VERSION = "v:version" ) -// Config properies list +// Config properies const ( MAIN_RUN_USER = "main:run-user" MAIN_RUN_GROUP = "main:run-group" MAIN_PREFIX = "main:prefix" + PROCFILE_VERSION1 = "procfile:version1" + PROCFILE_VERSION2 = "procfile:version2" PATHS_WORKING_DIR = "paths:working-dir" PATHS_HELPER_DIR = "paths:helper-dir" PATHS_SYSTEMD_DIR = "paths:systemd-dir" @@ -150,12 +152,12 @@ func checkForRoot() { user, err = system.CurrentUser() if err != nil { - fmt.Println(err.Error()) + printError(err.Error()) os.Exit(1) } if !user.IsRoot() { - fmt.Println("This utility must have superuser privileges (root)") + printError("This utility must have superuser privileges (root)") os.Exit(1) } } @@ -272,10 +274,10 @@ func validateConfig() { errs := knf.Validate(validators) if len(errs) != 0 { - fmt.Println("Errors while config validation:") + printError("Errors while config validation:") for _, err := range errs { - fmt.Printf(" %v\n", err) + printError(" %v\n", err) } os.Exit(1) @@ -326,6 +328,16 @@ func installApplication(appName string) { printErrorAndExit(err.Error()) } + if app.ProcVersion == 1 && !knf.GetB(PROCFILE_VERSION1, true) { + printError("Proc format version 1 support is disabled") + os.Exit(1) + } + + if app.ProcVersion == 2 && !knf.GetB(PROCFILE_VERSION2, true) { + printError("Proc format version 2 support is disabled") + os.Exit(1) + } + if arg.GetB(ARG_DRY_START) { os.Exit(0) } @@ -412,10 +424,20 @@ func detectProvider(format string) (string, error) { } } +// printError prints error message to console +func printError(f string, a ...interface{}) { + fmtc.Fprintf(os.Stderr, "{r}"+f+"{!}\n", a...) +} + +// printError prints warning message to console +func printWarn(f string, a ...interface{}) { + fmtc.Fprintf(os.Stderr, "{y}"+f+"{!}\n", a...) +} + // printErrorAndExit print error mesage and exit with exit code 1 -func printErrorAndExit(message string, a ...interface{}) { - log.Crit(message) - fmt.Printf(message+"\n", a...) +func printErrorAndExit(f string, a ...interface{}) { + log.Crit(f, a...) + printError(f, a...) os.Exit(1) } diff --git a/common/init-exporter.conf b/common/init-exporter.conf index f6f5027..e443975 100644 --- a/common/init-exporter.conf +++ b/common/init-exporter.conf @@ -11,6 +11,14 @@ # Prefix used for exported units and helpers prefix: fb- +[procfile] + + # Enable/disable support of version 1 proc files + version1: false + + # Enable/disable support of version 2 proc files + version2: true + [paths] # Working dir diff --git a/common/init-exporter.spec b/common/init-exporter.spec index 38a10f7..f8b45f7 100644 --- a/common/init-exporter.spec +++ b/common/init-exporter.spec @@ -42,7 +42,7 @@ Summary: Utility for exporting services described by Procfile to init system Name: init-exporter -Version: 0.8.0 +Version: 0.9.0 Release: 0%{?dist} Group: Development/Tools License: MIT @@ -70,8 +70,14 @@ Utility for exporting services described by Procfile to init system. %setup -q %build -export GOPATH=$(pwd) +export GOPATH=$(pwd) + +pushd src/github.com/funbox/%{name} + %{__make} %{?_smp_mflags} +popd + go build -o %{name} src/github.com/funbox/%{name}/%{name}.go +go build -o %{name}-converter src/github.com/funbox/%{name}/%{name}-converter.go %install rm -rf %{buildroot} @@ -82,7 +88,11 @@ install -dm 755 %{buildroot}%{_logdir}/%{name} install -dm 755 %{buildroot}%{_loc_prefix}/%{name} install -dm 755 %{buildroot}%{_localstatedir}/local/%{name}/helpers -install -pm 755 %{name} %{buildroot}%{_bindir}/ +install -pm 755 src/github.com/funbox/%{name}/%{name} \ + %{buildroot}%{_bindir}/ + +install -pm 755 src/github.com/funbox/%{name}/%{name}-converter \ + %{buildroot}%{_bindir}/ ln -sf %{_bindir}/%{name} %{buildroot}%{_bindir}/upstart-export ln -sf %{_bindir}/%{name} %{buildroot}%{_bindir}/systemd-export @@ -101,12 +111,18 @@ rm -rf %{buildroot} %dir %{_logdir}/%{name} %dir %{_localstatedir}/local/%{name}/helpers %{_bindir}/init-exporter +%{_bindir}/init-exporter-converter %{_bindir}/upstart-export %{_bindir}/systemd-export ############################################################################### %changelog +* Fri Mar 31 2017 Anton Novojilov - 0.9.0-0 +- Format support configuration feature +- Pre and post commands support +- Added format converter + * Thu Mar 09 2017 Anton Novojilov - 0.8.0-0 - ek package updated to v7 diff --git a/converter/converter.go b/converter/converter.go new file mode 100644 index 0000000..f018127 --- /dev/null +++ b/converter/converter.go @@ -0,0 +1,274 @@ +package converter + +// ////////////////////////////////////////////////////////////////////////////////// // +// // +// Copyright (c) 2006-2017 FB GROUP LLC // +// // +// ////////////////////////////////////////////////////////////////////////////////// // + +import ( + "bytes" + "io/ioutil" + "os" + "runtime" + "text/template" + + "pkg.re/essentialkaos/ek.v7/arg" + "pkg.re/essentialkaos/ek.v7/fmtc" + "pkg.re/essentialkaos/ek.v7/knf" + "pkg.re/essentialkaos/ek.v7/usage" + + "github.com/funbox/init-exporter/procfile" +) + +// ////////////////////////////////////////////////////////////////////////////////// // + +// App props +const ( + APP = "init-exporter-converter" + VER = "0.1.0" + DESC = "Utility for converting procfiles from v1 to v2 format" +) + +// ////////////////////////////////////////////////////////////////////////////////// // + +// Supported arguments +const ( + ARG_CONFIG = "c:config" + ARG_APP_NAME = "n:appname" + ARG_IN_PLACE = "i:in-place" + ARG_NO_COLORS = "nc:no-colors" + ARG_HELP = "h:help" + ARG_VERSION = "v:version" +) + +// Config properies +const ( + MAIN_PREFIX = "main:prefix" + PATHS_WORKING_DIR = "paths:working-dir" + DEFAULTS_NPROC = "defaults:nproc" + DEFAULTS_NOFILE = "defaults:nofile" + DEFAULTS_RESPAWN = "defaults:respawn" + DEFAULTS_RESPAWN_COUNT = "defaults:respawn-count" + DEFAULTS_RESPAWN_INTERVAL = "defaults:respawn-interval" + DEFAULTS_KILL_TIMEOUT = "defaults:kill-timeout" +) + +// ////////////////////////////////////////////////////////////////////////////////// // + +// PROCFILE_TEMPLATE is template used for generation v2 Procfile +const PROCFILE_TEMPLATE = `version: 2 + +start_on_runlevel: 2 +stop_on_runlevel: 5 + +{{ if .Config.IsRespawnEnabled -}} +respawn: + count: {{ .Config.RespawnCount }} + interval: {{ .Config.RespawnInterval }} + +{{ end -}} + +limits: + nofile: {{ .Config.LimitFile }} + nproc: {{ .Config.LimitProc }} + +working_directory: {{ .Config.WorkingDir }} + +commands: +{{- range .Application.Services }} + {{ .Name }}: + {{- if .HasPreCmd }}pre: {{ .PreCmd }}{{ end }} + command: {{ .Cmd }} + {{- if .HasPostCmd }}pre: {{ .PostCmd }}{{ end }} + {{- if .Options.IsCustomLogEnabled }}log: {{ .Options.LogPath }}{{ end }} + {{- if .Options.IsEnvSet}} + env: + {{- range $k, $v := .Options.Env }} + {{ $k }}: {{ $v -}} + {{ end -}} + {{ end }} +{{ end -}} +` + +// ////////////////////////////////////////////////////////////////////////////////// // + +type templateData struct { + Config *procfile.Config + Application *procfile.Application +} + +// ////////////////////////////////////////////////////////////////////////////////// // + +var argMap = arg.Map{ + ARG_CONFIG: {}, + ARG_APP_NAME: {}, + ARG_IN_PLACE: {Type: arg.BOOL}, + ARG_NO_COLORS: {Type: arg.BOOL}, + ARG_HELP: {Type: arg.BOOL}, + ARG_VERSION: {Type: arg.BOOL}, +} + +// ////////////////////////////////////////////////////////////////////////////////// // + +func Init() { + runtime.GOMAXPROCS(1) + + args, errs := arg.Parse(argMap) + + if len(errs) != 0 { + fmtc.Println("Error while arguments parsing:") + + for _, err := range errs { + fmtc.Printf(" %v\n", err) + } + + os.Exit(1) + } + + if arg.GetB(ARG_NO_COLORS) { + fmtc.DisableColors = true + } + + if arg.GetB(ARG_VERSION) { + showAbout() + return + } + + if arg.GetB(ARG_HELP) || len(args) == 0 { + showUsage() + return + } + + process(args[0]) +} + +// process start data processing +func process(file string) { + var err error + + if !arg.Has(ARG_APP_NAME) { + printErrorAndExit("Application name must be defined through -n/--appname argument") + } + + if arg.Has(ARG_CONFIG) { + err = knf.Global(arg.GetS(ARG_CONFIG)) + + if err != nil { + printErrorAndExit(err.Error()) + } + } + + err = convert(file) + + if err != nil { + printErrorAndExit(err.Error()) + } +} + +// convert read procfile in v1 format and print v2 data or save it to file +func convert(file string) error { + fullAppName := knf.GetS(MAIN_PREFIX, "") + arg.GetS(ARG_APP_NAME) + + config := &procfile.Config{ + Name: fullAppName, + WorkingDir: knf.GetS(PATHS_WORKING_DIR, "/tmp"), + IsRespawnEnabled: knf.GetB(DEFAULTS_RESPAWN, true), + RespawnInterval: knf.GetI(DEFAULTS_RESPAWN_INTERVAL, 15), + RespawnCount: knf.GetI(DEFAULTS_RESPAWN_COUNT, 10), + KillTimeout: knf.GetI(DEFAULTS_KILL_TIMEOUT, 60), + LimitFile: knf.GetI(DEFAULTS_NOFILE, 10240), + LimitProc: knf.GetI(DEFAULTS_NPROC, 10240), + } + + app, err := procfile.Read(file, config) + + if err != nil { + return err + } + + if app.ProcVersion != 1 { + printErrorAndExit("Given procfile already converted to v2 format.") + } + + v2data, err := renderTemplate("proc_v2", PROCFILE_TEMPLATE, &templateData{config, app}) + + if err != nil { + return err + } + + if !arg.GetB(ARG_IN_PLACE) { + fmtc.Println(v2data) + return nil + } + + return writeData(file, v2data) +} + +// renderTemplate renders template data +func renderTemplate(name, templateData string, data interface{}) (string, error) { + templ, err := template.New(name).Parse(templateData) + + if err != nil { + return "", fmtc.Errorf("Can't render template: %v", err) + } + + var buffer bytes.Buffer + + ct := template.Must(templ, nil) + err = ct.Execute(&buffer, data) + + if err != nil { + return "", fmtc.Errorf("Can't render template: %v", err) + } + + return buffer.String(), nil +} + +func writeData(file, data string) error { + return ioutil.WriteFile(file, []byte(data), 0644) +} + +// printError prints error message to console +func printError(f string, a ...interface{}) { + fmtc.Fprintf(os.Stderr, "{r}"+f+"{!}\n", a...) +} + +// printError prints warning message to console +func printWarn(f string, a ...interface{}) { + fmtc.Fprintf(os.Stderr, "{y}"+f+"{!}\n", a...) +} + +// printErrorAndExit print error mesage and exit with exit code 1 +func printErrorAndExit(f string, a ...interface{}) { + printError(f, a...) + os.Exit(1) +} + +// ////////////////////////////////////////////////////////////////////////////////// // + +// showUsage print usage info to console +func showUsage() { + info := usage.NewInfo("", "procfile") + + info.AddOption(ARG_IN_PLACE, "Edit procfile in place") + info.AddOption(ARG_NO_COLORS, "Disable colors in output") + info.AddOption(ARG_HELP, "Show this help message") + info.AddOption(ARG_VERSION, "Show version") + + info.Render() +} + +// showAbout print version info to console +func showAbout() { + about := &usage.About{ + App: APP, + Version: VER, + Desc: DESC, + Year: 2006, + Owner: "FB Group", + License: "MIT License", + } + + about.Render() +} diff --git a/Dockerfile.systemd b/docker/Dockerfile.systemd similarity index 95% rename from Dockerfile.systemd rename to docker/Dockerfile.systemd index aaf0239..de6e17c 100644 --- a/Dockerfile.systemd +++ b/docker/Dockerfile.systemd @@ -3,7 +3,7 @@ FROM centos:centos7 ENV GOPATH /root ENV TARGET /root/src/github.com/funbox/init-exporter -RUN yum install -y https://yum.kaos.io/7/release/x86_64/kaos-repo-7.2-0.el7.noarch.rpm +RUN yum install -y https://yum.kaos.io/7/release/x86_64/kaos-repo-8.0-0.el7.noarch.rpm RUN yum clean all && yum -y update RUN yum -y install make golang diff --git a/Dockerfile.upstart b/docker/Dockerfile.upstart similarity index 82% rename from Dockerfile.upstart rename to docker/Dockerfile.upstart index 835e068..79407cb 100644 --- a/Dockerfile.upstart +++ b/docker/Dockerfile.upstart @@ -3,7 +3,7 @@ FROM centos:centos6 ENV GOPATH /root ENV TARGET /root/src/github.com/funbox/init-exporter -RUN yum install -y https://yum.kaos.io/6/release/i386/kaos-repo-7.2-0.el6.noarch.rpm +RUN yum install -y https://yum.kaos.io/6/release/x86_64/kaos-repo-8.0-0.el6.noarch.rpm RUN yum clean all && yum -y update RUN yum -y install make golang diff --git a/export/export_test.go b/export/export_test.go index a5555dc..8f946cd 100644 --- a/export/export_test.go +++ b/export/export_test.go @@ -182,14 +182,14 @@ func (s *ExportSuite) TestUpstartExport(c *C) { c.Assert(service1Helper[4:], DeepEquals, []string{ "[[ -r /etc/profile.d/rbenv.sh ]] && source /etc/profile.d/rbenv.sh", "", - "cd /srv/service/service1-dir && exec env STAGING=true /bin/echo service1 >>/srv/service/service1-dir/log/service1.log", + "cd /srv/service/service1-dir && exec env STAGING=true /bin/echo 'service1:pre' >>/srv/service/service1-dir/log/service1.log && exec env STAGING=true /bin/echo 'service1' >>/srv/service/service1-dir/log/service1.log && exec env STAGING=true /bin/echo 'service1:post' >>/srv/service/service1-dir/log/service1.log", ""}, ) c.Assert(service2Helper[4:], DeepEquals, []string{ "[[ -r /etc/profile.d/rbenv.sh ]] && source /etc/profile.d/rbenv.sh", "", - "cd /srv/service/working-dir && exec /bin/echo service2", + "cd /srv/service/working-dir && exec /bin/echo 'service2'", ""}, ) @@ -369,14 +369,14 @@ func (s *ExportSuite) TestSystemdExport(c *C) { c.Assert(service1Helper[4:], DeepEquals, []string{ "[[ -r /etc/profile.d/rbenv.sh ]] && source /etc/profile.d/rbenv.sh", "", - "exec /bin/echo service1 >>/srv/service/service1-dir/log/service1.log", + "exec /bin/echo 'service1:pre' >>/srv/service/service1-dir/log/service1.log && exec /bin/echo 'service1' >>/srv/service/service1-dir/log/service1.log && exec /bin/echo 'service1:post' >>/srv/service/service1-dir/log/service1.log", ""}, ) c.Assert(service2Helper[4:], DeepEquals, []string{ "[[ -r /etc/profile.d/rbenv.sh ]] && source /etc/profile.d/rbenv.sh", "", - "exec /bin/echo service2", + "exec /bin/echo 'service2'", ""}, ) @@ -407,7 +407,9 @@ func createTestApp(helperDir, targetDir string) *procfile.Application { service1 := &procfile.Service{ Name: "service1", - Cmd: "/bin/echo service1", + Cmd: "/bin/echo 'service1'", + PreCmd: "/bin/echo 'service1:pre'", + PostCmd: "/bin/echo 'service1:post'", Application: app, Options: &procfile.ServiceOptions{ Env: map[string]string{"STAGING": "true"}, @@ -424,7 +426,7 @@ func createTestApp(helperDir, targetDir string) *procfile.Application { service2 := &procfile.Service{ Name: "service2", - Cmd: "/bin/echo service2", + Cmd: "/bin/echo 'service2'", Application: app, Options: &procfile.ServiceOptions{ WorkingDir: "/srv/service/working-dir", diff --git a/export/systemd.go b/export/systemd.go index 5020501..977b263 100644 --- a/export/systemd.go +++ b/export/systemd.go @@ -30,7 +30,7 @@ const TEMPLATE_SYSTEMD_HELPER = `#!/bin/bash [[ -r /etc/profile.d/rbenv.sh ]] && source /etc/profile.d/rbenv.sh -exec {{.Service.Cmd}}{{ if .Service.Options.IsCustomLogEnabled }} >>{{.Service.Options.FullLogPath}}{{ end }} +{{ if .Service.HasPreCmd }}{{.Service.GetCommandExec "pre"}} && {{ end }}{{.Service.GetCommandExec ""}}{{ if .Service.HasPostCmd }} && {{.Service.GetCommandExec "post"}}{{ end }} ` // TEMPLATE_SYSTEMD_APP contains default application template @@ -136,8 +136,8 @@ func (sp *SystemdProvider) RenderAppTemplate(app *procfile.Application) (string, data := &systemdAppData{ Application: app, Wants: sp.renderWantsClause(app), - StartLevel: sp.randerLevel(app.StartLevel), - StopLevel: sp.randerLevel(app.StopLevel), + StartLevel: sp.renderLevel(app.StartLevel), + StopLevel: sp.renderLevel(app.StopLevel), ExportDate: timeutil.Format(time.Now(), "%Y/%m/%d %H:%M:%S"), } @@ -150,8 +150,8 @@ func (sp *SystemdProvider) RenderServiceTemplate(service *procfile.Service) (str data := systemdServiceData{ Application: service.Application, Service: service, - StartLevel: sp.randerLevel(service.Application.StartLevel), - StopLevel: sp.randerLevel(service.Application.StopLevel), + StartLevel: sp.renderLevel(service.Application.StartLevel), + StopLevel: sp.renderLevel(service.Application.StopLevel), ExportDate: timeutil.Format(time.Now(), "%Y/%m/%d %H:%M:%S"), } @@ -164,8 +164,8 @@ func (sp *SystemdProvider) RenderHelperTemplate(service *procfile.Service) (stri data := systemdServiceData{ Application: service.Application, Service: service, - StartLevel: sp.randerLevel(service.Application.StartLevel), - StopLevel: sp.randerLevel(service.Application.StopLevel), + StartLevel: sp.renderLevel(service.Application.StartLevel), + StopLevel: sp.renderLevel(service.Application.StopLevel), ExportDate: timeutil.Format(time.Now(), "%Y/%m/%d %H:%M:%S"), } @@ -174,8 +174,8 @@ func (sp *SystemdProvider) RenderHelperTemplate(service *procfile.Service) (stri // ////////////////////////////////////////////////////////////////////////////////// // -// randerLevel convert level number to upstart level name -func (sp *SystemdProvider) randerLevel(level int) string { +// renderLevel convert level number to upstart level name +func (sp *SystemdProvider) renderLevel(level int) string { switch level { case 1: return "rescue.target" diff --git a/export/upstart.go b/export/upstart.go index 781428e..956d170 100644 --- a/export/upstart.go +++ b/export/upstart.go @@ -29,7 +29,7 @@ const TEMPLATE_UPSTART_HELPER = `#!/bin/bash [[ -r /etc/profile.d/rbenv.sh ]] && source /etc/profile.d/rbenv.sh -cd {{.Service.Options.WorkingDir}} && exec {{ if .Service.Options.IsEnvSet }}env {{.Service.Options.EnvString}} {{ end }}{{.Service.Cmd}}{{ if .Service.Options.IsCustomLogEnabled }} >>{{.Service.Options.FullLogPath}}{{ end }} +cd {{.Service.Options.WorkingDir}} && {{ if .Service.HasPreCmd }}{{.Service.GetCommandExecWithEnv "pre"}} && {{ end }}{{.Service.GetCommandExecWithEnv ""}}{{ if .Service.HasPostCmd }} && {{.Service.GetCommandExecWithEnv "post"}}{{ end }} ` // TEMPLATE_UPSTART_APP contains default application template diff --git a/init-exporter-converter.go b/init-exporter-converter.go new file mode 100644 index 0000000..965f8e4 --- /dev/null +++ b/init-exporter-converter.go @@ -0,0 +1,17 @@ +package main + +// ////////////////////////////////////////////////////////////////////////////////// // +// // +// Copyright (c) 2006-2017 FB GROUP LLC // +// // +// ////////////////////////////////////////////////////////////////////////////////// // + +import ( + CONV "github.com/funbox/init-exporter/converter" +) + +// ////////////////////////////////////////////////////////////////////////////////// // + +func main() { + CONV.Init() +} diff --git a/procfile/procfile.go b/procfile/procfile.go index 1fd9088..601f972 100644 --- a/procfile/procfile.go +++ b/procfile/procfile.go @@ -7,8 +7,6 @@ package procfile // ////////////////////////////////////////////////////////////////////////////////// // import ( - "bufio" - "bytes" "fmt" "io/ioutil" "regexp" @@ -19,8 +17,6 @@ import ( "pkg.re/essentialkaos/ek.v7/fsutil" "pkg.re/essentialkaos/ek.v7/log" "pkg.re/essentialkaos/ek.v7/path" - - "pkg.re/essentialkaos/go-simpleyaml.v1" ) // ////////////////////////////////////////////////////////////////////////////////// // @@ -50,6 +46,8 @@ type Config struct { type Service struct { Name string // Service name Cmd string // Command + PreCmd string // Pre command + PostCmd string // Post command Options *ServiceOptions // Service options Application *Application // Pointer to parent application HelperPath string // Path to helper (will be set by exporter) @@ -160,6 +158,60 @@ func (so *ServiceOptions) Validate() error { return errs.Last() } +// HasPreCmd return true if pre command is defined +func (s *Service) HasPreCmd() bool { + return s.PreCmd != "" +} + +// HasPostCmd return true if post command is defined +func (s *Service) HasPostCmd() bool { + return s.PostCmd != "" +} + +// GetCommandExecWithEnv return full command exec with env vars setting +func (s *Service) GetCommandExecWithEnv(command string) string { + var result = "exec " + + if s.Options.IsEnvSet() { + result += "env " + s.Options.EnvString() + " " + } + + switch command { + case "pre": + result += s.PreCmd + case "post": + result += s.PostCmd + default: + result += s.Cmd + } + + if s.Options.IsCustomLogEnabled() { + result += " >>" + s.Options.FullLogPath() + } + + return result +} + +// GetCommandExec return full command exec +func (s *Service) GetCommandExec(command string) string { + var result = "exec " + + switch command { + case "pre": + result += s.PreCmd + case "post": + result += s.PostCmd + default: + result += s.Cmd + } + + if s.Options.IsCustomLogEnabled() { + result += " >>" + s.Options.FullLogPath() + } + + return result +} + // IsRespawnLimitSet return true if respawn options is set func (so *ServiceOptions) IsRespawnLimitSet() bool { return so.RespawnCount != 0 || so.RespawnInterval != 0 @@ -213,322 +265,6 @@ func (so *ServiceOptions) FullLogPath() string { // ////////////////////////////////////////////////////////////////////////////////// // -// 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'") - } - - cmd, options := parseV1Command(matches[2]) - - return &Service{Name: matches[1], Cmd: cmd, Options: options}, nil -} - -// parseV1Command parse command and extract command and working dir -func parseV1Command(cmd string) (string, *ServiceOptions) { - var options = &ServiceOptions{} - - if !strings.HasPrefix(cmd, "cd ") && !strings.Contains(cmd, "&&") { - return cmd, options - } - - cmdSlice := strings.Split(cmd, "&&") - command := strings.TrimSpace(cmdSlice[1]) - workingDir := strings.Replace(cmdSlice[0], "cd", "", -1) - - options.WorkingDir = strings.TrimSpace(workingDir) - - if strings.HasPrefix(command, "env ") { - evMap := make(map[string]string) - - subCommandSlice := strings.Fields(command) - - for i, commandPart := range subCommandSlice { - if commandPart == "env" { - continue - } - - if !strings.Contains(commandPart, "=") { - command = strings.Join(subCommandSlice[i:], " ") - break - } - - envSlice := strings.Split(commandPart, "=") - evMap[envSlice[0]] = envSlice[1] - } - - options.Env = evMap - } - - return command, options -} - -// 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 := parseCommands(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 parseCommands(yaml *simpleyaml.Yaml, commands map[interface{}]interface{}, config *Config) ([]*Service, error) { - var services []*Service - - commonOptions, err := parseOptions(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 - } - - serviceOptions, err := parseOptions(commandYaml) - - if err != nil { - return nil, err - } - - mergeServiceOptions(serviceOptions, commonOptions) - configureDefaults(serviceOptions, config) - - service := &Service{ - Name: serviceName, - Cmd: serviceCmd, - Options: serviceOptions, - } - - services = append(services, service) - } - - return services, nil -} - -// parseOptions parse service options in yaml based procfile -func parseOptions(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 -} - // determineProcVersion process procfile data and return procfile version func determineProcVersion(data []byte) int { if regexp.MustCompile(REGEXP_V2_VERSION).Match(data) { diff --git a/procfile/procfile_test.go b/procfile/procfile_test.go index efc1756..612349d 100644 --- a/procfile/procfile_test.go +++ b/procfile/procfile_test.go @@ -35,12 +35,19 @@ func (s *ProcfileSuite) TestProcV1Parsing(c *C) { c.Assert(app.Services[0].Name, Equals, "my_tail_cmd") c.Assert(app.Services[0].Cmd, Equals, "/usr/bin/tail -F /var/log/messages") + c.Assert(app.Services[0].Options, NotNil) + c.Assert(app.Services[0].Options.LogPath, Equals, "log/my_tail_cmd.log") c.Assert(app.Services[1].Name, Equals, "my_another_tail_cmd") c.Assert(app.Services[1].Cmd, Equals, "/usr/bin/tailf /var/log/messages") + c.Assert(app.Services[1].PreCmd, Equals, "echo my_another_tail_cmd") + c.Assert(app.Services[1].Options, NotNil) + c.Assert(app.Services[1].Options.LogPath, Equals, "log/my_another_tail_cmd.log") c.Assert(app.Services[2].Name, Equals, "cmd_with_cd") c.Assert(app.Services[2].Cmd, Equals, "/usr/bin/tail -F /var/log/messages") + c.Assert(app.Services[2].PreCmd, Equals, "echo cmd_with_cd_pre") + c.Assert(app.Services[2].PostCmd, Equals, "echo cmd_with_cd_post") c.Assert(app.Services[2].Options, NotNil) c.Assert(app.Services[2].Options.Env, HasLen, 2) c.Assert(app.Services[2].Options.Env["ENV_TEST"], Equals, "100") @@ -83,6 +90,8 @@ func (s *ProcfileSuite) TestProcV2Parsing(c *C) { case "my_another_tail_cmd": c.Assert(service.Cmd, Equals, "/usr/bin/tail -F /var/log/messages") + c.Assert(service.PreCmd, Equals, "/usr/bin/echo pre_command") + c.Assert(service.PostCmd, Equals, "/usr/bin/echo post_command") c.Assert(service.Options, NotNil) c.Assert(service.Options.WorkingDir, Equals, "/srv/projects/my_website/current") c.Assert(service.Options.IsCustomLogEnabled(), Equals, false) 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 +} diff --git a/testdata/procfile_v1 b/testdata/procfile_v1 index e09077a..edcf584 100644 --- a/testdata/procfile_v1 +++ b/testdata/procfile_v1 @@ -1,3 +1,3 @@ -my_tail_cmd: /usr/bin/tail -F /var/log/messages -my_another_tail_cmd: /usr/bin/tailf /var/log/messages -cmd_with_cd: cd /srv/service && env ENV_TEST=100 SOME_ENV=test /usr/bin/tail -F /var/log/messages +my_tail_cmd: /usr/bin/tail -F /var/log/messages >> log/my_tail_cmd.log 2>&1 +my_another_tail_cmd: echo my_another_tail_cmd && /usr/bin/tailf /var/log/messages >> log/my_another_tail_cmd.log +cmd_with_cd: cd /srv/service && echo cmd_with_cd_pre && env ENV_TEST=100 SOME_ENV=test /usr/bin/tail -F /var/log/messages && echo cmd_with_cd_post diff --git a/testdata/procfile_v2 b/testdata/procfile_v2 index 32a3626..4d7afaf 100644 --- a/testdata/procfile_v2 +++ b/testdata/procfile_v2 @@ -28,7 +28,9 @@ commands: working_directory: '/var/...' # if needs to be redefined my_another_tail_cmd: + pre: /usr/bin/echo pre_command command: /usr/bin/tail -F /var/log/messages + post: /usr/bin/echo post_command limits: nofile: 8192 nproc: 8192