From c3ab524900148038bfc5f17b7f98d1ea37f28d17 Mon Sep 17 00:00:00 2001 From: Joao Pereira Date: Thu, 11 Jul 2024 21:06:59 +0000 Subject: [PATCH] Add last_status_change to deployments object * indicates last time status.reason or status.value was updated * status_updated_at column is set to updated_at for exisiting deployments. Unfortunately updated_at is nullable, so those with null updated_at (unclear if this is a real scenario) will have status_updated_at set to the current time Co-authored-by: Joao Pereira Co-authored-by: Seth Boyles --- app/models/runtime/deployment_model.rb | 13 +++++ app/presenters/v3/deployment_presenter.rb | 3 +- ...16_add_status_updated_at_to_deployments.rb | 14 ++++++ .../resources/deployments/_object.md.erb | 3 +- spec/request/deployments_spec.rb | 36 +++++++++----- .../models/runtime/deployment_model_spec.rb | 49 +++++++++++++++++++ .../v3/deployment_presenter_spec.rb | 2 + 7 files changed, 106 insertions(+), 14 deletions(-) create mode 100644 db/migrations/20240709234116_add_status_updated_at_to_deployments.rb diff --git a/app/models/runtime/deployment_model.rb b/app/models/runtime/deployment_model.rb index f3326dc90c9..4dbc7372eaa 100644 --- a/app/models/runtime/deployment_model.rb +++ b/app/models/runtime/deployment_model.rb @@ -67,6 +67,11 @@ def deploying_count end end + def before_update + super + set_status_updated_at + end + def deploying? state == DEPLOYING_STATE end @@ -76,5 +81,13 @@ def cancelable? DeploymentModel::CANCELING_STATE] valid_states_for_cancel.include?(state) end + + private + + def set_status_updated_at + return unless column_changed?(:status_reason) || column_changed?(:status_value) + + self.status_updated_at = updated_at + end end end diff --git a/app/presenters/v3/deployment_presenter.rb b/app/presenters/v3/deployment_presenter.rb index 6d3adcfc42b..53247e928e7 100644 --- a/app/presenters/v3/deployment_presenter.rb +++ b/app/presenters/v3/deployment_presenter.rb @@ -14,7 +14,8 @@ def to_hash value: deployment.status_value, reason: deployment.status_reason, details: { - last_successful_healthcheck: deployment.last_healthy_at + last_successful_healthcheck: deployment.last_healthy_at, + last_status_change: deployment.status_updated_at } }, strategy: deployment.strategy, diff --git a/db/migrations/20240709234116_add_status_updated_at_to_deployments.rb b/db/migrations/20240709234116_add_status_updated_at_to_deployments.rb new file mode 100644 index 00000000000..3ceae004479 --- /dev/null +++ b/db/migrations/20240709234116_add_status_updated_at_to_deployments.rb @@ -0,0 +1,14 @@ +Sequel.migration do + up do + alter_table(:deployments) do + add_column :status_updated_at, DateTime, default: Sequel::CURRENT_TIMESTAMP, null: false + end + run 'update deployments set status_updated_at = updated_at where updated_at is not null' + end + + down do + alter_table(:deployments) do + drop_column :status_updated_at + end + end +end diff --git a/docs/v3/source/includes/resources/deployments/_object.md.erb b/docs/v3/source/includes/resources/deployments/_object.md.erb index e2aaac520e2..ee30f5c2124 100644 --- a/docs/v3/source/includes/resources/deployments/_object.md.erb +++ b/docs/v3/source/includes/resources/deployments/_object.md.erb @@ -14,7 +14,8 @@ Name | Type | Description **updated_at** | _[timestamp](#timestamps)_ | The time with zone when the object was last updated **status.value** | _string_ | The current status of the deployment; valid values are `ACTIVE` (meaning in progress) and `FINALIZED` (meaning finished, either successfully or not) **status.reason** | _string_ | The reason for the status of the deployment;
following list represents valid values:
1. If **status.value** is `ACTIVE`
- `DEPLOYING`
- `CANCELING`
2. If **status.value** is `FINALIZED`
- `DEPLOYED`
- `CANCELED`
- `SUPERSEDED` (another deployment created for app before completion)
-**status.details** | _object_ | The details for the status of the deployment shows a timestamp of the last successful healthcheck +**status.details.last_successful_healthcheck** | _[timestamp](#timestamps)_ | Timestamp of the last successful healthcheck +**status.details.last_status_change** | _[timestamp](#timestamps)_ | Timestamp of last change to status.value or status.reason **strategy** | _string_ | Strategy used for the deployment; supported strategies are `rolling` only **droplet.guid** | _string_ | The droplet guid that the deployment is transitioning the app to **previous_droplet.guid** | _string_ | The app's [current droplet guid](#get-current-droplet-association-for-an-app) before the deployment was created diff --git a/spec/request/deployments_spec.rb b/spec/request/deployments_spec.rb index f1399d44b9d..b9df2511eaa 100644 --- a/spec/request/deployments_spec.rb +++ b/spec/request/deployments_spec.rb @@ -39,7 +39,8 @@ 'value' => VCAP::CloudController::DeploymentModel::ACTIVE_STATUS_VALUE, 'reason' => VCAP::CloudController::DeploymentModel::DEPLOYING_STATUS_REASON, 'details' => { - 'last_successful_healthcheck' => iso8601 + 'last_successful_healthcheck' => iso8601, + 'last_status_change' => iso8601 } }, 'strategy' => 'rolling', @@ -136,7 +137,8 @@ 'value' => VCAP::CloudController::DeploymentModel::ACTIVE_STATUS_VALUE, 'reason' => VCAP::CloudController::DeploymentModel::DEPLOYING_STATUS_REASON, 'details' => { - 'last_successful_healthcheck' => iso8601 + 'last_successful_healthcheck' => iso8601, + 'last_status_change' => iso8601 } }, 'strategy' => 'rolling', @@ -218,7 +220,8 @@ 'value' => VCAP::CloudController::DeploymentModel::ACTIVE_STATUS_VALUE, 'reason' => VCAP::CloudController::DeploymentModel::DEPLOYING_STATUS_REASON, 'details' => { - 'last_successful_healthcheck' => iso8601 + 'last_successful_healthcheck' => iso8601, + 'last_status_change' => iso8601 } }, 'strategy' => 'rolling', @@ -336,7 +339,8 @@ 'value' => VCAP::CloudController::DeploymentModel::ACTIVE_STATUS_VALUE, 'reason' => VCAP::CloudController::DeploymentModel::DEPLOYING_STATUS_REASON, 'details' => { - 'last_successful_healthcheck' => iso8601 + 'last_successful_healthcheck' => iso8601, + 'last_status_change' => iso8601 } }, 'strategy' => 'rolling', @@ -414,7 +418,8 @@ 'value' => VCAP::CloudController::DeploymentModel::ACTIVE_STATUS_VALUE, 'reason' => VCAP::CloudController::DeploymentModel::DEPLOYING_STATUS_REASON, 'details' => { - 'last_successful_healthcheck' => iso8601 + 'last_successful_healthcheck' => iso8601, + 'last_status_change' => iso8601 } }, 'strategy' => 'rolling', @@ -494,7 +499,8 @@ 'value' => VCAP::CloudController::DeploymentModel::FINALIZED_STATUS_VALUE, 'reason' => VCAP::CloudController::DeploymentModel::DEPLOYED_STATUS_REASON, 'details' => { - 'last_successful_healthcheck' => iso8601 + 'last_successful_healthcheck' => iso8601, + 'last_status_change' => iso8601 } }, 'strategy' => 'rolling', @@ -661,7 +667,8 @@ 'value' => VCAP::CloudController::DeploymentModel::ACTIVE_STATUS_VALUE, 'reason' => VCAP::CloudController::DeploymentModel::DEPLOYING_STATUS_REASON, 'details' => { - 'last_successful_healthcheck' => iso8601 + 'last_successful_healthcheck' => iso8601, + 'last_status_change' => iso8601 } }, 'strategy' => 'rolling', @@ -721,7 +728,8 @@ 'value' => VCAP::CloudController::DeploymentModel::ACTIVE_STATUS_VALUE, 'reason' => VCAP::CloudController::DeploymentModel::DEPLOYING_STATUS_REASON, 'details' => { - 'last_successful_healthcheck' => iso8601 + 'last_successful_healthcheck' => iso8601, + 'last_status_change' => iso8601 } }, 'strategy' => 'rolling', @@ -839,7 +847,8 @@ 'value' => VCAP::CloudController::DeploymentModel::ACTIVE_STATUS_VALUE, 'reason' => VCAP::CloudController::DeploymentModel::DEPLOYING_STATUS_REASON, 'details' => { - 'last_successful_healthcheck' => iso8601 + 'last_successful_healthcheck' => iso8601, + 'last_status_change' => iso8601 } }, 'strategy' => 'rolling', @@ -929,7 +938,8 @@ 'value' => VCAP::CloudController::DeploymentModel::ACTIVE_STATUS_VALUE, 'reason' => VCAP::CloudController::DeploymentModel::DEPLOYING_STATUS_REASON, 'details' => { - 'last_successful_healthcheck' => iso8601 + 'last_successful_healthcheck' => iso8601, + 'last_status_change' => iso8601 } }, 'droplet' => { @@ -1051,7 +1061,8 @@ def json_for_deployment(deployment, app_model, droplet, status_value, status_rea value: status_value, reason: status_reason, details: { - last_successful_healthcheck: iso8601 + last_successful_healthcheck: iso8601, + last_status_change: iso8601 } }, strategy: 'rolling', @@ -1345,7 +1356,8 @@ def json_for_deployment(deployment, app_model, droplet, status_value, status_rea 'value' => VCAP::CloudController::DeploymentModel::ACTIVE_STATUS_VALUE, 'reason' => VCAP::CloudController::DeploymentModel::DEPLOYING_STATUS_REASON, 'details' => { - 'last_successful_healthcheck' => iso8601 + 'last_successful_healthcheck' => iso8601, + 'last_status_change' => iso8601 } }, 'strategy' => 'rolling', diff --git a/spec/unit/models/runtime/deployment_model_spec.rb b/spec/unit/models/runtime/deployment_model_spec.rb index ca19ca779bb..e77423f06bb 100644 --- a/spec/unit/models/runtime/deployment_model_spec.rb +++ b/spec/unit/models/runtime/deployment_model_spec.rb @@ -103,5 +103,54 @@ module VCAP::CloudController expect(deployment.cancelable?).to be(false) end end + + describe '#status_updated_at' do + let(:deployment) do + DeploymentModel.make( + app: app, + droplet: droplet, + deploying_web_process: deploying_web_process, + status_reason: DeploymentModel::DEPLOYING_STATUS_REASON, + status_value: DeploymentModel::ACTIVE_STATUS_VALUE + ) + end + + # Can't use Timecop with created_at since its set by the DB + let(:creation_time) { deployment.created_at } + let(:update_time) { deployment.created_at + 24.hours } + + before do + Timecop.freeze(creation_time) + end + + after do + Timecop.return + end + + it 'is defaulted with the created_at time' do + expect(deployment.status_updated_at).to eq(deployment.created_at) + end + + it 'updates when status_reason has changed' do + deployment.status_reason = DeploymentModel::CANCELING_STATUS_REASON + Timecop.freeze(update_time) + deployment.save + expect(deployment.status_updated_at).to eq update_time + end + + it 'updates when status_value has changed' do + deployment.status_value = DeploymentModel::FINALIZED_STATUS_VALUE + Timecop.freeze(update_time) + deployment.save + expect(deployment.status_updated_at).to eq update_time + end + + it 'doesnt update when status_value or status_reason is unchanged' do + deployment.strategy = 'faux_strategy' + Timecop.freeze(update_time) + deployment.save + expect(deployment.status_updated_at).to eq creation_time + end + end end end diff --git a/spec/unit/presenters/v3/deployment_presenter_spec.rb b/spec/unit/presenters/v3/deployment_presenter_spec.rb index 15b9e403631..36a8404b300 100644 --- a/spec/unit/presenters/v3/deployment_presenter_spec.rb +++ b/spec/unit/presenters/v3/deployment_presenter_spec.rb @@ -15,6 +15,7 @@ module VCAP::CloudController::Presenters::V3 previous_droplet: previous_droplet, deploying_web_process: process, last_healthy_at: '2019-07-12 19:01:54', + status_updated_at: '2019-07-11 19:01:54', state: deployment_state, status_value: VCAP::CloudController::DeploymentModel::ACTIVE_STATUS_VALUE, status_reason: VCAP::CloudController::DeploymentModel::DEPLOYING_STATUS_REASON @@ -29,6 +30,7 @@ module VCAP::CloudController::Presenters::V3 expect(result[:status][:value]).to eq(VCAP::CloudController::DeploymentModel::ACTIVE_STATUS_VALUE) expect(result[:status][:reason]).to eq(VCAP::CloudController::DeploymentModel::DEPLOYING_STATUS_REASON) expect(result[:status][:details][:last_successful_healthcheck]).to eq('2019-07-12 19:01:54') + expect(result[:status][:details][:last_status_change]).to eq('2019-07-11 19:01:54') expect(result[:strategy]).to eq('rolling')