class NdrDevSupport::Daemon::CDCredentials

Wrapper around Capistrano based Continuous Deployment of application credentials

Assumes there is a capistrano task “app:update_secrets” which can be used together with a target name, e.g. cap target app:update_secrets to update a capistrano target with secrets / credentials from one or more repositories. To use this daemon, a number of environment variables need to be set including CD_TARGETS and CD_URLS.

Public Class Methods

from_args(env) click to toggle source
# File lib/ndr_dev_support/daemon/cd_credentials.rb, line 19
def self.from_args(env)
  name = env['WORKER_NAME'].to_s
  cd_targets = env['CD_TARGETS'].to_s.split
  cd_urls = env['CD_URLS'].to_s.split

  new(name: name, cd_targets: cd_targets, cd_urls: cd_urls)
end
new(name:, cd_targets:, cd_urls:) click to toggle source
Calls superclass method NdrDevSupport::Daemon::Stoppable::new
# File lib/ndr_dev_support/daemon/cd_credentials.rb, line 27
def initialize(name:, cd_targets:, cd_urls:)
  super

  # Worker name can be used for clear logging:
  @name = name
  raise ArgumentError, 'No WORKER_NAME specified!' if name.blank?

  # Capistrano targets to use for deployments
  @cd_targets = cd_targets
  raise ArgumentError, 'No CD_TARGETS specified!' unless cd_targets&.present?

  # URLs to watch for continuous deployment
  @cd_urls = cd_urls
  raise ArgumentError, 'No CD_URLS specified!' unless cd_urls&.present?
end

Private Instance Methods

attachments() click to toggle source

Status / warning messages for slack notifications

# File lib/ndr_dev_support/daemon/cd_credentials.rb, line 123
def attachments
  attachments = []

  if @failed_targets.any?
    attachment = {
      color: 'danger',
      title: "#{@failed_targets.count} failed credential updates :rotating_light:",
      text: "Failed targets: `#{@failed_targets.join(', ')}`",
      footer: 'bundle exec cap target app:update_secrets',
      mrkdwn_in: ['text']
    }
    attachments << attachment
    puts attachment.inspect
  end

  if @changed_targets.any?
    text = "Changed targets: `#{@changed_targets.join(', ')}`\n"
    text << (if @unchanged_targets.any?
               "Unchanged targets: `#{@unchanged_targets.join(', ')}`"
             else
               'No unchanged targets'
             end)
    attachment = {
      color: 'good',
      title: "#{@changed_targets.size} successful credential updates",
      text: text,
      footer: 'bundle exec cap target app:update_secrets',
      mrkdwn_in: ['text']
    }
    attachments << attachment
    puts attachment.inspect
  end

  attachments
end
check_for_new_revisions() click to toggle source

Check for new revisions, and cache the latest one. If there are new revisions in the repositories available since the last check, return a hash of repo -> latest revision If no revisions have changed, returns nil

# File lib/ndr_dev_support/daemon/cd_credentials.rb, line 69
def check_for_new_revisions
  # TODO: implement this, by checking for updates to @cd_urls
  # Stub implementation, pretends things always changed
  { 'dummy_repo' => '0' }
end
deploy_credentials() click to toggle source

Deploy credentials to all targets. Should also notify slack if any changes deployed

# File lib/ndr_dev_support/daemon/cd_credentials.rb, line 76
def deploy_credentials
  log("Deploying to #{@cd_targets.join(', ')}...")
  @changed_targets = []
  @unchanged_targets = []
  @failed_targets = []
  @cd_targets.each do |target|
    deploy_to_target(target)
  end
  publish_results
end
deploy_to_target(target) click to toggle source

Deploy credentials to a single target.

# File lib/ndr_dev_support/daemon/cd_credentials.rb, line 88
def deploy_to_target(target)
  WithCleanRbenv.with_clean_rbenv do
    results = `rbenv exec bundle exec cap #{Shellwords.escape(target)} \
                 app:update_secrets < /dev/null`.split("\n")
    puts results
    if $CHILD_STATUS.exitstatus.zero?
      if results.include?('No changed secret files to upload')
        @unchanged_targets << target
        log("Unchanged target #{target}")
      elsif results.grep(/^Uploaded [0-9]+ changed secret files: /).any?
        @changed_targets << target
        log("Changed target #{target}")
      else
        @failed_targets << target
        log("Unparseable result deploying to target #{target}")
      end
    else
      @failed_targets << target
      log("Failed to deploy to target #{target}")
    end
  end
end
publish_results() click to toggle source
# File lib/ndr_dev_support/daemon/cd_credentials.rb, line 111
def publish_results
  attachments0 = attachments
  return if attachments0.empty?

  slack_publisher = NdrDevSupport::SlackMessagePublisher.new(ENV['SLACK_WEBHOOK_URL'],
                                                             username: 'Rake CI',
                                                             icon_emoji: ':robot_face:',
                                                             channel: ENV['SLACK_CHANNEL'])
  slack_publisher.post(attachments: attachments0)
end
run_once() click to toggle source
# File lib/ndr_dev_support/daemon/cd_credentials.rb, line 45
      def run_once
        log('running once...')

        # Keep state, watch repositories for changes, maybe save state to disk?
        if (revisions = check_for_new_revisions)
          log("deploying with revisions #{revisions}...")
          deploy_credentials # should also notify slack if any changes deployed, but not
        else
          log('nothing new to deploy')
        end
        log('completed single run.')
      rescue => e
        log(<<~MSG)
          Unhandled exception! #{e.class}: #{e.message}
          #{(e.backtrace || []).join("\n")}
        MSG

        raise e
      end