Repository: deis/deis

Issue:
docs: Clarify that Procfiles can be used in both the buildpack and Dockerfile workflows
@gmeans in IRC today was confused because in the [Using Dockerfiles](http://docs.deis.io/en/latest/using_deis/using-dockerfiles/) documentation, we explain that a user can specify process types in a Procfile. However, in [Using Buildpacks](http://docs.deis.io/en/latest/using_deis/using-buildpacks/) we don't mention the Procfile at all. 

If the Procfile is indeed respected in both workflows, we should explain this either in both docs or in a separate doc so it's not confusing.



Discussion:
What about docker image based deploys? Does the _"Process types other than cmd are not supported when using Docker images."_ statement at the end of [Using Docker Images](http://docs.deis.io/en/latest/using_deis/using-docker-images/) mean Procfiles can't be used?

that's what he just said.

> in the Using Dockerfiles documentation, we explain that a user can specify process types in a Procfile.

> that's what he just said.

I _think_ Stefan is referring to the `deis pull` workflow there, but he can confirm.

> Process types other than cmd are not supported when using Docker images

That could change if #2431 is merged at some point. The only requirement is a `/app/Procfile` inside the image.

Probably OT but is there any example showing how to use a Procfile with Dockerfile? Thanks 

@ngbinh see #2545


Resolving patch:
diff --git a/client/deis.py b/client/deis.py
--- a/client/deis.py
+++ b/client/deis.py
@@ -59,6 +59,7 @@
 import time
 import urlparse
 import webbrowser
+import yaml
 
 from dateutil import parser
 from dateutil import relativedelta
@@ -867,7 +868,8 @@ def builds(self, args):
     def builds_create(self, args):
         """
         Creates a new build of an application. Imports an <image> and deploys it to Deis
-        as a new release.
+        as a new release. If a Procfile is present in the current directory, it will be used
+        as the default process types for this application.
 
         Usage: deis builds:create <image> [options]
 
@@ -879,11 +881,28 @@ def builds_create(self, args):
         Options:
           -a --app=<app>
             The uniquely identifiable name for the application.
+          -p --procfile=<procfile>
+            A YAML string used to supply a Procfile to the application.
         """
         app = args.get('--app')
         if not app:
             app = self._session.app
         body = {'image': args['<image>']}
+        procfile = args.get('--procfile')
+        if procfile:
+            try:
+                body['procfile'] = yaml.load(procfile)
+            except yaml.YAMLError:
+                self._logger.error('could not parse --procfile')
+                sys.exit(1)
+        else:
+            # read in Procfile for default process types
+            if os.path.exists('Procfile'):
+                try:
+                    body['procfile'] = yaml.load(open('Procfile'))
+                except yaml.YAMLError:
+                    self._logger.error('could not parse Procfile')
+                    sys.exit(1)
         sys.stdout.write('Creating build... ')
         sys.stdout.flush()
         try:
diff --git a/client/setup.py b/client/setup.py
--- a/client/setup.py
+++ b/client/setup.py
@@ -57,7 +57,9 @@
       ],
       long_description=LONG_DESCRIPTION,
       install_requires=[
-          'docopt==0.6.2', 'python-dateutil==2.2', 'requests==2.4.3', 'termcolor==1.1.0'
+          'docopt==0.6.2', 'python-dateutil==2.2',
+          'PyYAML==3.11', 'requests==2.4.3',
+          'termcolor==1.1.0'
       ],
       zip_safe=True,
       **KWARGS)
diff --git a/controller/api/models.py b/controller/api/models.py
--- a/controller/api/models.py
+++ b/controller/api/models.py
@@ -404,7 +404,11 @@ def _get_command(self):
         # handle special case for Dockerfile deployments
         if self.type == 'cmd':
             return ''
-        else:
+        try:
+            # ensure they cannot break out and run commands on the host
+            return "bash -c '{}'".format(self.release.build.procfile[self.type])
+        # if the key is not present or if a parent attribute is None
+        except (KeyError, TypeError):
             return 'start {}'.format(self.type)
 
     _command = property(_get_command)


Test patch:
diff --git a/controller/api/tests/test_container.py b/controller/api/tests/test_container.py
--- a/controller/api/tests/test_container.py
+++ b/controller/api/tests/test_container.py
@@ -435,3 +435,36 @@ def test_scale_without_build_should_error(self):
                                     HTTP_AUTHORIZATION='token {}'.format(self.token))
         self.assertEqual(response.status_code, 400)
         self.assertEqual(response.data, "No build associated with this release")
+
+    def test_command_good(self):
+        """Test the default command for each container workflow"""
+        url = '/v1/apps'
+        response = self.client.post(url, HTTP_AUTHORIZATION='token {}'.format(self.token))
+        self.assertEqual(response.status_code, 201)
+        app_id = response.data['id']
+        app = App.objects.get(id=app_id)
+        user = User.objects.get(username='autotest')
+        build = Build.objects.create(owner=user,
+                                     app=app,
+                                     image="qwerty",
+                                     procfile={'web': 'node server.js',
+                                               'worker': 'node worker.js'})
+        # create an initial release
+        release = Release.objects.create(version=2,
+                                         owner=user,
+                                         app=app,
+                                         config=app.config_set.latest(),
+                                         build=build)
+        # create a container
+        c = Container.objects.create(owner=user,
+                                     app=app,
+                                     release=release,
+                                     type='web',
+                                     num=1)
+        self.assertEqual(c._command, "bash -c 'node server.js'")
+        c.type = 'worker'
+        self.assertEqual(c._command, "bash -c 'node worker.js'")
+        c.release.build.procfile = None
+        self.assertEqual(c._command, 'start worker')
+        c.type = 'cmd'
+        self.assertEqual(c._command, '')
diff --git a/tests/bin/build-deis-cli.sh b/tests/bin/build-deis-cli.sh
--- a/tests/bin/build-deis-cli.sh
+++ b/tests/bin/build-deis-cli.sh
@@ -4,5 +4,5 @@
 
 virtualenv --system-site-packages venv
 . venv/bin/activate
-pip install docopt==0.6.2 python-dateutil==2.2 requests==2.4.3 pyinstaller==2.1 termcolor==1.1.0
+pip install docopt==0.6.2 python-dateutil==2.2 PyYAML==3.11 requests==2.4.3 pyinstaller==2.1 termcolor==1.1.0
 make -C client/ client
diff --git a/tests/builds_test.go b/tests/builds_test.go
--- a/tests/builds_test.go
+++ b/tests/builds_test.go
@@ -16,7 +16,7 @@ import (
 
 var (
 	buildsListCmd   = "builds:list --app={{.AppName}}"
-	buildsCreateCmd = "builds:create {{.ImageID}} --app={{.AppName}}"
+	buildsCreateCmd = `builds:create {{.ImageID}} --app={{.AppName}} --procfile="worker: while true; do echo hi; sleep 3; done"`
 )
 
 func TestBuilds(t *testing.T) {
@@ -28,6 +28,7 @@ func TestBuilds(t *testing.T) {
 	// TODO: router needs a few seconds to wake up here--fixme!
 	time.Sleep(5000 * time.Millisecond)
 	appsOpenTest(t, params)
+	buildsScaleTest(t, params)
 	utils.AppsDestroyTest(t, params)
 }
 
@@ -84,3 +85,15 @@ func buildsCreateTest(t *testing.T, params *utils.DeisTestConfig) {
 		t.Fatal(err)
 	}
 }
+
+// buildsScaleTest ensures that we can use a Procfile-based workflow for `deis pull`.
+func buildsScaleTest(t *testing.T, params *utils.DeisTestConfig) {
+	if err := utils.Chdir(params.ExampleApp); err != nil {
+		t.Fatal(err)
+	}
+	utils.Execute(t, "scale worker=1 --app={{.AppName}}", params, false, "")
+	utils.Execute(t, "logs --app={{.AppName}}", params, false, "hi")
+	if err := utils.Chdir(".."); err != nil {
+		t.Fatal(err)
+	}
+}
diff --git a/tests/utils/itutils.go b/tests/utils/itutils.go
--- a/tests/utils/itutils.go
+++ b/tests/utils/itutils.go
@@ -84,11 +84,10 @@ func GetGlobalConfig() *DeisTestConfig {
 
 func doCurl(url string) ([]byte, error) {
 	response, err := http.Get(url)
-	defer response.Body.Close()
 	if err != nil {
 		return nil, err
 	}
-
+	defer response.Body.Close()
 	body, err := ioutil.ReadAll(response.Body)
 
 	if !strings.Contains(string(body), "Powered by Deis") {
@@ -103,7 +102,7 @@ func Curl(t *testing.T, params *DeisTestConfig) {
 	CurlWithFail(t, params, false, "")
 }
 
-// Curl connects to a Deis endpoint to see if the example app is running.
+// CurlWithFail connects to a Deis endpoint to see if the example app is running.
 func CurlWithFail(t *testing.T, params *DeisTestConfig, failFlag bool, expect string) {
 	url := "http://" + params.AppName + "." + params.Domain