diff --git a/app/services/environments/stop_stale_service.rb b/app/services/environments/stop_stale_service.rb index b2f1d74365a470db0495c9eea6eb73fd5b30f17d..7b7032cb6708c1b66ab9ed810e5f9773b56ce932 100644 --- a/app/services/environments/stop_stale_service.rb +++ b/app/services/environments/stop_stale_service.rb @@ -18,7 +18,7 @@ def execute ) end - ServiceResponse.success(message: 'Successfully scheduled stale environments to stop') + ServiceResponse.success(message: 'Successfully requested stop for all stale environments') end end end diff --git a/doc/api/environments.md b/doc/api/environments.md index 89b4bb6a1de5af854c76f441cd39d02a511f3c20..8bf3d3c8bce97c9ead58444335f0e2dfafecdb22 100644 --- a/doc/api/environments.md +++ b/doc/api/environments.md @@ -412,3 +412,28 @@ Example response: "updated_at": "2019-05-27T18:55:13.252Z" } ``` + +## Stop stale environments + +Issue stop request to all environments that were last modified or deployed to before a specified date. Excludes protected environments. Returns `200` if stop request was successful and `400` if the before date is invalid. For details of exactly when the environment is stopped, see [Stop an environment](../ci/environments/index.md#stop-an-environment). + +```plaintext +POST /projects/:id/environments/stop_stale +``` + +| Attribute | Type | Required | Description | +|-----------|----------------|----------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user | +| `before` | date | yes | Stop environments that have been modified or deployed to before the specified date. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`). Valid inputs are between 10 years ago and 1 week ago | + +```shell +curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/environments/stop_stale?before=10%2F10%2F2021" +``` + +Example response: + +```json +{ + "message": "Successfully requested stop for all stale environments" +} +``` diff --git a/doc/ci/environments/index.md b/doc/ci/environments/index.md index f2a513fdde511f96721752405e70cf1af57e8c63..c93100b355acc7d6f2c5be25334f361b95191fc3 100644 --- a/doc/ci/environments/index.md +++ b/doc/ci/environments/index.md @@ -436,6 +436,8 @@ There are multiple ways to clean up [dynamic environments](#create-a-dynamic-env - If you do _NOT_ use [merge request pipelines](../pipelines/merge_request_pipelines.md), GitLab stops an environment [when the associated feature branch is deleted](#stop-an-environment-when-a-branch-is-deleted). - If you set [an expiry period to an environment](../yaml/index.md#environmentauto_stop_in), GitLab stops an environment [when it's expired](#stop-an-environment-after-a-certain-time-period). +To stop stale environments, you can [use the API](../../api/environments.md#stop-stale-environments). + #### Stop an environment when a branch is deleted You can configure environments to stop when a branch is deleted. diff --git a/lib/api/environments.rb b/lib/api/environments.rb index cc3e41e343fe3199d9a431c16bf37f64d43c8e88..64510a9615afe9470818a977baa17b47e963ad50 100644 --- a/lib/api/environments.rb +++ b/lib/api/environments.rb @@ -188,6 +188,35 @@ class Environments < ::API::Base present environment, with: Entities::Environment, current_user: current_user end + desc 'Stop stale environments' do + detail 'It returns `200` if stale environment check was scheduled successfully' + failure [ + { code: 400, message: 'Bad request' }, + { code: 401, message: 'Unauthorized' } + ] + tags %w[environments] + end + params do + requires :before, + type: DateTime, + desc: 'Stop all environments that were last modified or deployed to before this date.' + end + post ':id/environments/stop_stale' do + authorize! :stop_environment, user_project + + bad_request!('Invalid Date') if params[:before] < 10.years.ago || params[:before] > 1.week.ago + + service_response = ::Environments::StopStaleService.new(user_project, current_user, params.slice(:before)).execute + + if service_response.error? + status 400 + else + status 200 + end + + present message: service_response.message + end + desc 'Get a specific environment' do success Entities::Environment failure [ diff --git a/spec/requests/api/environments_spec.rb b/spec/requests/api/environments_spec.rb index 049c45002071e07d9a8618da03116b2a97137e10..6164555ad1917b40f2fa8653ed6c4f1f8333b40d 100644 --- a/spec/requests/api/environments_spec.rb +++ b/spec/requests/api/environments_spec.rb @@ -4,12 +4,14 @@ RSpec.describe API::Environments, feature_category: :continuous_delivery do let_it_be(:user) { create(:user) } + let_it_be(:developer) { create(:user) } let_it_be(:non_member) { create(:user) } let_it_be(:project) { create(:project, :private, :repository, namespace: user.namespace) } let_it_be_with_reload(:environment) { create(:environment, project: project) } before do project.add_maintainer(user) + project.add_developer(developer) end describe 'GET /projects/:id/environments', :aggregate_failures do @@ -182,6 +184,50 @@ end end + describe 'POST /projects/:id/environments/stop_stale' do + context 'as a maintainer' do + it 'returns a 200' do + post api("/projects/#{project.id}/environments/stop_stale", user), params: { before: 1.week.ago.to_date.to_s } + + expect(response).to have_gitlab_http_status(:ok) + end + + it 'returns a 400 for bad input date' do + post api("/projects/#{project.id}/environments/stop_stale", user), params: { before: 1.day.ago.to_date.to_s } + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['message']).to eq('400 Bad request - Invalid Date') + end + + it 'returns a 400 for service error' do + expect_next_instance_of(::Environments::StopStaleService) do |service| + expect(service).to receive(:execute).and_return(ServiceResponse.error(message: 'Test Error')) + end + + post api("/projects/#{project.id}/environments/stop_stale", user), params: { before: 1.week.ago.to_date.to_s } + + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['message']).to eq('Test Error') + end + end + + context 'a non member' do + it 'rejects the request' do + post api("/projects/#{project.id}/environments/stop_stale", non_member), params: { before: 1.week.ago.to_date.to_s } + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'a developer' do + it 'rejects the request' do + post api("/projects/#{project.id}/environments/stop_stale", developer), params: { before: 1.week.ago.to_date.to_s } + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + end + describe 'PUT /projects/:id/environments/:environment_id' do it 'returns a 200 if name and external_url are changed' do url = 'https://mepmep.whatever.ninja'