diff --git a/Gemfile.lock b/Gemfile.lock index 669d0c51..d1aeb135 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - aptible-cli (0.26.6) + aptible-cli (0.26.7) activesupport (>= 4.0, < 6.0) aptible-api (~> 1.12) aptible-auth (~> 1.4) diff --git a/README.md b/README.md index 7f3022a5..304b7b98 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ Commands: aptible apps:deprovision [--app APP] # Deprovision an app aptible apps:rename OLD_HANDLE NEW_HANDLE [--environment ENVIRONMENT_HANDLE] # Rename an app handle. In order for the new app handle to appear in log drain and metric drain destinations, you must restart the app. aptible apps:scale [--app APP] SERVICE [--container-count COUNT] [--container-size SIZE_MB] [--container-profile PROFILE] # Scale a service + aptible apps:settings HANDLE # Display deployment related settings for an app aptible backup:list DB_HANDLE # List backups for a database aptible backup:orphaned # List backups associated with deprovisioned databases aptible backup:purge BACKUP_ID # Permanently delete a backup and any copies of it diff --git a/lib/aptible/cli/helpers/app.rb b/lib/aptible/cli/helpers/app.rb index 4beb072d..4bae1fb9 100644 --- a/lib/aptible/cli/helpers/app.rb +++ b/lib/aptible/cli/helpers/app.rb @@ -209,6 +209,17 @@ def current_configuration(app) ) end + def current_setting(app) + setting_link = app.links['current_setting'] + return unless setting_link + + Aptible::Api::Setting.find_by_url( + setting_link.href, + token: fetch_token, + headers: { 'Prefer' => 'no_sensitive_extras=false' } + ) + end + private def handle_strategies diff --git a/lib/aptible/cli/resource_formatter.rb b/lib/aptible/cli/resource_formatter.rb index 501c7c98..2978ca10 100644 --- a/lib/aptible/cli/resource_formatter.rb +++ b/lib/aptible/cli/resource_formatter.rb @@ -95,11 +95,14 @@ def inject_operation(node, operation) node.value('created_at', operation.created_at) end - def inject_app(node, app, account) + def inject_app(node, app, account, setting = nil, + include_services: true) node.value('id', app.id) node.value('handle', app.handle) node.value('created_at', app.created_at) + attach_account(node, account) + node.value('status', app.status) node.value('git_remote', app.git_repo) @@ -109,15 +112,24 @@ def inject_app(node, app, account) end end - node.list('services') do |services_list| - app.each_service do |service| - services_list.object do |n| - inject_service(n, service, NO_NESTING) + if include_services + node.list('services') do |services_list| + app.each_service do |service| + services_list.object do |n| + inject_service(n, service, NO_NESTING) + end end end end - attach_account(node, account) + unless setting.nil? + node.value('docker_image', + setting.settings['APTIBLE_DOCKER_IMAGE']) + node.value('private_registry_username', + setting.sensitive_settings['APTIBLE_PRIVATE_REGISTRY_USERNAME']) + node.value('private_registry_password', + setting.sensitive_settings['APTIBLE_PRIVATE_REGISTRY_PASSWORD']) + end end def inject_database_minimal(node, database, account) diff --git a/lib/aptible/cli/subcommands/apps.rb b/lib/aptible/cli/subcommands/apps.rb index 9e00bd31..8a73bcc9 100644 --- a/lib/aptible/cli/subcommands/apps.rb +++ b/lib/aptible/cli/subcommands/apps.rb @@ -26,7 +26,8 @@ def apps accounts.each do |account| account.each_app do |app| node.object do |n| - ResourceFormatter.inject_app(n, app, account) + setting = current_setting(app) + ResourceFormatter.inject_app(n, app, account, setting) end end end @@ -125,6 +126,32 @@ def apps end end + desc 'apps:settings HANDLE', 'Display deployment related settings for an app' + option :environment, aliases: '--env' + define_method 'apps:settings' do |handle| + telemetry(__method__, options.merge(handle: handle)) + + environment = nil + if options[:environment] + environment = environment_from_handle(options[:environment]) + end + app = app_from_handle(handle, environment) + + raise Thor::Error, "Could not find app #{handle}" if app.nil? + + app = with_sensitive(app) + setting = current_setting(app) + + Formatter.render(Renderer.current) do |root| + root.object do |node| + ResourceFormatter.inject_app( + node, app, app.account, setting, + include_services: false + ) + end + end + end + desc 'apps:rename OLD_HANDLE NEW_HANDLE [--environment'\ ' ENVIRONMENT_HANDLE]', 'Rename an app handle. In order'\ ' for the new app handle to appear in log drain and metric'\ diff --git a/lib/aptible/cli/version.rb b/lib/aptible/cli/version.rb index 2d129e40..9d67790e 100644 --- a/lib/aptible/cli/version.rb +++ b/lib/aptible/cli/version.rb @@ -1,5 +1,5 @@ module Aptible module CLI - VERSION = '0.26.6'.freeze + VERSION = '0.26.7'.freeze end end diff --git a/spec/aptible/cli/subcommands/apps_spec.rb b/spec/aptible/cli/subcommands/apps_spec.rb index 6cd87580..8f96b7fe 100644 --- a/spec/aptible/cli/subcommands/apps_spec.rb +++ b/spec/aptible/cli/subcommands/apps_spec.rb @@ -101,98 +101,218 @@ def explain .to eq("=== #{account2.handle}\n#{app2.handle}\n") end - it 'includes services in JSON' do - account = Fabricate(:account, handle: 'account') - app = Fabricate(:app, account: account, handle: 'app') - allow(Aptible::Api::Account).to receive(:all).and_return([account]) - allow(Aptible::Api::App).to receive(:all).and_return([app]) + context 'with JSON output format' do + around do |example| + ClimateControl.modify(APTIBLE_OUTPUT_FORMAT: 'json') { example.run } + end - s1 = Fabricate( - :service, - app: app, process_type: 's1', command: 'true', container_count: 2, - instance_class: 'm5' - ) - s2 = Fabricate( - :service, - app: app, process_type: 's2', container_memory_limit_mb: 2048, - instance_class: 'r5' - ) + it 'includes services in JSON' do + account = Fabricate(:account, handle: 'account') + app = Fabricate(:app, account: account, handle: 'app') + allow(Aptible::Api::Account).to receive(:all).and_return([account]) + allow(Aptible::Api::App).to receive(:all).and_return([app]) + + s1 = Fabricate( + :service, + app: app, process_type: 's1', command: 'true', container_count: 2, + instance_class: 'm5' + ) + s2 = Fabricate( + :service, + app: app, process_type: 's2', container_memory_limit_mb: 2048, + instance_class: 'r5' + ) - expected_json = [ - { - 'environment' => { - 'id' => account.id, - 'handle' => account.handle, - 'created_at' => fmt_time(account.created_at) - }, - 'handle' => app.handle, - 'id' => app.id, - 'status' => app.status, - 'git_remote' => app.git_repo, - 'created_at' => fmt_time(app.created_at), - 'services' => [ - { - 'service' => s1.process_type, - 'id' => s1.id, - 'command' => s1.command, - 'container_count' => s1.container_count, - 'container_profile' => 'm', - 'container_size' => s1.container_memory_limit_mb, - 'created_at' => fmt_time(s1.created_at) + expected_json = [ + { + 'environment' => { + 'id' => account.id, + 'handle' => account.handle, + 'created_at' => fmt_time(account.created_at) }, - { - 'service' => s2.process_type, - 'id' => s2.id, - 'command' => 'CMD', - 'container_count' => s2.container_count, - 'container_profile' => 'r', - 'container_size' => s2.container_memory_limit_mb, - 'created_at' => fmt_time(s2.created_at) - } - ] - } - ] + 'handle' => app.handle, + 'id' => app.id, + 'status' => app.status, + 'git_remote' => app.git_repo, + 'created_at' => fmt_time(app.created_at), + 'services' => [ + { + 'service' => s1.process_type, + 'id' => s1.id, + 'command' => s1.command, + 'container_count' => s1.container_count, + 'container_profile' => 'm', + 'container_size' => s1.container_memory_limit_mb, + 'created_at' => fmt_time(s1.created_at) + }, + { + 'service' => s2.process_type, + 'id' => s2.id, + 'command' => 'CMD', + 'container_count' => s2.container_count, + 'container_profile' => 'r', + 'container_size' => s2.container_memory_limit_mb, + 'created_at' => fmt_time(s2.created_at) + } + ] + } + ] - subject.send('apps') + subject.send('apps') - expect(captured_output_json).to eq(expected_json) - end + expect(captured_output_json).to eq(expected_json) + end - it 'includes the last deploy operation in JSON' do - account = Fabricate(:account, handle: 'account') - op = Fabricate(:operation, type: 'deploy', status: 'succeeded') - app = Fabricate(:app, account: account, handle: 'app', - last_deploy_operation: op) - allow(Aptible::Api::Account).to receive(:all).and_return([account]) - allow(Aptible::Api::App).to receive(:all).and_return([app]) - - expected_json = [ - { - 'environment' => { - 'id' => account.id, - 'handle' => account.handle, - 'created_at' => fmt_time(account.created_at) - }, - 'handle' => app.handle, - 'id' => app.id, - 'status' => app.status, - 'git_remote' => app.git_repo, - 'created_at' => fmt_time(app.created_at), - 'last_deploy_operation' => - { - 'id' => op.id, - 'status' => op.status, - 'git_ref' => op.git_ref, - 'user_email' => op.user_email, - 'created_at' => op.created_at + it 'includes the last deploy operation in JSON' do + account = Fabricate(:account, handle: 'account') + op = Fabricate(:operation, type: 'deploy', status: 'succeeded') + app = Fabricate(:app, account: account, handle: 'app', + last_deploy_operation: op) + allow(Aptible::Api::Account).to receive(:all).and_return([account]) + allow(Aptible::Api::App).to receive(:all).and_return([app]) + + expected_json = [ + { + 'environment' => { + 'id' => account.id, + 'handle' => account.handle, + 'created_at' => fmt_time(account.created_at) + }, + 'handle' => app.handle, + 'id' => app.id, + 'status' => app.status, + 'git_remote' => app.git_repo, + 'created_at' => fmt_time(app.created_at), + 'last_deploy_operation' => + { + 'id' => op.id, + 'status' => op.status, + 'git_ref' => op.git_ref, + 'user_email' => op.user_email, + 'created_at' => op.created_at + }, + 'services' => [] + } + ] + + subject.send('apps') + + expect(captured_output_json).to eq(expected_json) + end + + it 'includes docker image and registry settings' do + account = Fabricate(:account, handle: 'account') + setting = Fabricate( + :setting, + settings: { 'APTIBLE_DOCKER_IMAGE' => 'quay.io/myorg/myapp:latest' }, + sensitive_settings: { + 'APTIBLE_PRIVATE_REGISTRY_USERNAME' => 'registryuser', + 'APTIBLE_PRIVATE_REGISTRY_PASSWORD' => 'registrypass' + } + ) + app = Fabricate(:app, account: account, handle: 'app') + allow(Aptible::Api::Account).to receive(:all).and_return([account]) + allow(subject).to receive(:current_setting).with(app) + .and_return(setting) + + expected_json = [ + { + 'environment' => { + 'id' => account.id, + 'handle' => account.handle, + 'created_at' => fmt_time(account.created_at) }, - 'services' => [] + 'handle' => app.handle, + 'id' => app.id, + 'status' => app.status, + 'git_remote' => app.git_repo, + 'created_at' => fmt_time(app.created_at), + 'services' => [], + 'docker_image' => 'quay.io/myorg/myapp:latest', + 'private_registry_username' => 'registryuser', + 'private_registry_password' => 'registrypass' + } + ] + + subject.send('apps') + + expect(captured_output_json).to eq(expected_json) + end + + it 'omits docker image and registry settings when no current_setting' do + account = Fabricate(:account, handle: 'account') + app = Fabricate(:app, account: account, handle: 'app') + allow(Aptible::Api::Account).to receive(:all).and_return([account]) + allow(subject).to receive(:current_setting).with(app) + .and_return(nil) + + subject.send('apps') + + json = captured_output_json + expect(json.first).not_to have_key('docker_image') + expect(json.first).not_to have_key('private_registry_username') + expect(json.first).not_to have_key('private_registry_password') + end + end + end + + describe '#apps:settings' do + it 'displays app settings in JSON' do + ClimateControl.modify(APTIBLE_OUTPUT_FORMAT: 'json') do + setting = Fabricate( + :setting, + settings: { 'APTIBLE_DOCKER_IMAGE' => 'quay.io/myorg/myapp:latest' }, + sensitive_settings: { + 'APTIBLE_PRIVATE_REGISTRY_USERNAME' => 'registryuser', + 'APTIBLE_PRIVATE_REGISTRY_PASSWORD' => 'registrypass' + } + ) + allow(subject).to receive(:app_from_handle) + .with('hello', nil).and_return(app) + allow(subject).to receive(:current_setting).with(app) + .and_return(setting) + + subject.send('apps:settings', 'hello') + + json = captured_output_json + expect(json['handle']).to eq('hello') + expect(json['docker_image']).to eq('quay.io/myorg/myapp:latest') + expect(json['private_registry_username']).to eq('registryuser') + expect(json['private_registry_password']).to eq('registrypass') + expect(json).not_to have_key('services') + expect(json).not_to have_key('last_deploy_operation') + end + end + + it 'displays app settings in text' do + setting = Fabricate( + :setting, + settings: { 'APTIBLE_DOCKER_IMAGE' => 'quay.io/myorg/myapp:latest' }, + sensitive_settings: { + 'APTIBLE_PRIVATE_REGISTRY_USERNAME' => 'registryuser', + 'APTIBLE_PRIVATE_REGISTRY_PASSWORD' => 'registrypass' } - ] + ) + allow(subject).to receive(:app_from_handle) + .with('hello', nil).and_return(app) + allow(subject).to receive(:current_setting).with(app) + .and_return(setting) + + subject.send('apps:settings', 'hello') + + output = captured_output_text + expect(output).to include('quay.io/myorg/myapp:latest') + expect(output).to include('registryuser') + expect(output).to include('registrypass') + expect(output).not_to include('services') + end - subject.send('apps') + it 'raises an error when app is not found' do + allow(subject).to receive(:app_from_handle) + .with('nope', nil).and_return(nil) - expect(captured_output_json).to eq(expected_json) + expect { subject.send('apps:settings', 'nope') } + .to raise_error(Thor::Error, /Could not find app nope/) end end diff --git a/spec/fabricators/app_fabricator.rb b/spec/fabricators/app_fabricator.rb index 6149c768..23a79946 100644 --- a/spec/fabricators/app_fabricator.rb +++ b/spec/fabricators/app_fabricator.rb @@ -28,6 +28,7 @@ def each_configuration(&block) services { [] } configurations { [] } current_configuration { nil } + current_setting { nil } errors { Aptible::Resource::Errors.new } created_at { Time.now } @@ -37,6 +38,11 @@ def each_configuration(&block) href: "/accounts/#{attrs[:account].id}" ) } + if attrs[:current_setting] + hash[:current_setting] = OpenStruct.new( + href: "/settings/#{attrs[:current_setting].id}" + ) + end OpenStruct.new(hash) end diff --git a/spec/fabricators/setting_fabricator.rb b/spec/fabricators/setting_fabricator.rb index 1a00fb93..c978e08c 100644 --- a/spec/fabricators/setting_fabricator.rb +++ b/spec/fabricators/setting_fabricator.rb @@ -4,5 +4,5 @@ class StubSetting < StubAptibleResource; end settings { {} } sensitive_settings { {} } - after_create { |setting| vhost.settings << setting } + after_create { |setting| setting.vhost.settings << setting if setting.vhost } end