Skip to content

Commit

Permalink
Set warning header for outdated CF CLIs
Browse files Browse the repository at this point in the history
Introduces a new (optional) middleware which will warn CF CLI users if their version is below `min_cli_version`.
Needs to be explicitly enabled by setting the config flag `warn_if_below_min_cli_version` to `true`.
  • Loading branch information
johha committed Feb 27, 2024
1 parent 17c3b41 commit 2b3b22a
Show file tree
Hide file tree
Showing 5 changed files with 158 additions and 0 deletions.
1 change: 1 addition & 0 deletions lib/cloud_controller/config_schemas/base/api_schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ class ApiSchema < VCAP::Config
external_protocol: String,
internal_service_hostname: String,
optional(:internal_service_port) => Integer,
optional(:warn_if_below_min_cli_version) => bool,
info: {
name: String,
build: String,
Expand Down
2 changes: 2 additions & 0 deletions lib/cloud_controller/rack_app_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
require 'new_relic_custom_attributes'
require 'zipkin'
require 'block_v3_only_roles'
require 'below_min_cli_warning'

module VCAP::CloudController
class RackAppBuilder
Expand All @@ -24,6 +25,7 @@ def build(config, request_metrics, request_logs)
use CloudFoundry::Middleware::RequestMetrics, request_metrics
use CloudFoundry::Middleware::Cors, config.get(:allowed_cors_domains)
use CloudFoundry::Middleware::VcapRequestId
use CloudFoundry::Middleware::BelowMinCliWarning if config.get(:warn_if_below_min_cli_version)
use CloudFoundry::Middleware::NewRelicCustomAttributes if config.get(:newrelic_enabled)
use Honeycomb::Rack::Middleware, client: Honeycomb.client if config.get(:honeycomb)
use CloudFoundry::Middleware::SecurityContextSetter, configurer
Expand Down
54 changes: 54 additions & 0 deletions middleware/below_min_cli_warning.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
module CloudFoundry
module Middleware
class BelowMinCliWarning
def initialize(app)
@app = app
@min_cli_version = Gem::Version.new(VCAP::CloudController::Config.config.get(:info, :min_cli_version))
end

def call(env)
status, headers, body = @app.call(env)

included_endpoints = %w[/v3/spaces /v3/organizations /v2/spaces /v2/organizations]

if included_endpoints.any? { |ep| env['REQUEST_PATH'].include?(ep) } && is_below_min_cli_version?(env['HTTP_USER_AGENT'])
# Ensure existing warnings are appended by ',' (unicode %2C)
new_warning = env['X-Cf-Warnings'].nil? ? escaped_warning : "#{env['X-Cf-Warnings']}%2C#{escaped_warning}"
headers['X-Cf-Warnings'] = new_warning
end

[status, headers, body]
end

def escaped_warning
CGI.escape("\u{1F6A8} [WARNING] Outdated CF CLI version - please upgrade: https://github.com/cloudfoundry/cli/releases/latest \u{1F6A8}\n")
end

def is_below_min_cli_version?(user_agent_string)
regex = %r{
[cC][fF] # match 'cf', case insensitive
[^/]* # match any character that are not '/'
/ # match '/' character
(\d+\.\d+\.\d+) # capture the version number (expecting 3 groups of digits separated by '.')
(?:\+|\s) # match '+' character or a whitespace, non-capturing group
}x

match = user_agent_string.match(regex)
return false unless match

current_version = Gem::Version.new(match[1])

current_version < @min_cli_version
rescue StandardError => e
logger.warn("Warning: An error occurred while checking user agent version: #{e.message}")
false
end

private

def logger
@logger = Steno.logger('cc.deprecated_cf_cli_warning')
end
end
end
end
22 changes: 22 additions & 0 deletions spec/unit/lib/cloud_controller/rack_app_builder_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,28 @@ module VCAP::CloudController
expect(CloudFoundry::Middleware::CefLogs).to have_received(:new).with(anything, fake_logger, TestConfig.config_instance.get(:local_route))
end
end

describe 'Deprecated CF CLI Warning' do
before do
allow(CloudFoundry::Middleware::BelowMinCliWarning).to receive(:new)
end

context 'with min_cf_cli_version provided' do
before do
builder.build(TestConfig.override(info: { min_cli_version: '7.0.0' }, warn_if_below_min_cli_version: true), request_metrics, request_logs).to_app
end

it 'enables the DeprecatedCfCliWarning middleware' do
expect(CloudFoundry::Middleware::BelowMinCliWarning).to have_received(:new)
end
end

context 'without min_cf_cli_version provided' do
it 'does not enable the DeprecatedCfCliWarning middleware' do
expect(CloudFoundry::Middleware::BelowMinCliWarning).not_to have_received(:new)
end
end
end
end
end
end
79 changes: 79 additions & 0 deletions spec/unit/middleware/below_min_cli_warning_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
require 'spec_helper'
require 'below_min_cli_warning'

module CloudFoundry
module Middleware
RSpec.describe BelowMinCliWarning do
subject { described_class.new(app) }
let(:app) { double(:app, call: [200, {}, 'a body']) }
let(:env) { { 'HTTP_USER_AGENT' => 'mocked-user-agent', 'REQUEST_PATH' => '/v3/organizations' } }

before { TestConfig.override(info: { min_cli_version: '7.0.0' }, warn_if_below_min_cli_version: true) }

describe 'deprecated cf cli version middleware is called' do
context 'called with outdated cf cli version' do
before { allow(subject).to receive(:is_below_min_cli_version?).and_return(true) }

it 'sets X-Cf-Warnings header' do
_, headers = subject.call(env)
expect(headers['X-Cf-Warnings']).to eq(subject.escaped_warning)
end

it 'appends to the existing X-Cf-Warnings header' do
_, headers = subject.call(env.merge!({ 'X-Cf-Warnings' => 'a-warning' }))
expect(headers['X-Cf-Warnings']).to eq("a-warning%2C#{subject.escaped_warning}")
end
end

context 'called with current cf cli version' do
before { allow(subject).to receive(:is_below_min_cli_version?).and_return(false) }

it 'does not add X-Cf-Warnings header' do
_, headers = subject.call(env)
expect(headers['X-Cf-Warnings']).to be_nil
end
end

context 'checks the request path' do
before { allow(subject).to receive(:is_below_min_cli_version?).and_return(true) }

%w[/v3/processes /v2/services /something/stats].each do |endpoint|
it "does not set X-Cf-Warnings header for #{endpoint}" do
_, headers = subject.call(env.merge!({ 'REQUEST_PATH' => endpoint }))
expect(headers['X-Cf-Warnings']).to be_nil
end
end

%w[/v3/spaces /v2/spaces /v3/organizations /v2/organizations].each do |endpoint|
it "sets X-Cf-Warnings header for #{endpoint}" do
_, headers = subject.call(env.merge!({ 'REQUEST_PATH' => endpoint }))
expect(headers['X-Cf-Warnings']).to eq(subject.escaped_warning)
end
end
end
end

describe 'is_outdated_cf_cli_user_agent?' do
['some-client',
'cf7/7.5.0+0ad1d6398.2022-06-04 (go1.17.10; amd64 windows)',
'cf/7.7.2+b663981.2023-08-31 (go1.20.7; amd64 linux)',
'cf/8.7.5+8aa8625.2023-11-20 (go1.21.4; arm64 darwin)',
'cf8.exe/8.3.0+e6f8a853a.2022-03-11 (go1.17.7; amd64 windows)',
'cf8/8.7.4+db5d612.2023-10-20 (go1.21.3; amd64 linux)',
'cf.exe/7.4.0+e55633fed.2021-11-15 (go1.16.6; amd64 windows)',
'Cf/8.5.0+73aa161.2022-09-12 (go1.18.5; arm64 darwin)'].each do |user_agent|
it("returns false for #{user_agent}") { expect(subject).not_to be_is_below_min_cli_version(user_agent) }
end

['cf/6.46.0+29d6257f1.2019-07-09 (go1.12.7; amd64 windows)',
'CF/6.46.0+29d6257f1.2019-07-09 (go1.12.7; amd64 windows)',
'Cf/6.46.0+29d6257f1.2019-07-09 (go1.12.7; amd64 windows)',
'cf.exe/6.6.0+e25762999.2023-02-16 (go1.19.5; amd64 windows)',
'cf/6.43.0 (go1.10.8; amd64 linux)',
'cf6/6.53.0+bommel'].each do |user_agent|
it("returns true for #{user_agent}") { expect(subject).to be_is_below_min_cli_version(user_agent) }
end
end
end
end
end

0 comments on commit 2b3b22a

Please sign in to comment.