Reputation: 132
I'm implementing a plugin to Foreman v3.3.0 that should add some additional parameters to the Subnet model.
# app/models/subnet_extensions.rb
module SubnetExtensions
extend ActiveSupport::Concern
included do
has_one :subnet_bgp_config
end
end
# lib/engine.rb
class Engine < Rails::Engine
config.to_prepare
Subnet.include SubnetExtensions
end
end
Subnets in Foreman are implemented with single table inheritance (STI) and types Subnet::Ipv6
and Subnet::Ipv4
. My issue for the plugin comes up whenever the page for any subnet is rendered:
Association named ‘subnet_bgp_config’ was not found on Subnet::Ipv4; perhaps you misspelled it?
In the Rails console, I can verify that the association simply is not present for the subclasses:
# foreman-rake console (Rails 6.1.7)
irb(main):001:0> Subnet.reflect_on_all_associations.map(&:name).include? :subnet_bgp_config
=> true
irb(main):002:0> Subnet::Ipv4.reflect_on_all_associations.map(&:name).include? :subnet_bgp_config
=> false
I've validated, that this approach works just fine, when the has_one
association is written into the Foreman source code. So somehow the method of adding the plugin to Foreman breaks the inheritance.
Even including SubnetExtensions
in the console does not work (nor does it raise an error):
irb(main):001:0> Subnet::Ipv4.include ForemanSubnetsWithBGPConfig::SubnetExtensions
=> Subnet::Ipv4(id: integer, network: string, mask: string, priority: integer, name: text, vlanid: integer, created_at: datetime, updated_at: datetime, dhcp_id: integer, tftp_id: integer, gateway: string, dns_primary: string, dns_secondary: string, from: string, to: string, dns_id: integer, boot_mode: string, ipam: string, discovery_id: integer, type: string, description: text, mtu: integer, template_id: integer, httpboot_id: integer, netdb_id: integer, nic_delay: integer, externalipam_id: integer, externalipam_group: text, bmc_id: integer)
irb(main):002:0> Subnet::Ipv4.reflect_on_all_associations.map(&:name).include? :subnet_bgp_config
=> false
Can somebody give hints on what went wrong or pointers what to inspect/try next?
Upvotes: 0
Views: 44
Reputation: 132
Asking for help on the Foreman developer forum more or less confirms that the issue lies with the load order specific to Foreman. At least the current development branch, which has switched to Rails-7 and Zeitwerk autoloader does not show the same symptoms.
Upvotes: 0
Reputation: 463
It seems that the issue is due to the way Rails handles associations and inheritance in Single Table Inheritance (STI) with concerns added at runtime. Specifically, when using ActiveSupport::Concern
to inject a new association into an STI base class (Subnet
), the association may not automatically propagate to the subclasses (Subnet::Ipv4
and Subnet::Ipv6
). This can happen because concerns included after the subclasses have loaded might not register the new associations correctly.
Here are some approaches to resolve the issue:
prepend
in SubnetExtensions
Instead of include
, use prepend
when extending the Subnet
class with the plugin. This can help ensure that the association is added before Rails processes the STI subclasses.
module SubnetExtensions
extend ActiveSupport::Concern
included do
has_one :subnet_bgp_config
end
end
# Prepend to make sure it applies before subclasses are loaded
Subnet.prepend SubnetExtensions
If prepend
doesn’t resolve the issue, you can explicitly add the association to each subclass within the plugin, ensuring that Rails recognizes the association for each subclass:
module SubnetExtensions
extend ActiveSupport::Concern
included do
has_one :subnet_bgp_config
end
end
Subnet.include SubnetExtensions
Subnet::Ipv4.include SubnetExtensions
Subnet::Ipv6.include SubnetExtensions
This explicitly adds has_one :subnet_bgp_config
to both Subnet::Ipv4
and Subnet::Ipv6
, addressing the issue for each subclass directly.
Rails.application.config.to_prepare
If SubnetExtensions
needs to be loaded at runtime (for example, if the plugin is loaded after Rails has initialized), then try adding it in a Rails initializer with Rails.application.config.to_prepare
. This ensures that the plugin is reloaded on each request in development, allowing the association to be applied correctly.
In an initializer file, add:
# config/initializers/subnet_extensions.rb
Rails.application.config.to_prepare do
Subnet.include SubnetExtensions
Subnet::Ipv4.include SubnetExtensions
Subnet::Ipv6.include SubnetExtensions
end
This approach is particularly useful in development environments, where Rails classes may be reloaded, and the plugin code needs to be re-included each time.
after_initialize
CallbackIf none of the above methods work, you might consider using an after_initialize
callback in SubnetExtensions
to ensure the association is defined after Foreman and Rails have fully loaded.
In your plugin’s main file or an initializer:
Rails.application.config.after_initialize do
Subnet.include SubnetExtensions
end
This callback ensures that SubnetExtensions
is loaded and applied after Rails initializes the classes. However, this is generally a last-resort approach, as it depends on the initialization order.
Try each solution in order, as prepend
and adding include
on each subclass are generally cleaner and should be sufficient. If you still encounter issues, Rails.application.config.to_prepare
and after_initialize
can help ensure your plugin initializes correctly in Foreman’s context.
Upvotes: 0