lib/instance_agent/plugins/codedeploy/deployment_specification.rb
require 'openssl'
require 'instance_metadata'
require 'open-uri'
module InstanceAgent
module Plugins
module CodeDeployPlugin
class DeploymentSpecification
DEFAULT_FILE_EXISTS_BEHAVIOR = 'DISALLOW'
attr_accessor :deployment_id, :deployment_group_id, :deployment_group_name, :revision, :revision_source, :application_name, :deployment_type, :deployment_creator
attr_accessor :bucket, :key, :bundle_type, :version, :etag
attr_accessor :external_account, :repository, :commit_id, :anonymous, :external_auth_token
attr_accessor :file_exists_behavior
attr_accessor :local_location, :all_possible_lifecycle_events
attr_accessor :app_spec_path
class << self
attr_accessor :cert_store
end
def self.init_cert_store(ca_chain_path)
@cert_store = OpenSSL::X509::Store.new
begin
@cert_store.add_file ca_chain_path
rescue OpenSSL::X509::StoreError => e
raise "Could not load certificate store '#{ca_chain_path}'.\nCaused by: #{e.inspect}"
end
return @cert_store
end
@cert_store = init_cert_store(File.expand_path('../../../../certs/host-agent-deployment-signer-ca-chain.pem', File.dirname(__FILE__)))
def initialize(data)
raise 'Deployment Spec has no DeploymentId' unless property_set?(data, "DeploymentId")
raise 'Deployment Spec has no DeploymentGroupId' unless property_set?(data, "DeploymentGroupId")
raise 'Deployment Spec has no DeploymentGroupName' unless property_set?(data, "DeploymentGroupName")
raise 'Deployment Spec has no ApplicationName' unless property_set?(data, "ApplicationName")
@application_name = data["ApplicationName"]
@deployment_group_name = data["DeploymentGroupName"]
if data["DeploymentId"].start_with?("arn:")
@deployment_id = getDeploymentIdFromArn(data["DeploymentId"])
else
@deployment_id = data["DeploymentId"]
end
@deployment_group_id = data["DeploymentGroupId"]
@deployment_creator = data["DeploymentCreator"] || "user"
@deployment_type = data["DeploymentType"] || "IN_PLACE"
if property_set?(data, "AppSpecFilename")
@app_spec_path = data["AppSpecFilename"]
else
@app_spec_path = "appspec.yml"
end
raise 'Must specify a revison' unless data["Revision"]
@revision_source = data["Revision"]["RevisionType"]
raise 'Must specify a revision source' unless @revision_source
@file_exists_behavior = DEFAULT_FILE_EXISTS_BEHAVIOR
if property_set?(data, "AgentActionOverrides")
agentActionsOverrides = data["AgentActionOverrides"]
if property_set?(agentActionsOverrides, "AgentOverrides")
agentActionsOverridesMap = agentActionsOverrides["AgentOverrides"]
if property_set?(agentActionsOverridesMap, "FileExistsBehavior")
@file_exists_behavior = agentActionsOverridesMap["FileExistsBehavior"].upcase
end
end
end
if property_set?(data, 'AllPossibleLifecycleEvents')
@all_possible_lifecycle_events = data['AllPossibleLifecycleEvents']
end
case @revision_source
when 'S3'
@revision = data["Revision"]["S3Revision"]
raise 'S3Revision in Deployment Spec must specify Bucket, Key and BundleType' unless valid_s3_revision?(@revision)
raise 'BundleType in S3Revision must be tar, tgz or zip' unless valid_bundle_type?(@revision)
@bucket = @revision["Bucket"]
@key = @revision["Key"]
@bundle_type = @revision["BundleType"]
@version = @revision["Version"]
@etag = @revision["ETag"]
when 'GitHub'
@revision = data["Revision"]["GitHubRevision"]
raise 'GitHubRevision in Deployment Spec must specify Account, Repository and CommitId' unless valid_github_revision?(revision)
@external_account = revision["Account"]
@repository = revision["Repository"]
@commit_id = revision["CommitId"]
@external_auth_token = data["GitHubAccessToken"]
@anonymous = @external_auth_token.nil?
@bundle_type = @revision["BundleType"]
when 'Local File', 'Local Directory'
@revision = data["Revision"]["LocalRevision"]
raise 'LocalRevision in Deployment Spec must specify Location and BundleType' unless valid_local_revision?(revision)
raise 'BundleType in LocalRevision must be tar, tgz, zip, or directory' unless valid_local_bundle_type?(@revision)
@local_location = @revision["Location"]
@bundle_type = @revision["BundleType"]
else
raise 'Exactly one of S3Revision, GitHubRevision, or LocalRevision must be specified'
end
end
# Decrypts the envelope /deployment specs
# Params:
# envelope: deployment specification that's to be checked and decrypted
def self.parse(envelope)
raise 'Provided deployment spec was nil' if envelope.nil?
case envelope.format
when "PKCS7/JSON"
pkcs7 = OpenSSL::PKCS7.new(envelope.payload)
pkcs7.verify([], @cert_store, nil, OpenSSL::PKCS7::NOVERIFY)
# NOTE: the pkcs7.data field is only populated AFTER pkcs7.verify() is called!
parse_deployment_spec_data(pkcs7.data)
when "TEXT/JSON"
raise "Unsupported DeploymentSpecification format: #{envelope.format}" unless AWS::CodeDeploy::Local::Deployer.running_as_developer_utility?
# We only allow json unsigned messages from the local developer utility (codedeploy-local cli)
# This is because the local cli cannot actually sign messages since it doens't have the private key
# that the CodeDeploy service has.
parse_deployment_spec_data(envelope.payload)
else
raise "Unsupported DeploymentSpecification format: #{envelope.format}"
end
end
private
def self.parse_deployment_spec_data(deployment_spec_data)
deployment_spec = JSON.parse(deployment_spec_data)
sanitized_spec = deployment_spec.clone
sanitized_spec["GitHubAccessToken"] &&= "REDACTED"
InstanceAgent::Log.debug("#{self.to_s}: Parse: #{sanitized_spec}")
new(deployment_spec)
end
def property_set?(propertyHash, property)
propertyHash.has_key?(property) && !propertyHash[property].nil? && !propertyHash[property].empty?
end
def valid_s3_revision?(revision)
revision.nil? || %w(Bucket Key BundleType).all? { |k| revision.has_key?(k) }
end
def valid_github_revision?(revision)
required_fields = %w(Account Repository CommitId)
if !(revision.nil? || revision['Anonymous'].nil? || revision['Anonymous'])
required_fields << 'OAuthToken'
end
revision.nil? || required_fields.all? { |k| revision.has_key?(k) }
end
def valid_local_revision?(revision)
revision.nil? || %w(Location BundleType).all? { |k| revision.has_key?(k) }
end
private
def valid_bundle_type?(revision)
revision.nil? || %w(tar zip tgz).any? { |k| revision["BundleType"] == k }
end
def valid_local_bundle_type?(revision)
revision.nil? || %w(tar zip tgz directory).any? { |k| revision["BundleType"] == k }
end
private
def getDeploymentIdFromArn(arn)
# example arn format: "arn:aws:codedeploy:us-east-1:123412341234:deployment/12341234-1234-1234-1234-123412341234"
arn.split(":", 6)[5].split("/",2)[1]
end
end
end
end
end