-
Notifications
You must be signed in to change notification settings - Fork 195
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 greater than/less than versions for packages in treefile. #2151
Support greater than/less than versions for packages in treefile. #2151
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall looks like a great start, thanks so much for taking this on!
let mut to_whitespace_split: Vec<String> = vec![]; | ||
let mut start_index = 0; | ||
let mut looping_over_quoted_pkg = false; | ||
for (i, c) in element.chars().enumerate() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm OK if we try to handle the fully general case but...I would also be totally fine if we just required that users only include a single package on a line if using version qualifiers.
IOW,
packages:
- kernel grub2
- "'selinux-policy > 1.5'"
- "'podman > 2.0'"
but not:
packages:
- kernel grub2
- "'selinux-policy > 1.5' 'podman > 2.0'"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If if we make that a requirement, we can simply check:
if element.starts_with('\'') {
...
}
which would simplify all of this code a lot.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would indeed. I consulted with Jonathan about this and he recommended solving for the general case. I originally had a hacky solution using Rust's library that would shorten the code by a lot, but it felt a bit unintuitive and difficult to understand what was going on.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I consulted with Jonathan about this and he recommended solving for the general case
There might've a misunderstanding. It's 100% fine with me if you want to go the easy route (I initially suggested it myself in #2138 (comment)). That said, if you feel like tackling the generic case, that's totally cool with me too! :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, I see. I'm okay with either way, as well :)
I'll leave it as is for now, but let me know if I should change it!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we're keeping the code as is I'd need to think hard about whether it's correct; it looks plausible - the unit tests would help a lot too particularly if we have failure conditions like missing quotes, etc.
If we only handle the simple case I'd be much more confident it's correct.
tests/compose/test-misc-tweaks.sh
Outdated
|
||
echo gpgcheck=0 >> yumrepo.repo | ||
ln "$PWD/yumrepo.repo" config/yumrepo.repo | ||
# the top-level manifest doesn't have any packages, so just set it | ||
treefile_append "packages" '["foobar"]' | ||
treefile_append "packages" $'["\'foobar >= 0.5\' quuz \'corge < 1.0.0\'"]' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The quoting required here is... 🤯
It has to get through bash, then Python, then YAML, then rpm-ostree...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some feedback from my side:
- Can we assume the correctness of the package lists? e.g. can I assume there's no incorrect use of single quotes such as
'foo bar baz
,foo bar baz'
andfoo' 'bar baz
in the package lists. The last casefoo' 'bar baz
in particular will result in an empty string in thequoted_pkgs
. It might be helpful to check for this case and trim the empty string inquoted_pkgs
. - It might be helpful to wrap the return value of both functions (
whitespace_split_packages
andsplit_whitespace_unless_quoted
) in aResult<>
, and report/return error if necessary. - For commit messages it will be more clear to add the origin of the change at the start, e.g.
treefile: Support greater than/less than versions for packages in treefile
and no period at the end of subject line
Overall looks really good, and nice work on learning and working on Rust code 😄 . |
Thanks, @zonggen, for the helpful comments! I guess I have the same question as Allen: @cgwalters, can we assume that there's no incorrect usage of quotes in the treefile? If not, I'm happy to implement checks for whether there are missing/improper quotes and return errors if necessary in the functions. |
IMO, bad quoting should be hard errors because it likely implies someone mistyped something. |
aa38a1d
to
d5e653f
Compare
sorry I didn't quite get what you meant by incorrect quoting should be hard errors. Would you suggest that I modify the helper functions such that they return errors when things are misquoted? Also, I'm not sure if I did the unit tests correctly; I added a couple tests that show that nothing breaks when a user misquotes some packages, and show that it just leads to weird "packages" like ' ', or '<=', etc. |
rust/src/treefile.rs
Outdated
@@ -576,10 +576,42 @@ impl TreefileExternals { | |||
/// array elements. | |||
fn whitespace_split_packages(pkgs: &[String]) -> Vec<String> { | |||
pkgs.iter() | |||
.flat_map(|pkg| pkg.split_whitespace().map(String::from)) | |||
.flat_map(|element| split_whitespace_unless_quoted(element).map(String::from)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
BTW you can drop the .map(String::from)
now because the function is already returning String
and not &str
.
rust/src/treefile.rs
Outdated
// and returns split Strings in an Iterator, with quoted packages first. | ||
fn split_whitespace_unless_quoted(element: &String) -> impl Iterator<Item = String> { | ||
let mut quoted_pkgs: Vec<String> = vec![]; | ||
let mut to_whitespace_split: Vec<String> = vec![]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can likely drop the : Vec<String>
and let type inference handle it.
rust/src/treefile.rs
Outdated
start_index = i + 1; | ||
} | ||
if i == element.len() - 1 { | ||
to_whitespace_split.push(String::from(&element[start_index..])); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And related to the above...I think you can also drop String::from
here and build up instead a Vec<&str>
- i.e. pointers into the original element
. This saves intermediate allocations.
Performance doesn't matter here really - I'm mostly saying this because a powerful thing about Rust is you can fearlessly build up pointers into other allocated data, not duplicating and not worry about double frees/invalid pointers etc.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh that's a great point! Thanks :) It's cool how split_whitespace_unless_quoted()
can actually just work on the borrowed String and not have to allocate anything at all if we change the return type in this function and keep the .map(String::from)
in the outer function.
rust/src/treefile.rs
Outdated
} | ||
} | ||
let mut ret: Vec<String> = vec![]; | ||
ret.extend(quoted_pkgs); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's move this to the top - i.e. directly push into ret
rather than having a separate vector.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, this would make a lot more sense.
if i == element.len() - 1 { | ||
to_whitespace_split.push(String::from(&element[start_index..])); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And then here something like:
if looping_over_quoted_pkg {
bail!("Missing terminating quote: {}", element);
}
rust/src/treefile.rs
Outdated
fn test_split_whitespace_unless_quoted() { | ||
// test single quoted package | ||
let single_quoted_pkg = String::from("'foobar >= 1.0'"); | ||
let pkgs: Vec<String> = split_whitespace_unless_quoted(&single_quoted_pkg).collect(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also it would make sense for split_whitespace_unless_quoted
to take an &str
and not a &String
- that way the test cases like this can just pass a static pointer rather than an allocated string.
rust/src/treefile.rs
Outdated
@@ -1226,6 +1259,66 @@ etc-group-members: | |||
== "table" | |||
); | |||
} | |||
|
|||
#[test] | |||
fn test_split_whitespace_unless_quoted() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we change split_whitespace_unless_quoted
to be -> Result<impl Iterator<Item = String>>
, then I'd change this function to be -> Result<()>
too.
rust/src/treefile.rs
Outdated
|
||
// test multiple quoted packages | ||
let mult_quoted_pkg = String::from("'foobar >= 1.0' 'quuz < 0.5' 'corge > 2'"); | ||
let pkgs: Vec<String> = split_whitespace_unless_quoted(&mult_quoted_pkg).collect(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Then once it returns Result<>
this call would be:
let pkgs: Vec<String> = split_whitespace_unless_quoted(&mult_quoted_pkg)?.collect();
(Notice the added ?
)
rust/src/treefile.rs
Outdated
let missing_quotes = String::from("foobar >= 1.0 quuz"); | ||
let pkgs: Vec<String> = split_whitespace_unless_quoted(&missing_quotes).collect(); | ||
assert_ne!("foobar >= 1.0", pkgs[0]); | ||
assert_eq!(">=", pkgs[1]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Then these tests would be:
assert!(split_whitespace_unless_quoted(&missing_quotes).is_err())
/approve |
Yeah, essentially |
rust/src/treefile.rs
Outdated
let single_quoted_pkg = String::from("'foobar >= 1.0'"); | ||
let pkgs: Vec<String> = split_whitespace_unless_quoted(&single_quoted_pkg).collect(); | ||
let single_quoted_pkg = "'foobar >= 1.0'"; | ||
let pkgs: Vec<&str> = split_whitespace_unless_quoted(&single_quoted_pkg)?.collect(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is completely fine as is, but you can also just use:
let pkgs: Vec<_> = ...
Here the .collect()
call needs to know that we're collecting into a Vec<>
, but type inference can take over for there and know it must be &str
because that's what split_whitespace_unless_quoted()
returns.
This is totally fine as is though!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks :) Hopefully I'll get the hang of type inference and make better use of it soon. Still in "playing it safe" mode.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Absolutely! I'm writing this feedback not because there's any real problem with the code, but because it's a useful way to help learn!
I think we'll need to wait until CoreOS CI is up to merge, but LGTM! |
d82a5df
to
3bf8e02
Compare
@cgwalters Thanks so much for the detailed feedback! Learned a lot in the process. I'll merge when CI is up :) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM as well overall, nice work! Just some comments, but none of them are blockers. :)
rust/src/treefile.rs
Outdated
} | ||
} | ||
for item in to_whitespace_split.iter() { | ||
ret.extend(item.split_whitespace()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Couldn't we just get rid of to_whitespace_split
and just do the ret.extend()
directly in the loop above as we encounter it? This also has the property of maintaining the order of packages (it doesn't matter much to rpm-ostree/libdnf really, but it'd make writing the unit tests below slightly easier).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This would make a lot more sense, thanks!
@@ -1226,6 +1263,65 @@ etc-group-members: | |||
== "table" | |||
); | |||
} | |||
|
|||
#[test] | |||
fn test_split_whitespace_unless_quoted() -> Result<()> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice, love all the unit tests! :)
tests/compose/test-misc-tweaks.sh
Outdated
|
||
echo gpgcheck=0 >> yumrepo.repo | ||
ln "$PWD/yumrepo.repo" config/yumrepo.repo | ||
# the top-level manifest doesn't have any packages, so just set it | ||
treefile_append "packages" '["foobar"]' | ||
treefile_append "packages" $'["\'foobar >= 0.5\' quuz \'corge < 1.0.0\'"]' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, I think a more convincing example is building e.g. two corge
packages, one at 1.0 and one at 2.0, then have the request be for cargo < 2.0
and verify that corge-1.0
is the one that got pulled in.
89c09e3
to
b9fc51b
Compare
/approve (Let's wait to |
/lgtm |
Add a helper function for whitespace_split_packages() so that it now splits a String by whitespace only if it is not wrapped between single quotes. This should allow RHCOS to use syntax like podman > 1.4 in the treefile. Also add new unit tests and tweak existing compose tests to test this functionality.
b9fc51b
to
408a423
Compare
(Rebased this to retrigger the latest CI) |
[APPROVALNOTIFIER] This PR is APPROVED This pull-request has been approved by: cgwalters, jlebon, kelvinfan001 The full list of commands accepted by this bot can be found here. The pull request process is described here
Needs approval from an approver in each of these files:
Approvers can indicate their approval by writing |
Thin release. Mostly for a bunch of RHCOS-relevant fixes and enhancements: 752f4f0 (coreos#2178), 4d836dd (coreos#2151) and f608eb0 (coreos#2158).
Add a helper function for whitespace_split_packages() so that it now splits a String by whitespace only if it is not wrapped between single quotes.
This should allow RHCOS to use syntax like podman > 1.4 in the treefile.