# frozen_string_literal: true
require 'cartage/plugin'
# A reliable way to create packages.
class Cartage
# Manage packages in remote storage.
#
# == Configuration
#
# cartage-s3 is configured in the plugins.s3 section of the Cartage
# configuration file. It supports two primary keys:
#
# +destinations+:: A dictionary of destinations, as described below, that
# indicate the remote storage location. The destination keys
# will be used as the +destination+ value.
# +destination+:: The name of the target destination to be used. If missing,
# uses the +default+ location.
#
# For backwards compatability, a single destination may be specified in-line
# with these keys. This destination will become the +default+ destination
# unless one is already specified in the +destinations+ dictionary (which is
# an error).
#
# === Destinations
#
# A destination describes the remote location for packages. It supports two
# keys:
#
# +path+:: The path or bucket name where the targets will be uploaded.
# +credentials+:: A dictionary used to initialize a Fog::Storage object.
#
# === Examples
#
# An existing configuration would work implicitly. Assuming an AWS S3 bucket
# in us-west-2, the two configurations below are identical:
#
# # Implicit
# plugins:
# s3:
# path: PATHNAME
# credentials:
# provider: AWS
# aws_access_key_id: YOUR_AWS_ACCESS_KEY_ID
# aws_secret_access_key: YOUR_AWS_SECRET_ACCESS_KEY
# region: us-west-2
#
# # Explicit
# plugins:
# s3:
# destination: default
# destinations:
# default:
# path: PATHNAME
# credentials:
# provider: AWS
# aws_access_key_id: YOUR_AWS_ACCESS_KEY_ID
# aws_secret_access_key: YOUR_AWS_SECRET_ACCESS_KEY
# region: us-west-2
#
# A configuration for Rackspace CloudFiles (London datacentre), the explicit
# configuration would be:
#
# plugins:
# s3:
# destination: rackspace
# destinations:
# rackspace:
# path: PATHNAME
# credentials:
# provider: Rackspace
# rackspace_username: RACKSPACE_USERNAME
# rackspace_api_key: RACKSPACE_API_KEY
# rackspace_auth_url: lon.auth.api.rackspacecloud.com
#
# For Google Cloud Storage, it would be:
#
# plugins:
# s3:
# destination: google
# destinations:
# google:
# path: PATHNAME
# credentials:
# provider: Google
# google_storage_access_key_id: YOUR_SECRET_ACCESS_KEY_ID
# google_storage_secret_access_key: YOUR_SECRET_ACCESS_KEY
#
# === Remote Storage Security Considerations
#
# cartage-s3 has multiple modes:
#
# * Put (cartage s3 put) expects that the target path (or bucket)
# will already exist and that the user credentials present will grant direct
# write access to the target path without enumeration.
# * Get (cartage s3 get) expects that direct read access to the
# target path will be granted.
# * List (cartage s3 ls) expects that listing capability on the target
# path will be granted.
# * Remove (cartage s3 rm) expects that deletion capability on the
# target path and files will be granted.
#
# These permissions are only needed for the optionas listed.
class S3 < Cartage::Plugin
VERSION = '2.1' #:nodoc:
# Put packages and metadata to the remote location.
def put
check_config(require_destination: true)
cartage.display "Uploading to #{name}..."
put_file cartage.final_release_metadata_json
cartage.plugins.request_map(:build_package, :package_name).each do |name|
put_file name
end
end
# Get packages and metadata from the remote location into
# +local_path+.
def get(local_path)
check_config(require_destination: true)
local_path = Pathname(local_path)
cartage.display "Downloading from #{name} to #{local_path}..."
get_file local_path, cartage.final_release_metadata_json
cartage.plugins.request_map(:build_package, :package_name).each do |name|
get_file local_path, name
end
end
# List files in the remote destination. If +show_all+ is +false+, the
# default, only files related to the current Cartage configuration will be
# shown.
def list(show_all = false)
check_config(require_destination: true)
cartage.display "Showing packages in #{name}..."
connection.directories.get(destination.path).files.each do |file|
next unless show_all || file.key =~ /#{cartage.name}/
puts file.key
end
end
# Remove the metadata and packages from the remote destination.
def delete
check_config(require_destination: true)
cartage.display "Removing packages from #{name}..."
delete_file Pathname("#{cartage.final_name}-release-hashref.txt")
delete_file cartage.final_release_metadata_json
cartage.plugins.request_map(:build_package, :package_name).each do |name|
delete_file name
end
end
# Check that the configuration is correct. If +require_destination+ is
# present, an exception will be thrown if a destination is required and not
# present.
def check_config(require_destination: false)
verify_destinations(cartage.config(for_plugin: :s3).destinations)
fail "No destination #{name} present" if require_destination && !destination
end
private
attr_reader :name
attr_reader :destination
def resolve_plugin_config!(s3_config)
if s3_config.dig(:path) || s3_config.dig(:credentials)
if s3_config.dig(:destinations, :default)
fail ArgumentError,
'Cannot configure both an implicit and explicit default destination.'
end
unless s3_config.path && s3_config.credentials
fail ArgumentError, 'Cannot configure an implicit default ' \
'destination without both path and credentials.'
end
s3_config.destinations ||= OpenStruct.new
s3_config.destinations.default ||= OpenStruct.new(
path: s3_config.path,
credentials: s3_config.credentials
)
s3_config.delete_field(:path)
s3_config.delete_field(:credentials)
end
@name = s3_config.destination || 'default'
@destination = s3_config.dig(:destinations, name)
verify_destination!(name, destination) if destination
end
def verify_destinations(destinations)
if destinations.nil? || destinations.to_h.empty?
fail ArgumentError, 'No destinations present'
end
destinations.each_pair do |name, destination|
verify_destination(name, destination)
end
end
def verify_destination(name, destination, ¬ify)
notify ||= ->(message) { warn message }
destination.dig(:path) ||
notify.("Destination #{name} invalid: No path present")
destination.dig(:credentials, :provider) ||
notify.("Destination #{name} invalid: No provider present")
end
def verify_destination!(name, destination)
verify_destination(name, destination) { |message| fail ArgumentError, message }
end
#:nocov:
def connection
unless defined?(@connection)
require 'fog'
@connection = Fog::Storage.new(destination.credentials.to_h)
end
@connection
end
def put_file(file)
cartage.display "...put #{file.basename}"
connection.put_object destination.path, file.basename.to_s, file.read
end
def get_file(local_path, file)
cartage.display "...get #{file.basename}"
response = connection.get_object(destination.path, file.basename.to_s)
local_path.join(file.basename).write(response.body)
end
def delete_file(file)
cartage.display "...delete #{file.basename}"
connection.delete_object destination.path, file.basename.to_s
end
#:nocov:
end
end