Browse Source

Merge pull request #2 from andreacampi/COOK-634

COOK-634: refactor to use LWRPs
Joshua Timberman 12 years ago
parent
commit
c300569f5c

+ 115 - 247
README.md

@@ -1,18 +1,20 @@
 Application cookbook
 ====================
 
-This cookbook is initially designed to be able to describe and deploy web applications. Currently supported:
+This cookbook is designed to be able to describe and deploy web applications. It provides the basic infrastructure; other cookbooks are required to support specific combinations of frameworks and application servers. The following cookbooks are available at this time:
 
-* Rails
-* Java
-* Django
-* PHP
+* application\_java (Java and Tomcat)
+* application\_nginx (nginx reverse proxy)
+* application\_php (PHP with mod\_php\_apache2)
+* application\_python (Django with Gunicorn)
+* application\_rails (Rails with Passenger or Unicorn)
 
-Other application stacks (Rack, WSGI, etc) will be supported as new recipes at a later date.
+Backward compatibility
+----------------------
 
-This cookbook aims to provide primitives to install/deploy any kind of application driven entirely by data defined in an abstract way through a data bag.
+Previous versions of this cookbook used a set of recipes, with the configuration stored in an `apps` data bag.
 
-Note that as of version 0.99.10, this cookbook uses Chef 0.10's environments instead of the `app_environment` attribute. If you do not set up any environments for your nodes, they will be in the `_default` environment. See __Usage__ below for information on how to set up environments.
+This mode of operation has been DEPRECATED. The existing recipes will keep working for 3 months, and will then be removed. You are advised to upgrade your applications as soon as possible.
 
 Requirements
 ============
@@ -30,263 +32,146 @@ The following Opscode cookbooks are dependencies, as this cookbook supports auto
 * apache2
 * php
 
-Recipes
-=======
-
-The application cookbook contains the following recipes.
-
-default
--------
-
-Searches the `apps` data bag and checks that a server role in the app exists on this node, adds the app to the run state and uses the role for the app to locate the recipes that need to be used. The recipes listed in the "type" part of the data bag are included by this recipe, so only the "application" recipe needs to be in the node or role `run_list`.
-
-See below regarding the application data bag structure.
-
-django
-------
-
-Using the node's `run_state` that contains the current application in the search, this recipe will:
-
-* create an application specific virtualenv
-* install required packages and pips
-* set up the deployment scaffolding
-* creates `settings_local.py` file with the database connection information if required
-* performs a revision-based deploy
-
-This recipe can be used on nodes that are going to run the application, or on nodes that need to have the application code checkout available such as supporting utility nodes or a configured load balancer that needs static assets stored in the application repository.
-
-For pip requirements.txt files: ensure the requirements.txt file is present in the root of the application code (APP_ROOT/requirements.txt) or named after the node's current `chef_environment` in a directory named requirements (requirements/production.txt) and `pip install -r` will be run before migrations.
-
-In order to manage running database migrations (python manage.py migrate), you can use a role that sets the `run_migrations` attribute for the application (`my_app`, below) in the correct environment (production, below). Note the data bag item needs to have migrate set to true. See the data bag example below.
-
-    {
-      "name": "my_app_run_migrations",
-      "description": "Run db:migrate on demand for my_app",
-      "json_class": "Chef::Role",
-      "default_attributes": {
-      },
-      "override_attributes": {
-        "apps": {
-          "my_app": {
-            "production": {
-              "run_migrations": true
-            }
-          }
-        }
-      },
-      "chef_type": "role",
-      "run_list": [
-      ]
-    }
-
-Simply apply this role to the node's run list when it is time to run migrations, and the recipe will remove the role when done.  Since Django does not have a standard database migration function built into the core framework, we assume the popular [South framework](http://south.aeracode.org/) is being used.
-
-gunicorn
---------
-
-Requires `gunicorn` cookbook.
-
-Gunicorn is installed, default attributes are set for the node and an app specific gunicorn config and runit service are created.
-
-java_webapp
------------
-
-Using the node's `run_state` that contains the current application in the search, this recipe will:
-
-* install required packages
-* set up the deployment scaffolding
-* create the context configuration for the servlet container
-* performs a `remote_file` deploy.
-
-The servlet container context configuration (`context.xml`) exposes the following JNDI resources which can be referenced by the webapp's deployment descriptor (web.xml):
-
-* A JDBC datasource for all databases in the node's current `chef_environment`.  The datasource uses the information (including JDBC driver) specified in the data bag item for the application.
-* An Environment entry that matches the node's current `chef_environment` attribute value.  This is useful for loading environment specific properties files in the web application.
-
-This recipe assumes some sort of build process, such as Maven or a Continuous Integration server like Hudson, will create a deployable artifact and make it available for download via HTTP (such as S3 or artifactory).
-
-mod\_php\_apache2
------------------
-
-Requires `apache2` cookbook. Sets up a mod_php vhost template for the application using the `apache2` cookbook's `web_app` definition. See data bag example below.
-
-passenger\_apache2
-------------------
-
-Requires `apache2` and `passenger_apache2` cookbooks. Sets up a passenger vhost template for the application using the `apache2` cookbook's `web_app` definition. Use this with the `rails` recipe, in the list of recipes for a specific application type. See data bag example below.
-
-php
----
-
-Using the node's `run_state` that contains the current application in the search, this recipe will:
-
-* install required packages and pears/pecls
-* set up the deployment scaffolding
-* creates a `local_settings.php` (specific file name and project path is configurable) file with the database connection information if required
-* performs a revision-based deploy
-
-This recipe can be used on nodes that are going to run the application, or on nodes that need to have the application code checkout available such as supporting utility nodes or a configured load balancer that needs static assets stored in the application repository.
-
-Since PHP projects do not have a standard `local_settings.php` file (or format) that contains database connection information. This recipe assumes you will provide a template in an application specific cookbook.  See additional notes in the 'Application Data Bag' section below.
-
-rails
------
-
-Using the node's `run_state` that contains the current application in the search, this recipe will:
-
-* install required packages and gems
-* set up the deployment scaffolding
-* creates database and memcached configurations if required
-* performs a revision-based deploy.
-
-This recipe can be used on nodes that are going to run the application, or on nodes that need to have the application code checkout available such as supporting utility nodes or a configured load balancer that needs static assets stored in the application repository.
-
-For Gem Bundler: include `bundler` or `bundler08` in the gems list.  `bundle install` or `gem bundle` will be run before migrations.  The `bundle install` command is invoked with the `--deployment` and `--without` flags following [Bundler best practices](http://gembundler.com/deploying.html).
-
-For config.gem in environment: `rake gems:install RAILS_ENV=<node environment>` will be run when a Gem Bundler command is not.
-
-In order to manage running database migrations (rake db:migrate), you can use a role that sets the `run_migrations` attribute for the application (`my_app`, below) in the correct environment (production, below). Note the data bag item needs to have migrate set to true. See the data bag example below.
-
-    {
-      "name": "my_app_run_migrations",
-      "description": "Run db:migrate on demand for my_app",
-      "json_class": "Chef::Role",
-      "default_attributes": {
-      },
-      "override_attributes": {
-        "apps": {
-          "my_app": {
-            "production": {
-              "run_migrations": true
-            }
-          }
-        }
-      },
-      "chef_type": "role",
-      "run_list": [
-      ]
-    }
-
-Simply apply this role to the node's run list when it is time to run migrations, and the recipe will remove the role when done.
-
-tomcat
--------
-
-Requires `tomcat` cookbook.
-
-Tomcat is installed, default attributes are set for the node and the app specific context.xml is symlinked over to Tomcat's context directory as the root context (ROOT.xml).
-
-unicorn
--------
-
-Requires `unicorn` cookbook.
-
-Unicorn is installed, default attributes are set for the node and an app specific unicorn config and runit service are created.
-
 Deprecated Recipes
 ==================
 
-The following recipes are deprecated and have been removed from the cookbook. To retrieve an older version, reference commit 4396ce6.
-
-* `passenger-nginx`
-* `rails_nginx_ree_passenger`
-
-Application Data Bag
-=====================
-
-The applications data bag expects certain values in order to configure parts of the recipe. Below is a paste of the JSON, where the value is a description of the key. Use your own values, as required. Note that this data bag is also used by the `database` cookbook, so it will contain database information as well. Items that may be ambiguous have an example.
-
-The application used in examples is named `my_app` and the environment is `production`. Most top-level keys are Arrays, and each top-level key has an entry that describes what it is for, followed by the example entries. Entries that are hashes themselves will have the description in the value. In order to use the environment `production` you must create the environment as described below under __Usage__.
+The following recipes are deprecated:
 
-Note about "type": the recipes listed in the "type" will be included in the run list via `include_recipe` in the application default recipe based on the type matching one of the `server_roles` values.
+* `default`
+* `django`
+* `gunicorn`
+* `java_webapp`
+* `mod_php_apache2`
+* `passenger_apache2`
+* `php`
+* `rails`
+* `tomcat`
+* `unicorn`
 
-Note about packages, the version is optional. If specified, the version will be passed as a parameter to the resource. Otherwise it will use the latest available version per the default `:install` action for the package provider.
+Resources/Providers
+===================
 
-Rail's version additional notes
--------------------------------
+The `application` LWRP configures the basic properties of most applications, regardless of the framework or application server they use. These include:
 
-Note about `databases`, the data specified will be rendered as the `database.yml` file. In the `database` cookbook, this information is also used to set up privileges for the application user, and create the databases.
+* SCM information for the deployment, such as the repository URL and branch name;
+* deployment destination, including the filesystem path to deploy to;
+* any OS packages to install as dependencies;
+* optional callback to control the deployment.
 
-Note about gems, the version is optional. If specified, the version will be passed as a parameter to the resource. Otherwise it will use the latest available version per the default `:install` action for the package provider.
+This LWRP uses the `deploy_revision` LWRP to perform the bulk of its tasks, and many concepts and parameters map directly to it. Check the documentation for `deploy_revision` for more information.
 
-An example is data bag item is included in this cookbook at `examples/data_bags/apps/rails_app.json`.
+Configuration of framework-specific aspects of the application are performed by invoking a sub-resource; see the appropriate cookbook for more documentation.
 
-Java webapp version additional notes
-------------------------------------
+# Actions
 
-Note about `databases`, the data specified will be rendered as JNDI Datasource `Resources` in the servlet container context confiruation (`context.xml`) file. In the `database` cookbook, this information is also used to set up privileges for the application user, and create the databases.
+- :deploy: deploy an application, including any necessary configuration, restarting the associated service if necessary.
 
-An example is data bag item is included in this cookbook at `examples/data_bags/apps/java_app.json`.
+# Attribute Parameters
 
-Django version additional notes
--------------------------------
+- name: name attribute. The name of the application you are setting up. This will be used to derive the default value for other attribute
+- packages: an Array or Hash of packages to be installed before starting the deployment
+- path: target path of the deployment; it will be created if it does not exist
+- owner: the user that shall own the target path
+- owner: the group that shall own the target path
+- strategy: the underlying LWRP that will be used to perform the deployment. The default is `:deploy_revision`, and it should never be necessary to change it
+- scm_provider: the provider class to use for the deployment. It defaults to `Chef::Provider::Git`, you can set it to `Chef::Provider::Subversion` to deploy from an SVN repository
+- repository: the URL of the repository the application should be checked out from
+- revision: an identifier pointing to the revision that should be checkout out
+- deploy_key: the public key to use to access the repository via SSH
+- environment: a Hash of environment variables to set while running migrations
+- purge\_before\_symlink: an Array of paths (relative to the checkout) to remove before creating symlinks
+- create\_dirs\_before\_symlink: an Array paths (relative to the checkout) pointing to directories to create before creating symlinks
+- symlinks: a Hash of shared/dir/path => release/dir/path. It determines which files and dirs in the shared directory get symlinked to the current release directory
+- symlink\_before\_migrate: similar to symlinks, except that they will be linked before any migration is run
+- migrate: if `true` then migrations will be run; defaults to false
+- migration_command: a command to run to migrate the application from the previous to the current state
+- restart_command: a command to run when restarting the application
+- environment_name: the name of a framework-specific "environment" (for example the Rails environment). By default it is the same as the Chef environment, unless it is `_default`, in which case it is set to `production`
 
-Note about `databases`, the data specified will be rendered as the `settings_local.py` file. In the `database` cookbook, this information is also used to set up privileges for the application user, and create the databases.
+# Callback Attributes
 
-Note about pips, the version is optional. If specified, the version will be passed as a parameter to the resource. Otherwise it will use the latest available version per the default `:install` action for the python_pip package provider.
+You can also set a few attributes on this LWRP that are interpreted as callback to be called at specific points during a deployment.
+If you pass a block, it will be evaluated within a new context. If you pass a string, it will be interpreted as a path (relative to the release directory) to a file; if it exists, it will be loaded and evaluated as though it were a Chef recipe.
 
-The `local_settings_file` value may be used to supply an alternate name for the environment specific `settings_local.py`, since Django projects do not have a standard name for this file.
+The following callback attributes are available:
 
-An example is data bag item is included in this cookbook at `examples/data_bags/apps/django_app.json`.
+- before\_deploy: invoked immediately after initial setup and before the deployment proper is started. This callback will be invoked on every Chef run
+- before\_migrate
+- before\_symlink
+- before\_restart
+- after\_restart
 
-PHP version additional notes
-----------------------------
+# Sub-resources
 
-Note about `databases`, the data specified will be rendered as the `local_settings.php` file. In the `database` cookbook, this information is also used to set up privileges for the application user, and create the databases.
+Anything that is not a known attribute will be interpreted as the name of a sub-resource; the resource will be looked up, and any nested attribute will be passed to it. More than one sub-resource can be added to an application; the order is significant, with the latter sub-resources overriding any sub-resource that comes before.
 
-Note about pears/pecls, the version is optional. If specified, the version will be passed as a parameter to the resource. Otherwise it will use the latest available version per the default `:install` action for the php_pear package provider.
+Sub-resources can set their own values for some attributes; if they do, they will be merged together with the attribute set on the main resource. The attributes that support this behavior are the following:
 
-The `local_settings_file` value is used to supply the name, and relative local project path, for the environment specific `local_settings.php`, since PHP projects do not have a standard name (or location) for this file.
+- environment: environment variables from the application and from sub-resources will be merged together, with later resources overriding values set in the application or previous resources
+- migration_command: commands from the application and from sub-resources will be concatenated together joined with '&&' and run as a single shell command. The migration will only succeed if all the commands succeed
+- restart_command: commands from the application and from sub-resources will be evaluated in order
+- symlink\_before\_migrate: will be concatenated as a single array
+- symlink\_before\_migrate: will be merged
+- callbacks: sub-resources callbacks will be invoked first, followed by the application callbacks
 
-For applications that look for this file in the project root just supply a name:
+Usage
+=====
 
-MediaWiki:
+To use the application cookbook we recommend creating a cookbook named after the application, e.g. `my_app`. In `metadata.rb` you should declare a dependency on this cookbook and any framework cookbook the application may need. For example a Rails application may include:
 
-    "local_settings_file": "LocalSettings.php"
+    depends "application"
+    depends "application_rails"
 
-Wordpress:
+The default recipe should describe your application using the `application` LWRP; you may also include additional recipes, for example to set up a database, queues, search engines and other components of your application.
 
-    "local_settings_file": "wp-config.php"
+A recipe using this LWRP may look like this:
 
-For applications that expect the file nested within the project root, you can supply a relative path:
+    application "my_app" do
+      path "/deploy/to/dir"
+      owner "app-user"
+      group "app-group"
 
-CakePHP:
+      repository "http://git.example.com/my-app.git"
+      branch "production"
 
-    "local_settings_file": "app/config/database.php"
+      rails do
+        # Rails-specific configuration
+      end
 
-The template used to render this `local_settings.php` file is assumed to be provided in an application specific cookbook named after the application being deployed.  For example if you were deploying code for an application named `mediawiki` you would create a cookbook named `mediawiki` and in that cookbook place a template named `LocalSettings.php.erb`:
+      passenger_apache2 do
+        # Passenger-specific configuration
+      end
+    end
 
-    mediawiki/
-    +-- files
-    |   +-- default
-    |       +-- schema.sql
-    +-- metadata.rb
-    +-- README.md
-    +-- recipes
-    |   +-- db_bootstrap.rb
-    |   +-- default.rb
-    +-- templates
-        +-- default
-            +-- LocalSettings.php.erb
+You can of course use any code necessary to determine the value of attributes:
 
-The template will be passed the following variables which can be used to dynamically fill values in the ERB:
+    application "my_app" do
+      repository "http://git.example.com/my-app.git"
+      branch node.chef_environment == "production" ? "production" : "develop"
+    end
 
-* path - fill path to the 'current' project path
-* host - database master fqdn
-* database - environment specific database information from the application's data bag item
-* app - Ruby mash representation of the complete application data bag item for this app, useful if other arbitrary config data has been stashed in the data bag item.
+Attributes from the application and from sub-resources are merged together:
 
-A few example `local_settings` templates are included in this cookbook at `examples/templates/defaults/*`:
+    application "my_app" do
+      restart_command "kill -1 `cat /var/run/one.pid`"
+      environment "LC_ALL" => "en", "FOO" => "bar"
 
-* MediaWiki - LocalSettings.php.erb
-* Wordpress - wp-config.php.erb
+      rails do
+        restart_command "touch /tmp/something"
+        environment "LC_ALL" => "en_US"
+      end
 
-An example is data bag item is included in this cookbook at `examples/data_bags/apps/php_app.json`.
+      passenger_apache2 do
+        environment "FOO" => "baz"
+      end
+    end
 
-Usage
-=====
+    # at the end, you will have:
+    #
+    # restart_command #=> kill -1 `cat /var/run/one.pid` && touch /tmp/something
+    # environment #=> LC_ALL=en_US FOO=baz
 
-To use the application cookbook, we recommend creating a role named after the application, e.g. `my_app`. This role should match one of the `server_roles` entries, that will correspond to a `type` entry, in the databag. Create a Ruby DSL role in your chef-repo, or create the role directly with knife.
+To use the application cookbook, we recommend creating a role named after the application, e.g. `my_app`. Create a Ruby DSL role in your chef-repo, or create the role directly with knife.
 
     % knife role show my_app -Fj
     {
@@ -297,38 +182,21 @@ To use the application cookbook, we recommend creating a role named after the ap
       },
       "description": "",
       "run_list": [
-        "recipe[application]"
+        "recipe[my_app]"
       ],
       "override_attributes": {
       }
     }
 
-Also recommended is a cookbook named after the application, e.g. `my_app`, for additional application specific setup such as other config files for queues, search engines and other components of your application. The `my_app` recipe can be used in the run list of the role, if it includes the `application` recipe.
-
-You should also create an environment. We use `production` in the examples and the documentation above. An example is in the source code's "examples" directory, and the JSON for an environment is below:
-
-    % knife environment show production -Fj
-    {
-      "name": "production",
-      "description": "",
-      "cookbook_versions": {
-      },
-      "json_class": "Chef::Environment",
-      "chef_type": "environment",
-      "default_attributes": {
-      },
-      "override_attributes": {
-      }
-    }
-
 License and Author
 ==================
 
 Author:: Adam Jacob (<adam@opscode.com>)
+Author:: Andrea Campi (<andrea.campi@zephirworks.com.com>)
 Author:: Joshua Timberman (<joshua@opscode.com>)
 Author:: Seth Chisamore (<schisamo@opscode.com>)
 
-Copyright 2009-2011, Opscode, Inc.
+Copyright 2009-2012, Opscode, Inc.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.

+ 178 - 0
libraries/default.rb

@@ -0,0 +1,178 @@
+#
+# Author:: Noah Kantrowitz <noah@opscode.com>
+# Cookbook Name:: application
+# Library:: default
+#
+# Copyright:: 2011-2012, Opscode, Inc <legal@opscode.com>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+class Chef
+  class Resource
+    # Globally update the blocklists to prevent infinite recursion in #to_json and similar
+    FORBIDDEN_IVARS.concat [:@application, :@application_provider]
+    HIDDEN_IVARS.concat [:@application, :@application_provider]
+
+    class Application
+      module OptionsCollector
+        def options
+          @options ||= {}
+        end
+
+        def method_missing(method_sym, value=nil, &block)
+          super
+        rescue NameError
+          value ||= block
+          method_sym = method_sym.to_s.chomp('=').to_sym
+          options[method_sym] = value if value
+          options[method_sym] ||= nil
+        end
+      end
+    end
+
+    module ApplicationBase
+      def self.included(klass)
+        klass.actions :before_compile, :before_deploy, :before_migrate, :before_symlink, :before_restart, :after_restart
+        klass.attribute :id, :kind_of => String, :name_attribute => true
+        klass.attribute :environment, :kind_of => Hash, :default => {}
+        klass.attribute :purge_before_symlink, :kind_of => Array, :default => []
+        klass.attribute :create_dirs_before_symlink, :kind_of => Array, :default => []
+        klass.attribute :symlinks, :kind_of => Hash, :default => {}
+        klass.attribute :symlink_before_migrate, :kind_of => Hash, :default => {}
+        klass.attribute :migration_command, :kind_of => [String, NilClass], :default => nil
+        klass.attribute :application
+        klass.attribute :application_provider
+        klass.attribute :type
+      end
+
+      def restart_command(arg=nil, &block)
+        arg ||= block
+        raise "Invalid restart command" unless !arg || arg.is_a?(String) || arg.is_a?(Proc)
+        @restart_command = arg if arg
+        @restart_command
+      end
+
+      def method_missing(name, *args)
+        if application.respond_to? name
+          application.send(name, *args)
+        else
+          super
+        end
+      end
+
+      def release_path
+        application_provider.release_path
+      end
+
+      class OptionsBlock
+        include Chef::Resource::Application::OptionsCollector
+      end
+
+      def options_block(options=nil, &block)
+        options ||= {}
+        if block
+          collector = OptionsBlock.new
+          collector.instance_eval(&block)
+          options.update(collector.options)
+        end
+        options
+      end
+
+      def find_matching_role(role, single=true, &block)
+        return nil if !role
+        nodes = []
+        if node['roles'].include? role
+          nodes << node
+        end
+        if !single || nodes.empty?
+          search(:node, "role:#{role} AND chef_environment:#{node.chef_environment}") do |n|
+            nodes << n
+          end
+        end
+        if block
+          nodes.each do |n|
+            yield n
+          end
+        else
+          if single
+            nodes.first
+          else
+            nodes
+          end
+        end
+      end
+
+      def find_database_server(role)
+        dbm = find_matching_role(role)
+        Chef::Log.warn("No node with role #{role}") if role && !dbm
+
+        if respond_to?(:database) && database.has_key?('host')
+          database['host']
+        elsif dbm && dbm.attribute?('cloud')
+          dbm['cloud']['local_ipv4']
+        elsif dbm
+          dbm['ipaddress']
+        end
+      end
+    end
+  end
+
+  class Provider
+    module ApplicationBase
+
+      def self.included(klass)
+        klass.extend Chef::Mixin::FromFile
+      end
+
+      def release_path
+        if !@deploy_provider
+          #@deploy_provider = Chef::Platform.provider_for_resource(@run_context.resource_collection.find(:deploy_revision => @new_resource.id))
+          @deploy_provider = Chef::Platform.provider_for_resource(@deploy_resource)
+          @deploy_provider.load_current_resource
+        end
+        @deploy_provider.release_path
+      end
+
+      def callback(what, callback_code=nil)
+        Chef::Log.debug("Got callback #{what}: #{callback_code.inspect}")
+        @collection = Chef::ResourceCollection.new
+        case callback_code
+        when Proc
+          Chef::Log.info "#{@new_resource} running callback #{what}"
+          recipe_eval(&callback_code)
+        when String
+          callback_file = "#{release_path}/#{callback_code}"
+          unless ::File.exist?(callback_file)
+            raise RuntimeError, "Can't find your callback file #{callback_file}"
+          end
+          run_callback_from_file(callback_file)
+        when nil
+          nil
+        else
+          raise RuntimeError, "You gave me a callback I don't know what to do with: #{callback_code.inspect}"
+        end
+      end
+
+      def run_callback_from_file(callback_file)
+        if ::File.exist?(callback_file)
+          Dir.chdir(release_path) do
+            Chef::Log.info "#{@new_resource} running deploy hook #{callback_file}"
+            recipe_eval { from_file(callback_file) }
+          end
+        end
+      end
+
+    end
+  end
+end

+ 3 - 3
metadata.rb

@@ -1,9 +1,9 @@
 maintainer       "Opscode, Inc."
 maintainer_email "cookbooks@opscode.com"
 license          "Apache 2.0"
-description      "Deploys and configures a variety of applications defined from databag 'apps'"
+description      "Deploys and configures a variety of applications"
 long_description  IO.read(File.join(File.dirname(__FILE__), 'README.md'))
-version          "0.99.14"
+version          "1.1.0"
 recipe           "application", "Loads application databags and selects recipes to use"
 recipe           "application::django", "Deploys a Django application specified in a data bag with the deploy_revision resource"
 recipe           "application::gunicorn", "Sets up the deployed Django application with Gunicorn as the web server"
@@ -15,7 +15,7 @@ recipe           "application::rails", "Deploys a Rails application specified in
 recipe           "application::tomcat", "Sets up the deployed Java application with Tomcat as the servlet container"
 recipe           "application::unicorn", "Sets up the deployed Rails application with Unicorn as the web server"
 
-%w{ runit unicorn apache2 passenger_apache2 tomcat gunicorn apache2 php }.each do |cb|
+%w{ runit unicorn apache2 passenger_apache2 tomcat python gunicorn php }.each do |cb|
   depends cb
 end
 

+ 142 - 0
providers/default.rb

@@ -0,0 +1,142 @@
+#
+# Author:: Noah Kantrowitz <noah@opscode.com>
+# Cookbook Name:: application
+# Provider:: default
+#
+# Copyright:: 2011-2012, Opscode, Inc <legal@opscode.com>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+include Chef::Provider::ApplicationBase
+
+action :deploy do
+  # Alias to a variable so I can use in sub-resources
+  new_resource = @new_resource
+  app_provider = self
+
+  new_resource.sub_resources.each do |resource|
+    resource.application_provider self
+    resource.run_action :before_compile
+  end
+
+  new_resource.packages.each do |pkg,ver|
+    package pkg do
+      action :install
+      version ver if ver && ver.length > 0
+    end
+  end
+
+  directory new_resource.path do
+    owner new_resource.owner
+    group new_resource.group
+    mode '0755'
+    recursive true
+  end
+
+  directory "#{new_resource.path}/shared" do
+    owner new_resource.owner
+    group new_resource.group
+    mode '0755'
+    recursive true
+  end
+
+  if new_resource.deploy_key
+    file "#{new_resource.path}/id_deploy" do
+      content new_resource.deploy_key
+      owner new_resource.owner
+      group new_resource.group
+      mode '0600'
+    end
+
+    template "#{new_resource.path}/deploy-ssh-wrapper" do
+      source "deploy-ssh-wrapper.erb"
+      cookbook "application"
+      owner new_resource.owner
+      group new_resource.group
+      mode "0755"
+      variables :id => new_resource.name, :deploy_to => new_resource.path
+    end
+  end
+
+  ruby_block "#{new_resource.name} before_deploy" do
+    block do
+      new_resource.sub_resources.each do |resource|
+        resource.run_action :before_deploy
+      end
+      callback(:before_deploy, new_resource.before_deploy)
+    end
+  end
+
+  @deploy_resource = send(new_resource.strategy.to_sym, new_resource.name) do
+    scm_provider new_resource.scm_provider
+    revision new_resource.revision
+    repository new_resource.repository
+    user new_resource.owner
+    group new_resource.group
+    deploy_to new_resource.path
+    ssh_wrapper "#{new_resource.path}/deploy-ssh-wrapper" if new_resource.deploy_key
+    shallow_clone true
+    all_environments = ([new_resource.environment]+new_resource.sub_resources.map{|res| res.environment}).inject({}){|acc, val| acc.merge(val)}
+    environment all_environments
+    migrate new_resource.migrate
+    all_migration_commands = ([new_resource.migration_command]+new_resource.sub_resources.map{|res| res.migration_command}).select{|cmd| cmd && !cmd.empty?}
+    migration_command all_migration_commands.join(' && ')
+    restart_command do
+      ([new_resource]+new_resource.sub_resources).each do |res|
+        cmd = res.restart_command
+        if cmd.is_a? Proc
+          provider = Chef::Platform.provider_for_resource(res)
+          provider.load_current_resource
+          provider.instance_eval(&cmd)
+        elsif cmd && !cmd.empty?
+          execute cmd do
+            user new_resource.owner
+            group new_resource.group
+            environment all_environments
+          end
+        end
+      end
+    end
+    purge_before_symlink new_resource.purge_before_symlink
+    create_dirs_before_symlink new_resource.create_dirs_before_symlink
+    symlinks new_resource.symlinks
+    all_symlinks_before_migrate = [new_resource.symlink_before_migrate]+new_resource.sub_resources.map{|res| res.symlink_before_migrate}
+    symlink_before_migrate all_symlinks_before_migrate.inject({}){|acc, val| acc.merge(val)}
+    before_migrate do
+      app_provider.send(:run_actions_with_context, :before_migrate, @run_context)
+    end
+    before_symlink do
+      app_provider.send(:run_actions_with_context, :before_symlink, @run_context)
+    end
+    before_restart do
+      app_provider.send(:run_actions_with_context, :before_restart, @run_context)
+    end
+    after_restart do
+      app_provider.send(:run_actions_with_context, :after_restart, @run_context)
+    end
+  end
+
+end
+
+protected
+
+def run_actions_with_context(action, context)
+  new_resource.sub_resources.each do |resource|
+    saved_run_context = resource.instance_variable_get :@run_context
+    resource.instance_variable_set :@run_context, context
+    resource.run_action action
+    resource.instance_variable_set :@run_context, saved_run_context
+  end
+  callback(action, new_resource.send(action))
+end

+ 2 - 0
recipes/django.rb

@@ -17,6 +17,8 @@
 # limitations under the License.
 #
 
+Chef::Log.warn "The application::django recipe is deprecated and will go away on Aug 1st. See the README for migration information."
+
 app = node.run_state[:current_app]
 
 include_recipe "python"

+ 2 - 0
recipes/gunicorn.rb

@@ -17,6 +17,8 @@
 # limitations under the License.
 #
 
+Chef::Log.warn "The application::gunicorn recipe is deprecated and will go away on Aug 1st. See the README for migration information."
+
 app = node.run_state[:current_app] 
 
 ve = resources(:python_virtualenv => app['id'])

+ 2 - 0
recipes/java_webapp.rb

@@ -17,6 +17,8 @@
 # limitations under the License.
 #
 
+Chef::Log.warn "The application::java_webapp recipe is deprecated and will go away on Aug 1st. See the README for migration information."
+
 app = node.run_state[:current_app]
 
 ###

+ 2 - 0
recipes/mod_php_apache2.rb

@@ -17,6 +17,8 @@
 # limitations under the License.
 #
 
+Chef::Log.warn "The application::mod_php_apache2 recipe is deprecated and will go away on Aug 1st. See the README for migration information."
+
 app = node.run_state[:current_app]
 
 node.default['apache']['listen_ports'] = [ "8080" ]

+ 2 - 0
recipes/passenger_apache2.rb

@@ -17,6 +17,8 @@
 # limitations under the License.
 #
 
+Chef::Log.warn "The application::passenger_apache2 recipe is deprecated and will go away on Aug 1st. See the README for migration information."
+
 app = node.run_state[:current_app] 
 
 include_recipe "apache2"

+ 2 - 0
recipes/php.rb

@@ -17,6 +17,8 @@
 # limitations under the License.
 #
 
+Chef::Log.warn "The application::php recipe is deprecated and will go away on Aug 1st. See the README for migration information."
+
 app = node.run_state[:current_app]
 
 include_recipe "php"

+ 2 - 0
recipes/rails.rb

@@ -17,6 +17,8 @@
 # limitations under the License.
 #
 
+Chef::Log.warn "The application::rails recipe is deprecated and will go away on Aug 1st. See the README for migration information."
+
 app = node.run_state[:current_app]
 
 # make the _default chef_environment look like the Rails production environment

+ 2 - 0
recipes/tomcat.rb

@@ -17,6 +17,8 @@
 # limitations under the License.
 #
 
+Chef::Log.warn "The application::tomcat recipe is deprecated and will go away on Aug 1st. See the README for migration information."
+
 app = node.run_state[:current_app]
 
 include_recipe "tomcat"

+ 2 - 0
recipes/unicorn.rb

@@ -17,6 +17,8 @@
 # limitations under the License.
 #
 
+Chef::Log.warn "The application::unicorn recipe is deprecated and will go away on Aug 1st. See the README for migration information."
+
 app = node.run_state[:current_app] 
 
 include_recipe "unicorn"

+ 118 - 0
resources/default.rb

@@ -0,0 +1,118 @@
+#
+# Author:: Noah Kantrowitz <noah@opscode.com>
+# Cookbook Name:: application
+# Resource:: default
+#
+# Copyright:: 2011-2012, Opscode, Inc <legal@opscode.com>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'weakref'
+
+include Chef::Mixin::RecipeDefinitionDSLCore
+
+def initialize(*args)
+  super
+  @action = :deploy
+  @sub_resources = []
+end
+
+actions :deploy
+
+attribute :name, :kind_of => String, :name_attribute => true
+attribute :environment_name, :kind_of => String, :default => (node.chef_environment =~ /_default/ ? "production" : node.chef_environment)
+attribute :path, :kind_of => String
+attribute :owner, :kind_of => String
+attribute :group, :kind_of => String
+attribute :strategy, :kind_of => [String, Symbol], :default => :deploy_revision
+attribute :scm_provider, :kind_of => [Class, String, Symbol]
+attribute :revision, :kind_of => String
+attribute :repository, :kind_of => String
+attribute :environment, :kind_of => Hash, :default => {}
+attribute :deploy_key, :kind_of => [String, NilClass], :default => nil
+attribute :force, :kind_of => [TrueClass, FalseClass], :default => false
+attribute :purge_before_symlink, :kind_of => Array, :default => []
+attribute :create_dirs_before_symlink, :kind_of => Array, :default => []
+attribute :symlinks, :kind_of => Hash, :default => {}
+attribute :symlink_before_migrate, :kind_of => Hash, :default => {}
+attribute :migrate, :kind_of => [TrueClass, FalseClass], :default => false
+attribute :migration_command, :kind_of => [String, NilClass], :default => nil
+attribute :restart_command, :kind_of => [String, NilClass], :default => nil
+attribute :packages, :kind_of => [Array, Hash], :default => []
+attr_reader :sub_resources
+
+# Callback fires before deploy is started.
+def before_deploy(arg=nil, &block)
+  arg ||= block
+  set_or_return(:before_deploy, arg, :kind_of => [Proc, String])
+end
+
+# Callback fires before migration is run.
+def before_migrate(arg=nil, &block)
+  arg ||= block
+  set_or_return(:before_migrate, arg, :kind_of => [Proc, String])
+end
+
+# Callback fires before symlinking
+def before_symlink(arg=nil, &block)
+  arg ||= block
+  set_or_return(:before_symlink, arg, :kind_of => [Proc, String])
+end
+
+# Callback fires before restart
+def before_restart(arg=nil, &block)
+  arg ||= block
+  set_or_return(:before_restart, arg, :kind_of => [Proc, String])
+end
+
+# Callback fires after restart
+def after_restart(arg=nil, &block)
+  arg ||= block
+  set_or_return(:after_restart, arg, :kind_of => [Proc, String])
+end
+
+def method_missing(name, *args, &block)
+  # Build the set of names to check for a valid resource
+  lookup_path = ["application_#{name}"]
+  run_context.cookbook_collection.each do |cookbook_name, cookbook_ver|
+    if cookbook_name.start_with?("application_")
+      lookup_path << "#{cookbook_name}_#{name}"
+    end
+  end
+  lookup_path << name
+  resource = nil
+  # Try to find our resource
+  lookup_path.each do |resource_name|
+    begin
+      Chef::Log.debug "Trying to load application resource #{resource_name} for #{name}"
+      resource = super(resource_name.to_sym, self.name, &block)
+      break
+    rescue NameError => e
+      # Works on any MRI ruby
+      if e.name == resource_name.to_sym || e.inspect =~ /`#{resource_name}'/
+        next
+      else
+        raise e
+      end
+    end
+  end
+  raise NameError, "No resource found for #{name}. Tried #{lookup_path.join(', ')}" unless resource
+  # Enforce action :nothing in case people forget
+  resource.action :nothing
+  # Make this a weakref to prevent a cycle between the application resource and the sub resources
+  resource.application WeakRef.new(self)
+  resource.type name
+  @sub_resources << resource
+  resource
+end