237 lines
6.0 KiB
Ruby
237 lines
6.0 KiB
Ruby
require 'travis/model'
|
|
require 'active_support/core_ext/hash/deep_dup'
|
|
require 'travis/model/build/config/language'
|
|
|
|
# Job models a unit of work that is run on a remote worker.
|
|
#
|
|
# There currently only one job type:
|
|
#
|
|
# * Job::Test belongs to a Build (one or many Job::Test instances make up a
|
|
# build matrix) and executes a test suite with parameters defined in the
|
|
# configuration.
|
|
class Job < Travis::Model
|
|
require 'travis/model/job/queue'
|
|
require 'travis/model/job/test'
|
|
require 'travis/model/env_helpers'
|
|
|
|
SAFELISTED_ADDONS = %w(
|
|
apt
|
|
apt_packages
|
|
apt_sources
|
|
firefox
|
|
hosts
|
|
mariadb
|
|
postgresql
|
|
ssh_known_hosts
|
|
).freeze
|
|
|
|
class << self
|
|
# what we return from the json api
|
|
def queued(queue = nil)
|
|
scope = where(state: [:created, :queued])
|
|
scope = scope.where(queue: queue) if queue
|
|
scope
|
|
end
|
|
|
|
# what needs to be queued up
|
|
def queueable(queue = nil)
|
|
scope = where(state: :created).order('jobs.id')
|
|
scope = scope.where(queue: queue) if queue
|
|
scope
|
|
end
|
|
|
|
# what already is queued or started
|
|
def running(queue = nil)
|
|
scope = where(state: [:queued, :received, :started]).order('jobs.id')
|
|
scope = scope.where(queue: queue) if queue
|
|
scope
|
|
end
|
|
|
|
def unfinished
|
|
# TODO conflate Job and Job::Test and use States::FINISHED_STATES
|
|
where('state NOT IN (?)', [:finished, :passed, :failed, :errored, :canceled])
|
|
end
|
|
|
|
def owned_by(owner)
|
|
where(owner_id: owner.id, owner_type: owner.class.to_s)
|
|
end
|
|
end
|
|
|
|
include Travis::Model::EnvHelpers
|
|
|
|
has_one :log, dependent: :destroy
|
|
has_many :events, as: :source
|
|
has_many :annotations, dependent: :destroy
|
|
|
|
belongs_to :repository
|
|
belongs_to :commit
|
|
belongs_to :source, polymorphic: true, autosave: true
|
|
belongs_to :owner, polymorphic: true
|
|
|
|
validates :repository_id, :commit_id, :source_id, :source_type, :owner_id, :owner_type, presence: true
|
|
|
|
serialize :config
|
|
|
|
delegate :request_id, to: :source # TODO denormalize
|
|
delegate :pull_request?, to: :commit
|
|
delegate :secure_env_enabled?, :addons_enabled?, to: :source
|
|
|
|
after_initialize do
|
|
self.config = {} if config.nil? rescue nil
|
|
end
|
|
|
|
before_create do
|
|
build_log
|
|
self.state = :created if self.state.nil?
|
|
self.queue = Queue.for(self).name
|
|
end
|
|
|
|
after_commit on: :create do
|
|
notify(:create)
|
|
end
|
|
|
|
def propagate(name, *args)
|
|
# if we propagate cancel, we can't send it as "cancel", because
|
|
# it would trigger cancelling the entire matrix
|
|
if name == :cancel
|
|
name = :cancel_job
|
|
end
|
|
Metriks.timer("job.propagate.#{name}").time do
|
|
source.send(name, *args)
|
|
end
|
|
true
|
|
end
|
|
|
|
def duration
|
|
started_at && finished_at ? finished_at - started_at : nil
|
|
end
|
|
|
|
def ssh_key
|
|
config[:source_key]
|
|
end
|
|
|
|
def config=(config)
|
|
super normalize_config(config)
|
|
end
|
|
|
|
def obfuscated_config
|
|
normalize_config(config).deep_dup.tap do |config|
|
|
delete_addons(config)
|
|
config.delete(:source_key)
|
|
if config[:env]
|
|
obfuscated_env = process_env(config[:env]) { |env| obfuscate_env(env) }
|
|
config[:env] = obfuscated_env ? obfuscated_env.join(' ') : nil
|
|
end
|
|
if config[:global_env]
|
|
obfuscated_env = process_env(config[:global_env]) { |env| obfuscate_env(env) }
|
|
config[:global_env] = obfuscated_env ? obfuscated_env.join(' ') : nil
|
|
end
|
|
end
|
|
end
|
|
|
|
def decrypted_config
|
|
normalize_config(self.config).deep_dup.tap do |config|
|
|
config[:env] = process_env(config[:env]) { |env| decrypt_env(env) } if config[:env]
|
|
config[:global_env] = process_env(config[:global_env]) { |env| decrypt_env(env) } if config[:global_env]
|
|
if config[:addons]
|
|
if addons_enabled?
|
|
config[:addons] = decrypt_addons(config[:addons])
|
|
else
|
|
delete_addons(config)
|
|
end
|
|
end
|
|
end
|
|
rescue => e
|
|
logger.warn "[job id:#{id}] Config could not be decrypted due to #{e.message}"
|
|
{}
|
|
end
|
|
|
|
def matches_config?(other)
|
|
config = self.config.slice(*other.keys)
|
|
config = config.merge(branch: commit.branch) if other.key?(:branch) # TODO test this
|
|
return false if config.size == 0
|
|
config.all? { |key, value| value == other[key] || commit.branch == other[key] }
|
|
end
|
|
|
|
def log_content=(content)
|
|
create_log! unless log
|
|
log.update_attributes!(content: content, aggregated_at: Time.now)
|
|
end
|
|
|
|
# compatibility, we still use result in webhooks
|
|
def result
|
|
state.try(:to_sym) == :passed ? 0 : 1
|
|
end
|
|
|
|
private
|
|
|
|
def delete_addons(config)
|
|
if config[:addons].is_a?(Hash)
|
|
config[:addons].keep_if { |key, _| SAFELISTED_ADDONS.include? key.to_s }
|
|
else
|
|
config.delete(:addons)
|
|
end
|
|
end
|
|
|
|
def normalize_config(config)
|
|
config = YAML.load(config) if config.is_a? String
|
|
config = config ? config.deep_symbolize_keys : {}
|
|
|
|
if config[:deploy]
|
|
if config[:addons].is_a? Hash
|
|
config[:addons][:deploy] = config.delete(:deploy)
|
|
else
|
|
config.delete(:addons)
|
|
config[:addons] = { deploy: config.delete(:deploy) }
|
|
end
|
|
end
|
|
|
|
config
|
|
end
|
|
|
|
def process_env(env)
|
|
env = [env] unless env.is_a?(Array)
|
|
env = normalize_env(env)
|
|
env = if secure_env_enabled?
|
|
yield(env)
|
|
else
|
|
remove_encrypted_env_vars(env)
|
|
end
|
|
env.compact.presence
|
|
end
|
|
|
|
def remove_encrypted_env_vars(env)
|
|
env.reject do |var|
|
|
var.is_a?(Hash) && var.has_key?(:secure)
|
|
end
|
|
end
|
|
|
|
def normalize_env(env)
|
|
env.map do |line|
|
|
if line.is_a?(Hash) && !line.has_key?(:secure)
|
|
line.map { |k, v| "#{k}=#{v}" }.join(' ')
|
|
else
|
|
line
|
|
end
|
|
end
|
|
end
|
|
|
|
def decrypt_addons(addons)
|
|
decrypt(addons)
|
|
end
|
|
|
|
def decrypt_env(env)
|
|
env.map do |var|
|
|
decrypt(var) do |var|
|
|
var.dup.insert(0, 'SECURE ') unless var.include?('SECURE ')
|
|
end
|
|
end
|
|
rescue
|
|
{}
|
|
end
|
|
|
|
def decrypt(v, &block)
|
|
repository.key.secure.decrypt(v, &block)
|
|
end
|
|
end
|