capistrano / sshkit

A toolkit for deploying code and assets to servers in a repeatable, testable, reliable way.
MIT License
1.14k stars 253 forks source link

Support for a `source` option to commands #282

Closed h-lame closed 8 years ago

h-lame commented 8 years ago

As discussed in #35 it's a not uncommon deployment strategy for rails apps to have config files that use <%= ENV['something'] %> to set a value based on environment variables. We see this most commonly with DATABASE_URL or RAILS_SERVE_STATIC_FILES variables. One way of populating the environment is to place a file in /etc/default/ which we source in /etc/init.d/ scripts for the app or the .bash_profile of the app user.

To support using capistrano to deploy apps and execute scripts on servers configured in this way I'd like to add source methods to sshkit (similar to with or within) to specify files to source before running a command and thus setting up the env properly. I've got this working as a quick monkeypatch for an app and wondered if there's any desire for it before I proceed to turn it into a full PR.

My current implementation is as follows (a full PR would come with specs):

module SSHKit
  module Backend
    class Abstract
      def source(file, &block)
        @_source_files = (@source_files ||= [])
        @source_files = @_source_files + [file]
        yield
      ensure
        @source_files = @_source_files
        remove_instance_variable(:@_source_files)
      end

      private

      def command(*args)
        options = args.extract_options!
        SSHKit::Command.new(*[*args, options.merge({in: @pwd.nil? ? nil : File.join(@pwd), env: @env, host: @host, user: @user, group: @group, source_files: @source_files})])
      end
    end
  end

  class Configuration
    attr_writer :default_source_files
    def default_source_files
      @default_source_files ||= []
    end
  end

  class Command
    def source_files_list
      (SSHKit.config.default_source_files) + (options[:source_files] || [])
    end

    def source_files_string
      source_files_list.collect do |source_file|
        "source #{source_file}"
      end.join(' && ')
    end

    def source(&block)
      return yield if source_files_list.empty?
      "#{source_files_string} && %s" % yield
    end

    def to_command
      return command.to_s unless should_map?
      within do
        umask do
          source do
            with do
              user do
                in_background do
                  group do
                    to_s
                  end
                end
              end
            end
          end
        end
      end
    end
  end
end
leehambley commented 8 years ago

Surely a simpler way is to use the Command Map to make the "command" a shim/wrapper that loads the env first, this is an old unix principle (see /etc/init.d files) as well as being the way rbenv, and bundler shims work. If your configuration management is able to drop the "dotfiles" with the env variables in there, you should be adding a myapp_rake shim to wrap rake in loading the command, I'm quite against adding something like this to SSHKit, as we try really, really hard to make sure that you have a clean env for your commands, and that running them from other tools is unsurprising.

h-lame commented 8 years ago

Thanks for the quick response and advice @leehambley! I'll definitely take a look at setting up shims as that does sound clearer. I guess they have to be set up by my cap script not my configuration management because I'll need one for the binfiles of every gem that is bundle installed which currently happens via cap. That said, maybe I can just shim bundle as everything goes through that - and that could probably be done from configuration management.

Totally agree about not adding features you don't want to support and if this does go no-where I'll drop this patch in my own app for similar reasons - I'd rather not maintain something that is so opposite what.

In case I can convince you though I don't really see how source would be (much) different to the default_env configuration and with dsl. They both let you set up the environment for a command to run in. The difference is that for default_env and with the data has to come from the local machine, but with source the data can be read from the remote.

leehambley commented 8 years ago

In case I can convince you though I don't really see how source would be (much) different to the default_env configuration and with dsl. They both let you set up the environment for a command to run in. The difference is that for default_env and with the data has to come from the local machine, but with source the data can be read from the remote.

I guess the primary reason, is that what is sourced, depends a lot on how you get a shell, or run something, and which shell you use, we have some docs, for Bash here http://capistranorb.com/documentation/faq/why-does-something-work-in-my-ssh-session-but-not-in-capistrano/ (bottom of the page) - but other shells are different, and systemd, init.d, monit, etc, etc all operate differently, not to mention Apache, and Nginx, and things triggered via Cron, so we're very fond of entry points (shims) which setup the enivironment, and are always, always always used.

I've seen in the past that for e.g, a company I consulted with had a shim on the path ourapp_production_ruby which was a shim (actually, more or less the rbenv shim) but also exported env vars, this was used in all cronjobs, as the PassengerRuby for Passenger mod_ruby and by the deployment scripts via our SSH Command Map.

h-lame commented 8 years ago

I'm going to close this now as it doesn't feel like it's good fit for sshkit. Thanks for the discussion @leehambley, you've helped me to approach the problem from another angle.

For anyone who finds this our solution was instead to make sure that our application was always launched with a known environment regardless of the shell used. Even though it's a very good idea we didn't use shims, we were able to use .pam_environment files to setup the environment as these are invoked for all shells on our servers. (See https://help.ubuntu.com/community/EnvironmentVariables#Persistent_environment_variables for more info - this might not work for your OS, but it was fine for us)