diff --git a/app/actions/stack_update.rb b/app/actions/stack_update.rb index 5d373c09bc2..eb0c7fb6faf 100644 --- a/app/actions/stack_update.rb +++ b/app/actions/stack_update.rb @@ -15,6 +15,7 @@ def update(stack, message) stack_updates = {} stack_updates[:state] = message.state if message.requested?(:state) stack_updates[:state_reason] = message.state_reason if message.state_reason_requested? + stack_updates[:description] = message.description if message.requested?(:description) stack.update(stack_updates) if stack_updates.any? MetadataUpdate.update(stack, message) diff --git a/app/messages/stack_update_message.rb b/app/messages/stack_update_message.rb index 57afeea42c1..fa7664258cd 100644 --- a/app/messages/stack_update_message.rb +++ b/app/messages/stack_update_message.rb @@ -3,11 +3,12 @@ module VCAP::CloudController class StackUpdateMessage < MetadataBaseMessage - register_allowed_keys %i[state state_reason] + register_allowed_keys %i[state state_reason description] validates_with NoAdditionalKeysValidator validates :state, inclusion: { in: StackStates::VALID_STATES, message: "must be one of #{StackStates::VALID_STATES.join(', ')}" }, allow_nil: false, if: :state_requested? validates :state_reason, length: { maximum: 5000 }, allow_nil: true + validates :description, length: { maximum: 250 }, allow_nil: true def state_requested? requested?(:state) diff --git a/docs/openapi/apis/cf/latest/components/requestBodies/StackCreateRequestBody.yaml b/docs/openapi/apis/cf/latest/components/requestBodies/StackCreateRequestBody.yaml index 758be34716b..241712bf42f 100644 --- a/docs/openapi/apis/cf/latest/components/requestBodies/StackCreateRequestBody.yaml +++ b/docs/openapi/apis/cf/latest/components/requestBodies/StackCreateRequestBody.yaml @@ -11,7 +11,7 @@ content: maxLength: 250 description: type: [string, "null"] - description: Description of the stack; must no longer than 250 characters + description: Description of the stack; must be no longer than 250 characters maxLength: 250 metadata: $ref: '../schemas/Metadata.yaml' diff --git a/docs/openapi/apis/cf/latest/paths/Stacks.yaml b/docs/openapi/apis/cf/latest/paths/Stacks.yaml index c69c09e0e66..73700235f04 100644 --- a/docs/openapi/apis/cf/latest/paths/Stacks.yaml +++ b/docs/openapi/apis/cf/latest/paths/Stacks.yaml @@ -153,6 +153,10 @@ application/json: schema: properties: + description: + type: string + description: Description of the stack; must be no longer than 250 characters + maxLength: 250 metadata: $ref: ../components/schemas/Metadata.yaml examples: @@ -164,6 +168,7 @@ key: value annotations: note: detailed information + description: Example stack description state: ACTIVE state_reason: Optional plain text describing the stack state change responses: diff --git a/docs/v3/source/includes/resources/stacks/_create.md.erb b/docs/v3/source/includes/resources/stacks/_create.md.erb index e97fee9f0f0..a4768ff4136 100644 --- a/docs/v3/source/includes/resources/stacks/_create.md.erb +++ b/docs/v3/source/includes/resources/stacks/_create.md.erb @@ -41,7 +41,7 @@ Name | Type | Description Name | Type | Description | Default ---- | ---- | ----------- | ------- -**description** | _string_ | Description of the stack; must no longer than 250 characters | `null` +**description** | _string_ | Description of the stack; must be no longer than 250 characters | `null` **metadata.labels** | [_label object_](#labels) | Labels applied to the stack **metadata.annotations** | [_annotation object_](#annotations) | Annotations applied to the stack **state** | string | The state of the stack; valid states are: `ACTIVE`, `RESTRICTED`, `DEPRECATED`, `DISABLED` diff --git a/docs/v3/source/includes/resources/stacks/_update.md.erb b/docs/v3/source/includes/resources/stacks/_update.md.erb index e950a4c6089..38a45c0d0a6 100644 --- a/docs/v3/source/includes/resources/stacks/_update.md.erb +++ b/docs/v3/source/includes/resources/stacks/_update.md.erb @@ -9,7 +9,7 @@ curl "https://api.example.org/v3/stacks/[guid]" \ -X PATCH \ -H "Authorization: bearer [token]" \ -H "Content-Type: application/json" \ - -d '{ "metadata": { "labels": { "key": "value" }, "annotations": {"note": "detailed information"}},"state":"ACTIVE", "state_reason": "Optional plain text describing the stack state change" }' + -d '{ "metadata": { "labels": { "key": "value" }, "annotations": {"note": "detailed information"}},"description": "Example stack description", "state":"ACTIVE", "state_reason": "Optional plain text describing the stack state change" }' ``` @@ -31,6 +31,7 @@ Content-Type: application/json Name | Type | Description ---- | ---- | ----------- +**description** | _string_ | Description of the stack; must be no longer than 250 characters **metadata.labels** | [_label object_](#labels) | Labels applied to the stack **metadata.annotations** | [_annotation object_](#annotations) | Annotations applied to the stack **state** | string | The state of the stack; valid states are: `ACTIVE`, `RESTRICTED`, `DEPRECATED`, `DISABLED` diff --git a/spec/request/stacks_spec.rb b/spec/request/stacks_spec.rb index acff9384930..7823ee89d47 100644 --- a/spec/request/stacks_spec.rb +++ b/spec/request/stacks_spec.rb @@ -763,6 +763,23 @@ } ) end + + it 'updates the description of a stack' do + patch "/v3/stacks/#{stack.guid}", { description: 'Preview stack, do not use in production' }.to_json, headers + + expect(last_response.status).to eq(200) + expect(parsed_response['description']).to eq('Preview stack, do not use in production') + + stack.reload + expect(stack.description).to eq('Preview stack, do not use in production') + end + + it 'returns 422 when description exceeds the maximum length' do + patch "/v3/stacks/#{stack.guid}", { description: 'A' * 251 }.to_json, headers + + expect(last_response.status).to eq(422) + expect(last_response).to have_error_message('Description is too long (maximum is 250 characters)') + end end describe 'DELETE /v3/stacks/:guid' do diff --git a/spec/unit/actions/stack_update_spec.rb b/spec/unit/actions/stack_update_spec.rb index bc3e6d4691c..cec0fb0e5d8 100644 --- a/spec/unit/actions/stack_update_spec.rb +++ b/spec/unit/actions/stack_update_spec.rb @@ -95,5 +95,27 @@ module VCAP::CloudController expect(stack.state_reason).to eq('Existing reason') end end + + context 'when updating description' do + let(:stack) { create(:stack, description: 'Original description') } + + it 'updates description when provided' do + message = StackUpdateMessage.new({ description: 'Preview stack' }) + + stack_update.update(stack, message) + stack.reload + + expect(stack.description).to eq('Preview stack') + end + + it 'preserves description when not requested' do + message = StackUpdateMessage.new({ state: 'RESTRICTED' }) + + stack_update.update(stack, message) + stack.reload + + expect(stack.description).to eq('Original description') + end + end end end diff --git a/spec/unit/messages/stack_create_message_spec.rb b/spec/unit/messages/stack_create_message_spec.rb index aadf7b9191b..39734560e18 100644 --- a/spec/unit/messages/stack_create_message_spec.rb +++ b/spec/unit/messages/stack_create_message_spec.rb @@ -64,6 +64,14 @@ end describe 'description' do + context 'when it is not provided' do + let(:params) { valid_params } + + it 'is valid' do + expect(subject).to be_valid + end + end + context 'when it is just long enough' do let(:params) { valid_params.merge({ description: 'A' * MAX_DESCRIPTION_LENGTH }) } diff --git a/spec/unit/messages/stack_update_message_spec.rb b/spec/unit/messages/stack_update_message_spec.rb index a069f73d470..9bdec35674c 100644 --- a/spec/unit/messages/stack_update_message_spec.rb +++ b/spec/unit/messages/stack_update_message_spec.rb @@ -120,5 +120,59 @@ end end end + + describe 'description' do + MAX_DESCRIPTION_LENGTH = 250 + + context 'when it is not provided' do + let(:params) { valid_params } + + it 'is valid' do + expect(subject).to be_valid + end + end + + context 'when it is provided' do + let(:params) { valid_params.merge({ description: 'Preview stack, do not use in production' }) } + + it 'is valid' do + expect(subject).to be_valid + expect(subject.description).to eq('Preview stack, do not use in production') + end + end + + context 'when it is at maximum length' do + let(:params) { valid_params.merge({ description: 'A' * MAX_DESCRIPTION_LENGTH }) } + + it 'is valid' do + expect(subject).to be_valid + end + end + + context 'when it exceeds maximum length' do + let(:params) { valid_params.merge({ description: 'A' * (MAX_DESCRIPTION_LENGTH + 1) }) } + + it 'returns an error' do + expect(subject).not_to be_valid + expect(subject.errors[:description]).to eq ["is too long (maximum is #{MAX_DESCRIPTION_LENGTH} characters)"] + end + end + + context 'when it is an empty string' do + let(:params) { valid_params.merge({ description: '' }) } + + it 'is valid' do + expect(subject).to be_valid + end + end + + context 'when it is explicitly null' do + let(:params) { valid_params.merge({ description: nil }) } + + it 'is valid' do + expect(subject).to be_valid + end + end + end end end