Skip to content

Commit e7a6b4a

Browse files
committed
Fix named instance review findings
1 parent b8b73db commit e7a6b4a

7 files changed

Lines changed: 109 additions & 20 deletions

File tree

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,10 @@ through `FLIPPER_CLOUD_CROSS_APP_TOKEN` and
123123
`FLIPPER_CLOUD_CROSS_APP_SYNC_SECRET`. Named instances never inherit the default
124124
`FLIPPER_CLOUD_TOKEN` or `FLIPPER_CLOUD_SYNC_SECRET`.
125125

126+
Applications sharing a Cloud project must register the same named group predicates
127+
and return the same `flipper_id` for shared actors. Cloud synchronizes group names and
128+
actor identifiers, not Ruby group definitions or application identity mappings.
129+
126130
## Flipper Cloud
127131

128132
Like Flipper and want more? Check out [Flipper Cloud](https://www.flippercloud.io?utm_source=oss&utm_medium=readme&utm_campaign=check_out), which comes with:

lib/flipper.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -340,7 +340,8 @@ def refresh_named_instance_accessors
340340
next if named_instance_accessor_names.include?(name)
341341

342342
validate_named_instance_name!(name)
343-
define_singleton_method(name) { named(name) }
343+
proxy = named(name)
344+
define_singleton_method(name) { proxy }
344345
named_instance_accessor_names.add(name)
345346
named_instance_accessor_methods[name] = method(name)
346347
end

lib/flipper/cloud/routes.rb

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# Default routes loaded by Flipper::Cloud::Engine
22
Rails.application.routes.draw do
3+
cloud_mounts = []
4+
35
if ENV["FLIPPER_CLOUD_TOKEN"] && !ENV.fetch("FLIPPER_CLOUD_SYNC_SECRET", "").empty?
46
require 'flipper/cloud'
57
config = Rails.application.config.flipper
@@ -9,24 +11,31 @@
911
memoizer_options: { preload: config.preload }
1012
)
1113

12-
mount cloud_app, at: config.cloud_path
14+
cloud_mounts << [config.cloud_path, cloud_app]
1315
end
1416

15-
if Flipper.configuration.respond_to?(:named_instance_names)
17+
if !Rails.application.config.flipper.test_help && Flipper.configuration.respond_to?(:named_instance_names)
1618
Flipper.configuration.named_instance_names.each do |name|
1719
named = Flipper.configuration.named_configuration(name)
1820
next unless named.cloud? && named.cloud_path
1921

2022
cloud_options = named.resolve_cloud_credentials
2123
sync_secret = cloud_options[:sync_secret]
22-
next if sync_secret.nil? || sync_secret.empty?
24+
next if sync_secret.nil? || sync_secret == false || sync_secret.empty?
2325

2426
require "flipper/cloud"
2527
cloud_app = Flipper::Cloud.app(Flipper.named(name),
2628
env_key: named.env_key,
2729
memoizer_options: { preload: named.preload }
2830
)
29-
mount cloud_app, at: named.cloud_path
31+
cloud_mounts << [named.cloud_path, cloud_app]
3032
end
3133
end
34+
35+
cloud_mounts.sort_by do |path, _|
36+
normalized_path = path.to_s.sub(%r{\A/+}, "").sub(%r{/+\z}, "")
37+
-normalized_path.length
38+
end.each do |path, cloud_app|
39+
mount cloud_app, at: path
40+
end
3241
end

lib/flipper/engine.rb

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,11 +74,16 @@ def self.default_strict_value
7474
config.use Flipper::Adapters::ActorLimit, flipper.actor_limit if flipper.actor_limit
7575
end
7676

77+
if flipper.test_help
78+
require "flipper/test_help"
79+
Flipper::TestHelp.flipper_configure_named_instances
80+
end
81+
7782
if Flipper.configuration.respond_to?(:named_instance_names)
7883
Flipper.configuration.named_instance_names.each do |name|
7984
named = Flipper.configuration.named_configuration(name)
8085
named.inherit_rails_configuration(flipper)
81-
if named.cloud?
86+
if named.cloud? && !flipper.test_help
8287
named.resolve_cloud_credentials({
8388
token: app.credentials.dig(:flipper, name, :cloud_token),
8489
sync_secret: app.credentials.dig(:flipper, name, :cloud_sync_secret),
@@ -127,6 +132,7 @@ def self.default_strict_value
127132
end
128133

129134
initializer "flipper.named_cloud_paths", after: :load_config_initializers do |app|
135+
next if app.config.flipper.test_help
130136
next unless Flipper.configuration.respond_to?(:named_instance_names)
131137

132138
named_paths = Flipper.configuration.named_instance_names.map do |name|

lib/flipper/test_help.rb

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,18 @@ def flipper_configure
1010
Flipper.configure do |config|
1111
config.adapter { adapter }
1212
config.default { Flipper.new(config.adapter) }
13+
flipper_configure_named_instances(config)
14+
end
15+
end
1316

14-
if config.respond_to?(:named_instance_names)
15-
config.named_instance_names.each do |name|
16-
named = config.named_configuration(name)
17-
named_adapter = Flipper::Adapters::Memory.new
18-
named.adapter { named_adapter }
19-
named.default { Flipper.new(named.adapter) }
20-
end
21-
end
17+
def flipper_configure_named_instances(config = Flipper.configuration)
18+
return unless config.respond_to?(:named_instance_names)
19+
20+
config.named_instance_names.each do |name|
21+
named = config.named_configuration(name)
22+
named_adapter = Flipper::Adapters::Memory.new
23+
named.adapter { named_adapter }
24+
named.default { Flipper.new(named.adapter) }
2225
end
2326
end
2427

@@ -29,11 +32,9 @@ def flipper_reset
2932
if Flipper.configuration.respond_to?(:named_instance_names)
3033
Flipper.configuration.named_instance_names.each do |name|
3134
named = Flipper.configuration.named_configuration(name)
32-
if named.cloud?
33-
named_adapter = Flipper::Adapters::Memory.new
34-
named.adapter { named_adapter }
35-
named.default { Flipper.new(named.adapter) }
36-
end
35+
named_adapter = Flipper::Adapters::Memory.new
36+
named.adapter { named_adapter }
37+
named.default { Flipper.new(named.adapter) }
3738
Flipper.named(name).features.each(&:remove) rescue nil
3839
end
3940
end

spec/flipper/engine_spec.rb

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -315,10 +315,26 @@ def get_all
315315
to raise_error(Flipper::InvalidConfigurationValue, /environment keys must be unique/)
316316
end
317317

318+
it "boots test apps with named Cloud configured and no credentials" do
319+
Rails.env = "test"
320+
initializer do
321+
Flipper.configure do |flipper_config|
322+
flipper_config.named(:cross_app) do |named|
323+
named.cloud(path: "_cross_app")
324+
end
325+
end
326+
end
327+
328+
expect { subject }.not_to raise_error
329+
expect(Flipper.cross_app.enabled?(:chat)).to be(false)
330+
expect(a_request(:any, /flippercloud/)).not_to have_been_made
331+
end
332+
318333
context "with named Cloud" do
319334
let(:app) { application.routes }
320335
let(:named_cloud_path) { "_cross_app" }
321336
let(:named_sync_secret) { "named-secret" }
337+
let(:named_cloud_options) { {path: named_cloud_path} }
322338
let(:request_body) do
323339
JSON.generate({
324340
"environment_id" => 1,
@@ -341,7 +357,7 @@ def get_all
341357
initializer do
342358
Flipper.configure do |flipper_config|
343359
flipper_config.named(:cross_app) do |named|
344-
named.cloud(path: named_cloud_path)
360+
named.cloud(named_cloud_options)
345361
named.register(:cross_app_group) { true }
346362
end
347363
end
@@ -372,6 +388,34 @@ def get_all
372388
expect(Flipper.cross_app.instance).to be_a(Flipper::Cloud::DSL)
373389
end
374390

391+
context "when nested under the default Cloud path" do
392+
let(:named_cloud_path) { "_flipper/cross_app" }
393+
394+
before do
395+
ENV["FLIPPER_CLOUD_TOKEN"] = "default-token"
396+
ENV["FLIPPER_CLOUD_SYNC_SECRET"] = "default-secret"
397+
end
398+
399+
after do
400+
ENV.delete("FLIPPER_CLOUD_TOKEN")
401+
ENV.delete("FLIPPER_CLOUD_SYNC_SECRET")
402+
end
403+
404+
it "routes the more specific named webhook first" do
405+
silence { application.initialize! }
406+
stub = stub_request(:get, /features\?_cb=\d+&exclude_gate_names=true/).with({
407+
headers: { "flipper-cloud-token" => "named-token" },
408+
}).to_return(status: 200, body: JSON.generate({features: {}}), headers: {})
409+
410+
post "/_flipper/cross_app", request_body, {
411+
"HTTP_FLIPPER_CLOUD_SIGNATURE" => signature_header_value,
412+
}
413+
414+
expect(last_response.status).to eq(200)
415+
expect(stub).to have_been_requested
416+
end
417+
end
418+
375419
context "when the path matches the default" do
376420
let(:named_cloud_path) { "/_flipper/" }
377421

@@ -400,6 +444,20 @@ def get_all
400444
expect(last_response.status).to eq(404)
401445
end
402446
end
447+
448+
context "with a false sync secret" do
449+
let(:named_cloud_options) { {path: named_cloud_path, sync_secret: false} }
450+
451+
it "does not mount a webhook" do
452+
silence { application.initialize! }
453+
454+
post "/_cross_app", request_body, {
455+
"HTTP_FLIPPER_CLOUD_SIGNATURE" => signature_header_value,
456+
}
457+
458+
expect(last_response.status).to eq(404)
459+
end
460+
end
403461
end
404462

405463
it "loads named Cloud credentials from the matching Rails credential scope" do

spec/flipper/test_help_spec.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,16 @@
4040
expect(a_request(:any, /flippercloud/)).not_to have_been_made
4141
end
4242

43+
it "shares Memory for a non-Cloud named instance added after test setup" do
44+
described_class.flipper_configure
45+
Flipper.configure { |config| config.named(:cross_app) }
46+
47+
described_class.flipper_reset
48+
Flipper.cross_app.enable(:chat)
49+
50+
expect(Thread.new { Flipper.cross_app.enabled?(:chat) }.value).to be(true)
51+
end
52+
4353
it "clears named features while preserving registered groups" do
4454
Flipper.configure do |config|
4555
config.named(:cross_app)

0 commit comments

Comments
 (0)