From f563a711804e041e73961ad26ad99fbe6b3a1ad5 Mon Sep 17 00:00:00 2001 From: Anton Novojilov Date: Thu, 31 Mar 2016 10:12:43 -0400 Subject: [PATCH 01/31] Imported first prototype of init-exporter --- .travis.yml | 14 + LICENSE | 20 ++ Makefile | 34 +++ common/init-exporter.conf | 43 +++ export/export_test.go | 425 ++++++++++++++++++++++++++++ export/exporter.go | 233 +++++++++++++++ export/provider.go | 66 +++++ export/systemd.go | 196 +++++++++++++ export/upstart.go | 152 ++++++++++ init-exporter.go | 420 +++++++++++++++++++++++++++ procfile/procfile.go | 578 ++++++++++++++++++++++++++++++++++++++ procfile/procfile_test.go | 127 +++++++++ readme.md | 13 + testdata/procfile_v1 | 2 + testdata/procfile_v2 | 37 +++ 15 files changed, 2360 insertions(+) create mode 100644 .travis.yml create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 common/init-exporter.conf create mode 100644 export/export_test.go create mode 100644 export/exporter.go create mode 100644 export/provider.go create mode 100644 export/systemd.go create mode 100644 export/upstart.go create mode 100644 init-exporter.go create mode 100644 procfile/procfile.go create mode 100644 procfile/procfile_test.go create mode 100644 testdata/procfile_v1 create mode 100644 testdata/procfile_v2 diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..13d16ee --- /dev/null +++ b/.travis.yml @@ -0,0 +1,14 @@ +language: go + +go: + - 1.6 + - tip + +sudo: false + +before_install: + - make deps + +script: + - make test + - make all diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..649df89 --- /dev/null +++ b/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2016 Anton Novojilov + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. + diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..17cc30e --- /dev/null +++ b/Makefile @@ -0,0 +1,34 @@ +######################################################################################## + +DESTDIR?= +PREFIX?=/usr + +######################################################################################## + +.PHONY = all clean install uninstall deps test + +######################################################################################## + +all: bin + +deps: + go get -v pkg.re/check.v1 + go get -v pkg.re/essentialkaos/ek.v1 + go get -v github.com/smallfish/simpleyaml + go get -v gopkg.in/yaml.v2 + +bin: + go build init-exporter.go + +test: + go test ./... + +install: + mkdir -p $(DESTDIR)$(PREFIX)/bin + cp init-exporter $(DESTDIR)$(PREFIX)/bin/ + +uninstall: + rm -f $(DESTDIR)$(PREFIX)/bin/init-exporter + +clean: + rm -f init-exporter diff --git a/common/init-exporter.conf b/common/init-exporter.conf new file mode 100644 index 0000000..9ec4c9f --- /dev/null +++ b/common/init-exporter.conf @@ -0,0 +1,43 @@ +# Default configuration for init-exporter + +[main] + + # Default run user + run-user: service + + # Default run group + run-group: service + + # Prefix used for exported units and helpers + prefix: fb- + +[paths] + + # Working dir + working-dir: /tmp + + # Path to directory with helpers + helper-dir: /var/local/init-exporter/helpers + + # Path to directory with systemd configs + systemd-dir: /etc/systemd/system + + # Path to directory with upstart configs + upstart-dir: /etc/init + +[log] + + # Enable or disable logging here + enabled: true + + # Log file directory + dir: /var/log/init-exporter + + # Path to log file + file: {log:dir}/init-exporter.log + + # Default log file permissions + perms: 0644 + + # Minimal log level (debug/info/warn/error/crit) + level: info diff --git a/export/export_test.go b/export/export_test.go new file mode 100644 index 0000000..6eb2954 --- /dev/null +++ b/export/export_test.go @@ -0,0 +1,425 @@ +package export + +// ////////////////////////////////////////////////////////////////////////////////// // +// // +// Copyright (c) 2006-2016 FB GROUP LLC // +// // +// ////////////////////////////////////////////////////////////////////////////////// // + +import ( + "fmt" + "io/ioutil" + "os" + "strings" + "testing" + + "github.com/funbox/init-exporter/procfile" + + "pkg.re/essentialkaos/ek.v1/fsutil" + "pkg.re/essentialkaos/ek.v1/log" + + . "pkg.re/check.v1" +) + +// ////////////////////////////////////////////////////////////////////////////////// // + +func Test(t *testing.T) { TestingT(t) } + +type ExportSuite struct { + HelperDir string + TargetDir string +} + +// ////////////////////////////////////////////////////////////////////////////////// // + +var _ = Suite(&ExportSuite{}) + +// ////////////////////////////////////////////////////////////////////////////////// // + +func (s *ExportSuite) SetUpSuite(c *C) { + // Disable logging + log.Set(os.DevNull, 0) +} + +func (s *ExportSuite) TestUpstartExport(c *C) { + helperDir := c.MkDir() + targetDir := c.MkDir() + + config := &Config{ + HelperDir: helperDir, + TargetDir: targetDir, + DisableAutoStart: true, + } + + exporter := NewExporter(config, NewUpstart()) + + c.Assert(exporter, NotNil) + + app := createTestApp(targetDir, helperDir) + + err := exporter.Install(app) + + c.Assert(err, IsNil) + + c.Assert(fsutil.IsExist(targetDir+"/test-application.conf"), Equals, true) + c.Assert(fsutil.IsRegular(targetDir+"/test-application.conf"), Equals, true) + c.Assert(fsutil.IsNonEmpty(targetDir+"/test-application.conf"), Equals, true) + + c.Assert(fsutil.IsExist(targetDir+"/test-application_service1.conf"), Equals, true) + c.Assert(fsutil.IsRegular(targetDir+"/test-application_service1.conf"), Equals, true) + c.Assert(fsutil.IsNonEmpty(targetDir+"/test-application_service1.conf"), Equals, true) + + c.Assert(fsutil.IsExist(targetDir+"/test-application_service2.conf"), Equals, true) + c.Assert(fsutil.IsRegular(targetDir+"/test-application_service2.conf"), Equals, true) + c.Assert(fsutil.IsNonEmpty(targetDir+"/test-application_service2.conf"), Equals, true) + + c.Assert(fsutil.IsExist(helperDir+"/test-application_service1.sh"), Equals, true) + c.Assert(fsutil.IsRegular(helperDir+"/test-application_service1.sh"), Equals, true) + c.Assert(fsutil.IsNonEmpty(helperDir+"/test-application_service1.sh"), Equals, true) + + c.Assert(fsutil.IsExist(helperDir+"/test-application_service2.sh"), Equals, true) + c.Assert(fsutil.IsRegular(helperDir+"/test-application_service2.sh"), Equals, true) + c.Assert(fsutil.IsNonEmpty(helperDir+"/test-application_service2.sh"), Equals, true) + + appUnitData, err := ioutil.ReadFile(targetDir + "/test-application.conf") + + c.Assert(err, IsNil) + c.Assert(appUnitData, NotNil) + + service1UnitData, err := ioutil.ReadFile(targetDir + "/test-application_service1.conf") + + c.Assert(err, IsNil) + c.Assert(service1UnitData, NotNil) + + service2UnitData, err := ioutil.ReadFile(targetDir + "/test-application_service2.conf") + + c.Assert(err, IsNil) + c.Assert(service2UnitData, NotNil) + + service1HelperData, err := ioutil.ReadFile(helperDir + "/test-application_service1.sh") + + c.Assert(err, IsNil) + c.Assert(service1HelperData, NotNil) + + service2HelperData, err := ioutil.ReadFile(helperDir + "/test-application_service2.sh") + + c.Assert(err, IsNil) + c.Assert(service2HelperData, NotNil) + + appUnit := strings.Split(string(appUnitData), "\n") + service1Unit := strings.Split(string(service1UnitData), "\n") + service2Unit := strings.Split(string(service2UnitData), "\n") + service1Helper := strings.Split(string(service1HelperData), "\n") + service2Helper := strings.Split(string(service2HelperData), "\n") + + c.Assert(appUnit, HasLen, 16) + c.Assert(service1Unit, HasLen, 18) + c.Assert(service2Unit, HasLen, 18) + c.Assert(service1Helper, HasLen, 7) + c.Assert(service2Helper, HasLen, 7) + + c.Assert(appUnit[2:], DeepEquals, + []string{ + "start on [3]", + "stop on [3]", + "", + "pre-start script", + "", + "bash << \"EOF\"", + " mkdir -p /var/log/test-application", + " chown -R service /var/log/test-application", + " chgrp -R service /var/log/test-application", + " chmod -R g+w /var/log/test-application", + "EOF", + "", + "end script", ""}, + ) + + c.Assert(service1Unit[2:], DeepEquals, + []string{ + "start on [3]", + "stop on [3]", + "", + "respawn", + "respawn limit 15 25", + "", + "kill timeout 10", + "", + "script", + " touch /var/log/test-application/service1.log", + " chown service /var/log/test-application/service1.log", + " chgrp service /var/log/test-application/service1.log", + " chmod g+w /var/log/test-application/service1.log", + fmt.Sprintf(" exec sudo -u service /bin/bash %s/test-application_service1.sh >> /srv/service/service1-dir/custom.log >> /var/log/test-application/service1.log 2>&1", helperDir), + "end script", ""}, + ) + + c.Assert(service2Unit[2:], DeepEquals, + []string{ + "start on [3]", + "stop on [3]", + "", + "respawn", + "", + "", + "kill timeout 0", + "", + "script", + " touch /var/log/test-application/service2.log", + " chown service /var/log/test-application/service2.log", + " chgrp service /var/log/test-application/service2.log", + " chmod g+w /var/log/test-application/service2.log", + fmt.Sprintf(" exec sudo -u service /bin/bash %s/test-application_service2.sh >> /var/log/test-application/service2.log 2>&1", helperDir), + "end script", ""}, + ) + + c.Assert(service1Helper[4:], DeepEquals, + []string{ + "[[ -r /etc/profile.d/rbenv.sh ]] && source /etc/profile.d/rbenv.sh", + "cd /srv/service/service1-dir && exec STAGING=true /bin/echo service1", + ""}, + ) + + 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", + ""}, + ) + + err = exporter.Uninstall(app) + + c.Assert(err, IsNil) + + c.Assert(fsutil.IsExist(targetDir+"/test-application.conf"), Equals, false) + c.Assert(fsutil.IsExist(targetDir+"/test-application_service1.conf"), Equals, false) + c.Assert(fsutil.IsExist(targetDir+"/test-application_service2.conf"), Equals, false) + c.Assert(fsutil.IsExist(helperDir+"/test-application_service1.sh"), Equals, false) + c.Assert(fsutil.IsExist(helperDir+"/test-application_service2.sh"), Equals, false) +} + +func (s *ExportSuite) TestSystemdExport(c *C) { + helperDir := c.MkDir() + targetDir := c.MkDir() + + config := &Config{ + HelperDir: helperDir, + TargetDir: targetDir, + DisableAutoStart: true, + } + + exporter := NewExporter(config, NewSystemd()) + + c.Assert(exporter, NotNil) + + app := createTestApp(targetDir, helperDir) + + err := exporter.Install(app) + + c.Assert(err, IsNil) + + c.Assert(fsutil.IsExist(targetDir+"/test-application.service"), Equals, true) + c.Assert(fsutil.IsRegular(targetDir+"/test-application.service"), Equals, true) + c.Assert(fsutil.IsNonEmpty(targetDir+"/test-application.service"), Equals, true) + + c.Assert(fsutil.IsExist(targetDir+"/test-application_service1.service"), Equals, true) + c.Assert(fsutil.IsRegular(targetDir+"/test-application_service1.service"), Equals, true) + c.Assert(fsutil.IsNonEmpty(targetDir+"/test-application_service1.service"), Equals, true) + + c.Assert(fsutil.IsExist(targetDir+"/test-application_service2.service"), Equals, true) + c.Assert(fsutil.IsRegular(targetDir+"/test-application_service2.service"), Equals, true) + c.Assert(fsutil.IsNonEmpty(targetDir+"/test-application_service2.service"), Equals, true) + + c.Assert(fsutil.IsExist(helperDir+"/test-application_service1.sh"), Equals, true) + c.Assert(fsutil.IsRegular(helperDir+"/test-application_service1.sh"), Equals, true) + c.Assert(fsutil.IsNonEmpty(helperDir+"/test-application_service1.sh"), Equals, true) + + c.Assert(fsutil.IsExist(helperDir+"/test-application_service2.sh"), Equals, true) + c.Assert(fsutil.IsRegular(helperDir+"/test-application_service2.sh"), Equals, true) + c.Assert(fsutil.IsNonEmpty(helperDir+"/test-application_service2.sh"), Equals, true) + + appUnitData, err := ioutil.ReadFile(targetDir + "/test-application.service") + + c.Assert(err, IsNil) + c.Assert(appUnitData, NotNil) + + service1UnitData, err := ioutil.ReadFile(targetDir + "/test-application_service1.service") + + c.Assert(err, IsNil) + c.Assert(service1UnitData, NotNil) + + service2UnitData, err := ioutil.ReadFile(targetDir + "/test-application_service2.service") + + c.Assert(err, IsNil) + c.Assert(service2UnitData, NotNil) + + service1HelperData, err := ioutil.ReadFile(helperDir + "/test-application_service1.sh") + + c.Assert(err, IsNil) + c.Assert(service1HelperData, NotNil) + + service2HelperData, err := ioutil.ReadFile(helperDir + "/test-application_service2.sh") + + c.Assert(err, IsNil) + c.Assert(service2HelperData, NotNil) + + appUnit := strings.Split(string(appUnitData), "\n") + service1Unit := strings.Split(string(service1UnitData), "\n") + service2Unit := strings.Split(string(service2UnitData), "\n") + service1Helper := strings.Split(string(service1HelperData), "\n") + service2Helper := strings.Split(string(service2HelperData), "\n") + + c.Assert(appUnit, HasLen, 22) + c.Assert(service1Unit, HasLen, 26) + c.Assert(service2Unit, HasLen, 26) + c.Assert(service1Helper, HasLen, 7) + c.Assert(service2Helper, HasLen, 7) + + c.Assert(appUnit[2:], DeepEquals, + []string{ + "[Unit]", + "", + "Description=Unit for test-application application", + "After=multi-user.target", + "Wants=test-application_service1.service test-application_service2.service", + "", + "[Service]", + "Type=oneshot", + "RemainAfterExit=true", + "", + "ExecStartPre=/bin/mkdir -p /var/log/test-application", + "ExecStartPre=/bin/chown -R service /var/log/test-application", + "ExecStartPre=/bin/chgrp -R service /var/log/test-application", + "ExecStartPre=/bin/chmod -R g+w /var/log/test-application", + "ExecStart=/bin/echo \"test-application started\"", + "ExecStop=/bin/echo \"test-application stopped\"", + "", + "[Install]", + "WantedBy=multi-user.target", ""}, + ) + + c.Assert(service1Unit[2:], DeepEquals, + []string{ + "[Unit]", + "", + "Description=Unit for service1 service (part of test-application application)", + "PartOf=test-application.service", + "", + "[Service]", + "Type=simple", + "", + "TimeoutStopSec=10", + "Restart=on-failure", + "StartLimitInterval=25", + "StartLimitBurst=15", + "", + "ExecStartPre=/bin/touch /var/log/test-application/service1.log", + "ExecStartPre=/bin/chown service /var/log/test-application/service1.log", + "ExecStartPre=/bin/chgrp service /var/log/test-application/service1.log", + "ExecStartPre=/bin/chmod g+w /var/log/test-application/service1.log", + "", + "User=service", + "Group=service", + "WorkingDirectory=/srv/service/service1-dir", + "Environment=STAGING=true", + fmt.Sprintf("ExecStart=/bin/bash %s/test-application_service1.sh >> /srv/service/service1-dir/custom.log >> /var/log/test-application/service1.log 2>&1", helperDir), + ""}, + ) + + c.Assert(service2Unit[2:], DeepEquals, + []string{ + "[Unit]", + "", + "Description=Unit for service2 service (part of test-application application)", + "PartOf=test-application.service", + "", + "[Service]", + "Type=simple", + "", + "TimeoutStopSec=0", + "Restart=on-failure", + "", + "", + "", + "ExecStartPre=/bin/touch /var/log/test-application/service2.log", + "ExecStartPre=/bin/chown service /var/log/test-application/service2.log", + "ExecStartPre=/bin/chgrp service /var/log/test-application/service2.log", + "ExecStartPre=/bin/chmod g+w /var/log/test-application/service2.log", + "", + "User=service", + "Group=service", + "WorkingDirectory=/srv/service/working-dir", + "", + fmt.Sprintf("ExecStart=/bin/bash %s/test-application_service2.sh >> /var/log/test-application/service2.log 2>&1", helperDir), + ""}, + ) + + c.Assert(service1Helper[4:], DeepEquals, + []string{ + "[[ -r /etc/profile.d/rbenv.sh ]] && source /etc/profile.d/rbenv.sh", + "exec /bin/echo service1", + ""}, + ) + + c.Assert(service2Helper[4:], DeepEquals, + []string{ + "[[ -r /etc/profile.d/rbenv.sh ]] && source /etc/profile.d/rbenv.sh", + "exec /bin/echo service2", + ""}, + ) + + err = exporter.Uninstall(app) + + c.Assert(err, IsNil) + + c.Assert(fsutil.IsExist(targetDir+"/test-application.service"), Equals, false) + c.Assert(fsutil.IsExist(targetDir+"/test-application_service1.service"), Equals, false) + c.Assert(fsutil.IsExist(targetDir+"/test-application_service2.service"), Equals, false) + c.Assert(fsutil.IsExist(helperDir+"/test-application_service1.sh"), Equals, false) + c.Assert(fsutil.IsExist(helperDir+"/test-application_service2.sh"), Equals, false) +} + +// ////////////////////////////////////////////////////////////////////////////////// // + +func createTestApp(helperDir, targetDir string) *procfile.Application { + app := &procfile.Application{ + Name: "test-application", + User: "service", + Group: "service", + StartLevel: 3, + StopLevel: 3, + WorkingDir: "/srv/service/working-dir", + ProcVersion: 2, + Services: []*procfile.Service{}, + } + + service1 := &procfile.Service{ + Name: "service1", + Cmd: "/bin/echo service1", + Application: app, + Options: &procfile.ServiceOptions{ + Env: map[string]string{"STAGING": "true"}, + WorkingDir: "/srv/service/service1-dir", + LogPath: "/srv/service/service1-dir/custom.log", + KillTimeout: 10, + Count: 2, + RespawnInterval: 25, + RespawnCount: 15, + RespawnEnabled: true, + }, + } + + service2 := &procfile.Service{ + Name: "service2", + Cmd: "/bin/echo service2", + Application: app, + Options: &procfile.ServiceOptions{ + WorkingDir: "/srv/service/working-dir", + RespawnEnabled: true, + }, + } + + app.Services = append(app.Services, service1, service2) + + return app +} diff --git a/export/exporter.go b/export/exporter.go new file mode 100644 index 0000000..debdaf4 --- /dev/null +++ b/export/exporter.go @@ -0,0 +1,233 @@ +package export + +// ////////////////////////////////////////////////////////////////////////////////// // +// // +// Copyright (c) 2006-2016 FB GROUP LLC // +// // +// ////////////////////////////////////////////////////////////////////////////////// // + +import ( + "fmt" + "io/ioutil" + "os" + + "pkg.re/essentialkaos/ek.v1/fsutil" + "pkg.re/essentialkaos/ek.v1/log" + "pkg.re/essentialkaos/ek.v1/path" + + "github.com/funbox/init-exporter/procfile" +) + +// ////////////////////////////////////////////////////////////////////////////////// // + +type Config struct { + HelperDir string + TargetDir string + DisableAutoStart bool +} + +type Exporter struct { + Provider Provider + Config *Config +} + +// ////////////////////////////////////////////////////////////////////////////////// // + +func NewExporter(config *Config, provider Provider) *Exporter { + return &Exporter{Config: config, Provider: provider} +} + +// ////////////////////////////////////////////////////////////////////////////////// // + +// Install install application to init system +func (e *Exporter) Install(app *procfile.Application) error { + var err error + + if e.IsInstalled(app) { + err = e.Uninstall(app) + + if err != nil { + return err + } + } + + err = e.writeAppUnit(app) + + if err != nil { + return err + } + + err = e.writeServicesUnits(app) + + if err != nil { + return err + } + + if !e.Config.DisableAutoStart { + err = e.Provider.EnableService(app.Name) + + if err != nil { + return err + } + + log.Debug("Service %s enabled", app.Name) + } + + return nil +} + +// Uninstall uninstall application from init system +func (e *Exporter) Uninstall(app *procfile.Application) error { + var err error + + if !e.IsInstalled(app) { + return fmt.Errorf("Application %s is not installed", app.Name) + } + + if !e.Config.DisableAutoStart { + err = e.Provider.DisableService(app.Name) + + if err != nil { + return err + } + } + + log.Debug("Service %s disabled", app.Name) + + unitPath := e.unitPath(app.Name) + err = os.Remove(unitPath) + + if err != nil { + return err + } + + log.Debug("Application unit %s deleted", unitPath) + + err = deleteByMask(e.Config.TargetDir, app.Name+"_*") + + if err != nil { + return err + } + + log.Debug("Service units deleted") + + err = deleteByMask(e.Config.HelperDir, app.Name+"_*.sh") + + if err != nil { + return err + } + + log.Debug("Helpers deleted") + + return nil +} + +// IsInstalled return true if app already installed +func (e *Exporter) IsInstalled(app *procfile.Application) bool { + return fsutil.IsExist(e.unitPath(app.Name)) +} + +// ////////////////////////////////////////////////////////////////////////////////// // + +// writeAppUnit write app init to file +func (e *Exporter) writeAppUnit(app *procfile.Application) error { + unitPath := e.unitPath(app.Name) + data, err := e.Provider.RenderAppTemplate(app) + + if err != nil { + return err + } + + log.Debug("Application unit saved as %s", unitPath) + + err = ioutil.WriteFile(unitPath, []byte(data), 0644) + + return err +} + +// writeAppUnit write services init to files +func (e *Exporter) writeServicesUnits(app *procfile.Application) error { + err := os.MkdirAll(e.Config.HelperDir, 0755) + + if err != nil { + return err + } + + for _, service := range app.Services { + fullServiceName := app.Name + "_" + service.Name + + service.HelperPath = e.helperPath(fullServiceName) + + helperData, err := e.Provider.RenderHelperTemplate(service) + + if err != nil { + return err + } + + unitData, err := e.Provider.RenderServiceTemplate(service) + + if err != nil { + return err + } + + unitPath := e.unitPath(fullServiceName) + + err = ioutil.WriteFile(unitPath, []byte(unitData), 0644) + + if err != nil { + return err + } + + log.Debug("Service unit saved as %s", unitPath) + + err = ioutil.WriteFile(service.HelperPath, []byte(helperData), 0755) + + if err != nil { + return err + } + + log.Debug("Helper saved as %s", service.HelperPath) + } + + return nil +} + +// unitPath return path for unit +func (e *Exporter) unitPath(name string) string { + return path.Join(e.Config.TargetDir, e.Provider.UnitName(name)) +} + +// helperPath return path for helper +func (e *Exporter) helperPath(name string) string { + return path.Join(e.Config.HelperDir, name+".sh") +} + +// ////////////////////////////////////////////////////////////////////////////////// // + +// deleteByMask delete all files witch +func deleteByMask(dir, mask string) error { + files := fsutil.List( + dir, true, + &fsutil.ListingFilter{ + MatchPatterns: []string{mask}, + }, + ) + + fsutil.ListToAbsolute(dir, files) + + if len(files) == 0 { + return nil + } + + for _, file := range files { + log.Debug("File %s removed", file) + + err := os.Remove(file) + + if err != nil { + return err + } + } + + return nil +} diff --git a/export/provider.go b/export/provider.go new file mode 100644 index 0000000..2338072 --- /dev/null +++ b/export/provider.go @@ -0,0 +1,66 @@ +package export + +// ////////////////////////////////////////////////////////////////////////////////// // +// // +// Copyright (c) 2006-2016 FB GROUP LLC // +// // +// ////////////////////////////////////////////////////////////////////////////////// // + +import ( + "bytes" + "fmt" + "text/template" + + "pkg.re/essentialkaos/ek.v1/log" + + "github.com/funbox/init-exporter/procfile" +) + +// ////////////////////////////////////////////////////////////////////////////////// // + +type Provider interface { + // UnitName return unit name with extension + UnitName(name string) string + + // RenderAppTemplate render unit template data with given app data and return + // app unit code + RenderAppTemplate(app *procfile.Application) (string, error) + + // RenderServiceTemplate render unit template data with given service data and + // return service unit code + RenderServiceTemplate(service *procfile.Service) (string, error) + + // RenderHelperTemplate render helper template data with given service data and + // return helper script code + RenderHelperTemplate(service *procfile.Service) (string, error) + + // EnableService enable service with given name + EnableService(appName string) error + + // DisableService disable service with given name + DisableService(appName string) error +} + +// ////////////////////////////////////////////////////////////////////////////////// // + +// renderTemplate renders template data +func renderTemplate(name, templateData string, data interface{}) (string, error) { + templ, err := template.New(name).Parse(templateData) + + if err != nil { + log.Error(err.Error()) + return "", fmt.Errorf("Can't render template") + } + + var buffer bytes.Buffer + + ct := template.Must(templ, nil) + err = ct.Execute(&buffer, data) + + if err != nil { + log.Error(err.Error()) + return "", fmt.Errorf("Can't render template") + } + + return buffer.String(), nil +} diff --git a/export/systemd.go b/export/systemd.go new file mode 100644 index 0000000..8f82d4a --- /dev/null +++ b/export/systemd.go @@ -0,0 +1,196 @@ +package export + +// ////////////////////////////////////////////////////////////////////////////////// // +// // +// Copyright (c) 2006-2016 FB GROUP LLC // +// // +// ////////////////////////////////////////////////////////////////////////////////// // + +import ( + "strings" + "time" + + "pkg.re/essentialkaos/ek.v1/system" + "pkg.re/essentialkaos/ek.v1/timeutil" + + "github.com/funbox/init-exporter/procfile" +) + +// ////////////////////////////////////////////////////////////////////////////////// // + +// SystemdProvider is systemd export provider +type SystemdProvider struct{} + +// ////////////////////////////////////////////////////////////////////////////////// // + +// TEMPLATE_SYSTEMD_HELPER contains default helper template +const TEMPLATE_SYSTEMD_HELPER = `#!/bin/bash + +# This helper generated {{.ExportDate}} by init-exporter for {{.Application.Name}} application + +[[ -r /etc/profile.d/rbenv.sh ]] && source /etc/profile.d/rbenv.sh +exec {{.Service.Cmd}} +` + +// TEMPLATE_SYSTEMD_APP contains default application template +const TEMPLATE_SYSTEMD_APP = `# This unit generated {{.ExportDate}} by init-exporter for {{.Application.Name}} application + +[Unit] + +Description=Unit for {{.Application.Name}} application +After={{.StartLevel}} +Wants={{.Wants}} + +[Service] +Type=oneshot +RemainAfterExit=true + +ExecStartPre=/bin/mkdir -p /var/log/{{.Application.Name}} +ExecStartPre=/bin/chown -R {{.Application.User}} /var/log/{{.Application.Name}} +ExecStartPre=/bin/chgrp -R {{.Application.Group}} /var/log/{{.Application.Name}} +ExecStartPre=/bin/chmod -R g+w /var/log/{{.Application.Name}} +ExecStart=/bin/echo "{{.Application.Name}} started" +ExecStop=/bin/echo "{{.Application.Name}} stopped" + +[Install] +WantedBy={{.StartLevel}} +` + +// TEMPLATE_SYSTEMD_SERVICE contains default service template +const TEMPLATE_SYSTEMD_SERVICE = `# This unit generated {{.ExportDate}} by init-exporter for {{.Application.Name}} application + +[Unit] + +Description=Unit for {{.Service.Name}} service (part of {{.Application.Name}} application) +PartOf={{.Application.Name}}.service + +[Service] +Type=simple + +TimeoutStopSec={{.Service.Options.KillTimeout}} +{{ if .Service.Options.RespawnEnabled }}Restart=on-failure{{ end }} +{{ if .Service.Options.RespawnLimitSet }}StartLimitInterval={{.Service.Options.RespawnInterval}}{{ end }} +{{ if .Service.Options.RespawnLimitSet }}StartLimitBurst={{.Service.Options.RespawnCount}}{{ end }} + +ExecStartPre=/bin/touch /var/log/{{.Application.Name}}/{{.Service.Name}}.log +ExecStartPre=/bin/chown {{.Application.User}} /var/log/{{.Application.Name}}/{{.Service.Name}}.log +ExecStartPre=/bin/chgrp {{.Application.Group}} /var/log/{{.Application.Name}}/{{.Service.Name}}.log +ExecStartPre=/bin/chmod g+w /var/log/{{.Application.Name}}/{{.Service.Name}}.log + +User={{.Application.User}} +Group={{.Application.Group}} +WorkingDirectory={{.Service.Options.WorkingDir}} +{{ if .Service.Options.EnvSet }}Environment={{.Service.Options.EnvString}}{{ end }} +ExecStart=/bin/bash {{.Service.HelperPath}} {{ if .Service.Options.CustomLogEnabled }}>> {{.Service.Options.LogPath}} {{end}}>> /var/log/{{.Application.Name}}/{{.Service.Name}}.log 2>&1 +` + +// ////////////////////////////////////////////////////////////////////////////////// // + +type systemdAppData struct { + Application *procfile.Application + ExportDate string + StartLevel string + StopLevel string + Wants string +} + +type systemdServiceData struct { + Application *procfile.Application + Service *procfile.Service + ExportDate string + StartLevel string + StopLevel string +} + +// ////////////////////////////////////////////////////////////////////////////////// // + +// NewSystemd create new SystemdProvider struct +func NewSystemd() *SystemdProvider { + return &SystemdProvider{} +} + +// ////////////////////////////////////////////////////////////////////////////////// // + +// UnitName return unit name with extension +func (sp *SystemdProvider) UnitName(name string) string { + return name + ".service" +} + +// EnableService enable service with given name +func (sp *SystemdProvider) EnableService(appName string) error { + return system.Exec("systemctl", "enable", sp.UnitName(appName)) +} + +// DisableService disable service with given name +func (sp *SystemdProvider) DisableService(appName string) error { + return system.Exec("systemctl", "disable", sp.UnitName(appName)) +} + +// RenderAppTemplate render unit template data with given app data and return +// app unit code +func (sp *SystemdProvider) RenderAppTemplate(app *procfile.Application) (string, error) { + data := &systemdAppData{ + Application: app, + Wants: sp.renderWantsClause(app), + StartLevel: sp.randerLevel(app.StartLevel), + StopLevel: sp.randerLevel(app.StopLevel), + ExportDate: timeutil.Format(time.Now(), "%Y/%m/%d %H:%M:%S"), + } + + return renderTemplate("systemd-app-template", TEMPLATE_SYSTEMD_APP, data) +} + +// RenderServiceTemplate render unit template data with given service data and +// return service unit code +func (sp *SystemdProvider) RenderServiceTemplate(service *procfile.Service) (string, error) { + data := systemdServiceData{ + Application: service.Application, + Service: service, + StartLevel: sp.randerLevel(service.Application.StartLevel), + StopLevel: sp.randerLevel(service.Application.StopLevel), + ExportDate: timeutil.Format(time.Now(), "%Y/%m/%d %H:%M:%S"), + } + + return renderTemplate("systemd-service-template", TEMPLATE_SYSTEMD_SERVICE, data) +} + +// RenderHelperTemplate render helper template data with given service data and +// return helper script code +func (sp *SystemdProvider) RenderHelperTemplate(service *procfile.Service) (string, error) { + data := systemdServiceData{ + Application: service.Application, + Service: service, + StartLevel: sp.randerLevel(service.Application.StartLevel), + StopLevel: sp.randerLevel(service.Application.StopLevel), + ExportDate: timeutil.Format(time.Now(), "%Y/%m/%d %H:%M:%S"), + } + + return renderTemplate("systemd-helper-template", TEMPLATE_SYSTEMD_HELPER, data) +} + +// ////////////////////////////////////////////////////////////////////////////////// // + +// randerLevel convert level number to upstart level name +func (sp *SystemdProvider) randerLevel(level int) string { + switch level { + case 1: + return "rescue.target" + case 5: + return "graphical.target" + case 6: + return "reboot.target" + default: + return "multi-user.target" + } +} + +// renderWantsClause render list of services in application for upstart config +func (sp *SystemdProvider) renderWantsClause(app *procfile.Application) string { + var wants []string + + for _, service := range app.Services { + wants = append(wants, sp.UnitName(app.Name+"_"+service.Name)) + } + + return strings.Join(wants, " ") +} diff --git a/export/upstart.go b/export/upstart.go new file mode 100644 index 0000000..94b37c9 --- /dev/null +++ b/export/upstart.go @@ -0,0 +1,152 @@ +package export + +// ////////////////////////////////////////////////////////////////////////////////// // +// // +// Copyright (c) 2006-2016 FB GROUP LLC // +// // +// ////////////////////////////////////////////////////////////////////////////////// // + +import ( + "fmt" + "time" + + "pkg.re/essentialkaos/ek.v1/timeutil" + + "github.com/funbox/init-exporter/procfile" +) + +// ////////////////////////////////////////////////////////////////////////////////// // + +// UpstartProvider is upstart export provider +type UpstartProvider struct{} + +// ////////////////////////////////////////////////////////////////////////////////// // + +// TEMPLATE_UPSTART_HELPER contains default helper template +const TEMPLATE_UPSTART_HELPER = `#!/bin/bash + +# This helper generated {{.ExportDate}} by init-exporter for {{.Application.Name}} application + +[[ -r /etc/profile.d/rbenv.sh ]] && source /etc/profile.d/rbenv.sh +cd {{.Service.Options.WorkingDir}} && exec {{ if .Service.Options.EnvSet }}{{.Service.Options.EnvString}} {{ end }}{{.Service.Cmd}} +` + +// TEMPLATE_UPSTART_APP contains default application template +const TEMPLATE_UPSTART_APP = `# This unit generated {{.ExportDate}} by init-exporter for {{.Application.Name}} application + +start on {{.StartLevel}} +stop on {{.StopLevel}} + +pre-start script + +bash << "EOF" + mkdir -p /var/log/{{.Application.Name}} + chown -R {{.Application.User}} /var/log/{{.Application.Name}} + chgrp -R {{.Application.Group}} /var/log/{{.Application.Name}} + chmod -R g+w /var/log/{{.Application.Name}} +EOF + +end script +` + +// TEMPLATE_UPSTART_SERVICE contains default service template +const TEMPLATE_UPSTART_SERVICE = `# This unit generated {{.ExportDate}} by init-exporter for {{.Application.Name}} application + +start on {{.StartLevel}} +stop on {{.StopLevel}} + +{{ if .Service.Options.RespawnEnabled }}respawn{{ end }} +{{ if .Service.Options.RespawnLimitSet }}respawn limit {{.Service.Options.RespawnCount}} {{.Service.Options.RespawnInterval}}{{ end }} + +kill timeout {{.Service.Options.KillTimeout}} + +script + touch /var/log/{{.Application.Name}}/{{.Service.Name}}.log + chown {{.Application.User}} /var/log/{{.Application.Name}}/{{.Service.Name}}.log + chgrp {{.Application.Group}} /var/log/{{.Application.Name}}/{{.Service.Name}}.log + chmod g+w /var/log/{{.Application.Name}}/{{.Service.Name}}.log + exec sudo -u {{.Application.User}} /bin/bash {{.Service.HelperPath}} {{ if .Service.Options.CustomLogEnabled }}>> {{.Service.Options.LogPath}} {{end}}>> /var/log/{{.Application.Name}}/{{.Service.Name}}.log 2>&1 +end script +` + +// ////////////////////////////////////////////////////////////////////////////////// // + +type upstartAppData struct { + Application *procfile.Application + ExportDate string + StartLevel string + StopLevel string +} + +type upstartServiceData struct { + Application *procfile.Application + Service *procfile.Service + ExportDate string + StartLevel string + StopLevel string +} + +// ////////////////////////////////////////////////////////////////////////////////// // + +// NewUpstart create new UpstartProvider struct +func NewUpstart() *UpstartProvider { + return &UpstartProvider{} +} + +// ////////////////////////////////////////////////////////////////////////////////// // + +// UnitName return unit name with extension +func (up *UpstartProvider) UnitName(name string) string { + return name + ".conf" +} + +// EnableService enable service with given name +func (up *UpstartProvider) EnableService(appName string) error { + return nil +} + +// DisableService disable service with given name +func (up *UpstartProvider) DisableService(appName string) error { + return nil +} + +// RenderAppTemplate render unit template data with given app data and return +// app unit code +func (up *UpstartProvider) RenderAppTemplate(app *procfile.Application) (string, error) { + data := &upstartAppData{ + Application: app, + StartLevel: fmt.Sprintf("[%d]", app.StartLevel), + StopLevel: fmt.Sprintf("[%d]", app.StopLevel), + ExportDate: timeutil.Format(time.Now(), "%Y/%m/%d %H:%M:%S"), + } + + return renderTemplate("upstart-app-template", TEMPLATE_UPSTART_APP, data) +} + +// RenderServiceTemplate render unit template data with given service data and +// return service unit code +func (up *UpstartProvider) RenderServiceTemplate(service *procfile.Service) (string, error) { + data := &upstartServiceData{ + Application: service.Application, + Service: service, + StartLevel: fmt.Sprintf("[%d]", service.Application.StartLevel), + StopLevel: fmt.Sprintf("[%d]", service.Application.StopLevel), + ExportDate: timeutil.Format(time.Now(), "%Y/%m/%d %H:%M:%S"), + } + + return renderTemplate("upstart-service-template", TEMPLATE_UPSTART_SERVICE, data) +} + +// RenderHelperTemplate render helper template data with given service data and +// return helper script code +func (up *UpstartProvider) RenderHelperTemplate(service *procfile.Service) (string, error) { + data := &upstartServiceData{ + Application: service.Application, + Service: service, + StartLevel: fmt.Sprintf("[%d]", service.Application.StartLevel), + StopLevel: fmt.Sprintf("[%d]", service.Application.StopLevel), + ExportDate: timeutil.Format(time.Now(), "%Y/%m/%d %H:%M:%S"), + } + + return renderTemplate("upstart-helper-template", TEMPLATE_UPSTART_HELPER, data) +} diff --git a/init-exporter.go b/init-exporter.go new file mode 100644 index 0000000..add10c0 --- /dev/null +++ b/init-exporter.go @@ -0,0 +1,420 @@ +package main + +// ////////////////////////////////////////////////////////////////////////////////// // +// // +// Copyright (c) 2006-2016 FB GROUP LLC // +// // +// ////////////////////////////////////////////////////////////////////////////////// // + +import ( + "fmt" + "os" + "runtime" + + "pkg.re/essentialkaos/ek.v1/arg" + "pkg.re/essentialkaos/ek.v1/env" + "pkg.re/essentialkaos/ek.v1/fsutil" + "pkg.re/essentialkaos/ek.v1/knf" + "pkg.re/essentialkaos/ek.v1/log" + "pkg.re/essentialkaos/ek.v1/system" + "pkg.re/essentialkaos/ek.v1/usage" + + "github.com/funbox/init-exporter/export" + "github.com/funbox/init-exporter/procfile" +) + +// ////////////////////////////////////////////////////////////////////////////////// // + +// App props +const ( + APP = "init-exporter" + VER = "0.2.0" + DESC = "Utility for exporting services described by Procfile to init system" +) + +// Supported arguments list +const ( + ARG_CONFIG = "c:config" + ARG_PROCFILE = "p:procfile" + ARG_APP_NAME = "n:appname" + ARG_DRY_START = "d:dry-start" + ARG_UNINSTALL = "u:unistall" + ARG_FORMAT = "f:format" + ARG_HELP = "h:help" + ARG_VERSION = "v:version" +) + +// Config properies list +const ( + MAIN_RUN_USER = "main:run-user" + MAIN_RUN_GROUP = "main:run-group" + MAIN_PREFIX = "main:prefix" + PATHS_WORKING_DIR = "paths:working-dir" + PATHS_HELPER_DIR = "paths:helper-dir" + PATHS_SYSTEMD_DIR = "paths:systemd-dir" + PATHS_UPSTART_DIR = "paths:upstart-dir" + LOG_ENABLED = "log:enabled" + LOG_DIR = "log:dir" + LOG_FILE = "log:file" + LOG_PERMS = "log:perms" + LOG_LEVEL = "log:level" +) + +const ( + // FORMAT_UPSTART contains name for upstart exporting format + FORMAT_UPSTART = "upstart" + // FORMAT_SYSTEMD contains name for systemd exporting format + FORMAT_SYSTEMD = "systemd" +) + +// CONFIG_FILE contains path to config file +const CONFIG_FILE = "/etc/init-exporter.conf" + +// ////////////////////////////////////////////////////////////////////////////////// // + +var argMap = arg.Map{ + ARG_APP_NAME: &arg.V{}, + ARG_PROCFILE: &arg.V{}, + ARG_DRY_START: &arg.V{Type: arg.BOOL}, + ARG_UNINSTALL: &arg.V{Type: arg.BOOL, Alias: "c:clear"}, + ARG_FORMAT: &arg.V{}, + ARG_HELP: &arg.V{Type: arg.BOOL}, + ARG_VERSION: &arg.V{Type: arg.BOOL}, +} + +var user *system.User + +// ////////////////////////////////////////////////////////////////////////////////// // + +func main() { + runtime.GOMAXPROCS(1) + + args, errs := arg.Parse(argMap) + + if len(errs) != 0 { + fmt.Println("Error while arguments parsing:") + + for _, err := range errs { + fmt.Printf(" %v\n", err) + } + + os.Exit(1) + } + + if arg.GetB(ARG_VERSION) { + showAbout() + return + } + + if arg.GetB(ARG_HELP) { + showUsage() + return + } + + if len(args) == 0 && !arg.Has(ARG_APP_NAME) { + showUsage() + return + } + + checkForRoot() + checkArguments() + loadConfig() + validateConfig() + setupLogger() + + switch { + case len(args) == 0: + startProcessing(arg.GetS(ARG_APP_NAME)) + default: + startProcessing(args[0]) + } +} + +// checkForRoot check superuser privileges +func checkForRoot() { + var err error + + user, err = system.CurrentUser() + + if err != nil { + fmt.Println(err.Error()) + os.Exit(1) + } + + if !user.IsRoot() { + fmt.Println("This utility must have superuser privileges (root)") + os.Exit(1) + } +} + +// checkArguments check given arguments +func checkArguments() { + if !arg.GetB(ARG_UNINSTALL) { + proc := arg.GetS(ARG_PROCFILE) + + switch { + case fsutil.IsExist(proc) == false: + printErrorAndExit("Procfile %s is not exist", proc) + + case fsutil.IsReadable(proc) == false: + printErrorAndExit("Procfile %s is not readable", proc) + + case fsutil.IsNonEmpty(proc) == false: + printErrorAndExit("Procfile %s is empty", proc) + } + } +} + +// loadConfig check config path and load config +func loadConfig() { + var err error + + switch { + case !fsutil.IsExist(CONFIG_FILE): + printErrorAndExit("Config %s is not exist", CONFIG_FILE) + + case !fsutil.IsReadable(CONFIG_FILE): + printErrorAndExit("Config %s is not readable", CONFIG_FILE) + + case !fsutil.IsNonEmpty(CONFIG_FILE): + printErrorAndExit("Config %s is empty", CONFIG_FILE) + } + + err = knf.Global(CONFIG_FILE) + + if err != nil { + printErrorAndExit(err.Error()) + } +} + +// validateConfig validate config values +func validateConfig() { + var permsChecker = func(config *knf.Config, prop string, value interface{}) error { + if !fsutil.CheckPerms(value.(string), config.GetS(prop)) { + switch value.(string) { + case "DRX": + return fmt.Errorf("Property %s must be path to readable directory", prop) + case "DWX": + return fmt.Errorf("Property %s must be path to writable directory", prop) + case "DRWX": + return fmt.Errorf("Property %s must be path to writable/readable directory", prop) + case "FR": + return fmt.Errorf("Property %s must be path to readable file", prop) + } + } + + return nil + } + + var userChecker = func(config *knf.Config, prop string, value interface{}) error { + if !system.IsUserExist(knf.GetS(prop)) { + return fmt.Errorf("Property %s contains user which not exist on this system", prop) + } + + return nil + } + + var groupChecker = func(config *knf.Config, prop string, value interface{}) error { + if !system.IsGroupExist(knf.GetS(prop)) { + return fmt.Errorf("Property %s contains group which not exist on this system", prop) + } + + return nil + } + + validators := []*knf.Validator{ + &knf.Validator{MAIN_RUN_USER, knf.Empty, nil}, + &knf.Validator{MAIN_RUN_GROUP, knf.Empty, nil}, + &knf.Validator{PATHS_WORKING_DIR, knf.Empty, nil}, + &knf.Validator{PATHS_HELPER_DIR, knf.Empty, nil}, + &knf.Validator{PATHS_SYSTEMD_DIR, knf.Empty, nil}, + &knf.Validator{PATHS_UPSTART_DIR, knf.Empty, nil}, + + &knf.Validator{MAIN_RUN_USER, userChecker, nil}, + &knf.Validator{MAIN_RUN_GROUP, groupChecker, nil}, + + &knf.Validator{PATHS_WORKING_DIR, permsChecker, "DRWX"}, + &knf.Validator{PATHS_HELPER_DIR, permsChecker, "DRWX"}, + } + + if knf.GetB(LOG_ENABLED, true) { + validators = append(validators, + &knf.Validator{LOG_DIR, knf.Empty, nil}, + &knf.Validator{LOG_FILE, knf.Empty, nil}, + &knf.Validator{LOG_DIR, permsChecker, "DWX"}, + ) + } + + errs := knf.Validate(validators) + + if len(errs) != 0 { + fmt.Println("Errors while config validation:") + + for _, err := range errs { + fmt.Printf(" %v\n", err) + } + + os.Exit(1) + } +} + +// setupLogger configure logging subsystem +func setupLogger() { + if !knf.GetB(LOG_ENABLED, true) { + log.Set(os.DevNull, 0) + return + } + + log.Set(knf.GetS(LOG_FILE), knf.GetM(LOG_PERMS, 0644)) + log.MinLevel(knf.GetS(LOG_LEVEL, "info")) +} + +// startProcessing start processing +func startProcessing(appName string) { + if !arg.GetB(ARG_UNINSTALL) { + installApplication(appName) + } else { + uninstallApplication(appName) + } +} + +// installApplication install application to init system +func installApplication(appName string) { + fullAppName := knf.GetS(MAIN_PREFIX) + appName + app, err := procfile.Read( + arg.GetS(ARG_PROCFILE), + &procfile.Config{ + Name: fullAppName, + User: knf.GetS(MAIN_RUN_USER), + Group: knf.GetS(MAIN_RUN_GROUP), + WorkingDir: knf.GetS(PATHS_WORKING_DIR), + }, + ) + + if err != nil { + printErrorAndExit(err.Error()) + } + + if arg.GetB(ARG_DRY_START) { + os.Exit(0) + } + + err = getExporter().Install(app) + + if err == nil { + log.Aux("User %s (%d) installed service %s", user.RealName, user.RealUID, app.Name) + } else { + printErrorAndExit(err.Error()) + } +} + +// uninstallApplication uninstall application from init system +func uninstallApplication(appName string) { + fullAppName := knf.GetS(MAIN_PREFIX) + appName + app := &procfile.Application{Name: fullAppName} + + err := getExporter().Uninstall(app) + + if err == nil { + log.Aux("User %s (%d) uninstalled service %s", user.RealName, user.RealUID, app.Name) + } else { + printErrorAndExit(err.Error()) + } +} + +// checkProviderTargetDir check permissions on target dir +func checkProviderTargetDir(dir string) error { + if !fsutil.CheckPerms("DRWX", dir) { + return fmt.Errorf("This utility require read/write access to directory %s", dir) + } + + return nil +} + +// getExporter create and configure exporter and return it +func getExporter() *export.Exporter { + providerName, err := detectProvider(arg.GetS(ARG_FORMAT)) + + if err != nil { + printErrorAndExit(err.Error()) + } + + var provider export.Provider + + exportConfig := &export.Config{HelperDir: knf.GetS(PATHS_HELPER_DIR)} + + switch providerName { + case FORMAT_UPSTART: + exportConfig.TargetDir = knf.GetS(PATHS_UPSTART_DIR) + provider = export.NewUpstart() + case FORMAT_SYSTEMD: + exportConfig.TargetDir = knf.GetS(PATHS_SYSTEMD_DIR) + provider = export.NewSystemd() + } + + err = checkProviderTargetDir(exportConfig.TargetDir) + + if err != nil { + printErrorAndExit(err.Error()) + } + + return export.NewExporter(exportConfig, provider) +} + +// detectProvider try to detect provider +func detectProvider(format string) (string, error) { + switch { + case format == FORMAT_SYSTEMD: + return FORMAT_SYSTEMD, nil + case format == FORMAT_UPSTART: + return FORMAT_UPSTART, nil + case os.Args[0] == "systemd-exporter": + return FORMAT_SYSTEMD, nil + case os.Args[0] == "upstart-exporter": + return FORMAT_UPSTART, nil + case env.Which("systemctl") != "": + return FORMAT_SYSTEMD, nil + case env.Which("initctl") != "": + return FORMAT_UPSTART, nil + default: + return "", fmt.Errorf("Can't find init provider") + } +} + +// printErrorAndExit print error mesage and exit with exit code 1 +func printErrorAndExit(message string, a ...interface{}) { + log.Crit(message) + fmt.Printf(message+"\n", a...) + os.Exit(1) +} + +// ////////////////////////////////////////////////////////////////////////////////// // + +// showUsage print usage info to console +func showUsage() { + info := usage.NewInfo("", "app-name") + + info.AddOption(ARG_CONFIG, "Path to config file", "file") + info.AddOption(ARG_PROCFILE, "Path to procfile", "file") + info.AddOption(ARG_DRY_START, "Dry start (don't export anything, just parse and test procfile)") + info.AddOption(ARG_UNINSTALL, "Remove scripts and helpers for a particular application") + info.AddOption(ARG_FORMAT, "Format of generated configs", "upstart|systemd") + 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/procfile/procfile.go b/procfile/procfile.go new file mode 100644 index 0000000..53dbf03 --- /dev/null +++ b/procfile/procfile.go @@ -0,0 +1,578 @@ +package procfile + +// ////////////////////////////////////////////////////////////////////////////////// // +// // +// Copyright (c) 2006-2016 FB GROUP LLC // +// // +// ////////////////////////////////////////////////////////////////////////////////// // + +import ( + "bufio" + "bytes" + "fmt" + "io/ioutil" + "regexp" + "sort" + "strings" + + "github.com/smallfish/simpleyaml" + + "pkg.re/essentialkaos/ek.v1/errutil" + "pkg.re/essentialkaos/ek.v1/fsutil" + "pkg.re/essentialkaos/ek.v1/log" + "pkg.re/essentialkaos/ek.v1/path" +) + +// ////////////////////////////////////////////////////////////////////////////////// // + +const ( + REGEXP_V1_LINE = `^([A-z\d_]+):\s*(.+)` + REGEXP_V2_VERSION = `(?m)^\s*version:\s*2\s*$` + REGEXP_PATH_CHECK = `\A[A-Za-z0-9_\-./]+\z` + REGEXP_VALUE_CHECK = `\A[A-Za-z0-9_\-]+\z` +) + +const ( + DEFAULT_RESPAWN_INTERVAL = 5 + DEFAULT_RESPAWN_COUNT = 10 +) + +// ////////////////////////////////////////////////////////////////////////////////// // + +type Config struct { + Name string // Application name + User string // Working user + Group string // Working group + WorkingDir string // Working directory +} + +type Service struct { + Name string // Service name + Cmd string // Command + Options *ServiceOptions // Service options + Application *Application // Pointer to parent application + HelperPath string // Path to helper (will be set by exporter) +} + +type ServiceOptions struct { + Env map[string]string // Environment variables + WorkingDir string // Working directory + LogPath string // Path to log file + KillTimeout int // Kill timeout in seconds + Count int // Exec count + RespawnInterval int // Respawn interval in seconds + RespawnCount int // Respawn count + RespawnEnabled bool // Respawn enabled flag +} + +type Application struct { + Name string // Name of application + Services []*Service // List of services in application + User string // Working user + Group string // Working group + StartLevel int // Start level + StopLevel int // Stop level + WorkingDir string // Working directory + ProcVersion int // Proc version 1/2 +} + +// ////////////////////////////////////////////////////////////////////////////////// // + +// Read reads and parse procfile content +func Read(path string, config *Config) (*Application, error) { + log.Debug("Processing file %s", path) + + if !fsutil.IsExist(path) { + return nil, fmt.Errorf("Procfile %s is not exist", path) + } + + if !fsutil.IsRegular(path) { + return nil, fmt.Errorf("%s is not a file", path) + } + + if !fsutil.IsNonEmpty(path) { + return nil, fmt.Errorf("Procfile %s is empty", path) + } + + if !fsutil.IsReadable(path) { + return nil, fmt.Errorf("Procfile %s is not readable", path) + } + + data, err := ioutil.ReadFile(path) + + if err != nil { + return nil, err + } + + switch determineProcVersion(data) { + + case 1: + return parseV1Procfile(data, config) + + case 2: + return parseV2Procfile(data, config) + + } + + return nil, fmt.Errorf("Can't determine version for procfile %s", path) +} + +// ////////////////////////////////////////////////////////////////////////////////// // + +// Validate validate all services in application +func (a *Application) Validate() error { + errs := errutil.NewErrors() + + errs.Add(checkRunLevel(a.StartLevel)) + errs.Add(checkRunLevel(a.StopLevel)) + + for _, service := range a.Services { + errs.Add(service.Validate()) + } + + return errs.Last() +} + +// Validate validate service props and options +func (s *Service) Validate() error { + errs := errutil.NewErrors() + + errs.Add(checkValue(s.Name)) + errs.Add(s.Options.Validate()) + + return errs.Last() +} + +// Validate validate service options +func (so *ServiceOptions) Validate() error { + errs := errutil.NewErrors() + + errs.Add(checkPath(so.WorkingDir)) + errs.Add(checkPath(so.LogPath)) + + for envName, envVal := range so.Env { + errs.Add(checkEnv(envName, envVal)) + } + + return errs.Last() +} + +// RespawnLimitSet return true if respawn options is set +func (so *ServiceOptions) RespawnLimitSet() bool { + return so.RespawnCount != 0 || so.RespawnInterval != 0 +} + +// CustomLogEnabled return true if service have custom log +func (so *ServiceOptions) CustomLogEnabled() bool { + return so.LogPath != "" +} + +// EnvSet return true if service have custom env vars +func (so *ServiceOptions) EnvSet() bool { + return len(so.Env) != 0 +} + +// EnvString return environment variables as string +func (so *ServiceOptions) EnvString() string { + if len(so.Env) == 0 { + return "" + } + + var clauses []string + + for k, v := range so.Env { + clauses = append(clauses, k+"="+v) + } + + sort.Strings(clauses) + + return strings.Join(clauses, " ") +} + +// ////////////////////////////////////////////////////////////////////////////////// // + +// 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 + } + + 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 &Service{Name: matches[1], Cmd: matches[2], Options: &ServiceOptions{}}, nil +} + +// 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) + + 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, + } + + app.Services, err = parseCommands(yaml, commands) + + if err != nil { + return nil, err + } + + if isYamlPropPresent(yaml, "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 isYamlPropPresent(yaml, "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 isYamlPropPresent(yaml, "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{}) ([]*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) + + service := &Service{ + Name: serviceName, + Cmd: serviceCmd, + Options: serviceOptions, + } + + services = append(services, service) + } + + return services, nil +} + +// parseOptions parse service options im yaml based procfile +func parseOptions(yaml *simpleyaml.Yaml) (*ServiceOptions, error) { + var err error + + options := &ServiceOptions{ + Env: make(map[string]string), + RespawnEnabled: true, + } + + if isYamlPropPresent(yaml, "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 isYamlPropPresent(yaml, "log") { + options.LogPath, err = yaml.Get("log").String() + + if err != nil { + return nil, fmt.Errorf("Can't parse log value: %v", err) + } + } + + if isYamlPropPresent(yaml, "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 isYamlPropPresent(yaml, "count") { + options.Count, err = yaml.Get("count").Int() + + if err != nil { + return nil, fmt.Errorf("Can't parse count value: %v", err) + } + } + + if isYamlPropPresent(yaml, "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 isYamlPropPresent(yaml, "respawn", "count") || isYamlPropPresent(yaml, "respawn", "interval") { + if isYamlPropPresent(yaml, "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) + } + } else { + options.RespawnCount = DEFAULT_RESPAWN_COUNT + } + + if isYamlPropPresent(yaml, "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 { + options.RespawnInterval = DEFAULT_RESPAWN_INTERVAL + } + + } else if isYamlPropPresent(yaml, "respawn") { + options.RespawnEnabled, err = yaml.Get("respawn").Bool() + + if err != nil { + return nil, fmt.Errorf("Can't parse respawn value: %v", err) + } + } + + return options, nil +} + +// isYamlPropPresent return true if property with defined named present in yaml file +func isYamlPropPresent(yaml *simpleyaml.Yaml, path ...string) bool { + return *yaml.GetPath(path...) != simpleyaml.Yaml{} +} + +// determineProcVersion process procfile data and return procfile version +func determineProcVersion(data []byte) int { + if regexp.MustCompile(REGEXP_V2_VERSION).Match(data) { + return 2 + } + + return 1 +} + +// convertMapType convert map with interface{} to map with string +func convertMapType(m map[interface{}]interface{}) map[string]string { + result := make(map[string]string) + + for k, v := range m { + result[k.(string)] = fmt.Sprint(v) + } + + return result +} + +// mergeServiceOptions merge two ServiceOptions structs +func mergeServiceOptions(dst, src *ServiceOptions) { + + mergeStringMaps(dst.Env, src.Env) + + if dst.WorkingDir == "" { + dst.WorkingDir = src.WorkingDir + } + + if dst.LogPath == "" { + dst.LogPath = src.LogPath + } + + if dst.KillTimeout == 0 { + dst.KillTimeout = src.KillTimeout + } + + if dst.RespawnInterval == 0 { + dst.RespawnInterval = src.RespawnInterval + } + + if dst.RespawnCount == 0 { + dst.RespawnCount = src.RespawnCount + } +} + +// mergeStringMaps merges two maps +func mergeStringMaps(dest, src map[string]string) { + for k, v := range src { + if dest[k] == "" { + dest[k] = v + } + } +} + +// checkPath check path value and return error if value is insecure +func checkPath(value string) error { + if value == "" { + return nil + } + + if !regexp.MustCompile(REGEXP_PATH_CHECK).MatchString(value) { + return fmt.Errorf("Path %s is insecure and can't be accepted", value) + } + + if !path.IsSafe(value) { + return fmt.Errorf("Path %s is not safe and can't be accepted", value) + } + + return nil +} + +// checkValue check any value and return error if value is insecure +func checkValue(value string) error { + if value == "" { + return nil + } + + if !regexp.MustCompile(REGEXP_VALUE_CHECK).MatchString(value) { + return fmt.Errorf("Value %s is insecure and can't be accepted", value) + } + + return nil +} + +// checkEnv check given env variable and return error if name or value is insecure +func checkEnv(name, value string) error { + if name == "" || value == "" { + return nil + } + + if !regexp.MustCompile(REGEXP_VALUE_CHECK).MatchString(name) { + return fmt.Errorf("Environment variable name %s is insecure and can't be accepted", value) + } + + if !regexp.MustCompile(REGEXP_VALUE_CHECK).MatchString(value) { + return fmt.Errorf("Environment variable value %s is insecure and can't be accepted", value) + } + + return nil +} + +// checkRunLevel check run level value and return error if value is insecure +func checkRunLevel(value int) error { + if value < 1 { + return fmt.Errorf("Run level can't be less than 1") + } + + if value > 6 { + return fmt.Errorf("Run level can't be greater than 6") + } + + return nil +} + +// addCrossLink add to all service structs pointer +// to parent application struct +func addCrossLink(app *Application) { + for _, service := range app.Services { + service.Application = app + } +} diff --git a/procfile/procfile_test.go b/procfile/procfile_test.go new file mode 100644 index 0000000..fa73749 --- /dev/null +++ b/procfile/procfile_test.go @@ -0,0 +1,127 @@ +package procfile + +// ////////////////////////////////////////////////////////////////////////////////// // +// // +// Copyright (c) 2006-2016 FB GROUP LLC // +// // +// ////////////////////////////////////////////////////////////////////////////////// // + +import ( + "testing" + + . "pkg.re/check.v1" +) + +// ////////////////////////////////////////////////////////////////////////////////// // + +func Test(t *testing.T) { TestingT(t) } + +type ProcfileSuite struct{} + +// ////////////////////////////////////////////////////////////////////////////////// // + +var _ = Suite(&ProcfileSuite{}) + +// ////////////////////////////////////////////////////////////////////////////////// // + +func (s *ProcfileSuite) TestProcV1Parsing(c *C) { + app, err := Read("../testdata/procfile_v1", &Config{Name: "test-app"}) + + c.Assert(err, IsNil) + c.Assert(app, NotNil) + + c.Assert(app.ProcVersion, Equals, 1) + c.Assert(app.Services, HasLen, 2) + + 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[1].Name, Equals, "my_another_tail_cmd") + c.Assert(app.Services[1].Cmd, Equals, "/usr/bin/tailf /var/log/messages") + + c.Assert(app.Validate(), IsNil) +} + +func (s *ProcfileSuite) TestProcV2Parsing(c *C) { + app, err := Read("../testdata/procfile_v2", &Config{Name: "test-app"}) + + c.Assert(err, IsNil) + c.Assert(app, NotNil) + + c.Assert(app.ProcVersion, Equals, 2) + c.Assert(app.Services, HasLen, 4) + + c.Assert(app.StartLevel, Equals, 2) + c.Assert(app.StopLevel, Equals, 5) + + for _, service := range app.Services { + switch service.Name { + case "my_tail_cmd": + c.Assert(service.Cmd, Equals, "/usr/bin/tail -F /var/log/messages") + c.Assert(service.Options, NotNil) + c.Assert(service.Options.WorkingDir, Equals, "/var/...") + c.Assert(service.Options.CustomLogEnabled(), Equals, false) + c.Assert(service.Options.RespawnCount, Equals, 5) + c.Assert(service.Options.RespawnInterval, Equals, 10) + c.Assert(service.Options.RespawnEnabled, Equals, true) + c.Assert(service.Options.Env, NotNil) + c.Assert(service.Options.Env["RAILS_ENV"], Equals, "staging") + c.Assert(service.Options.Env["TEST"], Equals, "true") + c.Assert(service.Options.EnvString(), Equals, "RAILS_ENV=staging TEST=true") + c.Assert(service.Application, NotNil) + c.Assert(service.Application.Name, Equals, "test-app") + + case "my_another_tail_cmd": + c.Assert(service.Cmd, Equals, "/usr/bin/tail -F /var/log/messages") + c.Assert(service.Options, NotNil) + c.Assert(service.Options.WorkingDir, Equals, "/srv/projects/my_website/current") + c.Assert(service.Options.CustomLogEnabled(), Equals, false) + c.Assert(service.Options.KillTimeout, Equals, 60) + c.Assert(service.Options.RespawnCount, Equals, 7) + c.Assert(service.Options.RespawnInterval, Equals, 22) + c.Assert(service.Options.RespawnEnabled, Equals, false) + c.Assert(service.Options.Env, NotNil) + c.Assert(service.Options.Env["RAILS_ENV"], Equals, "production") + c.Assert(service.Options.Env["TEST"], Equals, "true") + c.Assert(service.Options.EnvString(), Equals, "RAILS_ENV=production TEST=true") + c.Assert(service.Application, NotNil) + c.Assert(service.Application.Name, Equals, "test-app") + + case "my_one_another_tail_cmd": + c.Assert(service.Cmd, Equals, "/usr/bin/tail -F /var/log/messages") + c.Assert(service.Options, NotNil) + c.Assert(service.Options.WorkingDir, Equals, "/srv/projects/my_website/current") + c.Assert(service.Options.LogPath, Equals, "/var/log/messages_copy") + c.Assert(service.Options.CustomLogEnabled(), Equals, true) + c.Assert(service.Options.RespawnCount, Equals, 7) + c.Assert(service.Options.RespawnInterval, Equals, 22) + c.Assert(service.Options.RespawnEnabled, Equals, true) + c.Assert(service.Options.Env, NotNil) + c.Assert(service.Options.Env["RAILS_ENV"], Equals, "production") + c.Assert(service.Options.Env["TEST"], Equals, "true") + c.Assert(service.Options.EnvString(), Equals, "RAILS_ENV=production TEST=true") + c.Assert(service.Application, NotNil) + c.Assert(service.Application.Name, Equals, "test-app") + + case "my_multi_tail_cmd": + c.Assert(service.Cmd, Equals, "/usr/bin/tail -F /var/log/messages") + c.Assert(service.Options, NotNil) + c.Assert(service.Options.Count, Equals, 2) + c.Assert(service.Options.WorkingDir, Equals, "/srv/projects/my_website/current") + c.Assert(service.Options.CustomLogEnabled(), Equals, false) + c.Assert(service.Options.RespawnCount, Equals, 7) + c.Assert(service.Options.RespawnInterval, Equals, 22) + c.Assert(service.Options.RespawnEnabled, Equals, true) + c.Assert(service.Options.Env, NotNil) + c.Assert(service.Options.Env["RAILS_ENV"], Equals, "production") + c.Assert(service.Options.Env["TEST"], Equals, "true") + c.Assert(service.Options.EnvString(), Equals, "RAILS_ENV=production TEST=true") + c.Assert(service.Application, NotNil) + c.Assert(service.Application.Name, Equals, "test-app") + + default: + c.Fatalf("Unknown service %s", service.Name) + } + } + + c.Assert(app.Validate(), IsNil) +} diff --git a/readme.md b/readme.md index beaac11..201daa7 100644 --- a/readme.md +++ b/readme.md @@ -1 +1,14 @@ ## `init-exporter` + +Utility for exporting services described by Procfile to init system. + +#### Build Status + +| Repository | Status | +|------------|--------| +| Stable | [![Build Status](https://travis-ci.org/funbox/init-exporter.svg?branch=master)](https://travis-ci.org/funbox/init-exporter) | +| Unstable | [![Build Status](https://travis-ci.org/funbox/init-exporter.svg?branch=develop)](https://travis-ci.org/funbox/init-exporter) | + +#### License + +MIT diff --git a/testdata/procfile_v1 b/testdata/procfile_v1 new file mode 100644 index 0000000..aba1251 --- /dev/null +++ b/testdata/procfile_v1 @@ -0,0 +1,2 @@ +my_tail_cmd: /usr/bin/tail -F /var/log/messages +my_another_tail_cmd: /usr/bin/tailf /var/log/messages diff --git a/testdata/procfile_v2 b/testdata/procfile_v2 new file mode 100644 index 0000000..85a8c76 --- /dev/null +++ b/testdata/procfile_v2 @@ -0,0 +1,37 @@ +version: 2 + +start_on_runlevel: 2 +stop_on_runlevel: 5 + +env: + RAILS_ENV: production + TEST: true + +respawn: + count: 7 + interval: 22 + +working_directory: /srv/projects/my_website/current + +commands: + my_tail_cmd: + command: /usr/bin/tail -F /var/log/messages + respawn: + count: 5 + interval: 10 + env: + RAILS_ENV: staging # if needs to be redefined or extended + working_directory: '/var/...' # if needs to be redefined + + my_another_tail_cmd: + command: /usr/bin/tail -F /var/log/messages + kill_timeout: 60 + respawn: false # by default respawn option is enabled + + my_one_another_tail_cmd: + command: /usr/bin/tail -F /var/log/messages + log: /var/log/messages_copy + + my_multi_tail_cmd: + command: /usr/bin/tail -F /var/log/messages + count: 2 From e9c8327c5a7cdae69d6e8993ca73dcaae9e0911d Mon Sep 17 00:00:00 2001 From: Anton Novojilov Date: Thu, 31 Mar 2016 10:14:45 -0400 Subject: [PATCH 02/31] Fixes in license --- LICENSE | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/LICENSE b/LICENSE index 649df89..dd018fd 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2016 Anton Novojilov +Copyright (c) 2016 FB Group Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in @@ -17,4 +17,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - From c3b5cea7dab890988b081c37920fa60d44f0f19a Mon Sep 17 00:00:00 2001 From: Anton Novojilov Date: Fri, 1 Apr 2016 05:16:19 -0400 Subject: [PATCH 03/31] Added install instructions to readme + Fixes in Makefile --- Makefile | 8 +++++--- readme.md | 11 ++++++++++- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index 17cc30e..9166fd7 100644 --- a/Makefile +++ b/Makefile @@ -9,7 +9,7 @@ PREFIX?=/usr ######################################################################################## -all: bin +all: deps bin deps: go get -v pkg.re/check.v1 @@ -25,10 +25,12 @@ test: install: mkdir -p $(DESTDIR)$(PREFIX)/bin - cp init-exporter $(DESTDIR)$(PREFIX)/bin/ + cp init-exporter $(DESTDIR)$(PREFIX)/sbin/ + cp common/init-exporter.conf $(DESTDIR)/etc/ uninstall: - rm -f $(DESTDIR)$(PREFIX)/bin/init-exporter + rm -f $(DESTDIR)$(PREFIX)/sbin/init-exporter + rm -rf $(DESTDIR)/etc/init-exporter clean: rm -f init-exporter diff --git a/readme.md b/readme.md index 201daa7..23ddd0d 100644 --- a/readme.md +++ b/readme.md @@ -2,6 +2,15 @@ Utility for exporting services described by Procfile to init system. +#### Installation + +To build the init-exporter from scratch, make sure you have a working Go 1.5+ workspace ([instructions](https://golang.org/doc/install)), then: + +```bash +make all +make install +``` + #### Build Status | Repository | Status | @@ -11,4 +20,4 @@ Utility for exporting services described by Procfile to init system. #### License -MIT +init-exporter is released under the MIT license (see [LICENSE](LICENSE)) From ef99b16594078e431ce1bc44f3260ab953af6abf Mon Sep 17 00:00:00 2001 From: Anton Novojilov Date: Fri, 1 Apr 2016 05:19:07 -0400 Subject: [PATCH 04/31] Fixed wrong uninstall path fot config in Makefile --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 9166fd7..0974a96 100644 --- a/Makefile +++ b/Makefile @@ -30,7 +30,7 @@ install: uninstall: rm -f $(DESTDIR)$(PREFIX)/sbin/init-exporter - rm -rf $(DESTDIR)/etc/init-exporter + rm -rf $(DESTDIR)/etc/init-exporter.conf clean: rm -f init-exporter From da083596a828796ad8c9b787cefa43e43aa33fa0 Mon Sep 17 00:00:00 2001 From: Anton Novojilov Date: Fri, 1 Apr 2016 05:21:28 -0400 Subject: [PATCH 05/31] Added Go 1.5.3 to testing targets --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 13d16ee..a17fb01 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,7 @@ language: go go: + - 1.5.3 - 1.6 - tip From 379dfd2f0b795b0cac48111e3101d54328c5e311 Mon Sep 17 00:00:00 2001 From: Anton Novojilov Date: Fri, 1 Apr 2016 05:25:49 -0400 Subject: [PATCH 06/31] Small fixes in TravisCI config --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index a17fb01..8a88d05 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,4 +12,4 @@ before_install: script: - make test - - make all + - make bin From 167e307d42e5b37351a3d0be4bfd489ebf41f412 Mon Sep 17 00:00:00 2001 From: Anton Novojilov Date: Fri, 1 Apr 2016 05:32:26 -0400 Subject: [PATCH 07/31] Updated install instructions --- readme.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/readme.md b/readme.md index 23ddd0d..f3d84ba 100644 --- a/readme.md +++ b/readme.md @@ -7,10 +7,20 @@ Utility for exporting services described by Procfile to init system. To build the init-exporter from scratch, make sure you have a working Go 1.5+ workspace ([instructions](https://golang.org/doc/install)), then: ```bash +git clone https://github.com/funbox/init-exporter.git --depth=1 +cd init-exporter make all make install ``` +OR + +```bash +go get github.com/funbox/init-exporter +sudo cp $GOPATH/bin/init-exporter /usr/sbin/ +sudo cp $GOPATH/src/github.com/funbox/init-exporter/common/init-exporter.conf /etc/ +``` + #### Build Status | Repository | Status | From cd8bfea277a76bc414865054e5764bb647f8c4d7 Mon Sep 17 00:00:00 2001 From: Anton Novojilov Date: Fri, 1 Apr 2016 05:33:17 -0400 Subject: [PATCH 08/31] Updated install instructions --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index f3d84ba..b6f263f 100644 --- a/readme.md +++ b/readme.md @@ -10,7 +10,7 @@ To build the init-exporter from scratch, make sure you have a working Go 1.5+ wo git clone https://github.com/funbox/init-exporter.git --depth=1 cd init-exporter make all -make install +sudo make install ``` OR From d60e267d3e02bc38cf417b38bd0e44d157e88c3c Mon Sep 17 00:00:00 2001 From: Anton Novojilov Date: Fri, 1 Apr 2016 05:38:09 -0400 Subject: [PATCH 09/31] Updated install instructions --- readme.md | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/readme.md b/readme.md index b6f263f..0b1e4ed 100644 --- a/readme.md +++ b/readme.md @@ -7,20 +7,12 @@ Utility for exporting services described by Procfile to init system. To build the init-exporter from scratch, make sure you have a working Go 1.5+ workspace ([instructions](https://golang.org/doc/install)), then: ```bash -git clone https://github.com/funbox/init-exporter.git --depth=1 -cd init-exporter +go get -d github.com/funbox/init-exporter +cd $GOPATH/src/github.com/funbox/init-exporter make all sudo make install ``` -OR - -```bash -go get github.com/funbox/init-exporter -sudo cp $GOPATH/bin/init-exporter /usr/sbin/ -sudo cp $GOPATH/src/github.com/funbox/init-exporter/common/init-exporter.conf /etc/ -``` - #### Build Status | Repository | Status | From 91b299734eaba1caf29a005ede4fcaa82fe0a05b Mon Sep 17 00:00:00 2001 From: Miroslav Malkin Date: Fri, 15 Apr 2016 13:37:58 +0300 Subject: [PATCH 10/31] README from old upstart-exporter adopted (#2) * README from old upstart-exporter adopted * readme file fixed according to new command line arguments; examples added to usage instruction * docker files for testing utility with systemd and upstart added * newlines added to Dockerfiles --- Dockerfile.systemd | 22 +++++ Dockerfile.upstart | 22 +++++ Makefile | 6 ++ init-exporter.go | 8 +- readme.md | 198 +++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 255 insertions(+), 1 deletion(-) create mode 100644 Dockerfile.systemd create mode 100644 Dockerfile.upstart diff --git a/Dockerfile.systemd b/Dockerfile.systemd new file mode 100644 index 0000000..e201461 --- /dev/null +++ b/Dockerfile.systemd @@ -0,0 +1,22 @@ +FROM centos:centos7 + +ENV GOPATH /root +ENV TARGET /root/src/github.com/funbox/init-exporter + +RUN yum install -y http://release.yum.kaos.io/i386/kaos-repo-1.2.0-0.el6.noarch.rpm +RUN yum clean all && yum -y update + +RUN yum -y install make go + +COPY . $TARGET +RUN ls $TARGET -al +RUN cd $TARGET && make all && make install + +RUN useradd service +RUN mkdir -p /var/local/init-exporter/helpers && mkdir -p /var/log/init-exporter + +COPY ./testdata /root + +RUN yum clean all && rm -rf /tmp/* + +WORKDIR /root diff --git a/Dockerfile.upstart b/Dockerfile.upstart new file mode 100644 index 0000000..e4eec8d --- /dev/null +++ b/Dockerfile.upstart @@ -0,0 +1,22 @@ +FROM centos:centos6 + +ENV GOPATH /root +ENV TARGET /root/src/github.com/funbox/init-exporter + +RUN yum install -y http://release.yum.kaos.io/i386/kaos-repo-1.2.0-0.el6.noarch.rpm +RUN yum clean all && yum -y update + +RUN yum -y install go + +COPY . $TARGET +RUN ls $TARGET -al +RUN cd $TARGET && make all && make install + +RUN useradd service +RUN mkdir -p /var/local/init-exporter/helpers && mkdir -p /var/log/init-exporter + +COPY ./testdata /root + +RUN yum clean all && rm -rf /tmp/* + +WORKDIR /root diff --git a/Makefile b/Makefile index 0974a96..85a0e4e 100644 --- a/Makefile +++ b/Makefile @@ -34,3 +34,9 @@ uninstall: clean: rm -f init-exporter + +upstart-playground: + docker build -f ./Dockerfile.upstart -t upstart-playground . && docker run -ti --rm=true upstart-playground /bin/bash + +systemd-playground: + docker build -f ./Dockerfile.systemd -t systemd-playground . && docker run -ti --rm=true systemd-playground /bin/bash diff --git a/init-exporter.go b/init-exporter.go index add10c0..9646dfc 100644 --- a/init-exporter.go +++ b/init-exporter.go @@ -154,7 +154,7 @@ func checkArguments() { switch { case fsutil.IsExist(proc) == false: - printErrorAndExit("Procfile %s is not exist", proc) + printErrorAndExit("Procfile %s does not exist", proc) case fsutil.IsReadable(proc) == false: printErrorAndExit("Procfile %s is not readable", proc) @@ -402,6 +402,12 @@ func showUsage() { info.AddOption(ARG_HELP, "Show this help message") info.AddOption(ARG_VERSION, "Show version") + info.AddExample("-p ./myprocfile -f systemd myapp", "Export given procfile to systemd as myapp") + info.AddExample("-u -f systemd myapp", "Uninstall myapp from systemd") + + info.AddExample("-p ./myprocfile -f upstart myapp", "Export given procfile to upstart as myapp") + info.AddExample("-u -f upstart myapp", "Uninstall myapp from upstart") + info.Render() } diff --git a/readme.md b/readme.md index 0b1e4ed..41c928e 100644 --- a/readme.md +++ b/readme.md @@ -1,6 +1,7 @@ ## `init-exporter` Utility for exporting services described by Procfile to init system. +Supported init systems: upstart and systemd #### Installation @@ -13,6 +14,203 @@ make all sudo make install ``` +#### Configuration + +The export process can be configured through the config `/etc/init-exporter.conf` +The config is not installed by default. If this config is absent, the default values are the following: + +``` +# Default configuration for init-exporter + +[main] + + # Default run user + run-user: service + + # Default run group + run-group: service + + # Prefix used for exported units and helpers + prefix: fb- + +[paths] + + # Working dir + working-dir: /tmp + + # Path to directory with helpers + helper-dir: /var/local/init-exporter/helpers + + # Path to directory with systemd configs + systemd-dir: /etc/systemd/system + + # Path to directory with upstart configs + upstart-dir: /etc/init + +[log] + + # Enable or disable logging here + enabled: true + + # Log file directory + dir: /var/log/init-exporter + + # Path to log file + file: {log:dir}/init-exporter.log + + # Default log file permissions + perms: 0644 + + # Minimal log level (debug/info/warn/error/crit) + level: info +``` + +To give a certain user (i.e. `deployuser`) the ability to use this script, you can place the following lines into `sudoers` file: + + # Commands required for manipulating jobs + Cmnd_Alias UPSTART = /sbin/start, /sbin/stop, /sbin/restart + Cmnd_Alias UPEXPORT = /usr/local/bin/init-export + + ... + + # Add gem's binary path to this + Defaults secure_path = /sbin:/bin:/usr/sbin:/usr/bin:/usr/local/bin + + ... + + # Allow deploy user to manipulate jobs + deployuser ALL=(deployuser) NOPASSWD: ALL, (root) NOPASSWD: UPSTART, UPEXPORT + + +#### Usage + +Gem is able to process two versions of Procfiles, format of the Procfile is +defined in the `version` key. If the key is not present or is not equal to `2` +file will be parsed as Procfile v.1. + +##### Procfile v.1 + +After upstart-exporter is installed and configured, you may export background jobs +from an arbitrary Procfile-like file of the following format: + +```yaml + cmdlabel1: cmd1 + cmdlabel2: cmd2 +``` + +i.e. a file `./myprocfile` containing: + +```yaml + my_tail_cmd: /usr/bin/tail -F /var/log/messages + my_another_tail_cmd: /usr/bin/tail -F /var/log/messages +``` + +For security purposes, command labels are allowed to contain only letters, digits, and underscores. + +##### Procfile v.2 + +Another format of Procfile scripts is YAML config. A configuration script may +look like this: + +```yaml + version: 2 + start_on_runlevel: 3 + stop_on_runlevel: 3 + env: + RAILS_ENV: production + TEST: true + working_directory: /srv/projects/my_website/current + commands: + my_tail_cmd: + command: /usr/bin/tail -F /var/log/messages + respawn: + count: 5 + interval: 10 + env: + RAILS_ENV: staging # if needs to be redefined or extended + working_directory: '/var/...' # if needs to be redefined + my_another_tail_cmd: + command: /usr/bin/tail -F /var/log/messages + kill_timeout: 60 + respawn: false # by default respawn option is enabled + my_one_another_tail_cmd: + command: /usr/bin/tail -F /var/log/messages + log: /var/log/messages_copy + my_multi_tail_cmd: + command: /usr/bin/tail -F /var/log/messages + count: 2 +``` + +`start_on_runlevel` and `stop_on_runlevel` are two global options that can't be +redefined per-command. + +`working_directory` will generate the following line: + + cd 'your/working/directory' && your_command + +`env` params can be redefined and extended in per-command options. Note that +you can't remove a globally defined `env` variable. +For Procfile example given earlier the generated command will look like: + + env RAILS_ENV=staging TEST=true your_command + +`log` option lets you override the default log location (`/var/log/fb-my_website/my_one_another_tail_cmd.log`). + +`kill_timeout` option lets you override the default process kill timeout of 30 seconds. + +`respawn` option controls how often the job can fail. If the job restarts more +often than `count` times in `interval`, it won't be restarted anymore. + +Options `working_directory`, `env`, `log`, `respawn` can be +defined both as global and as per-command options. + +#### Exporting + +To export a Procfile you should run + + sudo upstart-export -p ./myprocfile -f format myapp + +where `myapp` is the application name. +This name only affects the names of generated files. +For security purposes, app name is also allowed to contain only letters, digits and underscores. +where format is `(upstart | systemd)` + +Assuming that default options are used, the following files and folders will be generated (in case of upstart format): + +in `/etc/init/`: + + fb-myapp-my_another_tail_cmd.conf + fb-myapp-my_tail_cmd.conf + fb-myapp.conf + +in `/var/local/init-exporter/helpers`: + + fb-myapp-my_another_tail_cmd.sh + fb-myapp-my_tail_cmd.sh + +Prefix `fb-` (which can be customised through config) is added to avoid collisions with other jobs. +After this `my_tail_cmd`, for example, will be able to be started as an Upstart job: + + sudo start fb-myapp-my_tail_cmd + + .. + + sudo stop fb-myapp-my_tail_cmd + +Its stdout/stderr will be redirected to `/var/log/fb-myapp/my_tail_cmd.log`. + +To start/stop all application commands at once, you can run: + + sudo start fb-myapp + ... + sudo stop fb-myapp + +To remove upstart scripts and helpers for a particular application you can run + + sudo init-export -u -f upstart myapp + +The logs are not cleared in this case. Also, all old application scripts are cleared before each export. + #### Build Status | Repository | Status | From f7a5a4ca0326f1bd5a7d89c1f5d5b709e3979bfe Mon Sep 17 00:00:00 2001 From: Anton Novojilov Date: Fri, 15 Apr 2016 13:44:59 +0300 Subject: [PATCH 11/31] Readme improvements --- readme.md | 129 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 72 insertions(+), 57 deletions(-) diff --git a/readme.md b/readme.md index 41c928e..5186079 100644 --- a/readme.md +++ b/readme.md @@ -19,7 +19,7 @@ sudo make install The export process can be configured through the config `/etc/init-exporter.conf` The config is not installed by default. If this config is absent, the default values are the following: -``` +```ini # Default configuration for init-exporter [main] @@ -67,20 +67,21 @@ The config is not installed by default. If this config is absent, the default va To give a certain user (i.e. `deployuser`) the ability to use this script, you can place the following lines into `sudoers` file: - # Commands required for manipulating jobs - Cmnd_Alias UPSTART = /sbin/start, /sbin/stop, /sbin/restart - Cmnd_Alias UPEXPORT = /usr/local/bin/init-export - - ... +```bash +# Commands required for manipulating jobs +Cmnd_Alias UPSTART = /sbin/start, /sbin/stop, /sbin/restart +Cmnd_Alias UPEXPORT = /usr/local/bin/init-export - # Add gem's binary path to this - Defaults secure_path = /sbin:/bin:/usr/sbin:/usr/bin:/usr/local/bin +... - ... +# Add gem's binary path to this +Defaults secure_path = /sbin:/bin:/usr/sbin:/usr/bin:/usr/local/bin - # Allow deploy user to manipulate jobs - deployuser ALL=(deployuser) NOPASSWD: ALL, (root) NOPASSWD: UPSTART, UPEXPORT +... +# Allow deploy user to manipulate jobs +deployuser ALL=(deployuser) NOPASSWD: ALL, (root) NOPASSWD: UPSTART, UPEXPORT +``` #### Usage @@ -94,15 +95,15 @@ After upstart-exporter is installed and configured, you may export background jo from an arbitrary Procfile-like file of the following format: ```yaml - cmdlabel1: cmd1 - cmdlabel2: cmd2 +cmdlabel1: cmd1 +cmdlabel2: cmd2 ``` i.e. a file `./myprocfile` containing: ```yaml - my_tail_cmd: /usr/bin/tail -F /var/log/messages - my_another_tail_cmd: /usr/bin/tail -F /var/log/messages +my_tail_cmd: /usr/bin/tail -F /var/log/messages +my_another_tail_cmd: /usr/bin/tail -F /var/log/messages ``` For security purposes, command labels are allowed to contain only letters, digits, and underscores. @@ -113,32 +114,32 @@ Another format of Procfile scripts is YAML config. A configuration script may look like this: ```yaml - version: 2 - start_on_runlevel: 3 - stop_on_runlevel: 3 +version: 2 +start_on_runlevel: 3 +stop_on_runlevel: 3 +env: + RAILS_ENV: production + TEST: true +working_directory: /srv/projects/my_website/current +commands: + my_tail_cmd: + command: /usr/bin/tail -F /var/log/messages + respawn: + count: 5 + interval: 10 env: - RAILS_ENV: production - TEST: true - working_directory: /srv/projects/my_website/current - commands: - my_tail_cmd: - command: /usr/bin/tail -F /var/log/messages - respawn: - count: 5 - interval: 10 - env: - RAILS_ENV: staging # if needs to be redefined or extended - working_directory: '/var/...' # if needs to be redefined - my_another_tail_cmd: - command: /usr/bin/tail -F /var/log/messages - kill_timeout: 60 - respawn: false # by default respawn option is enabled - my_one_another_tail_cmd: - command: /usr/bin/tail -F /var/log/messages - log: /var/log/messages_copy - my_multi_tail_cmd: - command: /usr/bin/tail -F /var/log/messages - count: 2 + RAILS_ENV: staging # if needs to be redefined or extended + working_directory: '/var/...' # if needs to be redefined + my_another_tail_cmd: + command: /usr/bin/tail -F /var/log/messages + kill_timeout: 60 + respawn: false # by default respawn option is enabled + my_one_another_tail_cmd: + command: /usr/bin/tail -F /var/log/messages + log: /var/log/messages_copy + my_multi_tail_cmd: + command: /usr/bin/tail -F /var/log/messages + count: 2 ``` `start_on_runlevel` and `stop_on_runlevel` are two global options that can't be @@ -146,13 +147,17 @@ redefined per-command. `working_directory` will generate the following line: - cd 'your/working/directory' && your_command +```bash +cd 'your/working/directory' && your_command +``` `env` params can be redefined and extended in per-command options. Note that you can't remove a globally defined `env` variable. For Procfile example given earlier the generated command will look like: - env RAILS_ENV=staging TEST=true your_command +```bash +env RAILS_ENV=staging TEST=true your_command +``` `log` option lets you override the default log location (`/var/log/fb-my_website/my_one_another_tail_cmd.log`). @@ -168,7 +173,9 @@ defined both as global and as per-command options. To export a Procfile you should run - sudo upstart-export -p ./myprocfile -f format myapp +```bash +sudo upstart-export -p ./myprocfile -f format myapp +``` where `myapp` is the application name. This name only affects the names of generated files. @@ -179,35 +186,43 @@ Assuming that default options are used, the following files and folders will be in `/etc/init/`: - fb-myapp-my_another_tail_cmd.conf - fb-myapp-my_tail_cmd.conf - fb-myapp.conf +``` +fb-myapp-my_another_tail_cmd.conf +fb-myapp-my_tail_cmd.conf +fb-myapp.conf +``` in `/var/local/init-exporter/helpers`: - fb-myapp-my_another_tail_cmd.sh - fb-myapp-my_tail_cmd.sh +``` +fb-myapp-my_another_tail_cmd.sh +fb-myapp-my_tail_cmd.sh +``` Prefix `fb-` (which can be customised through config) is added to avoid collisions with other jobs. After this `my_tail_cmd`, for example, will be able to be started as an Upstart job: - sudo start fb-myapp-my_tail_cmd - - .. - - sudo stop fb-myapp-my_tail_cmd +```bash +sudo start fb-myapp-my_tail_cmd +... +sudo stop fb-myapp-my_tail_cmd +``` Its stdout/stderr will be redirected to `/var/log/fb-myapp/my_tail_cmd.log`. To start/stop all application commands at once, you can run: - sudo start fb-myapp - ... - sudo stop fb-myapp +```bash +sudo start fb-myapp +... +sudo stop fb-myapp +``` To remove upstart scripts and helpers for a particular application you can run - sudo init-export -u -f upstart myapp +```bash +sudo init-export -u -f upstart myapp +``` The logs are not cleared in this case. Also, all old application scripts are cleared before each export. From 270a933df5ca31fec7c6de61028f8a94d5fd4d37 Mon Sep 17 00:00:00 2001 From: Anton Novojilov Date: Mon, 31 Oct 2016 08:30:14 -0400 Subject: [PATCH 12/31] ek updated to v5 + TravisCI config updated --- .travis.yml | 19 +- Makefile | 5 +- cli/cli.go | 436 ++++++++++++++++++++++++++++++++++++++++++ export/export_test.go | 4 +- export/exporter.go | 6 +- export/provider.go | 2 +- export/systemd.go | 4 +- export/upstart.go | 2 +- init-exporter.go | 413 +-------------------------------------- procfile/procfile.go | 8 +- 10 files changed, 471 insertions(+), 428 deletions(-) create mode 100644 cli/cli.go diff --git a/.travis.yml b/.travis.yml index 8a88d05..323124c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,11 +1,24 @@ +sudo: false + language: go go: - - 1.5.3 - - 1.6 + - 1.6.x + - 1.7.x - tip -sudo: false +branches: + only: + - master + - develop + +os: + - linux + +matrix: + fast_finish: true + allow_failures: + - go: tip before_install: - make deps diff --git a/Makefile b/Makefile index 85a0e4e..e7dc018 100644 --- a/Makefile +++ b/Makefile @@ -13,13 +13,16 @@ all: deps bin deps: go get -v pkg.re/check.v1 - go get -v pkg.re/essentialkaos/ek.v1 + go get -v pkg.re/essentialkaos/ek.v5 go get -v github.com/smallfish/simpleyaml go get -v gopkg.in/yaml.v2 bin: go build init-exporter.go +fmt: + find . -name "*.go" -exec gofmt -s -w {} \; + test: go test ./... diff --git a/cli/cli.go b/cli/cli.go new file mode 100644 index 0000000..f103c72 --- /dev/null +++ b/cli/cli.go @@ -0,0 +1,436 @@ +package cli + +// ////////////////////////////////////////////////////////////////////////////////// // +// // +// Copyright (c) 2006-2016 FB GROUP LLC // +// // +// ////////////////////////////////////////////////////////////////////////////////// // + +import ( + "fmt" + "os" + "runtime" + + "pkg.re/essentialkaos/ek.v5/arg" + "pkg.re/essentialkaos/ek.v5/env" + "pkg.re/essentialkaos/ek.v5/fmtc" + "pkg.re/essentialkaos/ek.v5/fsutil" + "pkg.re/essentialkaos/ek.v5/knf" + "pkg.re/essentialkaos/ek.v5/log" + "pkg.re/essentialkaos/ek.v5/system" + "pkg.re/essentialkaos/ek.v5/usage" + + "github.com/funbox/init-exporter/export" + "github.com/funbox/init-exporter/procfile" +) + +// ////////////////////////////////////////////////////////////////////////////////// // + +// App props +const ( + APP = "init-exporter" + VER = "0.3.0" + DESC = "Utility for exporting services described by Procfile to init system" +) + +// Supported arguments list +const ( + ARG_CONFIG = "c:config" + ARG_PROCFILE = "p:procfile" + ARG_APP_NAME = "n:appname" + ARG_DRY_START = "d:dry-start" + ARG_UNINSTALL = "u:unistall" + ARG_FORMAT = "f:format" + ARG_NO_COLORS = "nc:no-colors" + ARG_HELP = "h:help" + ARG_VERSION = "v:version" +) + +// Config properies list +const ( + MAIN_RUN_USER = "main:run-user" + MAIN_RUN_GROUP = "main:run-group" + MAIN_PREFIX = "main:prefix" + PATHS_WORKING_DIR = "paths:working-dir" + PATHS_HELPER_DIR = "paths:helper-dir" + PATHS_SYSTEMD_DIR = "paths:systemd-dir" + PATHS_UPSTART_DIR = "paths:upstart-dir" + LOG_ENABLED = "log:enabled" + LOG_DIR = "log:dir" + LOG_FILE = "log:file" + LOG_PERMS = "log:perms" + LOG_LEVEL = "log:level" +) + +const ( + // FORMAT_UPSTART contains name for upstart exporting format + FORMAT_UPSTART = "upstart" + // FORMAT_SYSTEMD contains name for systemd exporting format + FORMAT_SYSTEMD = "systemd" +) + +// CONFIG_FILE contains path to config file +const CONFIG_FILE = "/etc/init-exporter.conf" + +// ////////////////////////////////////////////////////////////////////////////////// // + +var argMap = arg.Map{ + ARG_APP_NAME: {}, + ARG_PROCFILE: {}, + ARG_DRY_START: {Type: arg.BOOL}, + ARG_UNINSTALL: {Type: arg.BOOL, Alias: "c:clear"}, + ARG_FORMAT: {}, + ARG_NO_COLORS: {Type: arg.BOOL}, + ARG_HELP: {Type: arg.BOOL}, + ARG_VERSION: {Type: arg.BOOL}, +} + +var user *system.User + +// ////////////////////////////////////////////////////////////////////////////////// // + +func Init() { + runtime.GOMAXPROCS(1) + + args, errs := arg.Parse(argMap) + + if len(errs) != 0 { + fmt.Println("Error while arguments parsing:") + + for _, err := range errs { + fmt.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) { + showUsage() + return + } + + if len(args) == 0 && !arg.Has(ARG_APP_NAME) { + showUsage() + return + } + + checkForRoot() + checkArguments() + loadConfig() + validateConfig() + setupLogger() + + switch { + case len(args) == 0: + startProcessing(arg.GetS(ARG_APP_NAME)) + default: + startProcessing(args[0]) + } +} + +// checkForRoot check superuser privileges +func checkForRoot() { + var err error + + user, err = system.CurrentUser() + + if err != nil { + fmt.Println(err.Error()) + os.Exit(1) + } + + if !user.IsRoot() { + fmt.Println("This utility must have superuser privileges (root)") + os.Exit(1) + } +} + +// checkArguments check given arguments +func checkArguments() { + if !arg.GetB(ARG_UNINSTALL) { + proc := arg.GetS(ARG_PROCFILE) + + switch { + case fsutil.IsExist(proc) == false: + printErrorAndExit("Procfile %s does not exist", proc) + + case fsutil.IsReadable(proc) == false: + printErrorAndExit("Procfile %s is not readable", proc) + + case fsutil.IsNonEmpty(proc) == false: + printErrorAndExit("Procfile %s is empty", proc) + } + } +} + +// loadConfig check config path and load config +func loadConfig() { + var err error + + switch { + case !fsutil.IsExist(CONFIG_FILE): + printErrorAndExit("Config %s is not exist", CONFIG_FILE) + + case !fsutil.IsReadable(CONFIG_FILE): + printErrorAndExit("Config %s is not readable", CONFIG_FILE) + + case !fsutil.IsNonEmpty(CONFIG_FILE): + printErrorAndExit("Config %s is empty", CONFIG_FILE) + } + + err = knf.Global(CONFIG_FILE) + + if err != nil { + printErrorAndExit(err.Error()) + } +} + +// validateConfig validate config values +func validateConfig() { + var permsChecker = func(config *knf.Config, prop string, value interface{}) error { + if !fsutil.CheckPerms(value.(string), config.GetS(prop)) { + switch value.(string) { + case "DRX": + return fmt.Errorf("Property %s must be path to readable directory", prop) + case "DWX": + return fmt.Errorf("Property %s must be path to writable directory", prop) + case "DRWX": + return fmt.Errorf("Property %s must be path to writable/readable directory", prop) + case "FR": + return fmt.Errorf("Property %s must be path to readable file", prop) + } + } + + return nil + } + + var userChecker = func(config *knf.Config, prop string, value interface{}) error { + if !system.IsUserExist(knf.GetS(prop)) { + return fmt.Errorf("Property %s contains user which not exist on this system", prop) + } + + return nil + } + + var groupChecker = func(config *knf.Config, prop string, value interface{}) error { + if !system.IsGroupExist(knf.GetS(prop)) { + return fmt.Errorf("Property %s contains group which not exist on this system", prop) + } + + return nil + } + + validators := []*knf.Validator{ + {MAIN_RUN_USER, knf.Empty, nil}, + {MAIN_RUN_GROUP, knf.Empty, nil}, + {PATHS_WORKING_DIR, knf.Empty, nil}, + {PATHS_HELPER_DIR, knf.Empty, nil}, + {PATHS_SYSTEMD_DIR, knf.Empty, nil}, + {PATHS_UPSTART_DIR, knf.Empty, nil}, + + {MAIN_RUN_USER, userChecker, nil}, + {MAIN_RUN_GROUP, groupChecker, nil}, + + {PATHS_WORKING_DIR, permsChecker, "DRWX"}, + {PATHS_HELPER_DIR, permsChecker, "DRWX"}, + } + + if knf.GetB(LOG_ENABLED, true) { + validators = append(validators, + &knf.Validator{LOG_DIR, knf.Empty, nil}, + &knf.Validator{LOG_FILE, knf.Empty, nil}, + &knf.Validator{LOG_DIR, permsChecker, "DWX"}, + ) + } + + errs := knf.Validate(validators) + + if len(errs) != 0 { + fmt.Println("Errors while config validation:") + + for _, err := range errs { + fmt.Printf(" %v\n", err) + } + + os.Exit(1) + } +} + +// setupLogger configure logging subsystem +func setupLogger() { + if !knf.GetB(LOG_ENABLED, true) { + log.Set(os.DevNull, 0) + return + } + + log.Set(knf.GetS(LOG_FILE), knf.GetM(LOG_PERMS, 0644)) + log.MinLevel(knf.GetS(LOG_LEVEL, "info")) +} + +// startProcessing start processing +func startProcessing(appName string) { + if !arg.GetB(ARG_UNINSTALL) { + installApplication(appName) + } else { + uninstallApplication(appName) + } +} + +// installApplication install application to init system +func installApplication(appName string) { + fullAppName := knf.GetS(MAIN_PREFIX) + appName + app, err := procfile.Read( + arg.GetS(ARG_PROCFILE), + &procfile.Config{ + Name: fullAppName, + User: knf.GetS(MAIN_RUN_USER), + Group: knf.GetS(MAIN_RUN_GROUP), + WorkingDir: knf.GetS(PATHS_WORKING_DIR), + }, + ) + + if err != nil { + printErrorAndExit(err.Error()) + } + + if arg.GetB(ARG_DRY_START) { + os.Exit(0) + } + + err = getExporter().Install(app) + + if err == nil { + log.Aux("User %s (%d) installed service %s", user.RealName, user.RealUID, app.Name) + } else { + printErrorAndExit(err.Error()) + } +} + +// uninstallApplication uninstall application from init system +func uninstallApplication(appName string) { + fullAppName := knf.GetS(MAIN_PREFIX) + appName + app := &procfile.Application{Name: fullAppName} + + err := getExporter().Uninstall(app) + + if err == nil { + log.Aux("User %s (%d) uninstalled service %s", user.RealName, user.RealUID, app.Name) + } else { + printErrorAndExit(err.Error()) + } +} + +// checkProviderTargetDir check permissions on target dir +func checkProviderTargetDir(dir string) error { + if !fsutil.CheckPerms("DRWX", dir) { + return fmt.Errorf("This utility require read/write access to directory %s", dir) + } + + return nil +} + +// getExporter create and configure exporter and return it +func getExporter() *export.Exporter { + providerName, err := detectProvider(arg.GetS(ARG_FORMAT)) + + if err != nil { + printErrorAndExit(err.Error()) + } + + var provider export.Provider + + exportConfig := &export.Config{HelperDir: knf.GetS(PATHS_HELPER_DIR)} + + switch providerName { + case FORMAT_UPSTART: + exportConfig.TargetDir = knf.GetS(PATHS_UPSTART_DIR) + provider = export.NewUpstart() + case FORMAT_SYSTEMD: + exportConfig.TargetDir = knf.GetS(PATHS_SYSTEMD_DIR) + provider = export.NewSystemd() + } + + err = checkProviderTargetDir(exportConfig.TargetDir) + + if err != nil { + printErrorAndExit(err.Error()) + } + + return export.NewExporter(exportConfig, provider) +} + +// detectProvider try to detect provider +func detectProvider(format string) (string, error) { + switch { + case format == FORMAT_SYSTEMD: + return FORMAT_SYSTEMD, nil + case format == FORMAT_UPSTART: + return FORMAT_UPSTART, nil + case os.Args[0] == "systemd-exporter": + return FORMAT_SYSTEMD, nil + case os.Args[0] == "upstart-exporter": + return FORMAT_UPSTART, nil + case env.Which("systemctl") != "": + return FORMAT_SYSTEMD, nil + case env.Which("initctl") != "": + return FORMAT_UPSTART, nil + default: + return "", fmt.Errorf("Can't find init provider") + } +} + +// printErrorAndExit print error mesage and exit with exit code 1 +func printErrorAndExit(message string, a ...interface{}) { + log.Crit(message) + fmt.Printf(message+"\n", a...) + os.Exit(1) +} + +// ////////////////////////////////////////////////////////////////////////////////// // + +// showUsage print usage info to console +func showUsage() { + usage.Breadcrumbs = true + + info := usage.NewInfo("", "app-name") + + info.AddOption(ARG_CONFIG, "Path to config file", "file") + info.AddOption(ARG_PROCFILE, "Path to procfile", "file") + info.AddOption(ARG_DRY_START, "Dry start {s-}(don't export anything, just parse and test procfile){!}") + info.AddOption(ARG_UNINSTALL, "Remove scripts and helpers for a particular application") + info.AddOption(ARG_FORMAT, "Format of generated configs", "upstart|systemd") + info.AddOption(ARG_NO_COLORS, "Disable colors in output") + info.AddOption(ARG_HELP, "Show this help message") + info.AddOption(ARG_VERSION, "Show version") + + info.AddExample("-p ./myprocfile -f systemd myapp", "Export given procfile to systemd as myapp") + info.AddExample("-u -f systemd myapp", "Uninstall myapp from systemd") + + info.AddExample("-p ./myprocfile -f upstart myapp", "Export given procfile to upstart as myapp") + info.AddExample("-u -f upstart myapp", "Uninstall myapp from upstart") + + 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/export/export_test.go b/export/export_test.go index 6eb2954..61f6779 100644 --- a/export/export_test.go +++ b/export/export_test.go @@ -15,8 +15,8 @@ import ( "github.com/funbox/init-exporter/procfile" - "pkg.re/essentialkaos/ek.v1/fsutil" - "pkg.re/essentialkaos/ek.v1/log" + "pkg.re/essentialkaos/ek.v5/fsutil" + "pkg.re/essentialkaos/ek.v5/log" . "pkg.re/check.v1" ) diff --git a/export/exporter.go b/export/exporter.go index debdaf4..7f54576 100644 --- a/export/exporter.go +++ b/export/exporter.go @@ -11,9 +11,9 @@ import ( "io/ioutil" "os" - "pkg.re/essentialkaos/ek.v1/fsutil" - "pkg.re/essentialkaos/ek.v1/log" - "pkg.re/essentialkaos/ek.v1/path" + "pkg.re/essentialkaos/ek.v5/fsutil" + "pkg.re/essentialkaos/ek.v5/log" + "pkg.re/essentialkaos/ek.v5/path" "github.com/funbox/init-exporter/procfile" ) diff --git a/export/provider.go b/export/provider.go index 2338072..dbf6c1f 100644 --- a/export/provider.go +++ b/export/provider.go @@ -11,7 +11,7 @@ import ( "fmt" "text/template" - "pkg.re/essentialkaos/ek.v1/log" + "pkg.re/essentialkaos/ek.v5/log" "github.com/funbox/init-exporter/procfile" ) diff --git a/export/systemd.go b/export/systemd.go index 8f82d4a..bd19751 100644 --- a/export/systemd.go +++ b/export/systemd.go @@ -10,8 +10,8 @@ import ( "strings" "time" - "pkg.re/essentialkaos/ek.v1/system" - "pkg.re/essentialkaos/ek.v1/timeutil" + "pkg.re/essentialkaos/ek.v5/system" + "pkg.re/essentialkaos/ek.v5/timeutil" "github.com/funbox/init-exporter/procfile" ) diff --git a/export/upstart.go b/export/upstart.go index 94b37c9..7ec8773 100644 --- a/export/upstart.go +++ b/export/upstart.go @@ -10,7 +10,7 @@ import ( "fmt" "time" - "pkg.re/essentialkaos/ek.v1/timeutil" + "pkg.re/essentialkaos/ek.v5/timeutil" "github.com/funbox/init-exporter/procfile" ) diff --git a/init-exporter.go b/init-exporter.go index 9646dfc..db566fc 100644 --- a/init-exporter.go +++ b/init-exporter.go @@ -7,420 +7,11 @@ package main // ////////////////////////////////////////////////////////////////////////////////// // import ( - "fmt" - "os" - "runtime" - - "pkg.re/essentialkaos/ek.v1/arg" - "pkg.re/essentialkaos/ek.v1/env" - "pkg.re/essentialkaos/ek.v1/fsutil" - "pkg.re/essentialkaos/ek.v1/knf" - "pkg.re/essentialkaos/ek.v1/log" - "pkg.re/essentialkaos/ek.v1/system" - "pkg.re/essentialkaos/ek.v1/usage" - - "github.com/funbox/init-exporter/export" - "github.com/funbox/init-exporter/procfile" + CLI "github.com/funbox/init-exporter/cli" ) // ////////////////////////////////////////////////////////////////////////////////// // -// App props -const ( - APP = "init-exporter" - VER = "0.2.0" - DESC = "Utility for exporting services described by Procfile to init system" -) - -// Supported arguments list -const ( - ARG_CONFIG = "c:config" - ARG_PROCFILE = "p:procfile" - ARG_APP_NAME = "n:appname" - ARG_DRY_START = "d:dry-start" - ARG_UNINSTALL = "u:unistall" - ARG_FORMAT = "f:format" - ARG_HELP = "h:help" - ARG_VERSION = "v:version" -) - -// Config properies list -const ( - MAIN_RUN_USER = "main:run-user" - MAIN_RUN_GROUP = "main:run-group" - MAIN_PREFIX = "main:prefix" - PATHS_WORKING_DIR = "paths:working-dir" - PATHS_HELPER_DIR = "paths:helper-dir" - PATHS_SYSTEMD_DIR = "paths:systemd-dir" - PATHS_UPSTART_DIR = "paths:upstart-dir" - LOG_ENABLED = "log:enabled" - LOG_DIR = "log:dir" - LOG_FILE = "log:file" - LOG_PERMS = "log:perms" - LOG_LEVEL = "log:level" -) - -const ( - // FORMAT_UPSTART contains name for upstart exporting format - FORMAT_UPSTART = "upstart" - // FORMAT_SYSTEMD contains name for systemd exporting format - FORMAT_SYSTEMD = "systemd" -) - -// CONFIG_FILE contains path to config file -const CONFIG_FILE = "/etc/init-exporter.conf" - -// ////////////////////////////////////////////////////////////////////////////////// // - -var argMap = arg.Map{ - ARG_APP_NAME: &arg.V{}, - ARG_PROCFILE: &arg.V{}, - ARG_DRY_START: &arg.V{Type: arg.BOOL}, - ARG_UNINSTALL: &arg.V{Type: arg.BOOL, Alias: "c:clear"}, - ARG_FORMAT: &arg.V{}, - ARG_HELP: &arg.V{Type: arg.BOOL}, - ARG_VERSION: &arg.V{Type: arg.BOOL}, -} - -var user *system.User - -// ////////////////////////////////////////////////////////////////////////////////// // - func main() { - runtime.GOMAXPROCS(1) - - args, errs := arg.Parse(argMap) - - if len(errs) != 0 { - fmt.Println("Error while arguments parsing:") - - for _, err := range errs { - fmt.Printf(" %v\n", err) - } - - os.Exit(1) - } - - if arg.GetB(ARG_VERSION) { - showAbout() - return - } - - if arg.GetB(ARG_HELP) { - showUsage() - return - } - - if len(args) == 0 && !arg.Has(ARG_APP_NAME) { - showUsage() - return - } - - checkForRoot() - checkArguments() - loadConfig() - validateConfig() - setupLogger() - - switch { - case len(args) == 0: - startProcessing(arg.GetS(ARG_APP_NAME)) - default: - startProcessing(args[0]) - } -} - -// checkForRoot check superuser privileges -func checkForRoot() { - var err error - - user, err = system.CurrentUser() - - if err != nil { - fmt.Println(err.Error()) - os.Exit(1) - } - - if !user.IsRoot() { - fmt.Println("This utility must have superuser privileges (root)") - os.Exit(1) - } -} - -// checkArguments check given arguments -func checkArguments() { - if !arg.GetB(ARG_UNINSTALL) { - proc := arg.GetS(ARG_PROCFILE) - - switch { - case fsutil.IsExist(proc) == false: - printErrorAndExit("Procfile %s does not exist", proc) - - case fsutil.IsReadable(proc) == false: - printErrorAndExit("Procfile %s is not readable", proc) - - case fsutil.IsNonEmpty(proc) == false: - printErrorAndExit("Procfile %s is empty", proc) - } - } -} - -// loadConfig check config path and load config -func loadConfig() { - var err error - - switch { - case !fsutil.IsExist(CONFIG_FILE): - printErrorAndExit("Config %s is not exist", CONFIG_FILE) - - case !fsutil.IsReadable(CONFIG_FILE): - printErrorAndExit("Config %s is not readable", CONFIG_FILE) - - case !fsutil.IsNonEmpty(CONFIG_FILE): - printErrorAndExit("Config %s is empty", CONFIG_FILE) - } - - err = knf.Global(CONFIG_FILE) - - if err != nil { - printErrorAndExit(err.Error()) - } -} - -// validateConfig validate config values -func validateConfig() { - var permsChecker = func(config *knf.Config, prop string, value interface{}) error { - if !fsutil.CheckPerms(value.(string), config.GetS(prop)) { - switch value.(string) { - case "DRX": - return fmt.Errorf("Property %s must be path to readable directory", prop) - case "DWX": - return fmt.Errorf("Property %s must be path to writable directory", prop) - case "DRWX": - return fmt.Errorf("Property %s must be path to writable/readable directory", prop) - case "FR": - return fmt.Errorf("Property %s must be path to readable file", prop) - } - } - - return nil - } - - var userChecker = func(config *knf.Config, prop string, value interface{}) error { - if !system.IsUserExist(knf.GetS(prop)) { - return fmt.Errorf("Property %s contains user which not exist on this system", prop) - } - - return nil - } - - var groupChecker = func(config *knf.Config, prop string, value interface{}) error { - if !system.IsGroupExist(knf.GetS(prop)) { - return fmt.Errorf("Property %s contains group which not exist on this system", prop) - } - - return nil - } - - validators := []*knf.Validator{ - &knf.Validator{MAIN_RUN_USER, knf.Empty, nil}, - &knf.Validator{MAIN_RUN_GROUP, knf.Empty, nil}, - &knf.Validator{PATHS_WORKING_DIR, knf.Empty, nil}, - &knf.Validator{PATHS_HELPER_DIR, knf.Empty, nil}, - &knf.Validator{PATHS_SYSTEMD_DIR, knf.Empty, nil}, - &knf.Validator{PATHS_UPSTART_DIR, knf.Empty, nil}, - - &knf.Validator{MAIN_RUN_USER, userChecker, nil}, - &knf.Validator{MAIN_RUN_GROUP, groupChecker, nil}, - - &knf.Validator{PATHS_WORKING_DIR, permsChecker, "DRWX"}, - &knf.Validator{PATHS_HELPER_DIR, permsChecker, "DRWX"}, - } - - if knf.GetB(LOG_ENABLED, true) { - validators = append(validators, - &knf.Validator{LOG_DIR, knf.Empty, nil}, - &knf.Validator{LOG_FILE, knf.Empty, nil}, - &knf.Validator{LOG_DIR, permsChecker, "DWX"}, - ) - } - - errs := knf.Validate(validators) - - if len(errs) != 0 { - fmt.Println("Errors while config validation:") - - for _, err := range errs { - fmt.Printf(" %v\n", err) - } - - os.Exit(1) - } -} - -// setupLogger configure logging subsystem -func setupLogger() { - if !knf.GetB(LOG_ENABLED, true) { - log.Set(os.DevNull, 0) - return - } - - log.Set(knf.GetS(LOG_FILE), knf.GetM(LOG_PERMS, 0644)) - log.MinLevel(knf.GetS(LOG_LEVEL, "info")) -} - -// startProcessing start processing -func startProcessing(appName string) { - if !arg.GetB(ARG_UNINSTALL) { - installApplication(appName) - } else { - uninstallApplication(appName) - } -} - -// installApplication install application to init system -func installApplication(appName string) { - fullAppName := knf.GetS(MAIN_PREFIX) + appName - app, err := procfile.Read( - arg.GetS(ARG_PROCFILE), - &procfile.Config{ - Name: fullAppName, - User: knf.GetS(MAIN_RUN_USER), - Group: knf.GetS(MAIN_RUN_GROUP), - WorkingDir: knf.GetS(PATHS_WORKING_DIR), - }, - ) - - if err != nil { - printErrorAndExit(err.Error()) - } - - if arg.GetB(ARG_DRY_START) { - os.Exit(0) - } - - err = getExporter().Install(app) - - if err == nil { - log.Aux("User %s (%d) installed service %s", user.RealName, user.RealUID, app.Name) - } else { - printErrorAndExit(err.Error()) - } -} - -// uninstallApplication uninstall application from init system -func uninstallApplication(appName string) { - fullAppName := knf.GetS(MAIN_PREFIX) + appName - app := &procfile.Application{Name: fullAppName} - - err := getExporter().Uninstall(app) - - if err == nil { - log.Aux("User %s (%d) uninstalled service %s", user.RealName, user.RealUID, app.Name) - } else { - printErrorAndExit(err.Error()) - } -} - -// checkProviderTargetDir check permissions on target dir -func checkProviderTargetDir(dir string) error { - if !fsutil.CheckPerms("DRWX", dir) { - return fmt.Errorf("This utility require read/write access to directory %s", dir) - } - - return nil -} - -// getExporter create and configure exporter and return it -func getExporter() *export.Exporter { - providerName, err := detectProvider(arg.GetS(ARG_FORMAT)) - - if err != nil { - printErrorAndExit(err.Error()) - } - - var provider export.Provider - - exportConfig := &export.Config{HelperDir: knf.GetS(PATHS_HELPER_DIR)} - - switch providerName { - case FORMAT_UPSTART: - exportConfig.TargetDir = knf.GetS(PATHS_UPSTART_DIR) - provider = export.NewUpstart() - case FORMAT_SYSTEMD: - exportConfig.TargetDir = knf.GetS(PATHS_SYSTEMD_DIR) - provider = export.NewSystemd() - } - - err = checkProviderTargetDir(exportConfig.TargetDir) - - if err != nil { - printErrorAndExit(err.Error()) - } - - return export.NewExporter(exportConfig, provider) -} - -// detectProvider try to detect provider -func detectProvider(format string) (string, error) { - switch { - case format == FORMAT_SYSTEMD: - return FORMAT_SYSTEMD, nil - case format == FORMAT_UPSTART: - return FORMAT_UPSTART, nil - case os.Args[0] == "systemd-exporter": - return FORMAT_SYSTEMD, nil - case os.Args[0] == "upstart-exporter": - return FORMAT_UPSTART, nil - case env.Which("systemctl") != "": - return FORMAT_SYSTEMD, nil - case env.Which("initctl") != "": - return FORMAT_UPSTART, nil - default: - return "", fmt.Errorf("Can't find init provider") - } -} - -// printErrorAndExit print error mesage and exit with exit code 1 -func printErrorAndExit(message string, a ...interface{}) { - log.Crit(message) - fmt.Printf(message+"\n", a...) - os.Exit(1) -} - -// ////////////////////////////////////////////////////////////////////////////////// // - -// showUsage print usage info to console -func showUsage() { - info := usage.NewInfo("", "app-name") - - info.AddOption(ARG_CONFIG, "Path to config file", "file") - info.AddOption(ARG_PROCFILE, "Path to procfile", "file") - info.AddOption(ARG_DRY_START, "Dry start (don't export anything, just parse and test procfile)") - info.AddOption(ARG_UNINSTALL, "Remove scripts and helpers for a particular application") - info.AddOption(ARG_FORMAT, "Format of generated configs", "upstart|systemd") - info.AddOption(ARG_HELP, "Show this help message") - info.AddOption(ARG_VERSION, "Show version") - - info.AddExample("-p ./myprocfile -f systemd myapp", "Export given procfile to systemd as myapp") - info.AddExample("-u -f systemd myapp", "Uninstall myapp from systemd") - - info.AddExample("-p ./myprocfile -f upstart myapp", "Export given procfile to upstart as myapp") - info.AddExample("-u -f upstart myapp", "Uninstall myapp from upstart") - - 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() + CLI.Init() } diff --git a/procfile/procfile.go b/procfile/procfile.go index 53dbf03..d013939 100644 --- a/procfile/procfile.go +++ b/procfile/procfile.go @@ -17,10 +17,10 @@ import ( "github.com/smallfish/simpleyaml" - "pkg.re/essentialkaos/ek.v1/errutil" - "pkg.re/essentialkaos/ek.v1/fsutil" - "pkg.re/essentialkaos/ek.v1/log" - "pkg.re/essentialkaos/ek.v1/path" + "pkg.re/essentialkaos/ek.v5/errutil" + "pkg.re/essentialkaos/ek.v5/fsutil" + "pkg.re/essentialkaos/ek.v5/log" + "pkg.re/essentialkaos/ek.v5/path" ) // ////////////////////////////////////////////////////////////////////////////////// // From 94a3a090869a54e567a1f7176bdfbc258931761d Mon Sep 17 00:00:00 2001 From: Anton Novojilov Date: Mon, 31 Oct 2016 08:48:43 -0400 Subject: [PATCH 13/31] Added TravisCI badge --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index 5186079..e229c4d 100644 --- a/readme.md +++ b/readme.md @@ -1,4 +1,4 @@ -## `init-exporter` +## `init-exporter` [![Build Status](https://travis-ci.org/funbox/init-exporter.svg?branch=master)](https://travis-ci.org/funbox/init-exporter) Utility for exporting services described by Procfile to init system. Supported init systems: upstart and systemd From 3c9bc6a0f940ab9d4d2abc10181494c39790584d Mon Sep 17 00:00:00 2001 From: Anton Novojilov Date: Mon, 5 Dec 2016 06:57:01 -0500 Subject: [PATCH 14/31] Added glide config --- glide.yaml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 glide.yaml diff --git a/glide.yaml b/glide.yaml new file mode 100644 index 0000000..c404666 --- /dev/null +++ b/glide.yaml @@ -0,0 +1,19 @@ +package: github.com/funbox/init-exporter +import: +- package: github.com/smallfish/simpleyaml +- package: pkg.re/essentialkaos/ek.v5 + version: ^5.5.0 + subpackages: + - arg + - env + - errutil + - fmtc + - fsutil + - knf + - log + - path + - system + - timeutil + - usage +testImport: +- package: pkg.re/check.v1 From ee2e67e586885ee99968568712f135f35bc9cf3d Mon Sep 17 00:00:00 2001 From: Anton Novojilov Date: Fri, 9 Dec 2016 08:31:00 -0500 Subject: [PATCH 15/31] Added glide.lock and .gitignore --- .gitignore | 2 ++ glide.lock | 25 +++++++++++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 .gitignore create mode 100644 glide.lock diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d0f8e90 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +vendor +*~ diff --git a/glide.lock b/glide.lock new file mode 100644 index 0000000..3d86e27 --- /dev/null +++ b/glide.lock @@ -0,0 +1,25 @@ +hash: ef8374368ab40975d7203df88155c34ddbd94be207e6bfb90b20e2b497b038c8 +updated: 2016-12-09T08:29:17.649595279-05:00 +imports: +- name: github.com/smallfish/simpleyaml + version: e0865b8fe0bb302895f2862494f1d5b2f471face +- name: gopkg.in/yaml.v2 + version: a5b47d31c556af34a302ce5d659e6fea44d90de0 +- name: pkg.re/essentialkaos/ek.v5 + version: 1c71efbd9d6a5dad9aa2e30a279c5bcfa7769970 + subpackages: + - arg + - env + - errutil + - fmtc + - fsutil + - knf + - log + - path + - pluralize + - system + - timeutil + - usage +testImports: +- name: pkg.re/check.v1 + version: 20d25e2804050c1cd24a7eea1e7a6447dd0e74ec From 1172b0f6e91e98d323fd1f9edfd1d8a7afbe28a1 Mon Sep 17 00:00:00 2001 From: Anton Novojilov Date: Fri, 9 Dec 2016 08:51:07 -0500 Subject: [PATCH 16/31] Improved Makefile --- Makefile | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index e7dc018..55c8d17 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ PREFIX?=/usr ######################################################################################## -.PHONY = all clean install uninstall deps test +.PHONY = all clean install uninstall deps deps-glide test ######################################################################################## @@ -17,6 +17,9 @@ deps: go get -v github.com/smallfish/simpleyaml go get -v gopkg.in/yaml.v2 +deps-glide: + glide install + bin: go build init-exporter.go From 07287c8814e7ce785731c5e30b5c1069679cd0d1 Mon Sep 17 00:00:00 2001 From: Anton Novojilov Date: Fri, 9 Dec 2016 09:09:04 -0500 Subject: [PATCH 17/31] Added spec file --- common/init-exporter.spec | 109 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 common/init-exporter.spec diff --git a/common/init-exporter.spec b/common/init-exporter.spec new file mode 100644 index 0000000..7d8a9c4 --- /dev/null +++ b/common/init-exporter.spec @@ -0,0 +1,109 @@ +############################################################################### + +# rpmbuilder:relative-pack true + +############################################################################### + +%define _posixroot / +%define _root /root +%define _bin /bin +%define _sbin /sbin +%define _srv /srv +%define _home /home +%define _lib32 %{_posixroot}lib +%define _lib64 %{_posixroot}lib64 +%define _libdir32 %{_prefix}%{_lib32} +%define _libdir64 %{_prefix}%{_lib64} +%define _logdir %{_localstatedir}/log +%define _rundir %{_localstatedir}/run +%define _lockdir %{_localstatedir}/lock/subsys +%define _cachedir %{_localstatedir}/cache +%define _spooldir %{_localstatedir}/spool +%define _crondir %{_sysconfdir}/cron.d +%define _loc_prefix %{_prefix}/local +%define _loc_exec_prefix %{_loc_prefix} +%define _loc_bindir %{_loc_exec_prefix}/bin +%define _loc_libdir %{_loc_exec_prefix}/%{_lib} +%define _loc_libdir32 %{_loc_exec_prefix}/%{_lib32} +%define _loc_libdir64 %{_loc_exec_prefix}/%{_lib64} +%define _loc_libexecdir %{_loc_exec_prefix}/libexec +%define _loc_sbindir %{_loc_exec_prefix}/sbin +%define _loc_bindir %{_loc_exec_prefix}/bin +%define _loc_datarootdir %{_loc_prefix}/share +%define _loc_includedir %{_loc_prefix}/include +%define _rpmstatedir %{_sharedstatedir}/rpm-state +%define _pkgconfigdir %{_libdir}/pkgconfig + +############################################################################### + +%define debug_package %{nil} + +############################################################################### + +Summary: Utility for exporting services described by Procfile to init system +Name: init-exporter +Version: 0.3.0 +Release: 0%{?dist} +Group: Development/Tools +License: MIT +URL: http://github.com/andyone/init-exporter + +Source0: %{name}-%{version}.tar.gz + +BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root-%(%{__id_u} -n) + +BuildRequires: golang >= 1.7 + +Provides: upstart-exporter = %{version}-%{release} +Provides: systemd-exporter = %{version}-%{release} + +Provides: %{name} = %{version}-%{release} + +############################################################################### + +%description +Utility for exporting services described by Procfile to init system. + +############################################################################### + +%prep +%setup -q + +%build +export GOPATH=$(pwd) +go build -o %{name} src/github.com/funbox/%{name}/%{name}.go + +%install +rm -rf %{buildroot} + +install -dm 755 %{buildroot}%{_bindir} +install -dm 755 %{buildroot}%{_sysconfdir} +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}/ + +ln -sf %{_bindir}/%{name} %{buildroot}%{_bindir}/upstart-exporter +ln -sf %{_bindir}/%{name} %{buildroot}%{_bindir}/systemd-exporter + +install -pm 755 src/github.com/funbox/%{name}/common/%{name}.conf \ + %{buildroot}%{_sysconfdir}/ + +%clean +rm -rf %{buildroot} + +############################################################################### + +%files +%defattr(-,root,root,-) +%config(noreplace) %{_sysconfdir}/%{name}.conf +%dir %{_logdir}/%{name} +%dir %{_localstatedir}/local/%{name}/helpers +%{_bindir}/*-exporter + +############################################################################### + +%changelog +* Fri Dec 09 2016 Anton Novojilov - 0.3.0-0 +- Initial build From d42dc10a5fb9b595a391bc14dc2ea32dfae35eca Mon Sep 17 00:00:00 2001 From: Anton Novojilov Date: Thu, 29 Dec 2016 07:14:52 -0500 Subject: [PATCH 18/31] Improved procfile v1 parsing + ek package updated to v6 + minor fixes --- Makefile | 4 +-- cli/cli.go | 30 ++++++++++++++++------- export/export_test.go | 20 +++++++-------- export/exporter.go | 6 ++--- export/provider.go | 2 +- export/systemd.go | 11 +++++---- export/upstart.go | 9 ++++--- glide.lock | 10 ++++---- glide.yaml | 4 +-- procfile/procfile.go | 51 +++++++++++++++++++++++++++++++++++---- procfile/procfile_test.go | 11 ++++++++- testdata/procfile_v1 | 1 + 12 files changed, 112 insertions(+), 47 deletions(-) diff --git a/Makefile b/Makefile index 55c8d17..8851ddc 100644 --- a/Makefile +++ b/Makefile @@ -9,11 +9,11 @@ PREFIX?=/usr ######################################################################################## -all: deps bin +all: bin deps: go get -v pkg.re/check.v1 - go get -v pkg.re/essentialkaos/ek.v5 + go get -v pkg.re/essentialkaos/ek.v6 go get -v github.com/smallfish/simpleyaml go get -v gopkg.in/yaml.v2 diff --git a/cli/cli.go b/cli/cli.go index f103c72..35cf868 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -11,14 +11,14 @@ import ( "os" "runtime" - "pkg.re/essentialkaos/ek.v5/arg" - "pkg.re/essentialkaos/ek.v5/env" - "pkg.re/essentialkaos/ek.v5/fmtc" - "pkg.re/essentialkaos/ek.v5/fsutil" - "pkg.re/essentialkaos/ek.v5/knf" - "pkg.re/essentialkaos/ek.v5/log" - "pkg.re/essentialkaos/ek.v5/system" - "pkg.re/essentialkaos/ek.v5/usage" + "pkg.re/essentialkaos/ek.v6/arg" + "pkg.re/essentialkaos/ek.v6/env" + "pkg.re/essentialkaos/ek.v6/fmtc" + "pkg.re/essentialkaos/ek.v6/fsutil" + "pkg.re/essentialkaos/ek.v6/knf" + "pkg.re/essentialkaos/ek.v6/log" + "pkg.re/essentialkaos/ek.v6/system" + "pkg.re/essentialkaos/ek.v6/usage" "github.com/funbox/init-exporter/export" "github.com/funbox/init-exporter/procfile" @@ -29,7 +29,7 @@ import ( // App props const ( APP = "init-exporter" - VER = "0.3.0" + VER = "0.4.0" DESC = "Utility for exporting services described by Procfile to init system" ) @@ -288,6 +288,7 @@ func startProcessing(appName string) { // installApplication install application to init system func installApplication(appName string) { fullAppName := knf.GetS(MAIN_PREFIX) + appName + app, err := procfile.Read( arg.GetS(ARG_PROCFILE), &procfile.Config{ @@ -302,6 +303,8 @@ func installApplication(appName string) { printErrorAndExit(err.Error()) } + checkProviderCompatibility(app) + if arg.GetB(ARG_DRY_START) { os.Exit(0) } @@ -329,6 +332,15 @@ func uninstallApplication(appName string) { } } +// checkProviderCompatibility check provider and procfile compatibility +func checkProviderCompatibility(app *procfile.Application) { + providerName, _ := detectProvider(arg.GetS(ARG_FORMAT)) + + if providerName == FORMAT_SYSTEMD && app.ProcVersion == 1 { + printErrorAndExit("Systemd export doesn't support v1 procfiles") + } +} + // checkProviderTargetDir check permissions on target dir func checkProviderTargetDir(dir string) error { if !fsutil.CheckPerms("DRWX", dir) { diff --git a/export/export_test.go b/export/export_test.go index 61f6779..b98a68d 100644 --- a/export/export_test.go +++ b/export/export_test.go @@ -15,8 +15,8 @@ import ( "github.com/funbox/init-exporter/procfile" - "pkg.re/essentialkaos/ek.v5/fsutil" - "pkg.re/essentialkaos/ek.v5/log" + "pkg.re/essentialkaos/ek.v6/fsutil" + "pkg.re/essentialkaos/ek.v6/log" . "pkg.re/check.v1" ) @@ -115,8 +115,8 @@ func (s *ExportSuite) TestUpstartExport(c *C) { c.Assert(appUnit, HasLen, 16) c.Assert(service1Unit, HasLen, 18) c.Assert(service2Unit, HasLen, 18) - c.Assert(service1Helper, HasLen, 7) - c.Assert(service2Helper, HasLen, 7) + c.Assert(service1Helper, HasLen, 8) + c.Assert(service2Helper, HasLen, 8) c.Assert(appUnit[2:], DeepEquals, []string{ @@ -175,14 +175,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", + "[[ -r /etc/profile.d/rbenv.sh ]] && source /etc/profile.d/rbenv.sh", "", "cd /srv/service/service1-dir && exec STAGING=true /bin/echo service1", ""}, ) c.Assert(service2Helper[4:], DeepEquals, []string{ - "[[ -r /etc/profile.d/rbenv.sh ]] && source /etc/profile.d/rbenv.sh", + "[[ -r /etc/profile.d/rbenv.sh ]] && source /etc/profile.d/rbenv.sh", "", "cd /srv/service/working-dir && exec /bin/echo service2", ""}, ) @@ -272,8 +272,8 @@ func (s *ExportSuite) TestSystemdExport(c *C) { c.Assert(appUnit, HasLen, 22) c.Assert(service1Unit, HasLen, 26) c.Assert(service2Unit, HasLen, 26) - c.Assert(service1Helper, HasLen, 7) - c.Assert(service2Helper, HasLen, 7) + c.Assert(service1Helper, HasLen, 8) + c.Assert(service2Helper, HasLen, 8) c.Assert(appUnit[2:], DeepEquals, []string{ @@ -356,14 +356,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", + "[[ -r /etc/profile.d/rbenv.sh ]] && source /etc/profile.d/rbenv.sh", "", "exec /bin/echo service1", ""}, ) c.Assert(service2Helper[4:], DeepEquals, []string{ - "[[ -r /etc/profile.d/rbenv.sh ]] && source /etc/profile.d/rbenv.sh", + "[[ -r /etc/profile.d/rbenv.sh ]] && source /etc/profile.d/rbenv.sh", "", "exec /bin/echo service2", ""}, ) diff --git a/export/exporter.go b/export/exporter.go index 7f54576..0fa4c2f 100644 --- a/export/exporter.go +++ b/export/exporter.go @@ -11,9 +11,9 @@ import ( "io/ioutil" "os" - "pkg.re/essentialkaos/ek.v5/fsutil" - "pkg.re/essentialkaos/ek.v5/log" - "pkg.re/essentialkaos/ek.v5/path" + "pkg.re/essentialkaos/ek.v6/fsutil" + "pkg.re/essentialkaos/ek.v6/log" + "pkg.re/essentialkaos/ek.v6/path" "github.com/funbox/init-exporter/procfile" ) diff --git a/export/provider.go b/export/provider.go index dbf6c1f..ce3fcf6 100644 --- a/export/provider.go +++ b/export/provider.go @@ -11,7 +11,7 @@ import ( "fmt" "text/template" - "pkg.re/essentialkaos/ek.v5/log" + "pkg.re/essentialkaos/ek.v6/log" "github.com/funbox/init-exporter/procfile" ) diff --git a/export/systemd.go b/export/systemd.go index bd19751..967b270 100644 --- a/export/systemd.go +++ b/export/systemd.go @@ -10,8 +10,8 @@ import ( "strings" "time" - "pkg.re/essentialkaos/ek.v5/system" - "pkg.re/essentialkaos/ek.v5/timeutil" + "pkg.re/essentialkaos/ek.v6/system" + "pkg.re/essentialkaos/ek.v6/timeutil" "github.com/funbox/init-exporter/procfile" ) @@ -26,14 +26,15 @@ type SystemdProvider struct{} // TEMPLATE_SYSTEMD_HELPER contains default helper template const TEMPLATE_SYSTEMD_HELPER = `#!/bin/bash -# This helper generated {{.ExportDate}} by init-exporter for {{.Application.Name}} application +# This helper generated {{.ExportDate}} by init-exporter/systemd for {{.Application.Name}} application [[ -r /etc/profile.d/rbenv.sh ]] && source /etc/profile.d/rbenv.sh + exec {{.Service.Cmd}} ` // TEMPLATE_SYSTEMD_APP contains default application template -const TEMPLATE_SYSTEMD_APP = `# This unit generated {{.ExportDate}} by init-exporter for {{.Application.Name}} application +const TEMPLATE_SYSTEMD_APP = `# This unit generated {{.ExportDate}} by init-exporter/systemd for {{.Application.Name}} application [Unit] @@ -57,7 +58,7 @@ WantedBy={{.StartLevel}} ` // TEMPLATE_SYSTEMD_SERVICE contains default service template -const TEMPLATE_SYSTEMD_SERVICE = `# This unit generated {{.ExportDate}} by init-exporter for {{.Application.Name}} application +const TEMPLATE_SYSTEMD_SERVICE = `# This unit generated {{.ExportDate}} by init-exporter/systemd for {{.Application.Name}} application [Unit] diff --git a/export/upstart.go b/export/upstart.go index 7ec8773..c5e2d23 100644 --- a/export/upstart.go +++ b/export/upstart.go @@ -10,7 +10,7 @@ import ( "fmt" "time" - "pkg.re/essentialkaos/ek.v5/timeutil" + "pkg.re/essentialkaos/ek.v6/timeutil" "github.com/funbox/init-exporter/procfile" ) @@ -25,14 +25,15 @@ type UpstartProvider struct{} // TEMPLATE_UPSTART_HELPER contains default helper template const TEMPLATE_UPSTART_HELPER = `#!/bin/bash -# This helper generated {{.ExportDate}} by init-exporter for {{.Application.Name}} application +# This helper generated {{.ExportDate}} by init-exporter/upstart for {{.Application.Name}} application [[ -r /etc/profile.d/rbenv.sh ]] && source /etc/profile.d/rbenv.sh + cd {{.Service.Options.WorkingDir}} && exec {{ if .Service.Options.EnvSet }}{{.Service.Options.EnvString}} {{ end }}{{.Service.Cmd}} ` // TEMPLATE_UPSTART_APP contains default application template -const TEMPLATE_UPSTART_APP = `# This unit generated {{.ExportDate}} by init-exporter for {{.Application.Name}} application +const TEMPLATE_UPSTART_APP = `# This unit generated {{.ExportDate}} by init-exporter/upstart for {{.Application.Name}} application start on {{.StartLevel}} stop on {{.StopLevel}} @@ -50,7 +51,7 @@ end script ` // TEMPLATE_UPSTART_SERVICE contains default service template -const TEMPLATE_UPSTART_SERVICE = `# This unit generated {{.ExportDate}} by init-exporter for {{.Application.Name}} application +const TEMPLATE_UPSTART_SERVICE = `# This unit generated {{.ExportDate}} by init-exporter/upstart for {{.Application.Name}} application start on {{.StartLevel}} stop on {{.StopLevel}} diff --git a/glide.lock b/glide.lock index 3d86e27..fee9e1b 100644 --- a/glide.lock +++ b/glide.lock @@ -1,12 +1,12 @@ -hash: ef8374368ab40975d7203df88155c34ddbd94be207e6bfb90b20e2b497b038c8 -updated: 2016-12-09T08:29:17.649595279-05:00 +hash: 550842432f54d716dce1563e1f350df3c9e95dbbc940dee29d7b3f3bdbee05b2 +updated: 2016-12-28T16:30:15.622859378-05:00 imports: - name: github.com/smallfish/simpleyaml - version: e0865b8fe0bb302895f2862494f1d5b2f471face + version: f8280fef35a3fd1d4cf13c5a9fa638398eebb6ad - name: gopkg.in/yaml.v2 version: a5b47d31c556af34a302ce5d659e6fea44d90de0 -- name: pkg.re/essentialkaos/ek.v5 - version: 1c71efbd9d6a5dad9aa2e30a279c5bcfa7769970 +- name: pkg.re/essentialkaos/ek.v6 + version: c292a0da0a81e93c1161a291370c87369db8fc2c subpackages: - arg - env diff --git a/glide.yaml b/glide.yaml index c404666..af0e6b8 100644 --- a/glide.yaml +++ b/glide.yaml @@ -1,8 +1,8 @@ package: github.com/funbox/init-exporter import: - package: github.com/smallfish/simpleyaml -- package: pkg.re/essentialkaos/ek.v5 - version: ^5.5.0 +- package: pkg.re/essentialkaos/ek.v6 + version: ^6.0.0 subpackages: - arg - env diff --git a/procfile/procfile.go b/procfile/procfile.go index d013939..d82e710 100644 --- a/procfile/procfile.go +++ b/procfile/procfile.go @@ -17,10 +17,10 @@ import ( "github.com/smallfish/simpleyaml" - "pkg.re/essentialkaos/ek.v5/errutil" - "pkg.re/essentialkaos/ek.v5/fsutil" - "pkg.re/essentialkaos/ek.v5/log" - "pkg.re/essentialkaos/ek.v5/path" + "pkg.re/essentialkaos/ek.v6/errutil" + "pkg.re/essentialkaos/ek.v6/fsutil" + "pkg.re/essentialkaos/ek.v6/log" + "pkg.re/essentialkaos/ek.v6/path" ) // ////////////////////////////////////////////////////////////////////////////////// // @@ -254,7 +254,48 @@ func parseV1Line(line string) (*Service, error) { return nil, fmt.Errorf("Procfile v1 should have format: 'some_label: command'") } - return &Service{Name: matches[1], Cmd: matches[2], Options: &ServiceOptions{}}, nil + 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 diff --git a/procfile/procfile_test.go b/procfile/procfile_test.go index fa73749..89ca30c 100644 --- a/procfile/procfile_test.go +++ b/procfile/procfile_test.go @@ -31,13 +31,22 @@ func (s *ProcfileSuite) TestProcV1Parsing(c *C) { c.Assert(app, NotNil) c.Assert(app.ProcVersion, Equals, 1) - c.Assert(app.Services, HasLen, 2) + c.Assert(app.Services, HasLen, 3) 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[1].Name, Equals, "my_another_tail_cmd") c.Assert(app.Services[1].Cmd, Equals, "/usr/bin/tailf /var/log/messages") + 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].Options, NotNil) + c.Assert(app.Services[2].Options.Env, HasLen, 2) + c.Assert(app.Services[2].Options.Env["ENV_TEST"], Equals, "100") + c.Assert(app.Services[2].Options.Env["SOME_ENV"], Equals, "test") + c.Assert(app.Services[2].Options.WorkingDir, Equals, "/srv/service") + c.Assert(app.Validate(), IsNil) } diff --git a/testdata/procfile_v1 b/testdata/procfile_v1 index aba1251..e09077a 100644 --- a/testdata/procfile_v1 +++ b/testdata/procfile_v1 @@ -1,2 +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 From 2ad46cd1a52c52ad5890d68112f83e74a3193c95 Mon Sep 17 00:00:00 2001 From: Anton Novojilov Date: Thu, 29 Dec 2016 07:21:42 -0500 Subject: [PATCH 19/31] With new parser we can export data to systemd from v1 procfiles --- cli/cli.go | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/cli/cli.go b/cli/cli.go index 35cf868..235d224 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -303,8 +303,6 @@ func installApplication(appName string) { printErrorAndExit(err.Error()) } - checkProviderCompatibility(app) - if arg.GetB(ARG_DRY_START) { os.Exit(0) } @@ -332,15 +330,6 @@ func uninstallApplication(appName string) { } } -// checkProviderCompatibility check provider and procfile compatibility -func checkProviderCompatibility(app *procfile.Application) { - providerName, _ := detectProvider(arg.GetS(ARG_FORMAT)) - - if providerName == FORMAT_SYSTEMD && app.ProcVersion == 1 { - printErrorAndExit("Systemd export doesn't support v1 procfiles") - } -} - // checkProviderTargetDir check permissions on target dir func checkProviderTargetDir(dir string) error { if !fsutil.CheckPerms("DRWX", dir) { From 362856e3721050f9dc7f19d0b57536ab1036a2c1 Mon Sep 17 00:00:00 2001 From: Anton Novojilov Date: Tue, 10 Jan 2017 16:19:58 -0500 Subject: [PATCH 20/31] Updated copyright header --- cli/cli.go | 2 +- export/export_test.go | 2 +- export/exporter.go | 2 +- export/provider.go | 2 +- export/systemd.go | 2 +- export/upstart.go | 2 +- init-exporter.go | 2 +- procfile/procfile.go | 2 +- procfile/procfile_test.go | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/cli/cli.go b/cli/cli.go index 235d224..f64580a 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -2,7 +2,7 @@ package cli // ////////////////////////////////////////////////////////////////////////////////// // // // -// Copyright (c) 2006-2016 FB GROUP LLC // +// Copyright (c) 2006-2017 FB GROUP LLC // // // // ////////////////////////////////////////////////////////////////////////////////// // diff --git a/export/export_test.go b/export/export_test.go index b98a68d..3c21402 100644 --- a/export/export_test.go +++ b/export/export_test.go @@ -2,7 +2,7 @@ package export // ////////////////////////////////////////////////////////////////////////////////// // // // -// Copyright (c) 2006-2016 FB GROUP LLC // +// Copyright (c) 2006-2017 FB GROUP LLC // // // // ////////////////////////////////////////////////////////////////////////////////// // diff --git a/export/exporter.go b/export/exporter.go index 0fa4c2f..195fc31 100644 --- a/export/exporter.go +++ b/export/exporter.go @@ -2,7 +2,7 @@ package export // ////////////////////////////////////////////////////////////////////////////////// // // // -// Copyright (c) 2006-2016 FB GROUP LLC // +// Copyright (c) 2006-2017 FB GROUP LLC // // // // ////////////////////////////////////////////////////////////////////////////////// // diff --git a/export/provider.go b/export/provider.go index ce3fcf6..0af3d67 100644 --- a/export/provider.go +++ b/export/provider.go @@ -2,7 +2,7 @@ package export // ////////////////////////////////////////////////////////////////////////////////// // // // -// Copyright (c) 2006-2016 FB GROUP LLC // +// Copyright (c) 2006-2017 FB GROUP LLC // // // // ////////////////////////////////////////////////////////////////////////////////// // diff --git a/export/systemd.go b/export/systemd.go index 967b270..ec488e4 100644 --- a/export/systemd.go +++ b/export/systemd.go @@ -2,7 +2,7 @@ package export // ////////////////////////////////////////////////////////////////////////////////// // // // -// Copyright (c) 2006-2016 FB GROUP LLC // +// Copyright (c) 2006-2017 FB GROUP LLC // // // // ////////////////////////////////////////////////////////////////////////////////// // diff --git a/export/upstart.go b/export/upstart.go index c5e2d23..5f8ee2d 100644 --- a/export/upstart.go +++ b/export/upstart.go @@ -2,7 +2,7 @@ package export // ////////////////////////////////////////////////////////////////////////////////// // // // -// Copyright (c) 2006-2016 FB GROUP LLC // +// Copyright (c) 2006-2017 FB GROUP LLC // // // // ////////////////////////////////////////////////////////////////////////////////// // diff --git a/init-exporter.go b/init-exporter.go index db566fc..1650f3a 100644 --- a/init-exporter.go +++ b/init-exporter.go @@ -2,7 +2,7 @@ package main // ////////////////////////////////////////////////////////////////////////////////// // // // -// Copyright (c) 2006-2016 FB GROUP LLC // +// Copyright (c) 2006-2017 FB GROUP LLC // // // // ////////////////////////////////////////////////////////////////////////////////// // diff --git a/procfile/procfile.go b/procfile/procfile.go index d82e710..aebc2d2 100644 --- a/procfile/procfile.go +++ b/procfile/procfile.go @@ -2,7 +2,7 @@ package procfile // ////////////////////////////////////////////////////////////////////////////////// // // // -// Copyright (c) 2006-2016 FB GROUP LLC // +// Copyright (c) 2006-2017 FB GROUP LLC // // // // ////////////////////////////////////////////////////////////////////////////////// // diff --git a/procfile/procfile_test.go b/procfile/procfile_test.go index 89ca30c..e2076c0 100644 --- a/procfile/procfile_test.go +++ b/procfile/procfile_test.go @@ -2,7 +2,7 @@ package procfile // ////////////////////////////////////////////////////////////////////////////////// // // // -// Copyright (c) 2006-2016 FB GROUP LLC // +// Copyright (c) 2006-2017 FB GROUP LLC // // // // ////////////////////////////////////////////////////////////////////////////////// // From 22b134ff52bb6770b8a9c6f3446a526d9951f561 Mon Sep 17 00:00:00 2001 From: Anton Novojilov Date: Tue, 10 Jan 2017 16:34:05 -0500 Subject: [PATCH 21/31] Fixed bug with naming for upstart and systemd units --- Makefile | 2 +- cli/cli.go | 2 +- export/export_test.go | 174 +++++++++++++++++++++--------------------- export/exporter.go | 6 +- export/systemd.go | 2 +- 5 files changed, 93 insertions(+), 93 deletions(-) diff --git a/Makefile b/Makefile index 8851ddc..4845c94 100644 --- a/Makefile +++ b/Makefile @@ -27,7 +27,7 @@ fmt: find . -name "*.go" -exec gofmt -s -w {} \; test: - go test ./... + go test ./... -covermode=count install: mkdir -p $(DESTDIR)$(PREFIX)/bin diff --git a/cli/cli.go b/cli/cli.go index f64580a..bdac219 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -29,7 +29,7 @@ import ( // App props const ( APP = "init-exporter" - VER = "0.4.0" + VER = "0.4.1" DESC = "Utility for exporting services described by Procfile to init system" ) diff --git a/export/export_test.go b/export/export_test.go index 3c21402..c177dbb 100644 --- a/export/export_test.go +++ b/export/export_test.go @@ -61,47 +61,47 @@ func (s *ExportSuite) TestUpstartExport(c *C) { c.Assert(err, IsNil) - c.Assert(fsutil.IsExist(targetDir+"/test-application.conf"), Equals, true) - c.Assert(fsutil.IsRegular(targetDir+"/test-application.conf"), Equals, true) - c.Assert(fsutil.IsNonEmpty(targetDir+"/test-application.conf"), Equals, true) + c.Assert(fsutil.IsExist(targetDir+"/test_application.conf"), Equals, true) + c.Assert(fsutil.IsRegular(targetDir+"/test_application.conf"), Equals, true) + c.Assert(fsutil.IsNonEmpty(targetDir+"/test_application.conf"), Equals, true) - c.Assert(fsutil.IsExist(targetDir+"/test-application_service1.conf"), Equals, true) - c.Assert(fsutil.IsRegular(targetDir+"/test-application_service1.conf"), Equals, true) - c.Assert(fsutil.IsNonEmpty(targetDir+"/test-application_service1.conf"), Equals, true) + c.Assert(fsutil.IsExist(targetDir+"/test_application-service1.conf"), Equals, true) + c.Assert(fsutil.IsRegular(targetDir+"/test_application-service1.conf"), Equals, true) + c.Assert(fsutil.IsNonEmpty(targetDir+"/test_application-service1.conf"), Equals, true) - c.Assert(fsutil.IsExist(targetDir+"/test-application_service2.conf"), Equals, true) - c.Assert(fsutil.IsRegular(targetDir+"/test-application_service2.conf"), Equals, true) - c.Assert(fsutil.IsNonEmpty(targetDir+"/test-application_service2.conf"), Equals, true) + c.Assert(fsutil.IsExist(targetDir+"/test_application-service2.conf"), Equals, true) + c.Assert(fsutil.IsRegular(targetDir+"/test_application-service2.conf"), Equals, true) + c.Assert(fsutil.IsNonEmpty(targetDir+"/test_application-service2.conf"), Equals, true) - c.Assert(fsutil.IsExist(helperDir+"/test-application_service1.sh"), Equals, true) - c.Assert(fsutil.IsRegular(helperDir+"/test-application_service1.sh"), Equals, true) - c.Assert(fsutil.IsNonEmpty(helperDir+"/test-application_service1.sh"), Equals, true) + c.Assert(fsutil.IsExist(helperDir+"/test_application-service1.sh"), Equals, true) + c.Assert(fsutil.IsRegular(helperDir+"/test_application-service1.sh"), Equals, true) + c.Assert(fsutil.IsNonEmpty(helperDir+"/test_application-service1.sh"), Equals, true) - c.Assert(fsutil.IsExist(helperDir+"/test-application_service2.sh"), Equals, true) - c.Assert(fsutil.IsRegular(helperDir+"/test-application_service2.sh"), Equals, true) - c.Assert(fsutil.IsNonEmpty(helperDir+"/test-application_service2.sh"), Equals, true) + c.Assert(fsutil.IsExist(helperDir+"/test_application-service2.sh"), Equals, true) + c.Assert(fsutil.IsRegular(helperDir+"/test_application-service2.sh"), Equals, true) + c.Assert(fsutil.IsNonEmpty(helperDir+"/test_application-service2.sh"), Equals, true) - appUnitData, err := ioutil.ReadFile(targetDir + "/test-application.conf") + appUnitData, err := ioutil.ReadFile(targetDir + "/test_application.conf") c.Assert(err, IsNil) c.Assert(appUnitData, NotNil) - service1UnitData, err := ioutil.ReadFile(targetDir + "/test-application_service1.conf") + service1UnitData, err := ioutil.ReadFile(targetDir + "/test_application-service1.conf") c.Assert(err, IsNil) c.Assert(service1UnitData, NotNil) - service2UnitData, err := ioutil.ReadFile(targetDir + "/test-application_service2.conf") + service2UnitData, err := ioutil.ReadFile(targetDir + "/test_application-service2.conf") c.Assert(err, IsNil) c.Assert(service2UnitData, NotNil) - service1HelperData, err := ioutil.ReadFile(helperDir + "/test-application_service1.sh") + service1HelperData, err := ioutil.ReadFile(helperDir + "/test_application-service1.sh") c.Assert(err, IsNil) c.Assert(service1HelperData, NotNil) - service2HelperData, err := ioutil.ReadFile(helperDir + "/test-application_service2.sh") + service2HelperData, err := ioutil.ReadFile(helperDir + "/test_application-service2.sh") c.Assert(err, IsNil) c.Assert(service2HelperData, NotNil) @@ -126,10 +126,10 @@ func (s *ExportSuite) TestUpstartExport(c *C) { "pre-start script", "", "bash << \"EOF\"", - " mkdir -p /var/log/test-application", - " chown -R service /var/log/test-application", - " chgrp -R service /var/log/test-application", - " chmod -R g+w /var/log/test-application", + " mkdir -p /var/log/test_application", + " chown -R service /var/log/test_application", + " chgrp -R service /var/log/test_application", + " chmod -R g+w /var/log/test_application", "EOF", "", "end script", ""}, @@ -146,11 +146,11 @@ func (s *ExportSuite) TestUpstartExport(c *C) { "kill timeout 10", "", "script", - " touch /var/log/test-application/service1.log", - " chown service /var/log/test-application/service1.log", - " chgrp service /var/log/test-application/service1.log", - " chmod g+w /var/log/test-application/service1.log", - fmt.Sprintf(" exec sudo -u service /bin/bash %s/test-application_service1.sh >> /srv/service/service1-dir/custom.log >> /var/log/test-application/service1.log 2>&1", helperDir), + " touch /var/log/test_application/service1.log", + " chown service /var/log/test_application/service1.log", + " chgrp service /var/log/test_application/service1.log", + " chmod g+w /var/log/test_application/service1.log", + fmt.Sprintf(" exec sudo -u service /bin/bash %s/test_application-service1.sh >> /srv/service/service1-dir/custom.log >> /var/log/test_application/service1.log 2>&1", helperDir), "end script", ""}, ) @@ -165,11 +165,11 @@ func (s *ExportSuite) TestUpstartExport(c *C) { "kill timeout 0", "", "script", - " touch /var/log/test-application/service2.log", - " chown service /var/log/test-application/service2.log", - " chgrp service /var/log/test-application/service2.log", - " chmod g+w /var/log/test-application/service2.log", - fmt.Sprintf(" exec sudo -u service /bin/bash %s/test-application_service2.sh >> /var/log/test-application/service2.log 2>&1", helperDir), + " touch /var/log/test_application/service2.log", + " chown service /var/log/test_application/service2.log", + " chgrp service /var/log/test_application/service2.log", + " chmod g+w /var/log/test_application/service2.log", + fmt.Sprintf(" exec sudo -u service /bin/bash %s/test_application-service2.sh >> /var/log/test_application/service2.log 2>&1", helperDir), "end script", ""}, ) @@ -191,11 +191,11 @@ func (s *ExportSuite) TestUpstartExport(c *C) { c.Assert(err, IsNil) - c.Assert(fsutil.IsExist(targetDir+"/test-application.conf"), Equals, false) - c.Assert(fsutil.IsExist(targetDir+"/test-application_service1.conf"), Equals, false) - c.Assert(fsutil.IsExist(targetDir+"/test-application_service2.conf"), Equals, false) - c.Assert(fsutil.IsExist(helperDir+"/test-application_service1.sh"), Equals, false) - c.Assert(fsutil.IsExist(helperDir+"/test-application_service2.sh"), Equals, false) + c.Assert(fsutil.IsExist(targetDir+"/test_application.conf"), Equals, false) + c.Assert(fsutil.IsExist(targetDir+"/test_application-service1.conf"), Equals, false) + c.Assert(fsutil.IsExist(targetDir+"/test_application-service2.conf"), Equals, false) + c.Assert(fsutil.IsExist(helperDir+"/test_application-service1.sh"), Equals, false) + c.Assert(fsutil.IsExist(helperDir+"/test_application-service2.sh"), Equals, false) } func (s *ExportSuite) TestSystemdExport(c *C) { @@ -218,47 +218,47 @@ func (s *ExportSuite) TestSystemdExport(c *C) { c.Assert(err, IsNil) - c.Assert(fsutil.IsExist(targetDir+"/test-application.service"), Equals, true) - c.Assert(fsutil.IsRegular(targetDir+"/test-application.service"), Equals, true) - c.Assert(fsutil.IsNonEmpty(targetDir+"/test-application.service"), Equals, true) + c.Assert(fsutil.IsExist(targetDir+"/test_application.service"), Equals, true) + c.Assert(fsutil.IsRegular(targetDir+"/test_application.service"), Equals, true) + c.Assert(fsutil.IsNonEmpty(targetDir+"/test_application.service"), Equals, true) - c.Assert(fsutil.IsExist(targetDir+"/test-application_service1.service"), Equals, true) - c.Assert(fsutil.IsRegular(targetDir+"/test-application_service1.service"), Equals, true) - c.Assert(fsutil.IsNonEmpty(targetDir+"/test-application_service1.service"), Equals, true) + c.Assert(fsutil.IsExist(targetDir+"/test_application-service1.service"), Equals, true) + c.Assert(fsutil.IsRegular(targetDir+"/test_application-service1.service"), Equals, true) + c.Assert(fsutil.IsNonEmpty(targetDir+"/test_application-service1.service"), Equals, true) - c.Assert(fsutil.IsExist(targetDir+"/test-application_service2.service"), Equals, true) - c.Assert(fsutil.IsRegular(targetDir+"/test-application_service2.service"), Equals, true) - c.Assert(fsutil.IsNonEmpty(targetDir+"/test-application_service2.service"), Equals, true) + c.Assert(fsutil.IsExist(targetDir+"/test_application-service2.service"), Equals, true) + c.Assert(fsutil.IsRegular(targetDir+"/test_application-service2.service"), Equals, true) + c.Assert(fsutil.IsNonEmpty(targetDir+"/test_application-service2.service"), Equals, true) - c.Assert(fsutil.IsExist(helperDir+"/test-application_service1.sh"), Equals, true) - c.Assert(fsutil.IsRegular(helperDir+"/test-application_service1.sh"), Equals, true) - c.Assert(fsutil.IsNonEmpty(helperDir+"/test-application_service1.sh"), Equals, true) + c.Assert(fsutil.IsExist(helperDir+"/test_application-service1.sh"), Equals, true) + c.Assert(fsutil.IsRegular(helperDir+"/test_application-service1.sh"), Equals, true) + c.Assert(fsutil.IsNonEmpty(helperDir+"/test_application-service1.sh"), Equals, true) - c.Assert(fsutil.IsExist(helperDir+"/test-application_service2.sh"), Equals, true) - c.Assert(fsutil.IsRegular(helperDir+"/test-application_service2.sh"), Equals, true) - c.Assert(fsutil.IsNonEmpty(helperDir+"/test-application_service2.sh"), Equals, true) + c.Assert(fsutil.IsExist(helperDir+"/test_application-service2.sh"), Equals, true) + c.Assert(fsutil.IsRegular(helperDir+"/test_application-service2.sh"), Equals, true) + c.Assert(fsutil.IsNonEmpty(helperDir+"/test_application-service2.sh"), Equals, true) - appUnitData, err := ioutil.ReadFile(targetDir + "/test-application.service") + appUnitData, err := ioutil.ReadFile(targetDir + "/test_application.service") c.Assert(err, IsNil) c.Assert(appUnitData, NotNil) - service1UnitData, err := ioutil.ReadFile(targetDir + "/test-application_service1.service") + service1UnitData, err := ioutil.ReadFile(targetDir + "/test_application-service1.service") c.Assert(err, IsNil) c.Assert(service1UnitData, NotNil) - service2UnitData, err := ioutil.ReadFile(targetDir + "/test-application_service2.service") + service2UnitData, err := ioutil.ReadFile(targetDir + "/test_application-service2.service") c.Assert(err, IsNil) c.Assert(service2UnitData, NotNil) - service1HelperData, err := ioutil.ReadFile(helperDir + "/test-application_service1.sh") + service1HelperData, err := ioutil.ReadFile(helperDir + "/test_application-service1.sh") c.Assert(err, IsNil) c.Assert(service1HelperData, NotNil) - service2HelperData, err := ioutil.ReadFile(helperDir + "/test-application_service2.sh") + service2HelperData, err := ioutil.ReadFile(helperDir + "/test_application-service2.sh") c.Assert(err, IsNil) c.Assert(service2HelperData, NotNil) @@ -279,20 +279,20 @@ func (s *ExportSuite) TestSystemdExport(c *C) { []string{ "[Unit]", "", - "Description=Unit for test-application application", + "Description=Unit for test_application application", "After=multi-user.target", - "Wants=test-application_service1.service test-application_service2.service", + "Wants=test_application-service1.service test_application-service2.service", "", "[Service]", "Type=oneshot", "RemainAfterExit=true", "", - "ExecStartPre=/bin/mkdir -p /var/log/test-application", - "ExecStartPre=/bin/chown -R service /var/log/test-application", - "ExecStartPre=/bin/chgrp -R service /var/log/test-application", - "ExecStartPre=/bin/chmod -R g+w /var/log/test-application", - "ExecStart=/bin/echo \"test-application started\"", - "ExecStop=/bin/echo \"test-application stopped\"", + "ExecStartPre=/bin/mkdir -p /var/log/test_application", + "ExecStartPre=/bin/chown -R service /var/log/test_application", + "ExecStartPre=/bin/chgrp -R service /var/log/test_application", + "ExecStartPre=/bin/chmod -R g+w /var/log/test_application", + "ExecStart=/bin/echo \"test_application started\"", + "ExecStop=/bin/echo \"test_application stopped\"", "", "[Install]", "WantedBy=multi-user.target", ""}, @@ -302,8 +302,8 @@ func (s *ExportSuite) TestSystemdExport(c *C) { []string{ "[Unit]", "", - "Description=Unit for service1 service (part of test-application application)", - "PartOf=test-application.service", + "Description=Unit for service1 service (part of test_application application)", + "PartOf=test_application.service", "", "[Service]", "Type=simple", @@ -313,16 +313,16 @@ func (s *ExportSuite) TestSystemdExport(c *C) { "StartLimitInterval=25", "StartLimitBurst=15", "", - "ExecStartPre=/bin/touch /var/log/test-application/service1.log", - "ExecStartPre=/bin/chown service /var/log/test-application/service1.log", - "ExecStartPre=/bin/chgrp service /var/log/test-application/service1.log", - "ExecStartPre=/bin/chmod g+w /var/log/test-application/service1.log", + "ExecStartPre=/bin/touch /var/log/test_application/service1.log", + "ExecStartPre=/bin/chown service /var/log/test_application/service1.log", + "ExecStartPre=/bin/chgrp service /var/log/test_application/service1.log", + "ExecStartPre=/bin/chmod g+w /var/log/test_application/service1.log", "", "User=service", "Group=service", "WorkingDirectory=/srv/service/service1-dir", "Environment=STAGING=true", - fmt.Sprintf("ExecStart=/bin/bash %s/test-application_service1.sh >> /srv/service/service1-dir/custom.log >> /var/log/test-application/service1.log 2>&1", helperDir), + fmt.Sprintf("ExecStart=/bin/bash %s/test_application-service1.sh >> /srv/service/service1-dir/custom.log >> /var/log/test_application/service1.log 2>&1", helperDir), ""}, ) @@ -330,8 +330,8 @@ func (s *ExportSuite) TestSystemdExport(c *C) { []string{ "[Unit]", "", - "Description=Unit for service2 service (part of test-application application)", - "PartOf=test-application.service", + "Description=Unit for service2 service (part of test_application application)", + "PartOf=test_application.service", "", "[Service]", "Type=simple", @@ -341,16 +341,16 @@ func (s *ExportSuite) TestSystemdExport(c *C) { "", "", "", - "ExecStartPre=/bin/touch /var/log/test-application/service2.log", - "ExecStartPre=/bin/chown service /var/log/test-application/service2.log", - "ExecStartPre=/bin/chgrp service /var/log/test-application/service2.log", - "ExecStartPre=/bin/chmod g+w /var/log/test-application/service2.log", + "ExecStartPre=/bin/touch /var/log/test_application/service2.log", + "ExecStartPre=/bin/chown service /var/log/test_application/service2.log", + "ExecStartPre=/bin/chgrp service /var/log/test_application/service2.log", + "ExecStartPre=/bin/chmod g+w /var/log/test_application/service2.log", "", "User=service", "Group=service", "WorkingDirectory=/srv/service/working-dir", "", - fmt.Sprintf("ExecStart=/bin/bash %s/test-application_service2.sh >> /var/log/test-application/service2.log 2>&1", helperDir), + fmt.Sprintf("ExecStart=/bin/bash %s/test_application-service2.sh >> /var/log/test_application/service2.log 2>&1", helperDir), ""}, ) @@ -372,18 +372,18 @@ func (s *ExportSuite) TestSystemdExport(c *C) { c.Assert(err, IsNil) - c.Assert(fsutil.IsExist(targetDir+"/test-application.service"), Equals, false) - c.Assert(fsutil.IsExist(targetDir+"/test-application_service1.service"), Equals, false) - c.Assert(fsutil.IsExist(targetDir+"/test-application_service2.service"), Equals, false) - c.Assert(fsutil.IsExist(helperDir+"/test-application_service1.sh"), Equals, false) - c.Assert(fsutil.IsExist(helperDir+"/test-application_service2.sh"), Equals, false) + c.Assert(fsutil.IsExist(targetDir+"/test_application.service"), Equals, false) + c.Assert(fsutil.IsExist(targetDir+"/test_application-service1.service"), Equals, false) + c.Assert(fsutil.IsExist(targetDir+"/test_application-service2.service"), Equals, false) + c.Assert(fsutil.IsExist(helperDir+"/test_application-service1.sh"), Equals, false) + c.Assert(fsutil.IsExist(helperDir+"/test_application-service2.sh"), Equals, false) } // ////////////////////////////////////////////////////////////////////////////////// // func createTestApp(helperDir, targetDir string) *procfile.Application { app := &procfile.Application{ - Name: "test-application", + Name: "test_application", User: "service", Group: "service", StartLevel: 3, diff --git a/export/exporter.go b/export/exporter.go index 195fc31..8bcab9f 100644 --- a/export/exporter.go +++ b/export/exporter.go @@ -103,7 +103,7 @@ func (e *Exporter) Uninstall(app *procfile.Application) error { log.Debug("Application unit %s deleted", unitPath) - err = deleteByMask(e.Config.TargetDir, app.Name+"_*") + err = deleteByMask(e.Config.TargetDir, app.Name+"-*") if err != nil { return err @@ -111,7 +111,7 @@ func (e *Exporter) Uninstall(app *procfile.Application) error { log.Debug("Service units deleted") - err = deleteByMask(e.Config.HelperDir, app.Name+"_*.sh") + err = deleteByMask(e.Config.HelperDir, app.Name+"-*.sh") if err != nil { return err @@ -154,7 +154,7 @@ func (e *Exporter) writeServicesUnits(app *procfile.Application) error { } for _, service := range app.Services { - fullServiceName := app.Name + "_" + service.Name + fullServiceName := app.Name + "-" + service.Name service.HelperPath = e.helperPath(fullServiceName) diff --git a/export/systemd.go b/export/systemd.go index ec488e4..d6d3494 100644 --- a/export/systemd.go +++ b/export/systemd.go @@ -190,7 +190,7 @@ func (sp *SystemdProvider) renderWantsClause(app *procfile.Application) string { var wants []string for _, service := range app.Services { - wants = append(wants, sp.UnitName(app.Name+"_"+service.Name)) + wants = append(wants, sp.UnitName(app.Name+"-"+service.Name)) } return strings.Join(wants, " ") From 4c66b4741b9d291f968ecd93044c2a726ed61991 Mon Sep 17 00:00:00 2001 From: Anton Novojilov Date: Mon, 16 Jan 2017 08:34:15 -0500 Subject: [PATCH 22/31] Custom nofile/nproc limits support --- cli/cli.go | 11 +++- common/init-exporter.conf | 8 +++ common/init-exporter.spec | 4 +- export/export_test.go | 43 ++++++++++------ export/systemd.go | 13 +++-- export/upstart.go | 11 ++-- procfile/procfile.go | 102 ++++++++++++++++++++++++++++---------- procfile/procfile_test.go | 24 ++++++--- testdata/procfile_v2 | 9 ++++ 9 files changed, 165 insertions(+), 60 deletions(-) diff --git a/cli/cli.go b/cli/cli.go index bdac219..ac2f150 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -29,7 +29,7 @@ import ( // App props const ( APP = "init-exporter" - VER = "0.4.1" + VER = "0.5.0" DESC = "Utility for exporting services described by Procfile to init system" ) @@ -55,6 +55,8 @@ const ( PATHS_HELPER_DIR = "paths:helper-dir" PATHS_SYSTEMD_DIR = "paths:systemd-dir" PATHS_UPSTART_DIR = "paths:upstart-dir" + LIMITS_NPROC = "limits:nproc" + LIMITS_NOFILE = "limits:nofile" LOG_ENABLED = "log:enabled" LOG_DIR = "log:dir" LOG_FILE = "log:file" @@ -236,6 +238,11 @@ func validateConfig() { {PATHS_HELPER_DIR, knf.Empty, nil}, {PATHS_SYSTEMD_DIR, knf.Empty, nil}, {PATHS_UPSTART_DIR, knf.Empty, nil}, + {LIMITS_NOFILE, knf.Empty, nil}, + {LIMITS_NPROC, knf.Empty, nil}, + + {LIMITS_NOFILE, knf.Less, 0}, + {LIMITS_NPROC, knf.Less, 0}, {MAIN_RUN_USER, userChecker, nil}, {MAIN_RUN_GROUP, groupChecker, nil}, @@ -296,6 +303,8 @@ func installApplication(appName string) { User: knf.GetS(MAIN_RUN_USER), Group: knf.GetS(MAIN_RUN_GROUP), WorkingDir: knf.GetS(PATHS_WORKING_DIR), + LimitFile: knf.GetI(LIMITS_NOFILE, 0), + LimitProc: knf.GetI(LIMITS_NPROC, 0), }, ) diff --git a/common/init-exporter.conf b/common/init-exporter.conf index 9ec4c9f..3aeea10 100644 --- a/common/init-exporter.conf +++ b/common/init-exporter.conf @@ -25,6 +25,14 @@ # Path to directory with upstart configs upstart-dir: /etc/init +[limits] + + # Number of Processes + nproc: 10240 + + # Number of File Descriptors + nofile: 10240 + [log] # Enable or disable logging here diff --git a/common/init-exporter.spec b/common/init-exporter.spec index 7d8a9c4..654baae 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.3.0 +Version: 0.5.0 Release: 0%{?dist} Group: Development/Tools License: MIT @@ -105,5 +105,5 @@ rm -rf %{buildroot} ############################################################################### %changelog -* Fri Dec 09 2016 Anton Novojilov - 0.3.0-0 +* Fri Dec 09 2016 Anton Novojilov - 0.5.0-0 - Initial build diff --git a/export/export_test.go b/export/export_test.go index c177dbb..49a7d45 100644 --- a/export/export_test.go +++ b/export/export_test.go @@ -113,8 +113,8 @@ func (s *ExportSuite) TestUpstartExport(c *C) { service2Helper := strings.Split(string(service2HelperData), "\n") c.Assert(appUnit, HasLen, 16) - c.Assert(service1Unit, HasLen, 18) - c.Assert(service2Unit, HasLen, 18) + c.Assert(service1Unit, HasLen, 21) + c.Assert(service2Unit, HasLen, 21) c.Assert(service1Helper, HasLen, 8) c.Assert(service2Helper, HasLen, 8) @@ -145,6 +145,9 @@ func (s *ExportSuite) TestUpstartExport(c *C) { "", "kill timeout 10", "", + "limit nofile 1024 1024", + "", + "", "script", " touch /var/log/test_application/service1.log", " chown service /var/log/test_application/service1.log", @@ -164,6 +167,9 @@ func (s *ExportSuite) TestUpstartExport(c *C) { "", "kill timeout 0", "", + "limit nofile 4096 4096", + "limit nproc 4096 4096", + "", "script", " touch /var/log/test_application/service2.log", " chown service /var/log/test_application/service2.log", @@ -270,8 +276,8 @@ func (s *ExportSuite) TestSystemdExport(c *C) { service2Helper := strings.Split(string(service2HelperData), "\n") c.Assert(appUnit, HasLen, 22) - c.Assert(service1Unit, HasLen, 26) - c.Assert(service2Unit, HasLen, 26) + c.Assert(service1Unit, HasLen, 29) + c.Assert(service2Unit, HasLen, 29) c.Assert(service1Helper, HasLen, 8) c.Assert(service2Helper, HasLen, 8) @@ -313,6 +319,9 @@ func (s *ExportSuite) TestSystemdExport(c *C) { "StartLimitInterval=25", "StartLimitBurst=15", "", + "LimitNOFILE=1024", + "", + "", "ExecStartPre=/bin/touch /var/log/test_application/service1.log", "ExecStartPre=/bin/chown service /var/log/test_application/service1.log", "ExecStartPre=/bin/chgrp service /var/log/test_application/service1.log", @@ -341,6 +350,9 @@ func (s *ExportSuite) TestSystemdExport(c *C) { "", "", "", + "LimitNOFILE=4096", + "LimitNPROC=4096", + "", "ExecStartPre=/bin/touch /var/log/test_application/service2.log", "ExecStartPre=/bin/chown service /var/log/test_application/service2.log", "ExecStartPre=/bin/chgrp service /var/log/test_application/service2.log", @@ -398,14 +410,15 @@ func createTestApp(helperDir, targetDir string) *procfile.Application { Cmd: "/bin/echo service1", Application: app, Options: &procfile.ServiceOptions{ - Env: map[string]string{"STAGING": "true"}, - WorkingDir: "/srv/service/service1-dir", - LogPath: "/srv/service/service1-dir/custom.log", - KillTimeout: 10, - Count: 2, - RespawnInterval: 25, - RespawnCount: 15, - RespawnEnabled: true, + Env: map[string]string{"STAGING": "true"}, + WorkingDir: "/srv/service/service1-dir", + LogPath: "/srv/service/service1-dir/custom.log", + KillTimeout: 10, + Count: 2, + RespawnInterval: 25, + RespawnCount: 15, + IsRespawnEnabled: true, + LimitFile: 1024, }, } @@ -414,8 +427,10 @@ func createTestApp(helperDir, targetDir string) *procfile.Application { Cmd: "/bin/echo service2", Application: app, Options: &procfile.ServiceOptions{ - WorkingDir: "/srv/service/working-dir", - RespawnEnabled: true, + WorkingDir: "/srv/service/working-dir", + IsRespawnEnabled: true, + LimitFile: 4096, + LimitProc: 4096, }, } diff --git a/export/systemd.go b/export/systemd.go index d6d3494..488b760 100644 --- a/export/systemd.go +++ b/export/systemd.go @@ -69,9 +69,12 @@ PartOf={{.Application.Name}}.service Type=simple TimeoutStopSec={{.Service.Options.KillTimeout}} -{{ if .Service.Options.RespawnEnabled }}Restart=on-failure{{ end }} -{{ if .Service.Options.RespawnLimitSet }}StartLimitInterval={{.Service.Options.RespawnInterval}}{{ end }} -{{ if .Service.Options.RespawnLimitSet }}StartLimitBurst={{.Service.Options.RespawnCount}}{{ end }} +{{ if .Service.Options.IsRespawnEnabled }}Restart=on-failure{{ end }} +{{ if .Service.Options.IsRespawnLimitSet }}StartLimitInterval={{.Service.Options.RespawnInterval}}{{ end }} +{{ if .Service.Options.IsRespawnLimitSet }}StartLimitBurst={{.Service.Options.RespawnCount}}{{ end }} + +{{ if .Service.Options.IsFileLimitSet }}LimitNOFILE={{.Service.Options.LimitFile}}{{ end }} +{{ if .Service.Options.IsProcLimitSet }}LimitNPROC={{.Service.Options.LimitProc}}{{ end }} ExecStartPre=/bin/touch /var/log/{{.Application.Name}}/{{.Service.Name}}.log ExecStartPre=/bin/chown {{.Application.User}} /var/log/{{.Application.Name}}/{{.Service.Name}}.log @@ -81,8 +84,8 @@ ExecStartPre=/bin/chmod g+w /var/log/{{.Application.Name}}/{{.Service.Name}}.log User={{.Application.User}} Group={{.Application.Group}} WorkingDirectory={{.Service.Options.WorkingDir}} -{{ if .Service.Options.EnvSet }}Environment={{.Service.Options.EnvString}}{{ end }} -ExecStart=/bin/bash {{.Service.HelperPath}} {{ if .Service.Options.CustomLogEnabled }}>> {{.Service.Options.LogPath}} {{end}}>> /var/log/{{.Application.Name}}/{{.Service.Name}}.log 2>&1 +{{ if .Service.Options.IsEnvSet }}Environment={{.Service.Options.EnvString}}{{ end }} +ExecStart=/bin/bash {{.Service.HelperPath}} {{ if .Service.Options.IsCustomLogEnabled }}>> {{.Service.Options.LogPath}} {{end}}>> /var/log/{{.Application.Name}}/{{.Service.Name}}.log 2>&1 ` // ////////////////////////////////////////////////////////////////////////////////// // diff --git a/export/upstart.go b/export/upstart.go index 5f8ee2d..81167cb 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.EnvSet }}{{.Service.Options.EnvString}} {{ end }}{{.Service.Cmd}} +cd {{.Service.Options.WorkingDir}} && exec {{ if .Service.Options.IsEnvSet }}{{.Service.Options.EnvString}} {{ end }}{{.Service.Cmd}} ` // TEMPLATE_UPSTART_APP contains default application template @@ -56,17 +56,20 @@ const TEMPLATE_UPSTART_SERVICE = `# This unit generated {{.ExportDate}} by init- start on {{.StartLevel}} stop on {{.StopLevel}} -{{ if .Service.Options.RespawnEnabled }}respawn{{ end }} -{{ if .Service.Options.RespawnLimitSet }}respawn limit {{.Service.Options.RespawnCount}} {{.Service.Options.RespawnInterval}}{{ end }} +{{ if .Service.Options.IsRespawnEnabled }}respawn{{ end }} +{{ if .Service.Options.IsRespawnLimitSet }}respawn limit {{.Service.Options.RespawnCount}} {{.Service.Options.RespawnInterval}}{{ end }} kill timeout {{.Service.Options.KillTimeout}} +{{ if .Service.Options.IsFileLimitSet }}limit nofile {{.Service.Options.LimitFile}} {{.Service.Options.LimitFile}}{{ end }} +{{ if .Service.Options.IsProcLimitSet }}limit nproc {{.Service.Options.LimitProc}} {{.Service.Options.LimitProc}}{{ end }} + script touch /var/log/{{.Application.Name}}/{{.Service.Name}}.log chown {{.Application.User}} /var/log/{{.Application.Name}}/{{.Service.Name}}.log chgrp {{.Application.Group}} /var/log/{{.Application.Name}}/{{.Service.Name}}.log chmod g+w /var/log/{{.Application.Name}}/{{.Service.Name}}.log - exec sudo -u {{.Application.User}} /bin/bash {{.Service.HelperPath}} {{ if .Service.Options.CustomLogEnabled }}>> {{.Service.Options.LogPath}} {{end}}>> /var/log/{{.Application.Name}}/{{.Service.Name}}.log 2>&1 + exec sudo -u {{.Application.User}} /bin/bash {{.Service.HelperPath}} {{ if .Service.Options.IsCustomLogEnabled }}>> {{.Service.Options.LogPath}} {{end}}>> /var/log/{{.Application.Name}}/{{.Service.Name}}.log 2>&1 end script ` diff --git a/procfile/procfile.go b/procfile/procfile.go index aebc2d2..c8e1e0d 100644 --- a/procfile/procfile.go +++ b/procfile/procfile.go @@ -44,6 +44,8 @@ type Config struct { User string // Working user Group string // Working group WorkingDir string // Working directory + LimitProc int // Global processes limit + LimitFile int // Global descriptors limit } type Service struct { @@ -55,14 +57,16 @@ type Service struct { } type ServiceOptions struct { - Env map[string]string // Environment variables - WorkingDir string // Working directory - LogPath string // Path to log file - KillTimeout int // Kill timeout in seconds - Count int // Exec count - RespawnInterval int // Respawn interval in seconds - RespawnCount int // Respawn count - RespawnEnabled bool // Respawn enabled flag + Env map[string]string // Environment variables + WorkingDir string // Working directory + LogPath string // Path to log file + KillTimeout int // Kill timeout in seconds + Count int // Exec count + RespawnInterval int // Respawn interval in seconds + RespawnCount int // Respawn count + IsRespawnEnabled bool // Respawn enabled flag + LimitProc int // Processes limit + LimitFile int // Descriptors limit } type Application struct { @@ -157,21 +161,31 @@ func (so *ServiceOptions) Validate() error { return errs.Last() } -// RespawnLimitSet return true if respawn options is set -func (so *ServiceOptions) RespawnLimitSet() bool { +// IsRespawnLimitSet return true if respawn options is set +func (so *ServiceOptions) IsRespawnLimitSet() bool { return so.RespawnCount != 0 || so.RespawnInterval != 0 } -// CustomLogEnabled return true if service have custom log -func (so *ServiceOptions) CustomLogEnabled() bool { +// IsCustomLogEnabled return true if service have custom log +func (so *ServiceOptions) IsCustomLogEnabled() bool { return so.LogPath != "" } -// EnvSet return true if service have custom env vars -func (so *ServiceOptions) EnvSet() bool { +// IsEnvSet return true if service have custom env vars +func (so *ServiceOptions) IsEnvSet() bool { return len(so.Env) != 0 } +// IsFileLimitSet return true if descriptors limit is set +func (so *ServiceOptions) IsFileLimitSet() bool { + return so.LimitFile != 0 +} + +// IsProcLimitSet return true if processes limit is set +func (so *ServiceOptions) IsProcLimitSet() bool { + return so.LimitProc != 0 +} + // EnvString return environment variables as string func (so *ServiceOptions) EnvString() string { if len(so.Env) == 0 { @@ -219,6 +233,14 @@ func parseV1Procfile(data []byte, config *Config) (*Application, error) { 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) } } @@ -316,7 +338,7 @@ func parseV2Procfile(data []byte, config *Config) (*Application, error) { return nil, fmt.Errorf("Commands missing in Procfile") } - services, err := parseCommands(yaml, commands) + services, err := parseCommands(yaml, commands, config) if err != nil { return nil, err @@ -333,12 +355,6 @@ func parseV2Procfile(data []byte, config *Config) (*Application, error) { Services: services, } - app.Services, err = parseCommands(yaml, commands) - - if err != nil { - return nil, err - } - if isYamlPropPresent(yaml, "working_directory") { app.WorkingDir, err = yaml.Get("working_directory").String() @@ -369,7 +385,7 @@ func parseV2Procfile(data []byte, config *Config) (*Application, error) { } // parseCommands parse command section in yaml based procfile -func parseCommands(yaml *simpleyaml.Yaml, commands map[interface{}]interface{}) ([]*Service, error) { +func parseCommands(yaml *simpleyaml.Yaml, commands map[interface{}]interface{}, config *Config) ([]*Service, error) { var services []*Service commonOptions, err := parseOptions(yaml) @@ -395,6 +411,14 @@ func parseCommands(yaml *simpleyaml.Yaml, commands map[interface{}]interface{}) mergeServiceOptions(serviceOptions, commonOptions) + if serviceOptions.LimitFile == 0 && config.LimitFile != 0 { + serviceOptions.LimitFile = config.LimitFile + } + + if serviceOptions.LimitProc == 0 && config.LimitProc != 0 { + serviceOptions.LimitProc = config.LimitProc + } + service := &Service{ Name: serviceName, Cmd: serviceCmd, @@ -407,13 +431,13 @@ func parseCommands(yaml *simpleyaml.Yaml, commands map[interface{}]interface{}) return services, nil } -// parseOptions parse service options im yaml based procfile +// 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), - RespawnEnabled: true, + Env: make(map[string]string), + IsRespawnEnabled: true, } if isYamlPropPresent(yaml, "working_directory") { @@ -480,13 +504,31 @@ func parseOptions(yaml *simpleyaml.Yaml) (*ServiceOptions, error) { } } else if isYamlPropPresent(yaml, "respawn") { - options.RespawnEnabled, err = yaml.Get("respawn").Bool() + options.IsRespawnEnabled, err = yaml.Get("respawn").Bool() if err != nil { return nil, fmt.Errorf("Can't parse respawn value: %v", err) } } + if isYamlPropPresent(yaml, "limits", "nproc") || isYamlPropPresent(yaml, "limits", "nofile") { + if isYamlPropPresent(yaml, "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 isYamlPropPresent(yaml, "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 } @@ -539,6 +581,14 @@ func mergeServiceOptions(dst, src *ServiceOptions) { if dst.RespawnCount == 0 { dst.RespawnCount = src.RespawnCount } + + if dst.LimitFile == 0 { + dst.LimitFile = src.LimitFile + } + + if dst.LimitProc == 0 { + dst.LimitProc = src.LimitProc + } } // mergeStringMaps merges two maps diff --git a/procfile/procfile_test.go b/procfile/procfile_test.go index e2076c0..f9c51dc 100644 --- a/procfile/procfile_test.go +++ b/procfile/procfile_test.go @@ -68,14 +68,16 @@ func (s *ProcfileSuite) TestProcV2Parsing(c *C) { c.Assert(service.Cmd, Equals, "/usr/bin/tail -F /var/log/messages") c.Assert(service.Options, NotNil) c.Assert(service.Options.WorkingDir, Equals, "/var/...") - c.Assert(service.Options.CustomLogEnabled(), Equals, false) + c.Assert(service.Options.IsCustomLogEnabled(), Equals, false) c.Assert(service.Options.RespawnCount, Equals, 5) c.Assert(service.Options.RespawnInterval, Equals, 10) - c.Assert(service.Options.RespawnEnabled, Equals, true) + c.Assert(service.Options.IsRespawnEnabled, Equals, true) c.Assert(service.Options.Env, NotNil) c.Assert(service.Options.Env["RAILS_ENV"], Equals, "staging") c.Assert(service.Options.Env["TEST"], Equals, "true") c.Assert(service.Options.EnvString(), Equals, "RAILS_ENV=staging TEST=true") + c.Assert(service.Options.LimitFile, Equals, 4096) + c.Assert(service.Options.LimitProc, Equals, 4096) c.Assert(service.Application, NotNil) c.Assert(service.Application.Name, Equals, "test-app") @@ -83,15 +85,17 @@ func (s *ProcfileSuite) TestProcV2Parsing(c *C) { c.Assert(service.Cmd, Equals, "/usr/bin/tail -F /var/log/messages") c.Assert(service.Options, NotNil) c.Assert(service.Options.WorkingDir, Equals, "/srv/projects/my_website/current") - c.Assert(service.Options.CustomLogEnabled(), Equals, false) + c.Assert(service.Options.IsCustomLogEnabled(), Equals, false) c.Assert(service.Options.KillTimeout, Equals, 60) c.Assert(service.Options.RespawnCount, Equals, 7) c.Assert(service.Options.RespawnInterval, Equals, 22) - c.Assert(service.Options.RespawnEnabled, Equals, false) + c.Assert(service.Options.IsRespawnEnabled, Equals, false) c.Assert(service.Options.Env, NotNil) c.Assert(service.Options.Env["RAILS_ENV"], Equals, "production") c.Assert(service.Options.Env["TEST"], Equals, "true") c.Assert(service.Options.EnvString(), Equals, "RAILS_ENV=production TEST=true") + c.Assert(service.Options.LimitFile, Equals, 8192) + c.Assert(service.Options.LimitProc, Equals, 8192) c.Assert(service.Application, NotNil) c.Assert(service.Application.Name, Equals, "test-app") @@ -100,14 +104,16 @@ func (s *ProcfileSuite) TestProcV2Parsing(c *C) { c.Assert(service.Options, NotNil) c.Assert(service.Options.WorkingDir, Equals, "/srv/projects/my_website/current") c.Assert(service.Options.LogPath, Equals, "/var/log/messages_copy") - c.Assert(service.Options.CustomLogEnabled(), Equals, true) + c.Assert(service.Options.IsCustomLogEnabled(), Equals, true) c.Assert(service.Options.RespawnCount, Equals, 7) c.Assert(service.Options.RespawnInterval, Equals, 22) - c.Assert(service.Options.RespawnEnabled, Equals, true) + c.Assert(service.Options.IsRespawnEnabled, Equals, true) c.Assert(service.Options.Env, NotNil) c.Assert(service.Options.Env["RAILS_ENV"], Equals, "production") c.Assert(service.Options.Env["TEST"], Equals, "true") c.Assert(service.Options.EnvString(), Equals, "RAILS_ENV=production TEST=true") + c.Assert(service.Options.LimitFile, Equals, 4096) + c.Assert(service.Options.LimitProc, Equals, 4096) c.Assert(service.Application, NotNil) c.Assert(service.Application.Name, Equals, "test-app") @@ -116,14 +122,16 @@ func (s *ProcfileSuite) TestProcV2Parsing(c *C) { c.Assert(service.Options, NotNil) c.Assert(service.Options.Count, Equals, 2) c.Assert(service.Options.WorkingDir, Equals, "/srv/projects/my_website/current") - c.Assert(service.Options.CustomLogEnabled(), Equals, false) + c.Assert(service.Options.IsCustomLogEnabled(), Equals, false) c.Assert(service.Options.RespawnCount, Equals, 7) c.Assert(service.Options.RespawnInterval, Equals, 22) - c.Assert(service.Options.RespawnEnabled, Equals, true) + c.Assert(service.Options.IsRespawnEnabled, Equals, true) c.Assert(service.Options.Env, NotNil) c.Assert(service.Options.Env["RAILS_ENV"], Equals, "production") c.Assert(service.Options.Env["TEST"], Equals, "true") c.Assert(service.Options.EnvString(), Equals, "RAILS_ENV=production TEST=true") + c.Assert(service.Options.LimitFile, Equals, 1024) + c.Assert(service.Options.LimitProc, Equals, 4096) c.Assert(service.Application, NotNil) c.Assert(service.Application.Name, Equals, "test-app") diff --git a/testdata/procfile_v2 b/testdata/procfile_v2 index 85a8c76..4907745 100644 --- a/testdata/procfile_v2 +++ b/testdata/procfile_v2 @@ -11,6 +11,10 @@ respawn: count: 7 interval: 22 +limits: + nofile: 4096 + nproc: 4096 + working_directory: /srv/projects/my_website/current commands: @@ -25,6 +29,9 @@ commands: my_another_tail_cmd: command: /usr/bin/tail -F /var/log/messages + limits: + nofile: 8192 + nproc: 8192 kill_timeout: 60 respawn: false # by default respawn option is enabled @@ -34,4 +41,6 @@ commands: my_multi_tail_cmd: command: /usr/bin/tail -F /var/log/messages + limits: + nofile: 1024 count: 2 From ec68887bd91401bb6ea5689c45178683778e31d4 Mon Sep 17 00:00:00 2001 From: Anton Novojilov Date: Wed, 18 Jan 2017 07:28:43 -0500 Subject: [PATCH 23/31] Removed unused config option --- cli/cli.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/cli/cli.go b/cli/cli.go index ac2f150..a6a3deb 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -29,13 +29,12 @@ import ( // App props const ( APP = "init-exporter" - VER = "0.5.0" + VER = "0.5.1" DESC = "Utility for exporting services described by Procfile to init system" ) // Supported arguments list const ( - ARG_CONFIG = "c:config" ARG_PROCFILE = "p:procfile" ARG_APP_NAME = "n:appname" ARG_DRY_START = "d:dry-start" @@ -413,7 +412,6 @@ func showUsage() { info := usage.NewInfo("", "app-name") - info.AddOption(ARG_CONFIG, "Path to config file", "file") info.AddOption(ARG_PROCFILE, "Path to procfile", "file") info.AddOption(ARG_DRY_START, "Dry start {s-}(don't export anything, just parse and test procfile){!}") info.AddOption(ARG_UNINSTALL, "Remove scripts and helpers for a particular application") From 5be67d06ad0b3a9a12dab0adec0b604ffd1d88ad Mon Sep 17 00:00:00 2001 From: Anton Novojilov Date: Wed, 18 Jan 2017 07:29:12 -0500 Subject: [PATCH 24/31] Version bump --- common/init-exporter.spec | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/common/init-exporter.spec b/common/init-exporter.spec index 654baae..b4727fe 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.5.0 +Version: 0.5.1 Release: 0%{?dist} Group: Development/Tools License: MIT @@ -105,5 +105,5 @@ rm -rf %{buildroot} ############################################################################### %changelog -* Fri Dec 09 2016 Anton Novojilov - 0.5.0-0 +* Fri Dec 09 2016 Anton Novojilov - 0.5.1-0 - Initial build From 626157eefbe391e29e5d90ca4e62099b60a1a119 Mon Sep 17 00:00:00 2001 From: Anton Novojilov Date: Thu, 2 Feb 2017 04:30:50 -0500 Subject: [PATCH 25/31] smallfish/simpleyaml replaced by essentialkaos/go-simpleyaml --- Makefile | 4 ++-- cli/cli.go | 2 +- glide.lock | 12 ++++++------ glide.yaml | 3 +-- procfile/procfile.go | 39 +++++++++++++++++---------------------- 5 files changed, 27 insertions(+), 33 deletions(-) diff --git a/Makefile b/Makefile index 4845c94..235d592 100644 --- a/Makefile +++ b/Makefile @@ -14,8 +14,8 @@ all: bin deps: go get -v pkg.re/check.v1 go get -v pkg.re/essentialkaos/ek.v6 - go get -v github.com/smallfish/simpleyaml - go get -v gopkg.in/yaml.v2 + go get -v pkg.re/essentialkaos/go-simpleyaml + go get -v pkg.re/yaml.v2 deps-glide: glide install diff --git a/cli/cli.go b/cli/cli.go index a6a3deb..3ff4d29 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -29,7 +29,7 @@ import ( // App props const ( APP = "init-exporter" - VER = "0.5.1" + VER = "0.6.0" DESC = "Utility for exporting services described by Procfile to init system" ) diff --git a/glide.lock b/glide.lock index fee9e1b..5a1ccaa 100644 --- a/glide.lock +++ b/glide.lock @@ -1,10 +1,6 @@ -hash: 550842432f54d716dce1563e1f350df3c9e95dbbc940dee29d7b3f3bdbee05b2 -updated: 2016-12-28T16:30:15.622859378-05:00 +hash: 352daf3640af334921e1e295d0738306592c7c4609878e4653936e2e6c7cc749 +updated: 2017-02-02T04:29:34.527741033-05:00 imports: -- name: github.com/smallfish/simpleyaml - version: f8280fef35a3fd1d4cf13c5a9fa638398eebb6ad -- name: gopkg.in/yaml.v2 - version: a5b47d31c556af34a302ce5d659e6fea44d90de0 - name: pkg.re/essentialkaos/ek.v6 version: c292a0da0a81e93c1161a291370c87369db8fc2c subpackages: @@ -20,6 +16,10 @@ imports: - system - timeutil - usage +- name: pkg.re/essentialkaos/go-simpleyaml.v1 + version: 6c4951f5cc065c5724d0b2f46a5036cc1f1f57a1 +- name: pkg.re/yaml.v2 + version: 4c78c975fe7c825c6d1466c42be594d1d6f3aba6 testImports: - name: pkg.re/check.v1 version: 20d25e2804050c1cd24a7eea1e7a6447dd0e74ec diff --git a/glide.yaml b/glide.yaml index af0e6b8..67c3ce3 100644 --- a/glide.yaml +++ b/glide.yaml @@ -1,8 +1,6 @@ package: github.com/funbox/init-exporter import: -- package: github.com/smallfish/simpleyaml - package: pkg.re/essentialkaos/ek.v6 - version: ^6.0.0 subpackages: - arg - env @@ -15,5 +13,6 @@ import: - system - timeutil - usage +- package: pkg.re/essentialkaos/go-simpleyaml.v1 testImport: - package: pkg.re/check.v1 diff --git a/procfile/procfile.go b/procfile/procfile.go index c8e1e0d..4cd8ed4 100644 --- a/procfile/procfile.go +++ b/procfile/procfile.go @@ -15,12 +15,12 @@ import ( "sort" "strings" - "github.com/smallfish/simpleyaml" - "pkg.re/essentialkaos/ek.v6/errutil" "pkg.re/essentialkaos/ek.v6/fsutil" "pkg.re/essentialkaos/ek.v6/log" "pkg.re/essentialkaos/ek.v6/path" + + "pkg.re/essentialkaos/go-simpleyaml.v1" ) // ////////////////////////////////////////////////////////////////////////////////// // @@ -355,7 +355,7 @@ func parseV2Procfile(data []byte, config *Config) (*Application, error) { Services: services, } - if isYamlPropPresent(yaml, "working_directory") { + if yaml.IsExist("working_directory") { app.WorkingDir, err = yaml.Get("working_directory").String() if err != nil { @@ -363,7 +363,7 @@ func parseV2Procfile(data []byte, config *Config) (*Application, error) { } } - if isYamlPropPresent(yaml, "start_on_runlevel") { + if yaml.IsExist("start_on_runlevel") { app.StartLevel, err = yaml.Get("start_on_runlevel").Int() if err != nil { @@ -371,7 +371,7 @@ func parseV2Procfile(data []byte, config *Config) (*Application, error) { } } - if isYamlPropPresent(yaml, "stop_on_runlevel") { + if yaml.IsExist("stop_on_runlevel") { app.StopLevel, err = yaml.Get("stop_on_runlevel").Int() if err != nil { @@ -440,7 +440,7 @@ func parseOptions(yaml *simpleyaml.Yaml) (*ServiceOptions, error) { IsRespawnEnabled: true, } - if isYamlPropPresent(yaml, "working_directory") { + if yaml.IsExist("working_directory") { options.WorkingDir, err = yaml.Get("working_directory").String() if err != nil { @@ -448,7 +448,7 @@ func parseOptions(yaml *simpleyaml.Yaml) (*ServiceOptions, error) { } } - if isYamlPropPresent(yaml, "log") { + if yaml.IsExist("log") { options.LogPath, err = yaml.Get("log").String() if err != nil { @@ -456,7 +456,7 @@ func parseOptions(yaml *simpleyaml.Yaml) (*ServiceOptions, error) { } } - if isYamlPropPresent(yaml, "kill_timeout") { + if yaml.IsExist("kill_timeout") { options.KillTimeout, err = yaml.Get("kill_timeout").Int() if err != nil { @@ -464,7 +464,7 @@ func parseOptions(yaml *simpleyaml.Yaml) (*ServiceOptions, error) { } } - if isYamlPropPresent(yaml, "count") { + if yaml.IsExist("count") { options.Count, err = yaml.Get("count").Int() if err != nil { @@ -472,7 +472,7 @@ func parseOptions(yaml *simpleyaml.Yaml) (*ServiceOptions, error) { } } - if isYamlPropPresent(yaml, "env") { + if yaml.IsExist("env") { env, err := yaml.Get("env").Map() if err != nil { @@ -482,8 +482,8 @@ func parseOptions(yaml *simpleyaml.Yaml) (*ServiceOptions, error) { options.Env = convertMapType(env) } - if isYamlPropPresent(yaml, "respawn", "count") || isYamlPropPresent(yaml, "respawn", "interval") { - if isYamlPropPresent(yaml, "respawn", "count") { + 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 { @@ -493,7 +493,7 @@ func parseOptions(yaml *simpleyaml.Yaml) (*ServiceOptions, error) { options.RespawnCount = DEFAULT_RESPAWN_COUNT } - if isYamlPropPresent(yaml, "respawn", "interval") { + if yaml.IsPathExist("respawn", "interval") { options.RespawnInterval, err = yaml.Get("respawn").Get("interval").Int() if err != nil { @@ -503,7 +503,7 @@ func parseOptions(yaml *simpleyaml.Yaml) (*ServiceOptions, error) { options.RespawnInterval = DEFAULT_RESPAWN_INTERVAL } - } else if isYamlPropPresent(yaml, "respawn") { + } else if yaml.IsExist("respawn") { options.IsRespawnEnabled, err = yaml.Get("respawn").Bool() if err != nil { @@ -511,8 +511,8 @@ func parseOptions(yaml *simpleyaml.Yaml) (*ServiceOptions, error) { } } - if isYamlPropPresent(yaml, "limits", "nproc") || isYamlPropPresent(yaml, "limits", "nofile") { - if isYamlPropPresent(yaml, "limits", "nofile") { + 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 { @@ -520,7 +520,7 @@ func parseOptions(yaml *simpleyaml.Yaml) (*ServiceOptions, error) { } } - if isYamlPropPresent(yaml, "limits", "nproc") { + if yaml.IsPathExist("limits", "nproc") { options.LimitProc, err = yaml.Get("limits").Get("nproc").Int() if err != nil { @@ -532,11 +532,6 @@ func parseOptions(yaml *simpleyaml.Yaml) (*ServiceOptions, error) { return options, nil } -// isYamlPropPresent return true if property with defined named present in yaml file -func isYamlPropPresent(yaml *simpleyaml.Yaml, path ...string) bool { - return *yaml.GetPath(path...) != simpleyaml.Yaml{} -} - // determineProcVersion process procfile data and return procfile version func determineProcVersion(data []byte) int { if regexp.MustCompile(REGEXP_V2_VERSION).Match(data) { From efb5387f72f2d251a355517104db018164ed43c7 Mon Sep 17 00:00:00 2001 From: Anton Novojilov Date: Thu, 2 Feb 2017 04:32:45 -0500 Subject: [PATCH 26/31] Fixed deps in makefile --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 235d592..cc0ba50 100644 --- a/Makefile +++ b/Makefile @@ -14,7 +14,7 @@ all: bin deps: go get -v pkg.re/check.v1 go get -v pkg.re/essentialkaos/ek.v6 - go get -v pkg.re/essentialkaos/go-simpleyaml + go get -v pkg.re/essentialkaos/go-simpleyaml.v1 go get -v pkg.re/yaml.v2 deps-glide: From 171e27fb809ede791e67736b62a60ce151124aba Mon Sep 17 00:00:00 2001 From: Anton Novojilov Date: Thu, 2 Feb 2017 04:34:57 -0500 Subject: [PATCH 27/31] Minor improvements in TravisCI config --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 323124c..7da6a91 100644 --- a/.travis.yml +++ b/.travis.yml @@ -26,3 +26,4 @@ before_install: script: - make test - make bin + - ./init-exporter --version From 65fff70f954fb6f72f7ef4678db0da62c97566a4 Mon Sep 17 00:00:00 2001 From: Anton Novojilov Date: Thu, 2 Feb 2017 12:39:19 +0300 Subject: [PATCH 28/31] Update readme.md --- readme.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/readme.md b/readme.md index e229c4d..b7785d6 100644 --- a/readme.md +++ b/readme.md @@ -3,7 +3,7 @@ Utility for exporting services described by Procfile to init system. Supported init systems: upstart and systemd -#### Installation +### Installation To build the init-exporter from scratch, make sure you have a working Go 1.5+ workspace ([instructions](https://golang.org/doc/install)), then: @@ -14,7 +14,7 @@ make all sudo make install ``` -#### Configuration +### Configuration The export process can be configured through the config `/etc/init-exporter.conf` The config is not installed by default. If this config is absent, the default values are the following: @@ -83,13 +83,13 @@ Defaults secure_path = /sbin:/bin:/usr/sbin:/usr/bin:/usr/local/bin deployuser ALL=(deployuser) NOPASSWD: ALL, (root) NOPASSWD: UPSTART, UPEXPORT ``` -#### Usage +### Usage Gem is able to process two versions of Procfiles, format of the Procfile is defined in the `version` key. If the key is not present or is not equal to `2` file will be parsed as Procfile v.1. -##### Procfile v.1 +#### Procfile v.1 After upstart-exporter is installed and configured, you may export background jobs from an arbitrary Procfile-like file of the following format: @@ -108,7 +108,7 @@ my_another_tail_cmd: /usr/bin/tail -F /var/log/messages For security purposes, command labels are allowed to contain only letters, digits, and underscores. -##### Procfile v.2 +#### Procfile v.2 Another format of Procfile scripts is YAML config. A configuration script may look like this: @@ -169,7 +169,7 @@ often than `count` times in `interval`, it won't be restarted anymore. Options `working_directory`, `env`, `log`, `respawn` can be defined both as global and as per-command options. -#### Exporting +### Exporting To export a Procfile you should run @@ -226,13 +226,13 @@ sudo init-export -u -f upstart myapp The logs are not cleared in this case. Also, all old application scripts are cleared before each export. -#### Build Status +### Build Status | Repository | Status | |------------|--------| | Stable | [![Build Status](https://travis-ci.org/funbox/init-exporter.svg?branch=master)](https://travis-ci.org/funbox/init-exporter) | | Unstable | [![Build Status](https://travis-ci.org/funbox/init-exporter.svg?branch=develop)](https://travis-ci.org/funbox/init-exporter) | -#### License +### License init-exporter is released under the MIT license (see [LICENSE](LICENSE)) From a2234b6c52650981ff3a28befcc6fdcacbf6d35a Mon Sep 17 00:00:00 2001 From: Anton Novojilov Date: Thu, 2 Feb 2017 04:40:54 -0500 Subject: [PATCH 29/31] Added ToC to readme --- readme.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/readme.md b/readme.md index b7785d6..b037040 100644 --- a/readme.md +++ b/readme.md @@ -3,6 +3,15 @@ Utility for exporting services described by Procfile to init system. Supported init systems: upstart and systemd +* [Installation](#installation) +* [Configuration](#configuration) +* [Usage](#usage) + * [Procfile v.1](#procfile-v1) + * [Procfile v.2](#procfile-v2) +* [Exporting](#exporting) +* [Build Status](#build-status) +* [License](#license) + ### Installation To build the init-exporter from scratch, make sure you have a working Go 1.5+ workspace ([instructions](https://golang.org/doc/install)), then: From dc85e98372407a0257b1e79f38ff467748a99ba1 Mon Sep 17 00:00:00 2001 From: Anton Novojilov Date: Thu, 2 Feb 2017 12:53:14 +0300 Subject: [PATCH 30/31] readme fixes --- readme.md | 29 ++++++++++------------------- 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/readme.md b/readme.md index b037040..2eae8c7 100644 --- a/readme.md +++ b/readme.md @@ -25,8 +25,7 @@ sudo make install ### Configuration -The export process can be configured through the config `/etc/init-exporter.conf` -The config is not installed by default. If this config is absent, the default values are the following: +The export process can be configured through the config `/etc/init-exporter.conf`: ```ini # Default configuration for init-exporter @@ -79,28 +78,22 @@ To give a certain user (i.e. `deployuser`) the ability to use this script, you c ```bash # Commands required for manipulating jobs Cmnd_Alias UPSTART = /sbin/start, /sbin/stop, /sbin/restart -Cmnd_Alias UPEXPORT = /usr/local/bin/init-export - -... - -# Add gem's binary path to this -Defaults secure_path = /sbin:/bin:/usr/sbin:/usr/bin:/usr/local/bin +Cmnd_Alias SYSTEMD = /usr/bin/systemctl +Cmnd_Alias EXPORTER = /usr/local/bin/init-exporter ... # Allow deploy user to manipulate jobs -deployuser ALL=(deployuser) NOPASSWD: ALL, (root) NOPASSWD: UPSTART, UPEXPORT +deployuser ALL=(deployuser) NOPASSWD: ALL, (root) NOPASSWD: UPSTART, SYSTEMD, EXPORTER ``` ### Usage -Gem is able to process two versions of Procfiles, format of the Procfile is -defined in the `version` key. If the key is not present or is not equal to `2` -file will be parsed as Procfile v.1. +`init-exporter` is able to process two versions of Procfiles. Utility automatically recognise used format. #### Procfile v.1 -After upstart-exporter is installed and configured, you may export background jobs +After init-exporter is installed and configured, you may export background jobs from an arbitrary Procfile-like file of the following format: ```yaml @@ -185,11 +178,9 @@ To export a Procfile you should run ```bash sudo upstart-export -p ./myprocfile -f format myapp ``` +Where `myapp` is the application name. This name only affects the names of generated files. For security purposes, app name is also allowed to contain only letters, digits and underscores. -where `myapp` is the application name. -This name only affects the names of generated files. -For security purposes, app name is also allowed to contain only letters, digits and underscores. -where format is `(upstart | systemd)` +Format is name of init system `(upstart | systemd)`. Assuming that default options are used, the following files and folders will be generated (in case of upstart format): @@ -217,7 +208,7 @@ sudo start fb-myapp-my_tail_cmd sudo stop fb-myapp-my_tail_cmd ``` -Its stdout/stderr will be redirected to `/var/log/fb-myapp/my_tail_cmd.log`. +It's stdout/stderr will be redirected to `/var/log/fb-myapp/my_tail_cmd.log`. To start/stop all application commands at once, you can run: @@ -227,7 +218,7 @@ sudo start fb-myapp sudo stop fb-myapp ``` -To remove upstart scripts and helpers for a particular application you can run +To remove init scripts and helpers for a particular application you can run ```bash sudo init-export -u -f upstart myapp From 861281d4df9e06a8e0b4ba27ea792a09a4c0344d Mon Sep 17 00:00:00 2001 From: Anton Novojilov Date: Thu, 2 Feb 2017 05:21:48 -0500 Subject: [PATCH 31/31] Updated spec --- common/init-exporter.spec | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/common/init-exporter.spec b/common/init-exporter.spec index b4727fe..da7d83c 100644 --- a/common/init-exporter.spec +++ b/common/init-exporter.spec @@ -42,17 +42,17 @@ Summary: Utility for exporting services described by Procfile to init system Name: init-exporter -Version: 0.5.1 +Version: 0.6.0 Release: 0%{?dist} Group: Development/Tools License: MIT -URL: http://github.com/andyone/init-exporter +URL: https://github.com/funbox/init-exporter Source0: %{name}-%{version}.tar.gz BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root-%(%{__id_u} -n) -BuildRequires: golang >= 1.7 +BuildRequires: golang >= 1.5 Provides: upstart-exporter = %{version}-%{release} Provides: systemd-exporter = %{version}-%{release} @@ -105,5 +105,5 @@ rm -rf %{buildroot} ############################################################################### %changelog -* Fri Dec 09 2016 Anton Novojilov - 0.5.1-0 +* Thu Feb 2 2017 Anton Novojilov - 0.6.0-0 - Initial build