Commit f7110642 authored by Kamil Trzciński's avatar Kamil Trzciński

Merge branch '25680-CI_ENVIRONMENT_URL' into 'master'

Add `$CI_ENVIRONMENT_URL` as a job variable

Closes #25680

See merge request !11695
parents 761e3764 2fa766e1
...@@ -138,6 +138,17 @@ module Ci ...@@ -138,6 +138,17 @@ module Ci
ExpandVariables.expand(environment, simple_variables) if environment ExpandVariables.expand(environment, simple_variables) if environment
end end
def environment_url
return @environment_url if defined?(@environment_url)
@environment_url =
if unexpanded_url = options&.dig(:environment, :url)
ExpandVariables.expand(unexpanded_url, simple_variables)
else
persisted_environment&.external_url
end
end
def has_environment? def has_environment?
environment.present? environment.present?
end end
...@@ -198,9 +209,7 @@ module Ci ...@@ -198,9 +209,7 @@ module Ci
# All variables, including those dependent on other variables # All variables, including those dependent on other variables
def variables def variables
variables = simple_variables simple_variables.concat(persisted_environment_variables)
variables += persisted_environment.predefined_variables if persisted_environment.present?
variables
end end
def merge_request def merge_request
...@@ -462,6 +471,18 @@ module Ci ...@@ -462,6 +471,18 @@ module Ci
variables.concat(legacy_variables) variables.concat(legacy_variables)
end end
def persisted_environment_variables
return [] unless persisted_environment
variables = persisted_environment.predefined_variables
if url = environment_url
variables << { key: 'CI_ENVIRONMENT_URL', value: url, public: true }
end
variables
end
def legacy_variables def legacy_variables
variables = [ variables = [
{ key: 'CI_BUILD_ID', value: id.to_s, public: true }, { key: 'CI_BUILD_ID', value: id.to_s, public: true },
......
class CreateDeploymentService < BaseService class CreateDeploymentService
def execute(deployable = nil) attr_reader :job
delegate :expanded_environment_name,
:environment_url,
:project,
to: :job
def initialize(job)
@job = job
end
def execute
return unless executable? return unless executable?
ActiveRecord::Base.transaction do ActiveRecord::Base.transaction do
@deployable = deployable environment.external_url = environment_url if environment_url
environment.fire_state_event(action)
@environment = environment return unless environment.save
@environment.external_url = expanded_url if expanded_url return if environment.stopped?
@environment.fire_state_event(action)
return unless @environment.save deploy.tap(&:update_merge_request_metrics!)
return if @environment.stopped?
deploy.tap do |deployment|
deployment.update_merge_request_metrics!
end
end end
end end
private private
def executable? def executable?
project && name.present? project && job.environment.present? && environment
end end
def deploy def deploy
project.deployments.create( project.deployments.create(
environment: @environment, environment: environment,
ref: params[:ref], ref: job.ref,
tag: params[:tag], tag: job.tag,
sha: params[:sha], sha: job.sha,
user: current_user, user: job.user,
deployable: @deployable, deployable: job,
on_stop: options[:on_stop]) on_stop: on_stop)
end end
def environment def environment
@environment ||= project.environments.find_or_create_by(name: expanded_name) @environment ||= job.persisted_environment
end
def expanded_name
ExpandVariables.expand(name, variables)
end
def expanded_url
return unless url
@expanded_url ||= ExpandVariables.expand(url, variables)
end
def name
params[:environment]
end
def url
options[:url]
end end
def options def environment_options
params[:options] || {} @environment_options ||= job.options&.dig(:environment) || {}
end end
def variables def on_stop
params[:variables] || [] environment_options[:on_stop]
end end
def action def action
options[:action] || 'start' environment_options[:action] || 'start'
end end
end end
...@@ -11,15 +11,6 @@ class BuildSuccessWorker ...@@ -11,15 +11,6 @@ class BuildSuccessWorker
private private
def create_deployment(build) def create_deployment(build)
service = CreateDeploymentService.new( CreateDeploymentService.new(build).execute
build.project, build.user,
environment: build.environment,
sha: build.sha,
ref: build.ref,
tag: build.tag,
options: build.options.to_h[:environment],
variables: build.variables)
service.execute(build)
end end
end end
---
title: Add $CI_ENVIRONMENT_URL to predefined variables for pipelines
merge_request: 11695
author:
...@@ -112,6 +112,10 @@ class Gitlab::Seeder::Pipelines ...@@ -112,6 +112,10 @@ class Gitlab::Seeder::Pipelines
setup_artifacts(build) setup_artifacts(build)
setup_build_log(build) setup_build_log(build)
build.project.environments.
find_or_create_by(name: build.expanded_environment_name)
build.save build.save
end end
end end
......
...@@ -212,12 +212,9 @@ class Gitlab::Seeder::CycleAnalytics ...@@ -212,12 +212,9 @@ class Gitlab::Seeder::CycleAnalytics
merge_requests.each do |merge_request| merge_requests.each do |merge_request|
Timecop.travel 12.hours.from_now Timecop.travel 12.hours.from_now
CreateDeploymentService.new(merge_request.project, @user, { job = merge_request.head_pipeline.builds.where.not(environment: nil).last
environment: 'production',
ref: 'master', CreateDeploymentService.new(job).execute
tag: false,
sha: @project.repository.commit('master').sha
}).execute
end end
end end
end end
......
...@@ -94,6 +94,12 @@ the name given in `.gitlab-ci.yml` (with any variables expanded), while the ...@@ -94,6 +94,12 @@ the name given in `.gitlab-ci.yml` (with any variables expanded), while the
second is a "cleaned-up" version of the name, suitable for use in URLs, DNS, second is a "cleaned-up" version of the name, suitable for use in URLs, DNS,
etc. etc.
>**Note:**
Starting with GitLab 9.3, the environment URL is exposed to the Runner via
`$CI_ENVIRONMENT_URL`. The URL would be expanded from `.gitlab-ci.yml`, or if
the URL was not defined there, the external URL from the environment would be
used.
To sum up, with the above `.gitlab-ci.yml` we have achieved that: To sum up, with the above `.gitlab-ci.yml` we have achieved that:
- All branches will run the `test` and `build` jobs. - All branches will run the `test` and `build` jobs.
......
...@@ -43,6 +43,7 @@ future GitLab releases.** ...@@ -43,6 +43,7 @@ future GitLab releases.**
| **CI_DEBUG_TRACE** | all | 1.7 | Whether [debug tracing](#debug-tracing) is enabled | | **CI_DEBUG_TRACE** | all | 1.7 | Whether [debug tracing](#debug-tracing) is enabled |
| **CI_ENVIRONMENT_NAME** | 8.15 | all | The name of the environment for this job | | **CI_ENVIRONMENT_NAME** | 8.15 | all | The name of the environment for this job |
| **CI_ENVIRONMENT_SLUG** | 8.15 | all | A simplified version of the environment name, suitable for inclusion in DNS, URLs, Kubernetes labels, etc. | | **CI_ENVIRONMENT_SLUG** | 8.15 | all | A simplified version of the environment name, suitable for inclusion in DNS, URLs, Kubernetes labels, etc. |
| **CI_ENVIRONMENT_URL** | 9.3 | all | The URL of the environment for this job |
| **CI_JOB_ID** | 9.0 | all | The unique id of the current job that GitLab CI uses internally | | **CI_JOB_ID** | 9.0 | all | The unique id of the current job that GitLab CI uses internally |
| **CI_JOB_MANUAL** | 8.12 | all | The flag to indicate that job was manually started | | **CI_JOB_MANUAL** | 8.12 | all | The flag to indicate that job was manually started |
| **CI_JOB_NAME** | 9.0 | 0.5 | The name of the job as defined in `.gitlab-ci.yml` | | **CI_JOB_NAME** | 9.0 | 0.5 | The name of the job as defined in `.gitlab-ci.yml` |
......
...@@ -64,7 +64,8 @@ FactoryGirl.define do ...@@ -64,7 +64,8 @@ FactoryGirl.define do
trait :teardown_environment do trait :teardown_environment do
environment 'staging' environment 'staging'
options environment: { name: 'staging', options environment: { name: 'staging',
action: 'stop' } action: 'stop',
url: 'http://staging.example.com/$CI_JOB_NAME' }
end end
trait :allowed_to_fail do trait :allowed_to_fail do
......
...@@ -427,6 +427,42 @@ describe Ci::Build, :models do ...@@ -427,6 +427,42 @@ describe Ci::Build, :models do
end end
end end
describe '#environment_url' do
subject { job.environment_url }
context 'when yaml environment uses $CI_COMMIT_REF_NAME' do
let(:job) do
create(:ci_build,
ref: 'master',
options: { environment: { url: 'http://review/$CI_COMMIT_REF_NAME' } })
end
it { is_expected.to eq('http://review/master') }
end
context 'when yaml environment uses yaml_variables containing symbol keys' do
let(:job) do
create(:ci_build,
yaml_variables: [{ key: :APP_HOST, value: 'host' }],
options: { environment: { url: 'http://review/$APP_HOST' } })
end
it { is_expected.to eq('http://review/host') }
end
context 'when yaml environment does not have url' do
let(:job) { create(:ci_build, environment: 'staging') }
let!(:environment) do
create(:environment, project: job.project, name: job.environment)
end
it 'returns the external_url from persisted environment' do
is_expected.to eq(environment.external_url)
end
end
end
describe '#starts_environment?' do describe '#starts_environment?' do
subject { build.starts_environment? } subject { build.starts_environment? }
...@@ -918,6 +954,10 @@ describe Ci::Build, :models do ...@@ -918,6 +954,10 @@ describe Ci::Build, :models do
it { is_expected.to eq(environment) } it { is_expected.to eq(environment) }
end end
context 'when there is no environment' do
it { is_expected.to be_nil }
end
end end
describe '#play' do describe '#play' do
...@@ -1176,11 +1216,6 @@ describe Ci::Build, :models do ...@@ -1176,11 +1216,6 @@ describe Ci::Build, :models do
end end
context 'when build has an environment' do context 'when build has an environment' do
before do
build.update(environment: 'production')
create(:environment, project: build.project, name: 'production', slug: 'prod-slug')
end
let(:environment_variables) do let(:environment_variables) do
[ [
{ key: 'CI_ENVIRONMENT_NAME', value: 'production', public: true }, { key: 'CI_ENVIRONMENT_NAME', value: 'production', public: true },
...@@ -1188,7 +1223,56 @@ describe Ci::Build, :models do ...@@ -1188,7 +1223,56 @@ describe Ci::Build, :models do
] ]
end end
it { environment_variables.each { |v| is_expected.to include(v) } } let!(:environment) do
create(:environment,
project: build.project,
name: 'production',
slug: 'prod-slug',
external_url: '')
end
before do
build.update(environment: 'production')
end
shared_examples 'containing environment variables' do
it { environment_variables.each { |v| is_expected.to include(v) } }
end
context 'when no URL was set' do
it_behaves_like 'containing environment variables'
it 'does not have CI_ENVIRONMENT_URL' do
keys = subject.map { |var| var[:key] }
expect(keys).not_to include('CI_ENVIRONMENT_URL')
end
end
context 'when an URL was set' do
let(:url) { 'http://host/test' }
before do
environment_variables <<
{ key: 'CI_ENVIRONMENT_URL', value: url, public: true }
end
context 'when the URL was set from the job' do
before do
build.update(options: { environment: { url: 'http://host/$CI_JOB_NAME' } })
end
it_behaves_like 'containing environment variables'
end
context 'when the URL was not set from the job, but environment' do
before do
environment.update(external_url: url)
end
it_behaves_like 'containing environment variables'
end
end
end end
context 'when build started manually' do context 'when build started manually' do
......
...@@ -296,5 +296,20 @@ describe Ci::CreatePipelineService, services: true do ...@@ -296,5 +296,20 @@ describe Ci::CreatePipelineService, services: true do
expect(Environment.find_by(name: "review/master")).not_to be_nil expect(Environment.find_by(name: "review/master")).not_to be_nil
end end
end end
context 'when environment with invalid name' do
before do
config = YAML.dump(deploy: { environment: { name: 'name,with,commas' }, script: 'ls' })
stub_ci_pipeline_yaml_file(config)
end
it 'does not create an environment' do
expect do
result = execute_service
expect(result).to be_persisted
end.not_to change { Environment.count }
end
end
end end
end end
require 'spec_helper' require 'spec_helper'
describe CreateDeploymentService, services: true do describe CreateDeploymentService, services: true do
let(:project) { create(:empty_project) }
let(:user) { create(:user) } let(:user) { create(:user) }
let(:options) { nil }
let(:job) do
create(:ci_build,
ref: 'master',
tag: false,
environment: 'production',
options: { environment: options })
end
let(:service) { described_class.new(project, user, params) } let(:project) { job.project }
describe '#execute' do let!(:environment) do
let(:options) { nil } create(:environment, project: project, name: 'production')
let(:params) do end
{
environment: 'production',
ref: 'master',
tag: false,
sha: '97de212e80737a608d939f648d959671fb0a0142',
options: options
}
end
subject { service.execute } let(:service) { described_class.new(job) }
context 'when no environments exist' do describe '#execute' do
it 'does create a new environment' do subject { service.execute }
expect { subject }.to change { Environment.count }.by(1)
end
it 'does create a deployment' do context 'when environment exists' do
it 'creates a deployment' do
expect(subject).to be_persisted expect(subject).to be_persisted
end end
end end
context 'when environment exist' do context 'when environment does not exist' do
let!(:environment) { create(:environment, project: project, name: 'production') } let(:environment) {}
it 'does not create a new environment' do
expect { subject }.not_to change { Environment.count }
end
it 'does create a deployment' do it 'does not create a deployment' do
expect(subject).to be_persisted expect do
expect(subject).to be_nil
end.not_to change { Deployment.count }
end end
end
context 'and start action is defined' do context 'when start action is defined' do
let(:options) { { action: 'start' } } let(:options) { { action: 'start' } }
context 'and environment is stopped' do context 'and environment is stopped' do
before do before do
environment.stop environment.stop
end end
it 'makes environment available' do it 'makes environment available' do
subject subject
expect(environment.reload).to be_available expect(environment.reload).to be_available
end end
it 'does create a deployment' do it 'creates a deployment' do
expect(subject).to be_persisted expect(subject).to be_persisted
end
end end
end end
end
context 'and stop action is defined' do context 'when stop action is defined' do
let(:options) { { action: 'stop' } } let(:options) { { action: 'stop' } }
context 'and environment is available' do
before do
environment.start
end
it 'makes environment stopped' do
subject
expect(environment.reload).to be_stopped
end
it 'does not create a deployment' do context 'and environment is available' do
expect(subject).to be_nil before do
end environment.start
end end
end
end
context 'for environment with invalid name' do it 'makes environment stopped' do
let(:params) do subject
{
environment: 'name,with,commas',
ref: 'master',
tag: false,
sha: '97de212e80737a608d939f648d959671fb0a0142'
}
end
it 'does not create a new environment' do expect(environment.reload).to be_stopped
expect { subject }.not_to change { Environment.count } end
end
it 'does not create a deployment' do it 'does not create a deployment' do
expect(subject).to be_nil expect(subject).to be_nil
end
end end
end end
context 'when variables are used' do context 'when variables are used' do
let(:params) do let(:options) do
{ { name: 'review-apps/$CI_COMMIT_REF_NAME',
environment: 'review-apps/$CI_COMMIT_REF_NAME', url: 'http://$CI_COMMIT_REF_NAME.review-apps.gitlab.com' }
ref: 'master',
tag: false,
sha: '97de212e80737a608d939f648d959671fb0a0142',
options: {
name: 'review-apps/$CI_COMMIT_REF_NAME',
url: 'http://$CI_COMMIT_REF_NAME.review-apps.gitlab.com'
},
variables: [
{ key: 'CI_COMMIT_REF_NAME', value: 'feature-review-apps' }
]
}
end end
it 'does create a new environment' do before do
expect { subject }.to change { Environment.count }.by(1) environment.update(name: 'review-apps/master')
job.update(environment: 'review-apps/$CI_COMMIT_REF_NAME')
expect(subject.environment.name).to eq('review-apps/feature-review-apps')
expect(subject.environment.external_url).to eq('http://feature-review-apps.review-apps.gitlab.com')
end end
it 'does create a new deployment' do it 'creates a new deployment' do
expect(subject).to be_persisted expect(subject).to be_persisted
end end
context 'and environment exist' do it 'does not create a new environment' do
let!(:environment) { create(:environment, project: project, name: 'review-apps/feature-review-apps') } expect { subject }.not_to change { Environment.count }
end
it 'does not create a new environment' do
expect { subject }.not_to change { Environment.count }
end
it 'updates external url' do
subject
expect(subject.environment.name).to eq('review-apps/feature-review-apps') it 'updates external url' do
expect(subject.environment.external_url).to eq('http://feature-review-apps.review-apps.gitlab.com') subject
end
it 'does create a new deployment' do expect(subject.environment.name).to eq('review-apps/master')
expect(subject).to be_persisted expect(subject.environment.external_url).to eq('http://master.review-apps.gitlab.com')
end
end end
end end
context 'when project was removed' do context 'when project was removed' do
let(:project) { nil } let(:environment) {}
before do
job.update(project: nil)
end
it 'does not create deployment or environment' do it 'does not create deployment or environment' do
expect { subject }.not_to raise_error expect { subject }.not_to raise_error
...@@ -162,34 +123,26 @@ describe CreateDeploymentService, services: true do ...@@ -162,34 +123,26 @@ describe CreateDeploymentService, services: true do
end end
describe 'processing of builds' do describe 'processing of builds' do
let(:environment) { nil } shared_examples 'does not create deployment' do
shared_examples 'does not create environment and deployment' do
it 'does not create a new environment' do
expect { subject }.not_to change { Environment.count }
end
it 'does not create a new deployment' do it 'does not create a new deployment' do
expect { subject }.not_to change { Deployment.count } expect { subject }.not_to change { Deployment.count }
end end
it 'does not call a service' do it 'does not call a service' do
expect_any_instance_of(described_class).not_to receive(:execute) expect_any_instance_of(described_class).not_to receive(:execute)
subject subject
end end
end end
shared_examples 'does create environment and deployment' do shared_examples 'creates deployment' do
it 'does create a new environment' do it 'creates a new deployment' do
expect { subject }.to change { Environment.count }.by(1)
end
it 'does create a new deployment' do
expect { subject }.to change { Deployment.count }.by(1) expect { subject }.to change { Deployment.count }.by(1)
end end
it 'does call a service' do it 'calls a service' do
expect_any_instance_of(described_class).to receive(:execute) expect_any_instance_of(described_class).to receive(:execute)
subject subject
end end
...@@ -199,7 +152,7 @@ describe CreateDeploymentService, services: true do ...@@ -199,7 +152,7 @@ describe CreateDeploymentService, services: true do
expect(Deployment.last.deployable).to eq(deployable) expect(Deployment.last.deployable).to eq(deployable)
end end
it 'create environment has URL set' do it 'updates environment URL' do
subject subject
expect(Deployment.last.environment.external_url).not_to be_nil expect(Deployment.last.environment.external_url).not_to be_nil
...@@ -207,41 +160,39 @@ describe CreateDeploymentService, services: true do ...@@ -207,41 +160,39 @@ describe CreateDeploymentService, services: true do
end end
context 'without environment specified' do context 'without environment specified' do
let(:build) { create(:ci_build, project: project) } let(:job) { create(:ci_build) }
it_behaves_like 'does not create environment and deployment' do it_behaves_like 'does not create deployment' do
subject { build.success } subject { job.success }
end end
end end
context 'when environment is specified' do context 'when environment is specified' do
let(:pipeline) { create(:ci_pipeline, project: project) } let(:deployable) { job }
let(:build) { create(:ci_build, pipeline: pipeline, environment: 'production', options: options) }
let(:options) do let(:options) do
{ environment: { name: 'production', url: 'http://gitlab.com' } } { environment: { name: 'production', url: 'http://gitlab.com' } }
end end
context 'when build succeeds' do context 'when job succeeds' do
it_behaves_like 'does create environment and deployment' do it_behaves_like 'creates deployment' do
let(:deployable) { build } subject { job.success }
subject { build.success }
end end
end end
context 'when build fails' do context 'when job fails' do
it_behaves_like 'does not create environment and deployment' do it_behaves_like 'does not create deployment' do
subject { build.drop } subject { job.drop }
end end
end end
context 'when build is retried' do context 'when job is retried' do
it_behaves_like 'does create environment and deployment' do it_behaves_like 'creates deployment' do
before do before do
project.add_developer(user) project.add_developer(user)
end end
let(:deployable) { Ci::Build.retry(build, user) } let(:deployable) { Ci::Build.retry(job, user) }
subject { deployable.success } subject { deployable.success }
end end
...@@ -250,15 +201,6 @@ describe CreateDeploymentService, services: true do ...@@ -250,15 +201,6 @@ describe CreateDeploymentService, services: true do
end end
describe "merge request metrics" do describe "merge request metrics" do
let(:params) do
{
environment: 'production',
ref: 'master',
tag: false,
sha: '97de212e80737a608d939f648d959671fb0a0142b'
}
end
let(:merge_request) { create(:merge_request, target_branch: 'master', source_branch: 'feature', source_project: project) } let(:merge_request) { create(:merge_request, target_branch: 'master', source_branch: 'feature', source_project: project) }
context "while updating the 'first_deployed_to_production_at' time" do context "while updating the 'first_deployed_to_production_at' time" do
...@@ -273,8 +215,8 @@ describe CreateDeploymentService, services: true do ...@@ -273,8 +215,8 @@ describe CreateDeploymentService, services: true do
end end
it "doesn't set the time if the deploy's environment is not 'production'" do it "doesn't set the time if the deploy's environment is not 'production'" do
staging_params = params.merge(environment: 'staging') job.update(environment: 'staging')
service = described_class.new(project, user, staging_params) service = described_class.new(job)
service.execute service.execute
expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_nil expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_nil
...@@ -298,7 +240,7 @@ describe CreateDeploymentService, services: true do ...@@ -298,7 +240,7 @@ describe CreateDeploymentService, services: true do
expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_like_time(time) expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_like_time(time)
# Current deploy # Current deploy
service = described_class.new(project, user, params) service = described_class.new(job)
Timecop.freeze(time + 12.hours) { service.execute } Timecop.freeze(time + 12.hours) { service.execute }
expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_like_time(time) expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_like_time(time)
...@@ -318,7 +260,7 @@ describe CreateDeploymentService, services: true do ...@@ -318,7 +260,7 @@ describe CreateDeploymentService, services: true do
expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_nil expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_nil
# Current deploy # Current deploy
service = described_class.new(project, user, params) service = described_class.new(job)
Timecop.freeze(time + 12.hours) { service.execute } Timecop.freeze(time + 12.hours) { service.execute }
expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_nil expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_nil
......
...@@ -51,12 +51,43 @@ module CycleAnalyticsHelpers ...@@ -51,12 +51,43 @@ module CycleAnalyticsHelpers
end end
def deploy_master(environment: 'production') def deploy_master(environment: 'production')
CreateDeploymentService.new(project, user, { dummy_job =
environment: environment, case environment
ref: 'master', when 'production'
tag: false, dummy_production_job
sha: project.repository.commit('master').sha when 'staging'
}).execute dummy_staging_job
else
raise ArgumentError
end
CreateDeploymentService.new(dummy_job).execute
end
def dummy_production_job
@dummy_job ||= new_dummy_job('production')
end
def dummy_staging_job
@dummy_job ||= new_dummy_job('staging')
end
def dummy_pipeline
@dummy_pipeline ||=
Ci::Pipeline.new(sha: project.repository.commit('master').sha)
end
def new_dummy_job(environment)
project.environments.find_or_create_by(name: environment)
Ci::Build.new(
project: project,
user: user,
environment: environment,
ref: 'master',
tag: false,
name: 'dummy',
pipeline: dummy_pipeline)
end end
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment