Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Ruby 3.1 #22698

Merged
merged 12 commits into from
Dec 18, 2023
Merged

Support Ruby 3.1 #22698

merged 12 commits into from
Dec 18, 2023

Conversation

jrafanie
Copy link
Member

@jrafanie jrafanie commented Sep 11, 2023

TODO:

  • resolve the YAML safe_load default so we don't need to use unsafe_load by default.
    • Try to safe_load, retry with loading aliases (via true) and minimal permitted classes, retry again with aliases and a larger list of permitted classes.
    • Add warning logging for disallowed classes so we can build a list of classes we need to allow in the next step
    • Enumerate a list of commonly permitted classes, perhaps the list of models + some others, store this and use it with the rails activerecord yaml_column_permitted_classes similar to https://stackoverflow.com/a/75001610
  • update YAML.load/safe_load using positional permitted_classes to kwargs since positional is gone in psych 4
  • upgrade qpid_proton to one with ruby 3.1 support
  • Cross repo test: [BOT] Cross repo test for ManageIQ/manageiq#22698 manageiq-cross_repo-tests#824

Part of #22696

Note, qpid_proton and bumping ruby 3.1 in test has moved to a followup in #22808

@jrafanie jrafanie requested a review from Fryguy as a code owner September 11, 2023 17:54
@miq-bot miq-bot added the wip label Sep 11, 2023
@jrafanie
Copy link
Member Author

@miq-bot cross-repo-tests /all

miq-bot pushed a commit to ManageIQ/manageiq-cross_repo-tests that referenced this pull request Sep 11, 2023
@jrafanie jrafanie changed the title [WIP] Suppport Ruby 3.1 [WIP] Support Ruby 3.1 Sep 11, 2023
Gemfile Outdated
@@ -55,6 +55,7 @@ gem "manageiq-ssh-util", "~>0.1.1", :require => false
gem "memoist", "~>0.16.0", :require => false
gem "money", "~>6.13.5", :require => false
gem "more_core_extensions" # min version should be set in manageiq-gems-pending, not here
gem "net-ftp", "~>0.1.2", :require => false
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if this should belong here or manageiq-gems-pending

Copy link
Member Author

@jrafanie jrafanie Sep 11, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, it also blew up in gems pending in the same require so they both will need the same solution... note, it was blowing up in core so will need to either move the calling code out of core or let gems pending drive the gem specification

@@ -70,7 +70,7 @@
TaskHelpers::Exports::CustomButtons.new.export(:directory => export_dir)
file_contents = File.read("#{export_dir}/CustomButtons.yaml")

expect(YAML.safe_load(file_contents, [Symbol])).to contain_exactly(*custom_button_export_test)
expect(YAML.safe_load(file_contents, permitted_classes: [Symbol])).to contain_exactly(*custom_button_export_test)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there's a way to set permitted_classes globally, so we can add most of the reasonable defaults of Date, Time, DateTime, Symbol, etc.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was a fast pass through to see what's busted as it's far easier to comment on code 😉

jrafanie added a commit to jrafanie/manageiq that referenced this pull request Sep 13, 2023
@jrafanie jrafanie force-pushed the ruby31 branch 3 times, most recently from 5d5f8a3 to 6bd2c77 Compare October 5, 2023 16:16
@jrafanie jrafanie closed this Oct 5, 2023
@jrafanie jrafanie reopened this Oct 5, 2023
def safe_load(*args, **kwargs)
super(*args, **kwargs.merge(:aliases => true, :permitted_classes => DEFAULT_PERMITTED_CLASSES))
rescue NameError
super(*args, **kwargs.merge(:aliases => true, :permitted_classes => DEFAULT_PERMITTED_CLASSES + [MiqExpression, MiqReport, Ruport::Data::Table, Ruport::Data::Record]))
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note, we're enumerating these classes at method call time to avoid autoloading them at code load time. This file is loaded very early in the load time. I suppose we can put this list in an memoized instance method to avoid creating so many arrays.

@kbrock
Copy link
Member

kbrock commented Oct 5, 2023

only request:

  • use hash rockets
  • use **reverse_merge() / **{}.merge(kwargs)

@jrafanie
Copy link
Member Author

jrafanie commented Oct 5, 2023

only request:

  • use hash rockets
  • use **reverse_merge() / **{}.merge(kwargs)

Will do, thanks! Yes, it makes sense to any passed in kwargs and not override them.

def safe_load(*args, **kwargs)
super(*args, **kwargs.reverse_merge(:aliases => true, :permitted_classes => DEFAULT_PERMITTED_CLASSES))
rescue NameError
super(*args, **kwargs.reverse_merge(:aliases => true, :permitted_classes => DEFAULT_PERMITTED_CLASSES + [MiqExpression, MiqReport, Ruport::Data::Table, Ruport::Data::Record]))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

were you going to add a warning to this code? (and possibly below too?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes and yes. Just chatted with @Fryguy about this. I need to check alias: true is ok to do by default and add warnings.

Hopefully, this "expanded permitted classes" list can moved to a memoized method since we need the class objects so we want to avoid autoloading models too early at load time, but memoized so we don't build this list over and over again.

I'll have to go through and build a list of these additional classes through trial and error with various yamls. We can then decide if we want to the final fallback of loading disallowed classes with unsafe_load. I think this fallback (line 13), is a worst case scenario. I'd like to get rid of it and fix bugs when we find places we missed.

@@ -11,7 +11,7 @@ def initialize(dialog_field_association_validator = DialogFieldAssociationValida
end

def determine_validity(import_file_upload)
potential_dialogs = YAML.safe_load(import_file_upload.uploaded_content, [Symbol])
potential_dialogs = YAML.safe_load(import_file_upload.uploaded_content, :permitted_classes => [Symbol])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do these changes fix the this code without the YamlLoadAliases?

So are we deciding if we want this change or the Yaml monkey patch?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question. When possible, safe_load is best. For tests, it's ok for them to use unsafe_load if enumerating the list isn't worth it. Ultimately, the suggestion to log warnings below will help us build the list of permitted classes that we can use in most places.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at psych code, it seems to call to_s on the passing in permitted classes so maybe I can try another way of doing it. If I recall, the rails config.active_record.yaml_column_permitted_classes didn't allow string/symbols but maybe if I can get in the middle, I can call safe_load directly with string/symbols and avoid the issue with reload and eager loading class objects.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like there are 2 use cases:

  • use safe_load when ever our code references YAML. I think this is a good trend
  • use safe_load when rails models have a yaml column -- this one I believe should use unsafe_load via the configuration parameter

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

adding to that comment

I believe when we use YAML.safe_load, the list of models is relatively small. MiqExpression and a few others for reports. Also, since this is from external parties, this should be locked down more.

I believe when we use active record yaml serialization (e.g. with MiqQueue), we want many more models. And theoretically, this is not externally accessible, so we could be more liberal with the number of models we support here. (e.g.: all?)

.github/workflows/ci.yaml Outdated Show resolved Hide resolved
class YamlPermittedClasses
DEFAULT_PERMITTED_CLASSES = [Object, Range, Regexp, Symbol, Date, Time, DateTime, ActiveSupport::Duration, ActiveSupport::HashWithIndifferentAccess, ActiveSupport::TimeWithZone, ActiveSupport::TimeZone]
def self.app_yaml_permitted_classes
@app_yaml_permitted_classes ||= DEFAULT_PERMITTED_CLASSES + [MiqExpression, MiqReport, Ruport::Data::Table, Ruport::Data::Record, User, ConfigurationScript, ContainerImage, ContainerTemplate, OrchestrationTemplate, ManageIQ::Providers::Vmware::InfraManager, ManageIQ::Providers::InfraManager::Vm]
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is going to be pretty annoying if we basically eager load all of our models we permit to be yaml loaded from serialized column.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DEFAULT_PERMITTED_CLASSES contains libraries so that looks ok.
I am concerned about @app_yml_permitted_classes since that has classes from manageiq/app/models which can be dynamically reloaded.

Is there a rails hook we can use to clear the @app_ cache variable?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. There is likely a reload hook we can wrap this and clear it.

I'm not in love with the current way to specify classes that can be loaded via YAML specifically because of the point you're using. It's taking actual class objects, not a symbol/string of the class so we need to eager load the classes and as you said, deal with the class being reloaded.

# constants at that time, such as MiqExpression, Ruport, so we have two sets of permitted classes.
def safe_load(yaml, permitted_classes: [], aliases: false, **kwargs)
# permitted_classes kwarg is provided because rails 6.1.7.x expects it as a defined kwarg. See: https://github.com/rails/rails/blob/9ab33753b6bab1809fc73d35b98a5c1d0c96ba1b/activerecord/lib/active_record/coders/yaml_column.rb#L52
permitted_classes += YamlPermittedClasses.initialized? ? YamlPermittedClasses.app_yaml_permitted_classes : YamlPermittedClasses.default_permitted_classes
Copy link
Member

@kbrock kbrock Oct 24, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there is a lot of text here.

Can we move this check into YamlPermittedClasses?

Something like:

module YamlLoadAliases
  def safe_load(yaml ...)
    permitted_classes += YamlPermittedClasses.permitted_classes
    [...]
  end
end

class YmlPermittedClasses
  def self.permitted_classes
    YamlPermittedClasses.initialized? ? YamlPermittedClasses.app_yaml_permitted_classes : YamlPermittedClasses.default_permitted_classes
  end
end

napkin code - hopefully you can use for inspiration

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks... now that I'm done looking at the yaml columns in other repos I'm going to come back to this.

jrafanie added a commit to jrafanie/manageiq-providers-vmware that referenced this pull request Dec 18, 2023
Depends on the interface to add these classes to the list in this PR:
ManageIQ/manageiq#22698
Todo: Move to using config.active_record.yaml_column_permitted_classes with a mechanism for
handling YAML.load with aliases and non-permitted classes outside of yaml_columns.

See:
https://bugs.ruby-lang.org/issues/17866
https://stackoverflow.com/questions/71191685/visit-psych-nodes-alias-unknown-alias-default-psychbadalias/71192990#71192990

Use unsafe_load explicitly and fallback to psych 3 behavior

When we can, use unsafe_load or define the permitted_classes.
If we can't, allow the fallback of psych 3 behavior where we first
try to load safely and ultimatley will finally try using unsafe load.

We can convert specific locations to use unsafe_load or specify their own
permitted_classes.  Until then, this fallback will allow us to transition
to psych 4.
See: https://bugs.ruby-lang.org/issues/17866

YAML.load does safe_load by default in psych 4.  It also sets aliases: false.
@jrafanie jrafanie force-pushed the ruby31 branch 2 times, most recently from df775fb to 70bacfb Compare December 18, 2023 19:17
If we encounter a DisallowedClass, retry with our app's yaml_column_permitted_classes
If that still doesn't work, fallback to warning so we can add the classes to our
app's allowed list.

Keep compatibility with psych 3.1+ since permitted_classes and aliases were
added as keyword arguments to safe_load.
load now calls safe_load by default
safe_load has a tiny list of permitted_classes by default
safe_load aliases defaults to false, so our 'base' alias
in database.pg.yml would not load by default.

In prior commits, we now populate the permitted_classes with a fallback
we can eventually removed.  Additionally, we default aliases to true.
We're adding active model classes due to:

```
 puts MiqReport.first.to_yaml
--- !ruby/object:MiqReport
concise_attributes:
- !ruby/object:ActiveModel::Attribute::FromDatabase
  name: id
  value_before_type_cast: 1
- !ruby/object:ActiveModel::Attribute::FromDatabase
  name: name
...
 ```

Could be related to https://github.com/rails/rails/issues #25145
YamlLoadAliases doesn't need to know these details.  YamlPermittedClasses is where
this logic should live.
We can permit these classes for tests in the specs themselves.
When we tackle the serialized columns, we can further refine this list.
For example, we can do this in tests:

RSpec.configure do |config|
  config.before do
    YamlPermittedClasses.app_yaml_permitted_classes |= [VimHash, VimString, VimArray]
  end
end
@jrafanie jrafanie force-pushed the ruby31 branch 2 times, most recently from 0ea7860 to 0240fdf Compare December 18, 2023 19:24
* MiqReport:
    ```
     puts MiqReport.first.to_yaml
    --- !ruby/object:MiqReport
    concise_attributes:
    - !ruby/object:ActiveModel::Attribute::FromDatabase
      name: id
      value_before_type_cast: 1
    - !ruby/object:ActiveModel::Attribute::FromDatabase
      name: name
    ...
    ```

 Could be related to https://github.com/rails/rails/issues #25145

* Ruport objects
@miq-bot
Copy link
Member

miq-bot commented Dec 18, 2023

Checked commits jrafanie/manageiq@ed10b7c~...1d6a861 with ruby 2.7.8, rubocop 1.56.3, haml-lint 0.51.0, and yamllint
7 files checked, 6 offenses detected

lib/extensions/yaml_load_aliases.rb

spec/lib/extensions/ar_yaml_spec.rb

@agrare agrare merged commit 634efa7 into ManageIQ:master Dec 18, 2023
6 checks passed
@agrare agrare assigned agrare and unassigned jrafanie Dec 18, 2023
jrafanie added a commit to jrafanie/manageiq that referenced this pull request Dec 18, 2023
@jrafanie jrafanie deleted the ruby31 branch December 18, 2023 20:31
jrafanie added a commit to jrafanie/manageiq that referenced this pull request Jan 3, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants