diff --git a/app/actions/space_diff_manifest.rb b/app/actions/space_diff_manifest.rb index a24525d469a..92afd515fcc 100644 --- a/app/actions/space_diff_manifest.rb +++ b/app/actions/space_diff_manifest.rb @@ -1,4 +1,5 @@ require 'presenters/v3/app_manifest_presenter' +require 'messages/app_manifest_message' require 'json-diff' module VCAP::CloudController @@ -62,7 +63,7 @@ def self.filter_manifest_app_hash(manifest_app_hash) # rubocop:todo Metrics/CyclomaticComplexity def self.generate_diff(app_manifests, space) json_diff = [] - recognized_top_level_keys = NamedAppManifestMessage.allowed_keys.map(&:to_s) + recognized_top_level_keys = AppManifestMessage.allowed_keys.map(&:to_s) app_manifests.each_with_index do |manifest_app_hash, index| manifest_app_hash = SpaceDiffManifest.filter_manifest_app_hash(manifest_app_hash) existing_app = space.app_models.find { |app| app.name == manifest_app_hash['name'] } diff --git a/app/controllers/v3/app_manifests_controller.rb b/app/controllers/v3/app_manifests_controller.rb index 9828cd7eda3..d7b32186039 100644 --- a/app/controllers/v3/app_manifests_controller.rb +++ b/app/controllers/v3/app_manifests_controller.rb @@ -5,36 +5,6 @@ class AppManifestsController < ApplicationController include AppSubResource - wrap_parameters :body, format: [:yaml] - - before_action :validate_content_type!, only: :apply_manifest - - def apply_manifest - message = AppManifestMessage.create_from_yml(parsed_app_manifest_params) - compound_error!(message.errors.full_messages) unless message.valid? - - app, space, org = AppFetcher.new.fetch(hashed_params[:guid]) - - app_not_found! unless app && permission_queryer.can_read_from_space?(space.guid, org.guid) - unauthorized! unless permission_queryer.can_write_to_space?(space.guid) - unsupported_for_docker_apps!(message) if incompatible_with_buildpacks(app.lifecycle_type, message) - - apply_manifest_action = AppApplyManifest.new(user_audit_info) - apply_manifest_job = VCAP::CloudController::Jobs::AppApplyManifestActionJob.new(app.guid, message, apply_manifest_action) - - record_apply_manifest_audit_event(app, message, space) - job = Jobs::Enqueuer.new(apply_manifest_job, queue: Jobs::Queues.generic).enqueue_pollable - TelemetryLogger.v3_emit( - 'apply-manifest', - { - 'app-id' => app.guid, - 'user-id' => current_user.guid - } - ) - - head HTTP::ACCEPTED, 'Location' => url_builder.build_url(path: "/v3/jobs/#{job.guid}") - end - def show app, space, org = AppFetcher.new.fetch(hashed_params[:guid]) @@ -45,44 +15,4 @@ def show manifest_yaml = manifest_presenter.to_hash.deep_stringify_keys.to_yaml render status: :ok, plain: manifest_yaml, content_type: YAML_CONTENT_TYPE end - - private - - def record_apply_manifest_audit_event(app, message, space) - audited_request_yaml = { 'applications' => [message.audit_hash] }.to_yaml - Repositories::AppEventRepository.new.record_app_apply_manifest(app, space, user_audit_info, audited_request_yaml) - end - - def unsupported_for_docker_apps!(manifest) - error_message = manifest.buildpacks ? 'Buildpacks' : 'Buildpack' - raise unprocessable(error_message + ' cannot be configured for a docker lifecycle app.') - end - - def incompatible_with_buildpacks(lifecycle_type, manifest) - lifecycle_type == 'docker' && (manifest.buildpack || manifest.buildpacks) - end - - def compound_error!(error_messages) - underlying_errors = error_messages.map { |message| unprocessable(message) } - raise CloudController::Errors::CompoundError.new(underlying_errors) - end - - def validate_content_type! - if !request_content_type_is_yaml? - logger.error("Context-type isn't yaml: #{request.content_type}") - bad_request!('Content-Type must be yaml') - end - end - - def request_content_type_is_yaml? - Mime::Type.lookup(request.content_type) == :yaml - end - - def parsed_app_manifest_params - parsed_application = parsed_yaml['applications'] && parsed_yaml['applications'].first - - raise bad_request!('Invalid app manifest') unless parsed_application.present? - - parsed_application - end end diff --git a/app/controllers/v3/apps_controller.rb b/app/controllers/v3/apps_controller.rb index 408b83fb5f0..015a65e7bca 100644 --- a/app/controllers/v3/apps_controller.rb +++ b/app/controllers/v3/apps_controller.rb @@ -20,7 +20,6 @@ require 'messages/update_environment_variables_message' require 'messages/app_manifest_message' require 'messages/app_builds_list_message' -require 'messages/named_app_manifest_message' require 'presenters/v3/app_presenter' require 'presenters/v3/app_env_presenter' require 'presenters/v3/app_environment_variables_presenter' diff --git a/app/controllers/v3/space_manifests_controller.rb b/app/controllers/v3/space_manifests_controller.rb index b213925bb2b..678ecff30ff 100644 --- a/app/controllers/v3/space_manifests_controller.rb +++ b/app/controllers/v3/space_manifests_controller.rb @@ -1,6 +1,6 @@ require 'presenters/v3/app_manifest_presenter' require 'repositories/app_event_repository' -require 'messages/named_app_manifest_message' +require 'messages/app_manifest_message' require 'actions/app_find_or_create_skeleton' require 'actions/app_create' require 'actions/space_diff_manifest' @@ -15,7 +15,7 @@ def apply_manifest space_not_found! unless space && permission_queryer.can_read_from_space?(space.guid, space.organization.guid) unauthorized! unless permission_queryer.can_write_to_space?(space.guid) - messages = parsed_app_manifests.map { |app_manifest| NamedAppManifestMessage.create_from_yml(app_manifest) } + messages = parsed_app_manifests.map { |app_manifest| AppManifestMessage.create_from_yml(app_manifest) } errors = messages.each_with_index.flat_map { |message, i| errors_for_message(message, i) } compound_error!(errors) unless errors.empty? @@ -49,7 +49,7 @@ def diff_manifest parsed_manifests = parsed_app_manifests.map(&:to_hash) - messages = parsed_app_manifests.map { |app_manifest| NamedAppManifestMessage.create_from_yml(app_manifest) } + messages = parsed_app_manifests.map { |app_manifest| AppManifestMessage.create_from_yml(app_manifest) } errors = messages.each_with_index.flat_map { |message, i| errors_for_message(message, i) } compound_error!(errors) unless errors.empty? diff --git a/app/jobs/app_apply_manifest_action_job.rb b/app/jobs/app_apply_manifest_action_job.rb deleted file mode 100644 index adea104e724..00000000000 --- a/app/jobs/app_apply_manifest_action_job.rb +++ /dev/null @@ -1,59 +0,0 @@ -module VCAP::CloudController - module Jobs - class AppApplyManifestActionJob < VCAP::CloudController::Jobs::CCJob - def initialize(app_guid, apply_manifest_message, apply_manifest_action) - @app_guid = app_guid - @apply_manifest_message = apply_manifest_message - @apply_manifest_action = apply_manifest_action - end - - def perform - logger = Steno.logger('cc.background') - logger.info("Applying app manifest to app: #{resource_guid}") - - apply_manifest_action.apply(resource_guid, apply_manifest_message) - rescue AppPatchEnvironmentVariables::InvalidApp, - AppUpdate::InvalidApp, - AppApplyManifest::NoDefaultDomain, - ProcessCreate::InvalidProcess, - ProcessScale::InvalidProcess, - ProcessScale::SidecarMemoryLessThanProcessMemory, - ProcessUpdate::InvalidProcess, - SidecarCreate::InvalidSidecar, - SidecarUpdate::InvalidSidecar, - ManifestRouteUpdate::InvalidRoute, - Route::InvalidOrganizationRelation, - AppApplyManifest::Error, - AppApplyManifest::ServiceBindingError => e - - error = CloudController::Errors::ApiError.new_from_details('UnprocessableEntity', e.message) - error.set_backtrace(e.backtrace) - raise error - end - - def job_name_in_configuration - :apply_manifest_job - end - - def max_attempts - 1 - end - - def resource_type - 'app' - end - - def display_name - 'app.apply_manifest' - end - - def resource_guid - @app_guid - end - - private - - attr_reader :apply_manifest_action, :apply_manifest_message - end - end -end diff --git a/app/messages/app_manifest_message.rb b/app/messages/app_manifest_message.rb index bc6bd7b5c15..0c2f97aa5ec 100644 --- a/app/messages/app_manifest_message.rb +++ b/app/messages/app_manifest_message.rb @@ -25,6 +25,7 @@ class AppManifestMessage < BaseMessage :instances, :metadata, :memory, + :name, :no_route, :processes, :random_route, @@ -53,6 +54,7 @@ def self.underscore_keys(hash) end end + validates :name, presence: { message: 'must not be empty' }, string: true validate :validate_top_level_web_process! validate :validate_processes!, if: ->(record) { record.requested?(:processes) } validate :validate_sidecars!, if: ->(record) { record.requested?(:sidecars) } diff --git a/app/messages/named_app_manifest_message.rb b/app/messages/named_app_manifest_message.rb deleted file mode 100644 index fa5d0898549..00000000000 --- a/app/messages/named_app_manifest_message.rb +++ /dev/null @@ -1,10 +0,0 @@ -require 'messages/app_manifest_message' -require 'messages/validators' - -module VCAP::CloudController - class NamedAppManifestMessage < AppManifestMessage - register_allowed_keys [:name] - - validates :name, presence: { message: 'must not be empty' }, string: true - end -end diff --git a/config/routes.rb b/config/routes.rb index b5b72683af7..92830e1e4ad 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -27,7 +27,6 @@ get '/apps/:guid/ssh_enabled', to: 'app_features#ssh_enabled' # app manifests - post '/apps/:guid/actions/apply_manifest', to: 'app_manifests#apply_manifest' get '/apps/:guid/manifest', to: 'app_manifests#show' # app revisions diff --git a/docs/v3/source/includes/experimental_resources/app_manifest/_apply.md b/docs/v3/source/includes/experimental_resources/app_manifest/_apply.md deleted file mode 100644 index d432db21191..00000000000 --- a/docs/v3/source/includes/experimental_resources/app_manifest/_apply.md +++ /dev/null @@ -1,41 +0,0 @@ -### Apply an app manifest - - -``` -Example Request -``` - -```shell -curl "https://api.example.org/v3/apps/[guid]/actions/apply_manifest" \ - -X POST \ - -H "Authorization: bearer [token]" \ - -H "Content-type: application/x-yaml" \ - --data-binary @/path/to/manifest.yml -``` - -``` -Example Response -``` - -```http -HTTP/1.1 202 Accepted -Location: https://api.example.org/v3/jobs/[guid] -``` - -Apply changes specified in a manifest to an app and its underlying processes. These changes are additive and will not modify any unspecified properties or remove any existing environment variables, routes, or services. - - - -#### Definition -`POST /v3/apps/:guid/actions/apply_manifest` - -#### Permitted Roles - | ---- | --- -Admin | -Space Developer | diff --git a/docs/v3/source/includes/experimental_resources/app_manifest/_header.md b/docs/v3/source/includes/experimental_resources/app_manifest/_header.md deleted file mode 100644 index 5551ee5f619..00000000000 --- a/docs/v3/source/includes/experimental_resources/app_manifest/_header.md +++ /dev/null @@ -1 +0,0 @@ -## App Manifest diff --git a/docs/v3/source/includes/experimental_resources/app_manifest/_object.md.erb b/docs/v3/source/includes/experimental_resources/app_manifest/_object.md.erb deleted file mode 100644 index 34ab6b202f9..00000000000 --- a/docs/v3/source/includes/experimental_resources/app_manifest/_object.md.erb +++ /dev/null @@ -1,118 +0,0 @@ -The app manifest is a method for applying bulk configuration to an app and its underlying processes. -### The app manifest specification - -``` -Example Manifest -``` - -```yaml ---- -applications: -- buildpacks: - - ruby_buildpack - - java_buildpack - env: - VAR1: value1 - VAR2: value2 - routes: - - route: route.example.com - - route: another-route.example.com - services: - - my-service1 - - my-service2 - - name: my-service-with-arbitrary-params - parameters: - key1: value1 - key2: value2 - stack: cflinuxfs3 - metadata: - annotations: - contact: "bob@example.com jane@example.com" - labels: - sensitive: true - processes: - - type: web - command: start-web.sh - disk_quota: 512M - health-check-http-endpoint: /healthcheck - health-check-type: http - health-check-invocation-timeout: 10 - instances: 3 - memory: 500M - timeout: 10 - - type: worker - command: start-worker.sh - disk_quota: 1G - health-check-type: process - instances: 2 - memory: 256M - timeout: 15 - sidecars: - - name: authenticator - process_types: [ 'web', 'worker' ] - command: bundle exec run-authenticator - memory: 800M - - name: upcaser - process_types: [ 'worker' ] - command: ./tr-server - memory: 900M - -``` - -#### App-level configuration -This configuration is specified at the top-level and applies to all of the app's processes. - -Field | Description ----- | ----------- -**buildpacks** | Must be an Array.
a) An empty array, which will automatically select the appropriate default buildpack according to the coding language.
b) An array of one or more URLs pointing to buildpacks.
c) An array of one or more installed buildpack names.
Replaces the legacy `buildpack` field -**env** | A key-value hash of environment variables to be used for the app when running -**no-route** | Boolean value; when set to `true`, any routes specified with the `routes` attribute will be ignored and any existing routes will be removed; ignored if `false` -**processes** | List of configurations for individual process types, see [_Process-level configuration_](#app-manifest-process-level-configuration) -**random-route** | Boolean value; creates a random route for the app if `true`; ignored if `false`, if `routes` is specified, if the app already has routes, or if `no-route` is specified -**default-route** | Boolean value; if true, a route for the app will be created using the app name as the hostname and the containing organization's default domain as the domain. If `false`, if `routes` is specified, if the app already has routes, or if `no-route` is specified, this field is ignored and results in noop -**routes** | An array of route hashes declaring HTTP and TCP routes to be mapped to the app. Each route is created if it does not already exist. Example route hash entry: `- route: www.examplecom/path` -**services** | An array of service-instances to bind to the app, see [_Service-level configuration_](#app-manifest-service-level-configuration) -**sidecars** | An array of configurations for individual sidecars, see [_Sidecar-level configuration_](#app-manifest-sidecar-level-configuration) -**stack** | The root filesystem to use with the buildpack, for example `cflinuxfs3` -**metadata.labels** | [Labels](#labels) applied to the app -**metadata.annotations** | [Annotations](#annotations) applied to the app -**buildpack** | **DEPRECATED in favor of the `buildpacks` field above** - - -#### Process-level configuration -This configuration is for the individual process. Each process is created if it does not already exist. - -With the exception of `type`, process-level fields can also be provided at the top-level and will apply to the `web` process only. - -If there is a process with `type: web` defined in the processes section, then all top level process configuration will be ignored. - -Field | Description ----- | ----------- -**type** | **(Required)** Process type, the identifier for the processes to be configured -**command** | The command used to start the process; this overrides start commands from [Procfiles](#procfiles) and buildpacks -**disk_quota** | The disk limit for all instances of the web process;
this attribute requires a unit of measurement: `B`, `K`, `KB`, `M`, `MB`, `G`, `GB`, `T`, or `TB` in upper case or lower case -**health-check-http-endpoint** | Endpoint called to determine if the app is healthy -**health-check-invocation-timeout** | The timeout in seconds for individual health check requests for http and port health checks -**health-check-type** | Type of health check to perform; `none` is deprecated and an alias to `process` -**instances** | The number of instances to run -**memory** | The memory limit for all instances of the web process;
this attribute requires a unit of measurement: `B`, `K`, `KB`, `M`, `MB`, `G`, `GB`, `T`, or `TB` in upper case or lower case -**timeout** | Time in seconds at which the health-check will report failure - -#### Service-level configuration -This configuration is _creating_ new service bindings between the app and a service instance. The `services` field can -take either an array of service instance name strings or an array of the following service-level fields. - -Field | Description ----- | ----------- -**name** | **(Required)** Service instance name; the name of the service instance to be bound to -**parameters** | A map of arbitrary key/value pairs to send to the service broker during binding - -#### Sidecar-level configuration -This configuration is for the individual sidecar. Each sidecar is created if it does not already exist. - -Field | Description ----- | ----------- -**name** | **(Required)** Sidecar name; the identifier for the sidecars to be configured -**command** | The command used to start the sidecar -**process_types** | List of processes to associate sidecar with -**memory** | Memory in mb that the sidecar will be allocated diff --git a/docs/v3/source/includes/experimental_resources/sidecars/_header.md b/docs/v3/source/includes/experimental_resources/sidecars/_header.md index 02739034500..98f63348706 100644 --- a/docs/v3/source/includes/experimental_resources/sidecars/_header.md +++ b/docs/v3/source/includes/experimental_resources/sidecars/_header.md @@ -12,7 +12,7 @@ Sidecars are useful for any app processes that need to communicate with another #### Steps to create a sidecar -The recommended way to create sidecars for your app is with an [app manifest](#app-manifest). +The recommended way to create sidecars for your app is with a [manifest](#manifests). ```yaml sidecars: diff --git a/docs/v3/source/index.html.md b/docs/v3/source/index.html.md index f45fe2b4814..f6728477949 100644 --- a/docs/v3/source/index.html.md +++ b/docs/v3/source/index.html.md @@ -342,9 +342,6 @@ includes: - resources/users/update - resources/users/delete - experimental_resources/header - - experimental_resources/app_manifest/header - - experimental_resources/app_manifest/object - - experimental_resources/app_manifest/apply - experimental_resources/app_restart/header - experimental_resources/app_restart/create - experimental_resources/revisions/header diff --git a/lib/cloud_controller/jobs.rb b/lib/cloud_controller/jobs.rb index 919a87836e5..763a75cee9f 100644 --- a/lib/cloud_controller/jobs.rb +++ b/lib/cloud_controller/jobs.rb @@ -1,6 +1,5 @@ require 'jobs/cc_job' require 'jobs/delete_action_job' -require 'jobs/app_apply_manifest_action_job' require 'jobs/space_apply_manifest_action_job' require 'jobs/enqueuer' require 'jobs/queues' diff --git a/spec/request/app_manifests_spec.rb b/spec/request/app_manifests_spec.rb index 124eb43eb51..8b5185c8b59 100644 --- a/spec/request/app_manifests_spec.rb +++ b/spec/request/app_manifests_spec.rb @@ -18,554 +18,6 @@ TestConfig.override(kubernetes: {}) end - describe 'POST /v3/apps/:guid/actions/apply_manifest' do - let(:buildpack) { VCAP::CloudController::Buildpack.make } - let(:service_instance) { VCAP::CloudController::ManagedServiceInstance.make(space: space) } - let(:client) { instance_double(VCAP::Services::ServiceBrokers::V2::Client, bind: {}) } - let(:yml_manifest) do - { - 'applications' => [ - { 'name' => 'blah', - 'instances' => 4, - 'memory' => '2048MB', - 'disk_quota' => '1.5GB', - 'buildpack' => buildpack.name, - 'stack' => buildpack.stack, - 'command' => 'new-command', - 'health_check_type' => 'http', - 'health_check_http_endpoint' => '/health', - 'timeout' => 42, - 'env' => { - 'k1' => 'mangos', - 'k2' => 'pears', - 'k3' => 'watermelon' - }, - 'metadata' => { - 'annotations' => { - 'potato' => 'idaho', - 'juice' => 'newton', - 'berry' => nil, - }, - 'labels' => { - 'potato' => 'yam', - 'downton' => nil, - 'myspace.com/songs' => 'missing', - }, - }, - 'routes' => [ - { 'route' => "https://#{route.host}.#{route.domain.name}" }, - { 'route' => "https://#{second_route.host}.#{second_route.domain.name}/path" } - ], - 'services' => [ - service_instance.name - ] - } - ] - }.to_yaml - end - - before do - stub_bind(service_instance) - VCAP::CloudController::LabelsUpdate.update(app_model, { 'potato' => 'french', - 'downton' => 'abbey road', }, VCAP::CloudController::AppLabelModel) - VCAP::CloudController::AnnotationsUpdate.update(app_model, { 'potato' => 'baked', - 'berry' => 'white', }, VCAP::CloudController::AppAnnotationModel) - end - - it 'applies the manifest' do - web_process = app_model.web_processes.first - expect(web_process.instances).to eq(1) - - post "/v3/apps/#{app_model.guid}/actions/apply_manifest", yml_manifest, yml_headers(user_header) - - expect(last_response.status).to eq(202) - job_guid = VCAP::CloudController::PollableJobModel.last.guid - expect(last_response.headers['Location']).to match(%r(/v3/jobs/#{job_guid})) - - Delayed::Worker.new.work_off - expect(VCAP::CloudController::PollableJobModel.find(guid: job_guid)).to be_complete - - web_process.reload - expect(web_process.instances).to eq(4) - expect(web_process.memory).to eq(2048) - expect(web_process.disk_quota).to eq(1536) - expect(web_process.command).to eq('new-command') - expect(web_process.health_check_type).to eq('http') - expect(web_process.health_check_http_endpoint).to eq('/health') - expect(web_process.health_check_timeout).to eq(42) - - app_model.reload - lifecycle_data = app_model.lifecycle_data - expect(lifecycle_data.buildpacks).to include(buildpack.name) - expect(lifecycle_data.stack).to eq(buildpack.stack) - expect(app_model.environment_variables).to match( - 'k1' => 'mangos', - 'k2' => 'pears', - 'k3' => 'watermelon' - ) - expect(app_model.routes).to match_array([route, second_route]) - - expect(app_model.service_bindings.length).to eq 1 - expect(app_model.service_bindings.first.service_instance).to eq service_instance - expect(app_model).to have_labels( - { key: 'potato', value: 'yam' }, - { prefix: 'myspace.com', key: 'songs', value: 'missing' }, - ) - expect(app_model).to have_annotations( - { key: 'potato', value: 'idaho' }, - { key: 'juice', value: 'newton' }, - ) - end - - context 'sidecars' do - let(:yml_manifest) do - { - 'applications' => [ - { - 'name' => 'blah', - 'sidecars' => sidecars_attributes - } - ] - }.to_yaml - end - - let(:sidecars_attributes) do - [ - { - 'process_types' => ['worker'], - 'command' => 'bundle exec sidecar_for_web_only', - 'name' => 'my-sidecar', - 'memory' => 300, - } - ] - end - - it 'creates new sidecars' do - expect { - post "/v3/apps/#{app_model.guid}/actions/apply_manifest", yml_manifest, yml_headers(user_header) - Delayed::Worker.new.work_off - }.to change { VCAP::CloudController::SidecarModel.count }.from(0).to(1) - - expect(last_response.status).to eq(202) - sidecar = VCAP::CloudController::SidecarModel.last - expect(sidecar.name).to eq('my-sidecar') - expect(sidecar.command).to eq('bundle exec sidecar_for_web_only') - expect(sidecar.process_types).to eq(['worker']) - expect(sidecar.memory).to eq(300) - end - - context 'when a sidecar already exists' do - let!(:sidecar) { VCAP::CloudController::SidecarModel.make(name: 'my-sidecar', app: app_model, command: 'rackup', memory: 200) } - let!(:sidecar_process_type) { VCAP::CloudController::SidecarProcessTypeModel.make(sidecar: sidecar, type: 'web', app_guid: app_model.guid) } - - it 'updates based on name' do - expect { - post "/v3/apps/#{app_model.guid}/actions/apply_manifest", yml_manifest, yml_headers(user_header) - Delayed::Worker.new.work_off - }.not_to change { VCAP::CloudController::SidecarModel.count } - - expect(last_response.status).to eq(202) - sidecar.reload - expect(sidecar.name).to eq('my-sidecar') - expect(sidecar.command).to eq('bundle exec sidecar_for_web_only') - expect(sidecar.process_types).to eq(['worker']) - expect(sidecar.memory).to eq(300) - end - - context 'when sidecar name is not provided' do - let(:sidecars_attributes) do - [ - { - 'process_types' => ['worker'], - 'command' => 'bundle exec sidecar_for_web_only', - } - ] - end - - it 'returns 422' do - post "/v3/apps/#{app_model.guid}/actions/apply_manifest", yml_manifest, yml_headers(user_header) - expect(last_response.status).to eq(422) - end - end - end - context 'telemetry' do - it 'should log the required fields when the app manifest is applied' do - Timecop.freeze do - expected_json = { - 'telemetry-source' => 'cloud_controller_ng', - 'telemetry-time' => Time.now.to_datetime.rfc3339, - 'apply-manifest' => { - 'api-version' => 'v3', - 'app-id' => Digest::SHA256.hexdigest(app_model.guid), - 'user-id' => Digest::SHA256.hexdigest(user.guid) - } - } - expect_any_instance_of(ActiveSupport::Logger).to receive(:info).with(JSON.generate(expected_json)) - post "/v3/apps/#{app_model.guid}/actions/apply_manifest", yml_manifest, yml_headers(user_header) - expect(last_response.status).to eq(202), last_response.body - end - end - end - end - - describe 'scaling proceses when there are existing sidecars' do - let!(:process) { VCAP::CloudController::ProcessModel.make(app: app_model, type: 'web', memory: 400) } - let!(:sidecar) { VCAP::CloudController::SidecarModel.make(name: 'my-sidecar', app: app_model, command: 'rackup', memory: 200) } - let!(:sidecar_process_type) { VCAP::CloudController::SidecarProcessTypeModel.make(sidecar: sidecar, type: 'web', app_guid: app_model.guid) } - - context 'when the new process memory is more than the cumulative sidecars memory' do - let(:yml_manifest) do - { - 'applications' => [ - { 'name' => app_model.name, - 'processes' => [{ - 'type' => 'web', - 'memory' => '300MB' - }] - } - ] - }.to_yaml - end - it 'returns a 200' do - post "/v3/apps/#{app_model.guid}/actions/apply_manifest", yml_manifest, yml_headers(user_header) - Delayed::Worker.new.work_off - expect(process.reload.memory).to eq 300 - expect(last_response.status).to eq(202) - end - end - end - - context 'yaml anchors' do - let(:yml_manifest) do - <<~YML - --- - applications: - - name: blah - processes: - - type: web - memory: &default_value 321M - disk_quota: *default_value - YML - end - - it 'does NOT accept yaml with anchors' do - post "/v3/apps/#{app_model.guid}/actions/apply_manifest", yml_manifest, yml_headers(user_header) - - expect(last_response.status).to eq(400) - parsed_response = MultiJson.load(last_response.body) - expect(parsed_response['errors'].first['detail']).to eq('Bad request: Manifest does not support Anchors and Aliases') - end - end - - context 'service bindings' do - it 'returns an appropriate error when fail to bind' do - allow(VCAP::Services::ServiceClientProvider).to receive(:provide).and_return(client) - allow(client).to receive(:bind).and_raise('Failed') - - post "/v3/apps/#{app_model.guid}/actions/apply_manifest", yml_manifest, yml_headers(user_header) - Delayed::Worker.new.work_off - - job_location = last_response.headers['Location'] - get job_location, nil, user_header - parsed_response = MultiJson.load(last_response.body) - - expect(VCAP::CloudController::ServiceBinding.count).to eq(0) - expect(parsed_response['errors'].first['detail']).to eq("For service '#{service_instance.name}': Failed") - end - end - - describe 'audit events' do - let!(:process) { nil } - - let(:yml_manifest) do - { - 'applications' => [ - { 'name' => 'blah', - 'instances' => 4, - 'memory' => '2048MB', - 'disk_quota' => '1.5GB', - 'buildpack' => buildpack.name, - 'stack' => buildpack.stack, - 'command' => 'new-command', - 'health_check_type' => 'http', - 'health_check_http_endpoint' => '/health', - 'timeout' => 42, - 'env' => { - 'k1' => 'mangos', - 'k2' => 'pears', - 'k3' => 'watermelon' - }, - 'routes' => [ - { 'route' => "https://#{route.host}.#{route.domain.name}" }, - { 'route' => "https://pants.#{second_route.domain.name}/path" } - ], - 'services' => [ - service_instance.name - ] - } - ] - }.to_yaml - end - - it 'creates audit events tagged with metadata.manifest_triggered' do - expect { - post "/v3/apps/#{app_model.guid}/actions/apply_manifest", yml_manifest, yml_headers(user_header) - Delayed::Worker.new.work_off - }.to change { VCAP::CloudController::Event.count }.by 10 - - manifest_triggered_events = VCAP::CloudController::Event.find_all { |event| event.metadata['manifest_triggered'] } - expect(manifest_triggered_events.map(&:type)).to match_array([ - 'audit.app.process.update', - 'audit.app.process.create', - 'audit.app.process.scale', - 'audit.app.update', - 'audit.app.update', - 'audit.app.map-route', - 'audit.route.create', - 'audit.app.map-route', - 'audit.service_binding.create', - ]) - - other_events = VCAP::CloudController::Event.find_all { |event| !event.metadata['manifest_triggered'] } - expect(other_events.map(&:type)).to eq(['audit.app.apply_manifest',]) - end - - context 'when no-route is included and the app has existing routes' do - let(:yml_manifest) do - { - 'applications' => [ - { 'name' => 'blah', - 'no_route' => true, - } - ] - }.to_yaml - end - - before do - VCAP::CloudController::RouteMappingModel.make(app: app_model, route: route) - end - - it 'creates audit.app.unmap-route audit events including metadata.manifest_triggered' do - expect { - post "/v3/apps/#{app_model.guid}/actions/apply_manifest", yml_manifest, yml_headers(user_header) - Delayed::Worker.new.work_off - }.to change { VCAP::CloudController::Event.count }.by 4 - - manifest_triggered_events = VCAP::CloudController::Event.find_all { |event| event.metadata['manifest_triggered'] } - expect(manifest_triggered_events.map(&:type)).to match_array([ - 'audit.app.update', - 'audit.app.update', - 'audit.app.unmap-route', - ]) - - other_events = VCAP::CloudController::Event.find_all { |event| !event.metadata['manifest_triggered'] } - expect(other_events.map(&:type)).to eq(['audit.app.apply_manifest',]) - end - end - end - - describe 'multiple processes' do - let(:web_process) do - { - 'type' => 'web', - 'instances' => 4, - 'command' => 'new-command', - 'memory' => '2048MB', - 'disk_quota' => '256MB', - 'health-check-type' => 'http', - 'health-check-http-endpoint' => '/test', - 'timeout' => 10, - } - end - - let(:worker_process) do - { - 'type' => 'worker', - 'instances' => 2, - 'command' => 'bar', - 'memory' => '512MB', - 'disk_quota' => '1024M', - 'health-check-type' => 'port', - 'timeout' => 150 - } - end - - let(:yml_manifest) do - { - 'applications' => [ - { - 'name' => 'blah', - 'processes' => [web_process, worker_process] - } - ] - }.to_yaml - end - - context 'when all the process types already exist' do - let!(:process2) { VCAP::CloudController::ProcessModel.make(app: app_model, type: 'worker') } - - it 'applies the manifest' do - web_process = app_model.web_processes.first - expect(web_process.instances).to eq(1) - - post "/v3/apps/#{app_model.guid}/actions/apply_manifest", yml_manifest, yml_headers(user_header) - - expect(last_response.status).to eq(202) - job_guid = VCAP::CloudController::PollableJobModel.last.guid - expect(last_response.headers['Location']).to match(%r(/v3/jobs/#{job_guid})) - - Delayed::Worker.new.work_off - background_job = VCAP::CloudController::PollableJobModel.find(guid: job_guid) - expect(background_job).to be_complete, "Failed due to: #{background_job.cf_api_error}" - - web_process.reload - expect(web_process.instances).to eq(4) - expect(web_process.memory).to eq(2048) - expect(web_process.disk_quota).to eq(256) - expect(web_process.command).to eq('new-command') - expect(web_process.health_check_type).to eq('http') - expect(web_process.health_check_http_endpoint).to eq('/test') - expect(web_process.health_check_timeout).to eq(10) - - process2.reload - expect(process2.instances).to eq(2) - expect(process2.memory).to eq(512) - expect(process2.disk_quota).to eq(1024) - expect(process2.command).to eq('bar') - expect(process2.health_check_type).to eq('port') - expect(process2.health_check_timeout).to eq(150) - end - end - - context 'when some of the process types do NOT exist for the app yet' do - it 'creates the processes and applies the manifest' do - web_process = app_model.web_processes.first - expect(web_process.instances).to eq(1) - - post "/v3/apps/#{app_model.guid}/actions/apply_manifest", yml_manifest, yml_headers(user_header) - - expect(last_response.status).to eq(202) - job_guid = VCAP::CloudController::PollableJobModel.last.guid - expect(last_response.headers['Location']).to match(%r(/v3/jobs/#{job_guid})) - - Delayed::Worker.new.work_off - background_job = VCAP::CloudController::PollableJobModel.find(guid: job_guid) - expect(background_job).to be_complete, "Failed due to: #{background_job.cf_api_error}" - - web_process.reload - expect(web_process.instances).to eq(4) - expect(web_process.memory).to eq(2048) - expect(web_process.disk_quota).to eq(256) - expect(web_process.command).to eq('new-command') - expect(web_process.health_check_type).to eq('http') - expect(web_process.health_check_http_endpoint).to eq('/test') - expect(web_process.health_check_timeout).to eq(10) - - process2 = VCAP::CloudController::ProcessModel.find(app_guid: app_model.guid, type: 'worker') - expect(process2.instances).to eq(2) - expect(process2.memory).to eq(512) - expect(process2.disk_quota).to eq(1024) - expect(process2.command).to eq('bar') - expect(process2.health_check_type).to eq('port') - expect(process2.health_check_timeout).to eq(150) - end - end - end - - describe 'multiple buildpacks' do - let(:buildpack) { VCAP::CloudController::Buildpack.make } - let(:buildpack2) { VCAP::CloudController::Buildpack.make } - let(:yml_manifest) do - { - 'applications' => [ - { - 'name' => 'blah', - 'buildpacks' => [buildpack.name, buildpack2.name] - } - ] - }.to_yaml - end - - it 'applies the manifest' do - post "/v3/apps/#{app_model.guid}/actions/apply_manifest", yml_manifest, yml_headers(user_header) - - expect(last_response.status).to eq(202) - job_guid = VCAP::CloudController::PollableJobModel.last.guid - expect(last_response.headers['Location']).to match(%r(/v3/jobs/#{job_guid})) - - Delayed::Worker.new.work_off - background_job = VCAP::CloudController::PollableJobModel.find(guid: job_guid) - expect(background_job).to be_complete, "Failed due to: #{background_job.cf_api_error}" - - app_model.reload - lifecycle_data = app_model.lifecycle_data - expect(lifecycle_data.buildpacks).to eq([buildpack.name, buildpack2.name]) - end - end - - describe 'kpack app' do - let(:app_model) { VCAP::CloudController::AppModel.make(:kpack, space: space, name: 'blah') } - let(:yml_manifest) do - { - 'applications' => [ - { - 'name' => 'blah', - 'buildpacks' => ['packeto-buildpacks/go', 'packeto-buildpacks/nodejs'] - } - ] - }.to_yaml - end - - it 'applies the manifest' do - post "/v3/apps/#{app_model.guid}/actions/apply_manifest", yml_manifest, yml_headers(user_header) - - expect(last_response.status).to eq(202) - job_guid = VCAP::CloudController::PollableJobModel.last.guid - expect(last_response.headers['Location']).to match(%r(/v3/jobs/#{job_guid})) - - Delayed::Worker.new.work_off - background_job = VCAP::CloudController::PollableJobModel.find(guid: job_guid) - expect(background_job).to be_complete, "Failed due to: #{background_job.cf_api_error}" - - app_model.reload - lifecycle_data = app_model.lifecycle_data - expect(lifecycle_data.buildpacks).to eq(['packeto-buildpacks/go', 'packeto-buildpacks/nodejs']) - expect(lifecycle_data).to be_a(VCAP::CloudController::KpackLifecycleDataModel) - end - end - - context 'when a deployment is in progress' do - before do - TestConfig.override(temporary_disable_deployments: false) - deployment = VCAP::CloudController::DeploymentModelTestFactory.make( - state: VCAP::CloudController::DeploymentModel::DEPLOYING_STATE, - app: app_model, - ) - expect(deployment.state).to eq(VCAP::CloudController::DeploymentModel::DEPLOYING_STATE) - post "/v3/apps/#{app_model.guid}/actions/apply_manifest", yml_manifest, yml_headers(user_header) - expect(last_response.status).to eq(202) - end - - context 'when the manifest attempts to update/scale non-web processes' do - let(:yml_manifest) do - { 'applications' => - [{ 'name' => 'blah', - 'processes' => [{ 'type' => 'worker', 'instances' => '3', 'command' => 'echo hi' }] - }] - }.to_yaml - end - - it 'succeeds' do - Delayed::Worker.new.work_off - job = VCAP::CloudController::PollableJobModel.last - - expect(last_response.headers['Location']).to match(%r(/v3/jobs/#{job.guid})) - expect(job.state).to eq('COMPLETE') - end - end - end - end - describe 'GET /v3/apps/:guid/manifest' do let(:app_model) { VCAP::CloudController::AppModel.make(space: space, environment_variables: { 'one' => 'tomato', 'two' => 'potato' }) } diff --git a/spec/unit/actions/app_find_or_create_skeleton_spec.rb b/spec/unit/actions/app_find_or_create_skeleton_spec.rb index 92126318f5b..ebb987edbb1 100644 --- a/spec/unit/actions/app_find_or_create_skeleton_spec.rb +++ b/spec/unit/actions/app_find_or_create_skeleton_spec.rb @@ -10,7 +10,7 @@ module VCAP::CloudController subject(:action) { AppFindOrCreateSkeleton.new(user_audit_info) } context 'when the app exists' do - let(:message) { NamedAppManifestMessage.create_from_yml({ name: name }) } + let(:message) { AppManifestMessage.create_from_yml({ name: name }) } let!(:app) { AppModel.make(name: name, space: space) } it 'returns the existing app' do @@ -20,7 +20,7 @@ module VCAP::CloudController context 'when the app does not exist' do context 'when the app is a buildpack app' do - let(:message) { NamedAppManifestMessage.create_from_yml({ name: name }) } + let(:message) { AppManifestMessage.create_from_yml({ name: name }) } it 'creates the app' do app = nil @@ -34,7 +34,7 @@ module VCAP::CloudController end context 'when the app is a docker app' do - let(:message) { NamedAppManifestMessage.create_from_yml({ name: name, docker: { image: 'my/image' } }) } + let(:message) { AppManifestMessage.create_from_yml({ name: name, docker: { image: 'my/image' } }) } it 'creates the app' do app = nil diff --git a/spec/unit/controllers/v3/app_manifests_controller_spec.rb b/spec/unit/controllers/v3/app_manifests_controller_spec.rb index d90629cbe94..5b12c499cf8 100644 --- a/spec/unit/controllers/v3/app_manifests_controller_spec.rb +++ b/spec/unit/controllers/v3/app_manifests_controller_spec.rb @@ -2,608 +2,6 @@ require 'permissions_spec_helper' RSpec.describe AppManifestsController, type: :controller do - describe '#apply_manifest' do - let(:app_model) { VCAP::CloudController::AppModel.make } - let(:space) { app_model.space } - let(:org) { space.organization } - let(:user) { VCAP::CloudController::User.make } - let(:app_apply_manifest_action) { instance_double(VCAP::CloudController::AppApplyManifest) } - let(:request_body) { { 'applications' => [{ 'name' => 'blah', 'instances' => 2 }] } } - - before do - set_current_user_as_role(role: 'admin', org: org, space: space, user: user) - allow(VCAP::CloudController::Jobs::AppApplyManifestActionJob).to receive(:new).and_call_original - allow(VCAP::CloudController::AppApplyManifest).to receive(:new).and_return(app_apply_manifest_action) - request.headers['CONTENT_TYPE'] = 'application/x-yaml' - end - - context 'permissions' do - describe 'authorization' do - role_to_expected_http_response = { - 'admin' => 202, - 'admin_read_only' => 403, - 'global_auditor' => 403, - 'space_developer' => 202, - 'space_manager' => 403, - 'space_auditor' => 403, - 'org_manager' => 403, - 'org_auditor' => 404, - 'org_billing_manager' => 404, - }.freeze - - role_to_expected_http_response.each do |role, expected_return_value| - context "as an #{role}" do - it "returns #{expected_return_value}" do - set_current_user_as_role(role: role, org: org, space: space, user: user) - - post :apply_manifest, params: { guid: app_model.guid }, body: request_body.to_yaml, as: :yaml - - expect(response.status).to eq(expected_return_value), "role #{role}: expected #{expected_return_value}, got: #{response.status}" - end - end - end - end - end - - context 'when the request body is invalid' do - context 'when the yaml is missing an applications array' do - let(:request_body) { { 'name' => 'blah', 'instances' => 4 } } - - it 'returns a 400' do - post :apply_manifest, params: { guid: app_model.guid }, body: request_body.to_yaml, as: :yaml - expect(response.status).to eq(400) - end - end - - context 'when the requested applications array is empty' do - let(:request_body) { { 'applications' => [] } } - - it 'returns a 400' do - post :apply_manifest, params: { guid: app_model.guid }, body: request_body.to_yaml, as: :yaml - expect(response.status).to eq(400) - end - - context 'when the app does not exist' do - let(:request_body) { { 'applications' => [{ 'name' => 'blah', 'instances' => 1, 'memory' => '4MB' }] } } - - it 'returns a 404' do - post :apply_manifest, params: { guid: 'no-such-app-guid' }, body: request_body.to_yaml, as: :yaml - expect(response.status).to eq(404) - end - end - end - - context 'when specified manifest fails validations' do - let(:request_body) do - { 'applications' => [{ 'name' => 'blah', 'instances' => -1, 'memory' => '10NOTaUnit', - 'command' => '', 'env' => 42, - 'health-check-http-endpoint' => '/endpoint', - 'health-check-invocation-timeout' => -22, - 'health-check-type' => 'foo', - 'timeout' => -42, - 'random-route' => -42, - 'routes' => [{ 'route' => 'garbage' }], - }] } - end - - it 'returns a 422 and validation errors' do - post :apply_manifest, params: { guid: app_model.guid }, body: request_body.to_yaml, as: :yaml - expect(response.status).to eq(422) - errors = parsed_body['errors'] - expect(errors.size).to eq(10) - expect(errors.map { |h| h.reject { |k, _| k == 'test_mode_info' } }).to match_array([ - { - 'detail' => 'Process "web": Memory must use a supported unit: B, K, KB, M, MB, G, GB, T, or TB', - 'title' => 'CF-UnprocessableEntity', - 'code' => 10008 - }, { - 'detail' => 'Process "web": Instances must be greater than or equal to 0', - 'title' => 'CF-UnprocessableEntity', - 'code' => 10008 - }, { - 'detail' => 'Process "web": Command must be between 1 and 4096 characters', - 'title' => 'CF-UnprocessableEntity', - 'code' => 10008 - }, { - 'detail' => 'Env must be an object of keys and values', - 'title' => 'CF-UnprocessableEntity', - 'code' => 10008 - }, { - 'detail' => 'Process "web": Health check type must be "http" to set a health check HTTP endpoint', - 'title' => 'CF-UnprocessableEntity', - 'code' => 10008 - }, { - 'detail' => 'Process "web": Health check type must be "port", "process", or "http"', - 'title' => 'CF-UnprocessableEntity', - 'code' => 10008 - }, { - 'detail' => 'Process "web": Health check invocation timeout must be greater than or equal to 1', - 'title' => 'CF-UnprocessableEntity', - 'code' => 10008 - }, { - 'detail' => 'Process "web": Timeout must be greater than or equal to 1', - 'title' => 'CF-UnprocessableEntity', - 'code' => 10008 - }, { - 'detail' => "The route 'garbage' is not a properly formed URL", - 'title' => 'CF-UnprocessableEntity', - 'code' => 10008 - }, { - 'detail' => 'Random-route must be a boolean', - 'title' => 'CF-UnprocessableEntity', - 'code' => 10008 - } - ]) - end - end - - context 'when the request payload is not yaml' do - let(:request_body) { { 'applications' => [{ 'name' => 'blah', 'instances' => 1 }] } } - before do - allow(CloudController::Errors::ApiError).to receive(:new_from_details).and_call_original - request.headers['CONTENT_TYPE'] = 'text/plain' - end - - it 'returns a 400' do - post :apply_manifest, params: { guid: app_model.guid }, body: request_body.to_yaml - expect(response.status).to eq(400) - # Verify we're getting the InvalidError we're expecting - expect(CloudController::Errors::ApiError).to have_received(:new_from_details).with('BadRequest', 'Content-Type must be yaml').exactly :once - end - end - end - - context 'when the request body includes a buildpack' do - let!(:php_buildpack) { VCAP::CloudController::Buildpack.make(name: 'php_buildpack') } - let(:request_body) do - { 'applications' => - [{ 'name' => 'blah', 'instances' => 4, 'buildpack' => 'php_buildpack' }] } - end - - it 'sets the buildpack' do - post :apply_manifest, params: { guid: app_model.guid }, body: request_body.to_yaml, as: :yaml - - expect(response.status).to eq(202) - app_apply_manifest_jobs = Delayed::Job.where(Sequel.lit("handler like '%AppApplyManifest%'")) - expect(app_apply_manifest_jobs.count).to eq 1 - - expect(VCAP::CloudController::Jobs::AppApplyManifestActionJob).to have_received(:new) do |app_guid, message, action| - expect(app_guid).to eq app_model.guid - expect(message.buildpack).to eq 'php_buildpack' - expect(action).to eq app_apply_manifest_action - end - end - - context 'and the value of buildpack is \"null\"' do - let(:request_body) do - { 'applications' => - [{ 'name' => 'blah', 'instances' => 4, 'buildpack' => 'null' }] } - end - - it 'should autodetect the buildpack' do - post :apply_manifest, params: { guid: app_model.guid }, body: request_body.to_yaml, as: :yaml - - expect(response.status).to eq(202) - app_apply_manifest_jobs = Delayed::Job.where(Sequel.lit("handler like '%AppApplyManifest%'")) - expect(app_apply_manifest_jobs.count).to eq(1) - - expect(VCAP::CloudController::Jobs::AppApplyManifestActionJob).to have_received(:new) do |_, message, _| - expect(message.app_update_message.buildpack_data.buildpacks).to eq([]) - end - end - end - - context 'for a docker app' do - let(:app_model) { VCAP::CloudController::AppModel.make(:docker) } - let(:request_body) do - { 'applications' => - [{ 'name' => 'blah', 'buildpack' => 'php_buildpack' }] } - end - - it 'returns an error' do - post :apply_manifest, params: { guid: app_model.guid }, body: request_body.to_yaml, as: :yaml - - expect(response.status).to eq(422) - errors = parsed_body['errors'] - expect(errors.size).to eq(1) - expect(errors.map { |h| h.reject { |k, _| k == 'test_mode_info' } }).to match_array([ - { - 'detail' => 'Buildpack cannot be configured for a docker lifecycle app.', - 'title' => 'CF-UnprocessableEntity', - 'code' => 10008 - } - ]) - end - end - end - - context 'when the request body includes a buildpacks' do - let!(:php_buildpack) { VCAP::CloudController::Buildpack.make(name: 'php_buildpack') } - let(:request_body) do - { 'applications' => - [{ 'name' => 'blah', 'instances' => 4, 'buildpacks' => ['php_buildpack'] }] } - end - - it 'sets the buildpacks' do - post :apply_manifest, params: { guid: app_model.guid }, body: request_body.to_yaml, as: :yaml - - expect(response.status).to eq(202) - app_apply_manifest_jobs = Delayed::Job.where(Sequel.lit("handler like '%AppApplyManifest%'")) - expect(app_apply_manifest_jobs.count).to eq 1 - - expect(VCAP::CloudController::Jobs::AppApplyManifestActionJob).to have_received(:new) do |app_guid, message, action| - expect(app_guid).to eq app_model.guid - expect(message.buildpacks).to eq ['php_buildpack'] - expect(action).to eq app_apply_manifest_action - end - end - - context 'for a docker app' do - let(:app_model) { VCAP::CloudController::AppModel.make(:docker) } - let(:request_body) do - { 'applications' => - [{ 'name' => 'blah', 'buildpacks' => ['php_buildpack'] }] } - end - - it 'returns an error' do - post :apply_manifest, params: { guid: app_model.guid }, body: request_body.to_yaml, as: :yaml - - expect(response.status).to eq(422) - errors = parsed_body['errors'] - expect(errors.size).to eq(1) - expect(errors.map { |h| h.reject { |k, _| k == 'test_mode_info' } }).to match_array([ - { - 'detail' => 'Buildpacks cannot be configured for a docker lifecycle app.', - 'title' => 'CF-UnprocessableEntity', - 'code' => 10008 - } - ]) - end - end - end - - context 'when the request body includes a stack' do - let(:request_body) do - { 'applications' => - [{ 'name' => 'blah', 'stack' => 'cflinuxfs3' }] } - end - - it 'sets the stack' do - post :apply_manifest, params: { guid: app_model.guid }, body: request_body.to_yaml, format: :yaml - - expect(response.status).to eq(202) - app_apply_manifest_jobs = Delayed::Job.where(Sequel.lit("handler like '%AppApplyManifest%'")) - expect(app_apply_manifest_jobs.count).to eq 1 - - expect(VCAP::CloudController::Jobs::AppApplyManifestActionJob).to have_received(:new) do |app_guid, message, action| - expect(app_guid).to eq app_model.guid - expect(message.stack).to eq 'cflinuxfs3' - expect(action).to eq app_apply_manifest_action - end - end - end - - context 'when the request body includes a command' do - let(:request_body) do - { 'applications' => - [{ 'name' => 'blah', 'command' => 'run-me.sh' }] } - end - - it 'sets the command' do - post :apply_manifest, params: { guid: app_model.guid }, body: request_body.to_yaml, as: :yaml - - expect(response.status).to eq(202) - app_apply_manifest_jobs = Delayed::Job.where(Sequel.lit("handler like '%AppApplyManifest%'")) - expect(app_apply_manifest_jobs.count).to eq 1 - - expect(VCAP::CloudController::Jobs::AppApplyManifestActionJob).to have_received(:new) do |app_guid, message, action| - expect(app_guid).to eq app_model.guid - expect(message.command).to eq 'run-me.sh' - expect(action).to eq app_apply_manifest_action - end - end - end - - context 'when the request body includes metadata' do - let(:request_body) do - { 'applications' => - [{ 'name' => 'blah', - 'metadata' => { - 'labels' => { - 'potato' => 'idaho', - 'myspace.com/songs' => 'missing', - }, - 'annotations' => { - 'potato' => 'yam', - 'juice' => 'newton', - }, - }, - }] }.to_yaml - end - - it 'applies the metadata' do - post :apply_manifest, body: request_body, params: { guid: app_model.guid } - - expect(response.status).to eq(202) - app_apply_manifest_jobs = Delayed::Job.where(Sequel.lit("handler like '%AppApplyManifest%'")) - expect(app_apply_manifest_jobs.count).to eq 1 - - expect(VCAP::CloudController::Jobs::AppApplyManifestActionJob).to have_received(:new) do |app_guid, message, action| - expect(app_guid).to eq app_model.guid - expect(message.app_update_message.labels).to eq({ - potato: 'idaho', - 'myspace.com/songs': 'missing' }) - expect(message.app_update_message.annotations).to eq({ - potato: 'yam', - juice: 'newton', }) - expect(action).to eq app_apply_manifest_action - end - end - end - - context 'when the request body includes a health-check-type' do - let(:request_body) do - { 'applications' => - [{ 'name' => 'blah', 'health-check-type' => 'process' }] } - end - - it 'sets the command' do - post :apply_manifest, params: { guid: app_model.guid }, body: request_body.to_yaml, as: :yaml - - expect(response.status).to eq(202) - app_apply_manifest_jobs = Delayed::Job.where(Sequel.lit("handler like '%AppApplyManifest%'")) - expect(app_apply_manifest_jobs.count).to eq 1 - - expect(VCAP::CloudController::Jobs::AppApplyManifestActionJob).to have_received(:new) do |app_guid, message, action| - expect(app_guid).to eq app_model.guid - expect(message.health_check_type).to eq 'process' - expect(action).to eq app_apply_manifest_action - end - end - end - - context 'when the request body includes a health-check-http-endpoint' do - let(:request_body) do - { 'applications' => - [{ 'name' => 'blah', 'health-check-http-endpoint' => '/health' }] } - end - - it 'sets the command' do - post :apply_manifest, params: { guid: app_model.guid }, body: request_body.to_yaml, as: :yaml - - expect(response.status).to eq(202) - app_apply_manifest_jobs = Delayed::Job.where(Sequel.lit("handler like '%AppApplyManifest%'")) - expect(app_apply_manifest_jobs.count).to eq 1 - - expect(VCAP::CloudController::Jobs::AppApplyManifestActionJob).to have_received(:new) do |app_guid, message, action| - expect(app_guid).to eq app_model.guid - expect(message.health_check_http_endpoint).to eq '/health' - expect(action).to eq app_apply_manifest_action - end - end - end - - context 'when the request body includes a health-check-invocation-timeout' do - let(:request_body) do - { 'applications' => - [{ 'name' => 'blah', 'health-check-invocation-timeout' => 55 }] } - end - - it 'sets the command' do - post :apply_manifest, params: { guid: app_model.guid }, body: request_body.to_yaml, as: :yaml - - expect(response.status).to eq(202) - app_apply_manifest_jobs = Delayed::Job.where(Sequel.lit("handler like '%AppApplyManifest%'")) - expect(app_apply_manifest_jobs.count).to eq 1 - - expect(VCAP::CloudController::Jobs::AppApplyManifestActionJob).to have_received(:new) do |app_guid, message, action| - expect(app_guid).to eq app_model.guid - expect(message.health_check_invocation_timeout).to eq 55 - expect(action).to eq app_apply_manifest_action - end - end - end - - context 'when the request body includes a timeout' do - let(:request_body) do - { 'applications' => - [{ 'name' => 'blah', 'timeout' => 9001 }] } - end - - it 'sets the command' do - post :apply_manifest, params: { guid: app_model.guid }, body: request_body.to_yaml, as: :yaml - - expect(response.status).to eq(202) - app_apply_manifest_jobs = Delayed::Job.where(Sequel.lit("handler like '%AppApplyManifest%'")) - expect(app_apply_manifest_jobs.count).to eq 1 - - expect(VCAP::CloudController::Jobs::AppApplyManifestActionJob).to have_received(:new) do |app_guid, message, action| - expect(app_guid).to eq app_model.guid - expect(message.timeout).to eq 9001 - expect(action).to eq app_apply_manifest_action - end - end - end - - context 'when the request body includes an environment variable' do - let(:request_body) do - { 'applications' => - [{ 'name' => 'blah', 'env' => { 'KEY100' => 'banana' } }] } - end - - it 'sets the environment' do - post :apply_manifest, params: { guid: app_model.guid }, body: request_body.to_yaml, as: :yaml - - expect(response.status).to eq(202) - app_apply_manifest_jobs = Delayed::Job.where(Sequel.lit("handler like '%AppApplyManifest%'")) - expect(app_apply_manifest_jobs.count).to eq 1 - - expect(VCAP::CloudController::Jobs::AppApplyManifestActionJob).to have_received(:new) do |app_guid, message, action| - expect(app_guid).to eq app_model.guid - expect(message.env).to eq({ KEY100: 'banana' }) - expect(action).to eq app_apply_manifest_action - end - end - end - - context 'when the request body includes a valid route' do - let(:request_body) do - { 'applications' => - [{ 'name' => 'blah', 'routes' => [{ 'route' => 'potato.yolo.io' }] }] } - end - - it 'sets the route' do - post :apply_manifest, params: { guid: app_model.guid }, body: request_body.to_yaml, as: :yaml - - expect(response.status).to eq(202) - app_apply_manifest_jobs = Delayed::Job.where(Sequel.lit("handler like '%AppApplyManifest%'")) - expect(app_apply_manifest_jobs.count).to eq 1 - - expect(VCAP::CloudController::Jobs::AppApplyManifestActionJob).to have_received(:new) do |app_guid, message, action| - expect(app_guid).to eq app_model.guid - expect(message.routes).to eq([{ route: 'potato.yolo.io' }]) - expect(action).to eq app_apply_manifest_action - end - end - end - - context 'when the request body includes service-bindings as an array of strings' do - let(:service_binder) { instance_double(ServiceBindingCreate, :create) } - let(:request_body) do - { 'applications' => - [{ 'name' => 'blah', 'services' => %w/shell exxon/ }] - } - end - - it 'binds the named services to the app' do - post :apply_manifest, params: { guid: app_model.guid }, body: request_body.to_yaml, as: :yaml - - expect(response.status).to eq(202) - app_apply_manifest_jobs = Delayed::Job.where(Sequel.lit("handler like '%AppApplyManifest%'")) - expect(app_apply_manifest_jobs.count).to eq 1 - - expect(VCAP::CloudController::Jobs::AppApplyManifestActionJob).to have_received(:new) do |app_guid, message, action| - expect(app_guid).to eq app_model.guid - expect(message.services).to match_array(%w/shell exxon/) - expect(action).to eq app_apply_manifest_action - end - end - end - - context 'when the request body includes service-bindings as an array of hashes' do - let(:service_binder) { instance_double(ServiceBindingCreate, :create) } - let(:request_body) do - { 'applications' => - [{ 'name' => 'blah', 'services' => [ - { - 'name' => 'has_parameters', - 'parameters' => { - 'foo' => 'bar' - } - }, - { - 'name' => 'no_parameters' - } - ] }] - } - end - - it 'binds the named services to the app' do - post :apply_manifest, params: { guid: app_model.guid }, body: request_body.to_yaml, as: :yaml - - expect(response.status).to eq(202) - app_apply_manifest_jobs = Delayed::Job.where(Sequel.lit("handler like '%AppApplyManifest%'")) - expect(app_apply_manifest_jobs.count).to eq 1 - - expect(VCAP::CloudController::Jobs::AppApplyManifestActionJob).to have_received(:new) do |app_guid, message, action| - expect(app_guid).to eq app_model.guid - expect(message.services).to match_array([ - { name: 'has_parameters', parameters: { foo: 'bar' } }, - { name: 'no_parameters' } - ]) - expect(action).to eq app_apply_manifest_action - end - end - end - - context 'when the request body contains a process' do - let(:request_body) do - { 'applications' => - [{ 'name' => 'blah', - 'processes' => [{ 'type' => 'worker', 'instances' => '2', 'command' => 'echo hi' }] - }] - } - end - it 'sets the instances' do - post :apply_manifest, params: { guid: app_model.guid }, body: request_body.to_yaml, as: :yaml - expect(response.status).to eq(202) - app_apply_manifest_jobs = Delayed::Job.where(Sequel.lit("handler like '%AppApplyManifest%'")) - expect(app_apply_manifest_jobs.count).to eq 1 - - expect(VCAP::CloudController::Jobs::AppApplyManifestActionJob).to have_received(:new) do |app_guid, message, action| - expect(app_guid).to eq app_model.guid - expect(message.processes).to include(instances: '2', command: 'echo hi', type: 'worker') - expect(action).to eq app_apply_manifest_action - end - end - end - - it 'successfully scales the app in a background job' do - post :apply_manifest, params: { guid: app_model.guid }, body: request_body.to_yaml, as: :yaml - - expect(response.status).to eq(202) - app_apply_manifest_jobs = Delayed::Job.where(Sequel.lit("handler like '%AppApplyManifest%'")) - expect(app_apply_manifest_jobs.count).to eq 1 - - expect(VCAP::CloudController::Jobs::AppApplyManifestActionJob).to have_received(:new) do |app_guid, message, action| - expect(app_guid).to eq app_model.guid - expect(message.instances).to eq 2 - expect(action).to eq app_apply_manifest_action - end - end - - it 'creates a job to track the applying the app manifest and returns it in the location header' do - set_current_user_as_role(role: 'admin', org: org, space: space, user: user) - - expect { - post :apply_manifest, params: { guid: app_model.guid }, body: request_body.to_yaml, as: :yaml - }.to change { - VCAP::CloudController::PollableJobModel.count - }.by(1) - - job = VCAP::CloudController::PollableJobModel.last - enqueued_job = Delayed::Job.last - expect(job.delayed_job_guid).to eq(enqueued_job.guid) - expect(job.operation).to eq('app.apply_manifest') - expect(job.state).to eq('PROCESSING') - expect(job.resource_guid).to eq(app_model.guid) - expect(job.resource_type).to eq('app') - - expect(response.status).to eq(202) - expect(response.headers['Location']).to include "#{link_prefix}/v3/jobs/#{job.guid}" - end - - describe 'emitting an audit event' do - let(:app_event_repository) { instance_double(VCAP::CloudController::Repositories::AppEventRepository) } - let(:request_body) do - { 'applications' => [{ 'name' => 'blah', 'buildpacks' => ['ruby_buildpack', 'go_buildpack'] }] } - end - - before do - allow(VCAP::CloudController::Repositories::AppEventRepository). - to receive(:new).and_return(app_event_repository) - allow(app_event_repository).to receive(:record_app_apply_manifest) - end - - it 'emits an "App Apply Manifest" audit event' do - post :apply_manifest, params: { guid: app_model.guid }, body: request_body.to_yaml, as: :yaml - - expect(app_event_repository).to have_received(:record_app_apply_manifest). - with(app_model, app_model.space, instance_of(VCAP::CloudController::UserAuditInfo), request_body.to_yaml) - end - end - end - describe '#show' do let(:app_model) { VCAP::CloudController::AppModel.make } let(:space) { app_model.space } diff --git a/spec/unit/jobs/app_apply_manifest_action_job_spec.rb b/spec/unit/jobs/app_apply_manifest_action_job_spec.rb deleted file mode 100644 index 208db5821bd..00000000000 --- a/spec/unit/jobs/app_apply_manifest_action_job_spec.rb +++ /dev/null @@ -1,147 +0,0 @@ -require 'spec_helper' - -module VCAP::CloudController - module Jobs - RSpec.describe AppApplyManifestActionJob, job_context: :worker do - let(:user) { User.make(admin: true) } - let(:apply_manifest_action) { instance_double(AppApplyManifest) } - let(:app) { AppModel.make(name: Sham.guid) } - let(:parsed_app_manifest) { AppManifestMessage.create_from_yml({ name: 'blah', instances: 4, routes: [{ route: 'foo.example.com' }] }) } - subject(:job) { AppApplyManifestActionJob.new(app.guid, parsed_app_manifest, apply_manifest_action) } - - before do - allow(apply_manifest_action).to receive(:apply).and_return([]) - end - - it { is_expected.to be_a_valid_job } - - it 'knows its job name' do - expect(job.job_name_in_configuration).to equal(:apply_manifest_job) - end - - it 'calls the apply action' do - job.perform - - expect(apply_manifest_action).to have_received(:apply).with(app.guid, parsed_app_manifest) - end - - context 'when the apply manifest action fails' do - before do - allow(apply_manifest_action).to receive(:apply).and_raise(StandardError) - end - - it 'bubbles up the error' do - expect { job.perform }.to raise_error(StandardError) - end - end - - context 'when an AppApplyManifest::NoDefaultDomain error occurs' do - it 'wraps the error in an ApiError' do - allow(apply_manifest_action).to receive(:apply).and_raise(AppApplyManifest::NoDefaultDomain, 'some sub-action failed!') - expect { - job.perform - }.to raise_error(CloudController::Errors::ApiError, /some sub-action failed!/) - end - end - - context 'when an AppUpdate::InvalidApp error occurs' do - it 'wraps the error in an ApiError' do - allow(apply_manifest_action).to receive(:apply).and_raise(AppUpdate::InvalidApp, 'Specified unknown buildpack name') - expect { - job.perform - }.to raise_error(CloudController::Errors::ApiError, /unknown buildpack name/) - end - end - - context 'when a ProcessCreate::InvalidProcess error occurs' do - it 'wraps the error in an ApiError' do - allow(apply_manifest_action).to receive(:apply).and_raise(ProcessCreate::InvalidProcess, 'Invalid health check type') - expect { - job.perform - }.to raise_error(CloudController::Errors::ApiError, /Invalid health check type/) - end - end - - context 'when a ProcessUpdate::InvalidProcess error occurs' do - it 'wraps the error in an ApiError' do - allow(apply_manifest_action).to receive(:apply).and_raise(ProcessUpdate::InvalidProcess, 'Invalid health check type') - expect { - job.perform - }.to raise_error(CloudController::Errors::ApiError, /Invalid health check type/) - end - end - - context 'when a SidecarCreate::InvalidSidecar error occurs' do - it 'wraps the error in an ApiError' do - allow(apply_manifest_action).to receive(:apply).and_raise(SidecarCreate::InvalidSidecar, 'command presence, command is not present') - expect { - job.perform - }.to raise_error(CloudController::Errors::ApiError, /command presence, command is not present/) - end - end - - context 'when a SidecarUpdate::InvalidSidecar error occurs' do - it 'wraps the error in an ApiError' do - allow(apply_manifest_action).to receive(:apply).and_raise(SidecarUpdate::InvalidSidecar, 'Invalid field on sidecar') - expect { - job.perform - }.to raise_error(CloudController::Errors::ApiError, /Invalid field on sidecar/) - end - end - - context 'when an AppPatchEnvironmentVariables::InvalidApp error occurs' do - it 'wraps the error in an ApiError' do - allow(apply_manifest_action).to receive(:apply).and_raise(AppPatchEnvironmentVariables::InvalidApp, 'Invalid env varz') - expect { - job.perform - }.to raise_error(CloudController::Errors::ApiError, /Invalid env varz/) - end - end - - context 'when an ManifestRouteUpdate::InvalidRoute error occurs' do - it 'wraps the error in an ApiError' do - allow(apply_manifest_action).to receive(:apply).and_raise(ManifestRouteUpdate::InvalidRoute, 'Invalid route') - expect { - job.perform - }.to raise_error(CloudController::Errors::ApiError, /Invalid route/) - end - end - - context 'when a Route::InvalidOrganizationRelation error occurs' do - it 'wraps the error in an ApiError' do - allow(apply_manifest_action).to receive(:apply).and_raise(Route::InvalidOrganizationRelation, 'Organization cannot use domain hello.there') - expect { - job.perform - }.to raise_error(CloudController::Errors::ApiError, /Organization cannot use domain hello\.there/) - end - end - - context 'when an ServiceBindingCreate::InvalidServiceBinding error occurs' do - it 'wraps the error in an ApiError' do - allow(apply_manifest_action).to receive(:apply).and_raise(AppApplyManifest::ServiceBindingError, 'Invalid binding name') - expect { - job.perform - }.to raise_error(CloudController::Errors::ApiError, /Invalid binding name/) - end - end - - describe '#resource_type' do - it 'returns a display name' do - expect(job.resource_type).to eq('app') - end - end - - describe '#display_name' do - it 'returns a display name for this action' do - expect(job.display_name).to eq('app.apply_manifest') - end - end - - describe '#resource_guid' do - it 'returns the given app guid' do - expect(job.resource_guid).to eq(app.guid) - end - end - end - end -end diff --git a/spec/unit/jobs/space_apply_manifest_action_job_spec.rb b/spec/unit/jobs/space_apply_manifest_action_job_spec.rb index 066aabeee51..f761cf44137 100644 --- a/spec/unit/jobs/space_apply_manifest_action_job_spec.rb +++ b/spec/unit/jobs/space_apply_manifest_action_job_spec.rb @@ -11,8 +11,8 @@ module Jobs let(:app2) { AppModel.make(name: 'cut', space: space) } let(:app_guid_message_hash) do { - app1.guid => NamedAppManifestMessage.create_from_yml({ name: app1.name, instances: 4, routes: [{ route: 'foo.example.com' }] }), - app2.guid => NamedAppManifestMessage.create_from_yml({ name: app2.name, instances: 5 }), + app1.guid => AppManifestMessage.create_from_yml({ name: app1.name, instances: 4, routes: [{ route: 'foo.example.com' }] }), + app2.guid => AppManifestMessage.create_from_yml({ name: app2.name, instances: 5 }), } end diff --git a/spec/unit/messages/app_manifest_message_spec.rb b/spec/unit/messages/app_manifest_message_spec.rb index 8ca0787c525..775abd61e61 100644 --- a/spec/unit/messages/app_manifest_message_spec.rb +++ b/spec/unit/messages/app_manifest_message_spec.rb @@ -13,40 +13,52 @@ module VCAP::CloudController end end + context 'when name is not specified' do + let(:params_from_yaml) { { instances: 3, memory: '2G' } } + + it 'is not valid' do + message = AppManifestMessage.create_from_yml(params_from_yaml) + + expect(message).to_not be_valid + expect(message.errors).to have(2).items + expect(message.errors.full_messages[0]).to match(/^Name must not be empty/) + end + end + describe 'memory' do context 'when memory unit is not part of expected set of values' do - let(:params_from_yaml) { { memory: '200INVALID' } } + let(:params_from_yaml) { { name: 'eugene', memory: '200INVALID' } } it 'is not valid' do message = AppManifestMessage.create_from_yml(params_from_yaml) expect(message).not_to be_valid - expect(message.errors.count).to eq(1) + expect(message.errors).to have(1).items expect(message.errors.full_messages).to include( 'Process "web": Memory must use a supported unit: B, K, KB, M, MB, G, GB, T, or TB') end end context 'when memory is not a positive amount' do - let(:params_from_yaml) { { memory: '-1MB' } } + let(:params_from_yaml) { { name: 'eugene', memory: '-1MB' } } it 'is not valid' do message = AppManifestMessage.create_from_yml(params_from_yaml) expect(message).not_to be_valid - expect(message.errors.count).to eq(1) + expect(message.errors).to have(1).items expect(message.errors.full_messages).to include('Process "web": Memory must be greater than 0MB') end end context 'when memory is in bytes' do - let(:params_from_yaml) { { memory: '-35B' } } + let(:params_from_yaml) { { name: 'eugene', memory: '-35B' } } it 'is not valid' do message = AppManifestMessage.create_from_yml(params_from_yaml) expect(message).not_to be_valid - expect(message.errors.count).to eq(1) + expect(message.errors).to have(1).items expect(message.errors.full_messages).to include('Process "web": Memory must be greater than 0MB') end end @@ -54,13 +66,13 @@ module VCAP::CloudController describe 'disk_quota' do context 'when disk_quota unit is not part of expected set of values' do - let(:params_from_yaml) { { disk_quota: '200INVALID' } } + let(:params_from_yaml) { { name: 'eugene', disk_quota: '200INVALID' } } it 'is not valid' do message = AppManifestMessage.create_from_yml(params_from_yaml) expect(message).not_to be_valid - expect(message.errors.count).to eq(1) + expect(message.errors).to have(1).items expect(message.errors.full_messages).to include( 'Process "web": Disk quota must use a supported unit: B, K, KB, M, MB, G, GB, T, or TB' ) @@ -68,25 +80,25 @@ module VCAP::CloudController end context 'when disk_quota is not a positive amount' do - let(:params_from_yaml) { { disk_quota: '-1MB' } } + let(:params_from_yaml) { { name: 'eugene', disk_quota: '-1MB' } } it 'is not valid' do message = AppManifestMessage.create_from_yml(params_from_yaml) expect(message).not_to be_valid - expect(message.errors.count).to eq(1) + expect(message.errors).to have(1).items expect(message.errors.full_messages).to include('Process "web": Disk quota must be greater than 0MB') end end context 'when disk_quota is not numeric' do - let(:params_from_yaml) { { disk_quota: 'gerg herscheiser' } } + let(:params_from_yaml) { { name: 'eugene', disk_quota: 'gerg herscheiser' } } it 'is not valid' do message = AppManifestMessage.create_from_yml(params_from_yaml) expect(message).not_to be_valid - expect(message.errors.count).to eq(1) + expect(message.errors).to have(1).items expect(message.errors.full_messages).to include('Process "web": Disk quota is not a number') end end @@ -95,7 +107,7 @@ module VCAP::CloudController describe 'buildpack' do context 'when providing a valid buildpack name' do let(:buildpack) { Buildpack.make } - let(:params_from_yaml) { { buildpack: buildpack.name } } + let(:params_from_yaml) { { name: 'eugene', buildpack: buildpack.name } } it 'is valid' do message = AppManifestMessage.create_from_yml(params_from_yaml) @@ -105,37 +117,37 @@ module VCAP::CloudController end context 'when the buildpack is not a string' do - let(:params_from_yaml) { { buildpack: 99 } } + let(:params_from_yaml) { { name: 'eugene', buildpack: 99 } } it 'is not valid' do message = AppManifestMessage.create_from_yml(params_from_yaml) expect(message).not_to be_valid - expect(message.errors.count).to eq(1) + expect(message.errors).to have(1).items expect(message.errors.full_messages).to include('Buildpack must be a string') end end context 'when the buildpack has fewer than 0 characters' do - let(:params_from_yaml) { { buildpack: '' } } + let(:params_from_yaml) { { name: 'eugene', buildpack: '' } } it 'is not valid' do message = AppManifestMessage.create_from_yml(params_from_yaml) expect(message).not_to be_valid - expect(message.errors.count).to eq(1) + expect(message.errors).to have(1).items expect(message.errors.full_messages).to include('Buildpack must be between 1 and 4096 characters') end end context 'when the buildpack has more than 4096 characters' do - let(:params_from_yaml) { { buildpack: 'a' * 4097 } } + let(:params_from_yaml) { { name: 'eugene', buildpack: 'a' * 4097 } } it 'is not valid' do message = AppManifestMessage.create_from_yml(params_from_yaml) expect(message).not_to be_valid - expect(message.errors.count).to eq(1) + expect(message.errors).to have(1).items expect(message.errors.full_messages).to include('Buildpack must be between 1 and 4096 characters') end end @@ -145,7 +157,7 @@ module VCAP::CloudController context 'when providing valid buildpack names' do let(:buildpack) { Buildpack.make } let(:buildpack2) { Buildpack.make } - let(:params_from_yaml) { { buildpacks: [buildpack.name, buildpack2.name] } } + let(:params_from_yaml) { { name: 'eugene', buildpacks: [buildpack.name, buildpack2.name] } } it 'is valid' do message = AppManifestMessage.create_from_yml(params_from_yaml) @@ -156,33 +168,33 @@ module VCAP::CloudController context 'when one of the buildpacks is not a string' do let(:buildpack) { Buildpack.make } - let(:params_from_yaml) { { buildpacks: [buildpack.name, 99] } } + let(:params_from_yaml) { { name: 'eugene', buildpacks: [buildpack.name, 99] } } it 'is not valid' do message = AppManifestMessage.create_from_yml(params_from_yaml) expect(message).not_to be_valid - expect(message.errors.count).to eq(1) + expect(message.errors).to have(1).items expect(message.errors.full_messages).to include('Buildpacks can only contain strings') end end context 'when both buildpack and buildpacks are requested' do let(:buildpack) { Buildpack.make } - let(:params_from_yaml) { { buildpacks: [buildpack.name], buildpack: 'some-buildpack' } } + let(:params_from_yaml) { { name: 'eugene', buildpacks: [buildpack.name], buildpack: 'some-buildpack' } } it 'is not valid' do message = AppManifestMessage.create_from_yml(params_from_yaml) expect(message).not_to be_valid - expect(message.errors.count).to eq(1) + expect(message.errors).to have(1).items expect(message.errors.full_messages).to include('Buildpack and Buildpacks fields cannot be used together.') end end end describe 'docker' do - let(:params_from_yaml) { { docker: { image: 'my/image' } } } + let(:params_from_yaml) { { name: 'eugene', docker: { image: 'my/image' } } } context 'when docker is enabled' do before do @@ -205,7 +217,7 @@ module VCAP::CloudController message = AppManifestMessage.create_from_yml(params_from_yaml) expect(message).not_to be_valid - expect(message.errors.count).to eq(1) + expect(message.errors).to have(1).items expect(message.errors.full_messages).to include('Feature Disabled: I am a banana') end end @@ -213,7 +225,7 @@ module VCAP::CloudController describe 'stack' do context 'when providing a valid stack name' do - let(:params_from_yaml) { { stack: 'cflinuxfs3' } } + let(:params_from_yaml) { { name: 'eugene', stack: 'cflinuxfs3' } } it 'is valid' do message = AppManifestMessage.create_from_yml(params_from_yaml) @@ -224,13 +236,13 @@ module VCAP::CloudController end context 'when the stack is not a string' do - let(:params_from_yaml) { { stack: 99 } } + let(:params_from_yaml) { { name: 'eugene', stack: 99 } } it 'is not valid' do message = AppManifestMessage.create_from_yml(params_from_yaml) expect(message).not_to be_valid - expect(message.errors.count).to eq(1) + expect(message.errors).to have(1).items expect(message.errors.full_messages).to include('Stack must be a string') end end @@ -238,37 +250,37 @@ module VCAP::CloudController describe 'instances' do context 'when instances is not an number' do - let(:params_from_yaml) { { instances: 'silly string thing' } } + let(:params_from_yaml) { { name: 'eugene', instances: 'silly string thing' } } it 'is not valid' do message = AppManifestMessage.create_from_yml(params_from_yaml) expect(message).not_to be_valid - expect(message.errors.count).to eq(1) + expect(message.errors).to have(1).items expect(message.errors.full_messages).to include('Process "web": Instances is not a number') end end context 'when instances is not an integer' do - let(:params_from_yaml) { { instances: 3.5 } } + let(:params_from_yaml) { { name: 'eugene', instances: 3.5 } } it 'is not valid' do message = AppManifestMessage.create_from_yml(params_from_yaml) expect(message).not_to be_valid - expect(message.errors.count).to eq(1) + expect(message.errors).to have(1).items expect(message.errors.full_messages).to include('Process "web": Instances must be an integer') end end context 'when instances is not a positive integer' do - let(:params_from_yaml) { { instances: -1 } } + let(:params_from_yaml) { { name: 'eugene', instances: -1 } } it 'is not valid' do message = AppManifestMessage.create_from_yml(params_from_yaml) expect(message).not_to be_valid - expect(message.errors.count).to eq(1) + expect(message.errors).to have(1).items expect(message.errors.full_messages).to include('Process "web": Instances must be greater than or equal to 0') end end @@ -278,13 +290,14 @@ module VCAP::CloudController context 'when env is not an object' do let(:params_from_yaml) do { + name: 'eugene', env: 'im a non-hash' } end it 'is not valid' do message = AppManifestMessage.create_from_yml(params_from_yaml) expect(message).to_not be_valid - expect(message.errors.count).to eq(1) + expect(message.errors).to have(1).items expect(message.errors.full_messages).to include('Env must be an object of keys and values') end end @@ -292,6 +305,7 @@ module VCAP::CloudController context 'when env has bad keys' do let(:params_from_yaml) do { + name: 'eugene', env: { "": 'null-key', VCAP_BAD_KEY: 1, @@ -303,7 +317,7 @@ module VCAP::CloudController it 'is not valid' do message = AppManifestMessage.create_from_yml(params_from_yaml) expect(message).to_not be_valid - expect(message.errors.count).to eq(4) + expect(message.errors).to have(4).items expect(message.errors.full_messages).to match_array([ 'Env cannot set PORT', 'Env cannot start with VCAP_', @@ -316,8 +330,9 @@ module VCAP::CloudController describe 'routes' do context 'when all routes are valid' do let(:params_from_yaml) do - { routes: - [ + { + name: 'eugene', + routes: [ { route: 'existing.example.com' }, { route: 'new.example.com' }, ] @@ -333,7 +348,9 @@ module VCAP::CloudController context 'when a route uri is invalid' do let(:params_from_yaml) do - { routes: + { + name: 'eugene', + routes: [ { route: 'blah' }, { route: 'anotherblah' }, @@ -351,7 +368,7 @@ module VCAP::CloudController end context 'when routes are malformed' do - let(:params_from_yaml) { { routes: ['blah'] } } + let(:params_from_yaml) { { name: 'eugene', routes: ['blah'] } } it 'is not valid' do message = AppManifestMessage.create_from_yml(params_from_yaml) @@ -362,7 +379,7 @@ module VCAP::CloudController end context 'when no-route is specified' do - let(:params_from_yaml) { { 'no-route' => no_route_val } } + let(:params_from_yaml) { { name: 'eugene', 'no-route' => no_route_val } } context 'when no-route is true' do let(:no_route_val) { true } @@ -386,6 +403,7 @@ module VCAP::CloudController context 'when no-route is true and routes are specified' do let(:params_from_yaml) do { + name: 'eugene', no_route: true, routes: [ @@ -402,7 +420,7 @@ module VCAP::CloudController end context 'when random_route is specified' do - let(:params_from_yaml) { { 'random_route' => random_route_val } } + let(:params_from_yaml) { { name: 'eugene', 'random_route' => random_route_val } } context 'when random_route is true' do let(:random_route_val) { true } @@ -426,6 +444,7 @@ module VCAP::CloudController context 'when random_route is true and routes are specified' do let(:params_from_yaml) do { + name: 'eugene', random_route: true, routes: [ @@ -442,7 +461,7 @@ module VCAP::CloudController end context 'when default_route is specified' do - let(:params_from_yaml) { { 'default_route' => default_route_val } } + let(:params_from_yaml) { { name: 'eugene', 'default_route' => default_route_val } } context 'when default_route is true' do let(:default_route_val) { true } @@ -466,6 +485,7 @@ module VCAP::CloudController context 'when default_route is true and routes are specified' do let(:params_from_yaml) do { + name: 'eugene', default_route: true, routes: [ @@ -486,6 +506,7 @@ module VCAP::CloudController context 'when services is not an array' do let(:params_from_yaml) do { + name: 'eugene', services: 'string' } end @@ -493,7 +514,7 @@ module VCAP::CloudController it 'is not valid' do message = AppManifestMessage.create_from_yml(params_from_yaml) expect(message).to_not be_valid - expect(message.errors.count).to eq(1) + expect(message.errors).to have(1).items expect(message.errors.full_messages).to include('Services must be a list of service instances') end end @@ -501,45 +522,45 @@ module VCAP::CloudController describe 'processes' do context 'when processes is not an array' do - let(:params_from_yaml) { { processes: 'string' } } + let(:params_from_yaml) { { name: 'eugene', processes: 'string' } } it 'is not valid' do message = AppManifestMessage.create_from_yml(params_from_yaml) expect(message).to_not be_valid - expect(message.errors.count).to eq(1) + expect(message.errors).to have(1).items expect(message.errors.full_messages).to include('Processes must be an array of process configurations') end end context 'when any process does not have a type' do - let(:params_from_yaml) { { processes: [{ 'instances' => 3 }] } } + let(:params_from_yaml) { { name: 'eugene', processes: [{ 'instances' => 3 }] } } it 'is not valid' do message = AppManifestMessage.create_from_yml(params_from_yaml) expect(message).to_not be_valid - expect(message.errors.count).to eq(1) + expect(message.errors).to have(1).items expect(message.errors.full_messages).to include('All Processes must specify a type') end end context 'when any process has a blank type' do - let(:params_from_yaml) { { processes: [{ 'type' => '', 'instances' => 3 }, { 'type' => nil, 'instances' => 2 }] } } + let(:params_from_yaml) { { name: 'eugene', processes: [{ 'type' => '', 'instances' => 3 }, { 'type' => nil, 'instances' => 2 }] } } it 'is not valid' do message = AppManifestMessage.create_from_yml(params_from_yaml) expect(message).to_not be_valid - expect(message.errors.count).to eq(1) + expect(message.errors).to have(1).items expect(message.errors.full_messages).to include('All Processes must specify a type') end end context 'when any process fails validation' do - let(:params_from_yaml) { { processes: [{ 'type' => 'totally-a-type', 'instances' => -1, 'timeout' => -5 }] } } + let(:params_from_yaml) { { name: 'eugene', processes: [{ 'type' => 'totally-a-type', 'instances' => -1, 'timeout' => -5 }] } } it 'has the type of the process in the error message' do message = AppManifestMessage.create_from_yml(params_from_yaml) expect(message).to_not be_valid - expect(message.errors.count).to eq(2) + expect(message.errors).to have(2).items expect(message.errors.full_messages).to include('Process "totally-a-type": Instances must be greater than or equal to 0') expect(message.errors.full_messages).to include('Process "totally-a-type": Timeout must be greater than or equal to 1') end @@ -572,13 +593,16 @@ module VCAP::CloudController } end let(:params_from_yaml) do - { processes: [process1, process2] } + { + name: 'eugene', + processes: [process1, process2] + } end it 'includes the type of the process in the error message' do message = AppManifestMessage.create_from_yml(params_from_yaml) expect(message).to_not be_valid - expect(message.errors.count).to eq(16) + expect(message.errors).to have(16).items expect(message.errors.full_messages).to match_array([ 'Process "type1": Command must be between 1 and 4096 characters', 'Process "type1": Disk quota must use a supported unit: B, K, KB, M, MB, G, GB, T, or TB', @@ -602,15 +626,15 @@ module VCAP::CloudController end context 'when there is more than one process with the same type' do - let(:params_from_yaml) { { processes: [{ 'type' => 'foo', 'instances' => 3 }, { 'type' => 'foo', 'instances' => 1 }, - { 'type' => 'bob', 'instances' => 5 }, { 'type' => 'bob', 'instances' => 1 } + let(:params_from_yaml) { { name: 'eugene', processes: [{ 'type' => 'foo', 'instances' => 3 }, { 'type' => 'foo', 'instances' => 1 }, + { 'type' => 'bob', 'instances' => 5 }, { 'type' => 'bob', 'instances' => 1 } ] } } it 'is not valid' do message = AppManifestMessage.create_from_yml(params_from_yaml) expect(message).to_not be_valid - expect(message.errors.count).to eq(2) + expect(message.errors).to have(2).items expect(message.errors.full_messages).to include('Process "foo" may only be present once') expect(message.errors.full_messages).to include('Process "bob" may only be present once') end @@ -619,12 +643,12 @@ module VCAP::CloudController describe 'sidecars' do context 'when sidecars is not an array' do - let(:params_from_yaml) { { sidecars: 'string' } } + let(:params_from_yaml) { { name: 'eugene', sidecars: 'string' } } it 'is not valid' do message = AppManifestMessage.create_from_yml(params_from_yaml) expect(message).not_to be_valid - expect(message.errors.count).to eq(1) + expect(message.errors).to have(1).items expect(message.errors.full_messages).to include('Sidecars must be an array of sidecar configurations') end end @@ -632,6 +656,7 @@ module VCAP::CloudController context 'when sidecars name is empty string' do let(:params_from_yaml) do { + name: 'eugene', sidecars: [{ name: '', command: 'rackup', process_types: ['web'] }] } end @@ -639,7 +664,7 @@ module VCAP::CloudController it 'is not valid' do message = AppManifestMessage.create_from_yml(params_from_yaml) expect(message).not_to be_valid - expect(message.errors.count).to eq(1) + expect(message.errors).to have(1).items expect(message.errors.full_messages).to include('Sidecar name can\'t be blank') end end @@ -647,6 +672,7 @@ module VCAP::CloudController context 'when sidecars command is empty string' do let(:params_from_yaml) do { + name: 'eugene', sidecars: [{ name: 'my_sidecar', command: '', process_types: ['web'] }] } end @@ -654,7 +680,7 @@ module VCAP::CloudController it 'is not valid' do message = AppManifestMessage.create_from_yml(params_from_yaml) expect(message).not_to be_valid - expect(message.errors.count).to eq(1) + expect(message.errors).to have(1).items expect(message.errors.full_messages).to include('Sidecar "my_sidecar": Command can\'t be blank') end end @@ -662,6 +688,7 @@ module VCAP::CloudController context 'when sidecars name is not supplied' do let(:params_from_yaml) do { + name: 'eugene', sidecars: [{ command: 'rackup', process_types: ['web'] }] } end @@ -669,7 +696,7 @@ module VCAP::CloudController it 'is not valid' do message = AppManifestMessage.create_from_yml(params_from_yaml) expect(message).not_to be_valid - expect(message.errors.count).to eq(2) + expect(message.errors).to have(2).items expect(message.errors.full_messages).to include('Sidecar name can\'t be blank') expect(message.errors.full_messages).to include('Sidecar name must be a string') end @@ -678,6 +705,7 @@ module VCAP::CloudController context 'when sidecars memory is not numeric' do let(:params_from_yaml) do { + name: 'eugene', sidecars: [{ command: 'rackup', process_types: ['web'], name: 'sylvester', memory: 'selective' }] } end @@ -685,7 +713,7 @@ module VCAP::CloudController it 'is not valid' do message = AppManifestMessage.create_from_yml(params_from_yaml) expect(message).not_to be_valid - expect(message.errors.count).to eq(1) + expect(message.errors).to have(1).items expect(message.errors.full_messages).to include('Sidecar "sylvester": Memory in mb is not a number') end end @@ -693,6 +721,7 @@ module VCAP::CloudController context 'when the sidecars are valid' do let(:params_from_yaml) do { + name: 'eugene', sidecars: [{ command: 'rackup', process_types: ['web'], name: 'sylvester', memory: '38M' }, { command: 'rackup', process_types: ['web'], name: 'cookie', memory: '2G' }, ] @@ -711,13 +740,14 @@ module VCAP::CloudController context 'when metadata is not an object' do let(:params_from_yaml) do { + name: 'eugene', metadata: 'im a non-hash' } end it 'is not valid' do message = AppManifestMessage.create_from_yml(params_from_yaml) expect(message).to_not be_valid - expect(message.errors.count).to eq(1) + expect(message.errors).to have(1).items expect(message.errors.full_messages).to include('Metadata must be an object') end end @@ -725,6 +755,7 @@ module VCAP::CloudController context 'when metadata.labels is not an object' do let(:params_from_yaml) do { + name: 'eugene', metadata: { labels: 'im a non-hash' } @@ -733,7 +764,7 @@ module VCAP::CloudController it 'is not valid' do message = AppManifestMessage.create_from_yml(params_from_yaml) expect(message).to_not be_valid - expect(message.errors.count).to eq(1) + expect(message.errors).to have(1).items expect(message.errors_on(:metadata)).to include("'labels' is not an object") end end @@ -741,6 +772,7 @@ module VCAP::CloudController context 'when metadata.labels has invalid keys' do let(:params_from_yaml) do { + name: 'eugene', metadata: { labels: { nil => 'value' } } @@ -749,7 +781,7 @@ module VCAP::CloudController it 'is not valid' do message = AppManifestMessage.create_from_yml(params_from_yaml) expect(message).to_not be_valid - expect(message.errors.count).to eq(1) + expect(message.errors).to have(1).items expect(message.errors_on(:metadata)).to match_array([ 'label key error: key cannot be empty string', ]) @@ -759,6 +791,7 @@ module VCAP::CloudController context 'when metadata.labels has invalid values' do let(:params_from_yaml) do { + name: 'eugene', metadata: { labels: { 'k1' => 'no spaces or ! allowed', @@ -769,7 +802,7 @@ module VCAP::CloudController it 'is not valid' do message = AppManifestMessage.create_from_yml(params_from_yaml) expect(message).to_not be_valid - expect(message.errors.count).to eq(1) + expect(message.errors).to have(1).items expect(message.errors_on(:metadata)).to match_array([ "label value error: 'no spaces or ! allowed' contains invalid characters", ]) @@ -779,6 +812,7 @@ module VCAP::CloudController context 'when metadata.annotations is not an object' do let(:params_from_yaml) do { + name: 'eugene', metadata: { annotations: 'im a non-hash' } @@ -787,7 +821,7 @@ module VCAP::CloudController it 'is not valid' do message = AppManifestMessage.create_from_yml(params_from_yaml) expect(message).to_not be_valid - expect(message.errors.count).to eq(1) + expect(message.errors).to have(1).items expect(message.errors_on(:metadata)).to include("'annotations' is not an object") end end @@ -795,6 +829,7 @@ module VCAP::CloudController context 'when metadata.annotations has invalid keys' do let(:params_from_yaml) do { + name: 'eugene', metadata: { annotations: { 'x' * 1000 => 'value' } } @@ -803,7 +838,7 @@ module VCAP::CloudController it 'is not valid' do message = AppManifestMessage.create_from_yml(params_from_yaml) expect(message).to_not be_valid - expect(message.errors.count).to eq(1) + expect(message.errors).to have(1).items expect(message.errors_on(:metadata)).to match_array([ "annotation key error: 'xxxxxxxx...' is greater than 63 characters", ]) @@ -813,6 +848,7 @@ module VCAP::CloudController context 'when metadata.annotations has invalid values' do let(:params_from_yaml) do { + name: 'eugene', metadata: { annotations: { 'too-large-value' => 'oversize-' + 'x' * 5000 @@ -823,7 +859,7 @@ module VCAP::CloudController it 'is not valid' do message = AppManifestMessage.create_from_yml(params_from_yaml) expect(message).to_not be_valid - expect(message.errors.count).to eq(1) + expect(message.errors).to have(1).items expect(message.errors_on(:metadata)).to match_array([ "annotation value error: 'oversize...' is greater than 5000 characters" ]) @@ -838,13 +874,13 @@ module VCAP::CloudController end let(:buildpack) { Buildpack.make } - let(:params_from_yaml) { { buildpack: buildpack.name, docker: { image: 'my/image' } } } + let(:params_from_yaml) { { name: 'eugene', buildpack: buildpack.name, docker: { image: 'my/image' } } } it 'is not valid' do message = AppManifestMessage.create_from_yml(params_from_yaml) expect(message).not_to be_valid - expect(message.errors.count).to eq(1) + expect(message.errors).to have(1).items expect(message.errors.full_messages).to include('Cannot specify both buildpack(s) and docker keys') end end @@ -856,13 +892,13 @@ module VCAP::CloudController let(:buildpack) { Buildpack.make } let(:buildpack2) { Buildpack.make } - let(:params_from_yaml) { { buildpacks: [buildpack.name, buildpack2.name], docker: { image: 'my/image' } } } + let(:params_from_yaml) { { name: 'eugene', buildpacks: [buildpack.name, buildpack2.name], docker: { image: 'my/image' } } } it 'is not valid' do message = AppManifestMessage.create_from_yml(params_from_yaml) expect(message).not_to be_valid - expect(message.errors.count).to eq(1) + expect(message.errors).to have(1).items expect(message.errors.full_messages).to include('Cannot specify both buildpack(s) and docker keys') end end @@ -871,6 +907,7 @@ module VCAP::CloudController context 'when there are multiple errors' do let(:params_from_yaml) do { + name: 'eugene', instances: -1, memory: 120, disk_quota: '-120KB', @@ -930,10 +967,11 @@ module VCAP::CloudController context 'with processes' do let(:parsed_yaml) do - { name: 'blah', processes: [ - { type: 'web', 'health-check-type': 'port', disk_quota: '23M' }, - { type: 'worker', 'health-check-type': 'port', disk_quota: '23M' }, - ] } + { + 'name' => 'eugene', name: 'blah', processes: [ + { type: 'web', 'health-check-type': 'port', disk_quota: '23M' }, + { type: 'worker', 'health-check-type': 'port', disk_quota: '23M' }, + ] } end it 'converts the processes keys into snake case for all processes' do @@ -954,10 +992,11 @@ module VCAP::CloudController context 'with environment variables' do let(:parsed_yaml) do - { name: 'blah', env: { ':ENV_VAR' => 'hunter1' }, processes: [ - { type: 'web', env: { ':ENV_VAR' => 'hunter2' } }, - { type: 'worker', env: { ':ENV_VAR' => 'hunter3' } }, - ] } + { + 'name' => 'eugene', name: 'blah', env: { ':ENV_VAR' => 'hunter1' }, processes: [ + { type: 'web', env: { ':ENV_VAR' => 'hunter2' } }, + { type: 'worker', env: { ':ENV_VAR' => 'hunter3' } }, + ] } end it 'does NOT try to underscore them (so they do NOT get lowercased)' do @@ -977,10 +1016,11 @@ module VCAP::CloudController context 'with services' do let(:parsed_yaml) do - { name: 'blah', services: ['hadoop'], processes: [ - { type: 'web', services: ['greenplumbdb'] }, - { type: 'worker', services: ['riak'] }, - ] } + { + 'name' => 'eugene', name: 'blah', services: ['hadoop'], processes: [ + { type: 'web', services: ['greenplumbdb'] }, + { type: 'worker', services: ['riak'] }, + ] } end it 'does NOT try to underscore the service names (they are strings not hashes)' do @@ -1129,7 +1169,7 @@ module VCAP::CloudController describe '#manifest_process_scale_messages' do context 'from app-level attributes' do - let(:parsed_yaml) { { 'disk_quota' => '1000GB', 'memory' => '200GB', instances: 5 } } + let(:parsed_yaml) { { 'name' => 'eugene', 'disk_quota' => '1000GB', 'memory' => '200GB', instances: 5 } } it 'returns a ManifestProcessScaleMessage containing mapped attributes' do message = AppManifestMessage.create_from_yml(parsed_yaml) @@ -1143,7 +1183,7 @@ module VCAP::CloudController end context 'it handles bytes' do - let(:parsed_yaml) { { 'disk_quota' => '7340032B', 'memory' => '3145728B', instances: 8 } } + let(:parsed_yaml) { { 'name' => 'eugene', 'disk_quota' => '7340032B', 'memory' => '3145728B', instances: 8 } } it 'returns a ManifestProcessScaleMessage containing mapped attributes' do message = AppManifestMessage.create_from_yml(parsed_yaml) @@ -1157,7 +1197,7 @@ module VCAP::CloudController end context 'it handles exactly 1MB' do - let(:parsed_yaml) { { 'disk_quota' => '1048576B', 'memory' => '1048576B', instances: 8 } } + let(:parsed_yaml) { { 'name' => 'eugene', 'disk_quota' => '1048576B', 'memory' => '1048576B', instances: 8 } } it 'returns a ManifestProcessScaleMessage containing mapped attributes' do message = AppManifestMessage.create_from_yml(parsed_yaml) @@ -1171,13 +1211,13 @@ module VCAP::CloudController end context 'it complains about 1MB - 1' do - let(:parsed_yaml) { { 'disk_quota' => '1048575B', 'memory' => '1048575B', instances: 8 } } + let(:parsed_yaml) { { 'name' => 'eugene', 'disk_quota' => '1048575B', 'memory' => '1048575B', instances: 8 } } it 'returns a ManifestProcessScaleMessage containing mapped attributes' do message = AppManifestMessage.create_from_yml(parsed_yaml) expect(message).not_to be_valid - expect(message.errors.count).to eq(2) + expect(message.errors).to have(2).items expect(message.errors.full_messages).to match_array([ 'Process "web": Memory must be greater than 0MB', 'Process "web": Disk quota must be greater than 0MB']) @@ -1196,7 +1236,7 @@ module VCAP::CloudController end context 'from nested process attributes' do - let(:parsed_yaml) { { 'processes' => [{ 'type' => 'web', 'disk_quota' => '1000GB', 'memory' => '200GB', instances: 5 }] } } + let(:parsed_yaml) { { 'name' => 'eugene', 'processes' => [{ 'type' => 'web', 'disk_quota' => '1000GB', 'memory' => '200GB', instances: 5 }] } } it 'returns a ManifestProcessScaleMessage containing mapped attributes' do message = AppManifestMessage.create_from_yml(parsed_yaml) @@ -1210,7 +1250,7 @@ module VCAP::CloudController end context 'it handles bytes' do - let(:parsed_yaml) { { 'processes' => [{ 'type' => 'web', 'disk_quota' => '7340032B', 'memory' => '3145728B', instances: 8 }] } } + let(:parsed_yaml) { { 'name' => 'eugene', 'processes' => [{ 'type' => 'web', 'disk_quota' => '7340032B', 'memory' => '3145728B', instances: 8 }] } } it 'returns a ManifestProcessScaleMessage containing mapped attributes' do message = AppManifestMessage.create_from_yml(parsed_yaml) @@ -1224,7 +1264,7 @@ module VCAP::CloudController end context 'it handles exactly 1MB' do - let(:parsed_yaml) { { 'processes' => [{ 'type' => 'web', 'disk_quota' => '1048576B', 'memory' => '1048576B', instances: 8 }] } } + let(:parsed_yaml) { { 'name' => 'eugene', 'processes' => [{ 'type' => 'web', 'disk_quota' => '1048576B', 'memory' => '1048576B', instances: 8 }] } } it 'returns a ManifestProcessScaleMessage containing mapped attributes' do message = AppManifestMessage.create_from_yml(parsed_yaml) @@ -1238,13 +1278,13 @@ module VCAP::CloudController end context 'it complains about 1MB - 1' do - let(:parsed_yaml) { { 'processes' => [{ 'type' => 'web', 'disk_quota' => '1048575B', 'memory' => '1048575B', instances: 8 }] } } + let(:parsed_yaml) { { 'name' => 'eugene', 'processes' => [{ 'type' => 'web', 'disk_quota' => '1048575B', 'memory' => '1048575B', instances: 8 }] } } it 'returns a ManifestProcessScaleMessage containing mapped attributes' do message = AppManifestMessage.create_from_yml(parsed_yaml) expect(message).not_to be_valid - expect(message.errors.count).to eq(2) + expect(message.errors).to have(2).items expect(message.errors.full_messages).to match_array([ 'Process "web": Memory must be greater than 0MB', 'Process "web": Disk quota must be greater than 0MB']) @@ -1253,8 +1293,10 @@ module VCAP::CloudController context 'when processes and app-level process properties are specified' do context 'there is a web process type on the process level' do - let(:parsed_yaml) { { 'memory' => '5GB', - instances: 1, + let(:parsed_yaml) { { + 'name' => 'eugene', + 'memory' => '5GB', + 'instances' => 1, 'disk_quota' => '30GB', 'processes' => [{ 'type' => 'web', 'disk_quota' => '1000GB', 'memory' => '200GB', instances: 5 }] } } @@ -1272,8 +1314,10 @@ module VCAP::CloudController end context 'there is not a web process type on the process level' do - let(:parsed_yaml) { { 'memory' => '5GB', - instances: 1, + let(:parsed_yaml) { { + 'name' => 'eugene', + 'memory' => '5GB', + 'instances' => 1, 'disk_quota' => '30GB', 'processes' => [{ 'type' => 'worker', 'disk_quota' => '1000GB', 'memory' => '200GB', instances: 5 }] } } @@ -1303,6 +1347,7 @@ module VCAP::CloudController context 'from app-level attributes' do let(:parsed_yaml) do { + 'name' => 'eugene', 'command' => command, 'health-check-type' => health_check_type, 'health-check-http-endpoint' => health_check_http_endpoint, @@ -1332,7 +1377,7 @@ module VCAP::CloudController context 'health checks' do context 'deprecated health check type none' do - let(:parsed_yaml) { { "health-check-type": 'none' } } + let(:parsed_yaml) { { 'name' => 'eugene', "health-check-type": 'none' } } it 'is converted to process' do message = AppManifestMessage.create_from_yml(parsed_yaml) @@ -1344,7 +1389,7 @@ module VCAP::CloudController context 'health check timeout without other health check parameters' do let(:health_check_timeout) { 10 } - let(:parsed_yaml) { { "timeout": health_check_timeout } } + let(:parsed_yaml) { { name: 'eugene', timeout: health_check_timeout } } it 'sets the health check timeout in the message' do message = AppManifestMessage.create_from_yml(parsed_yaml) @@ -1356,7 +1401,7 @@ module VCAP::CloudController context 'health check invocation timeout without other health check parameters' do let(:health_check_invocation_timeout) { 2493 } - let(:parsed_yaml) { { "health_check_invocation_timeout": health_check_invocation_timeout } } + let(:parsed_yaml) { { name: 'eugene', health_check_invocation_timeout: health_check_invocation_timeout } } it 'sets the health check timeout in the message' do message = AppManifestMessage.create_from_yml(parsed_yaml) @@ -1367,7 +1412,7 @@ module VCAP::CloudController end context 'when health check type is port' do - let(:parsed_yaml) { { 'health-check-type' => 'port' } } + let(:parsed_yaml) { { 'name' => 'eugene', 'health-check-type' => 'port' } } it 'does not set the endpoint' do message = AppManifestMessage.create_from_yml(parsed_yaml) @@ -1388,7 +1433,7 @@ module VCAP::CloudController end context 'when the health check endpoint is not specified' do - let(:parsed_yaml) { { 'health-check-type' => 'http' } } + let(:parsed_yaml) { { 'name' => 'eugene', 'health-check-type' => 'http' } } it 'returns nil as the endpoint' do message = AppManifestMessage.create_from_yml(parsed_yaml) @@ -1398,7 +1443,7 @@ module VCAP::CloudController end context 'when the health check type is nonsense' do - let(:parsed_yaml) { { 'health-check-type' => 'nonsense' } } + let(:parsed_yaml) { { 'name' => 'eugene', 'health-check-type' => 'nonsense' } } it 'returns the error' do message = AppManifestMessage.create_from_yml(parsed_yaml) @@ -1448,7 +1493,7 @@ module VCAP::CloudController context 'when no parameters are specified' do let(:parsed_yaml) do - {} + { 'name' => 'eugene' } end it 'does not set a command or health_check_type field' do @@ -1462,6 +1507,7 @@ module VCAP::CloudController context 'from nested process attributes' do let(:parsed_yaml) do { + 'name' => 'eugene', 'processes' => [{ 'type' => type, 'command' => command, @@ -1492,7 +1538,7 @@ module VCAP::CloudController context 'health checks' do context 'deprecated health check type none' do - let(:parsed_yaml) { { "health-check-type": 'none' } } + let(:parsed_yaml) { { name: 'eugene', "health-check-type": 'none' } } it 'is converted to process' do message = AppManifestMessage.create_from_yml(parsed_yaml) @@ -1504,7 +1550,7 @@ module VCAP::CloudController context 'health check timeout without other health check parameters' do let(:health_check_timeout) { 10 } - let(:parsed_yaml) { { "timeout": health_check_timeout } } + let(:parsed_yaml) { { name: 'eugene', timeout: health_check_timeout } } it 'sets the health check timeout in the message' do message = AppManifestMessage.create_from_yml(parsed_yaml) @@ -1515,7 +1561,7 @@ module VCAP::CloudController end context 'when health check type is not http and endpoint is not specified' do - let(:parsed_yaml) { { 'health-check-type' => 'port' } } + let(:parsed_yaml) { { 'name' => 'eugene', 'health-check-type' => 'port' } } it 'does not default endpoint to "/"' do message = AppManifestMessage.create_from_yml(parsed_yaml) @@ -1529,7 +1575,7 @@ module VCAP::CloudController context 'command' do context 'when command is not requested' do - let(:parsed_yaml) { { 'processes' => [{ 'type' => 'web' }] } } + let(:parsed_yaml) { { 'name' => 'eugene', 'processes' => [{ 'type' => 'web' }] } } it 'does not set the command field in the process update message' do message = AppManifestMessage.create_from_yml(parsed_yaml) @@ -1575,7 +1621,9 @@ module VCAP::CloudController context 'when processes and app-level process properties are specified' do context 'there is a web process type on the process level' do - let(:parsed_yaml) { { 'command' => 'ignoreme', + let(:parsed_yaml) { { + 'name' => 'eugene', + 'command' => 'ignoreme', 'health_check_http_endpoint' => '/not-here', 'health_check_type' => 'http', 'timeout' => 5, @@ -1596,7 +1644,9 @@ module VCAP::CloudController end context 'there is not a web process type on the process level' do - let(:parsed_yaml) { { 'command' => 'ignoreme', + let(:parsed_yaml) { { + 'name' => 'eugene', + 'command' => 'ignoreme', 'health_check_http_endpoint' => '/not-here', 'health_check_type' => 'http', 'timeout' => 5, @@ -1755,7 +1805,7 @@ module VCAP::CloudController end describe '#app_update_environment_variables_message' do - let(:parsed_yaml) { { 'env' => { 'foo' => 'bar', 'baz' => 4.44444444444, 'qux' => false } } } + let(:parsed_yaml) { { 'name' => 'eugene', 'env' => { 'foo' => 'bar', 'baz' => 4.44444444444, 'qux' => false } } } it 'returns a UpdateEnvironmentVariablesMessage containing the env vars' do message = AppManifestMessage.create_from_yml(parsed_yaml) expect(message).to be_valid @@ -1768,6 +1818,7 @@ module VCAP::CloudController context 'when no-route value is not a boolean' do let(:parsed_yaml) do { + 'name' => 'eugene', 'no-route' => 3, 'routes' => [ @@ -1785,7 +1836,7 @@ module VCAP::CloudController context 'when no routes are specified' do let(:parsed_yaml) do - {} + { 'name' => 'eugene' } end it 'does not set the routes in the message' do diff --git a/spec/unit/messages/named_app_manifest_message_spec.rb b/spec/unit/messages/named_app_manifest_message_spec.rb deleted file mode 100644 index cc7180733cb..00000000000 --- a/spec/unit/messages/named_app_manifest_message_spec.rb +++ /dev/null @@ -1,29 +0,0 @@ -require 'spec_helper' -require 'messages/named_app_manifest_message' - -module VCAP::CloudController - RSpec.describe NamedAppManifestMessage do - describe 'validations' do - context 'when unexpected keys are requested' do - let(:params) { { name: 'hawaiian', instances: 3, memory: '2G' } } - - it 'is valid' do - message = NamedAppManifestMessage.create_from_yml(params) - - expect(message).to be_valid - end - end - - context 'when name is not specified' do - let(:params) { { instances: 3, memory: '2G' } } - - it 'is not valid' do - message = NamedAppManifestMessage.create_from_yml(params) - - expect(message).to_not be_valid - expect(message.errors.full_messages[0]).to match(/^Name must not be empty/) - end - end - end - end -end