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

Fix save_to_file to be indentation independent #10283

Open
wants to merge 1 commit into
base: develop
Choose a base branch
from

Conversation

ShimShtein
Copy link
Member

Heredoc notation is very sensitive to leading spaces.
This implementation makes the save_to_file macro safe for indentation.

Copy link
Member

@ekohl ekohl left a comment

Choose a reason for hiding this comment

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

I don't think you're going to find any solution that works together with indent.

content += "\n" unless content.end_with?("\n")
"cat << #{delimiter} > #{filename}\n#{content}#{delimiter}"
content_echos = content.split("\n").map do |content_line|
"echo #{Shellwords.shellescape(content_line)} >> #{filename}"
Copy link
Member

Choose a reason for hiding this comment

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

Previously the code allowed to use variables like $var but now that's going to be escaped, making it backwards incompatible. If you want it really verbatim, we have the base64 method above.

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 should be possible now

Copy link
Member Author

Choose a reason for hiding this comment

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

Now the verbatim will escape everything and the else will escape only double quotes.

Copy link
Member

Choose a reason for hiding this comment

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

@adamruzicka you initially introduced this because you found a case where it broke. Do you think this is sufficient?

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't really see where this wouldn't work, but I don't have a good feeling about it.

As a counter-proposal, how about we keep base64, but on the ruby side we drop the newlines from it and then echo it? Something like

content = Base64.encode64(content).tr("\n", "")
"echo #{content} | base64 -d > #{filename}

Copy link
Member Author

Choose a reason for hiding this comment

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

@adamruzicka https://serverfault.com/questions/163371/linux-command-line-character-limit says that there is some limit.

I agree that it's quite long and we probably won't hit it soon, but I preferred not to rely on any of it.
I have tried to work with base64, but somehow I wasn't able to make it work properly with multiple lines and variable name. I can use a tempfile, but it seems a bit of overkill.

Copy link
Contributor

Choose a reason for hiding this comment

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

says that there is some limit.

Well, yes and no. From what I've seen, what's written there applies when launching things that are external to the shell, but it doesn't seem to apply to shell builtins, such as echo in this case.

I have tried to work with base64, but somehow I wasn't able to make it work properly with multiple lines and variable name

How about we mix the two approaches?

b64 = Base64.encode64(content)
content_echos = b64.each_line.map { |l| "echo #{l}" }.join
"(#{content_echos}) | base64 -d >#{filename}"

Copy link
Member Author

Choose a reason for hiding this comment

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

If you go the echo path, you need to accumulate the result somewhere. and once you have the whole base64 string accumulated, you can pass it into base64.
Let me try to accumulate the base64 value without newlines and then pipe it into base64.
Something like:

b64 = Base64.encode64(content)
content_var = b64.each_line.map { |l| "FOO+=#{l}" }.join("\n")
content_var + "echo $FOO | base64 -d >#{filename}"

Mostly I had trouble with the line endings. Maybe in this approach it will be a bit easier.

But let's say this works, we will have different implementations for verbatim and else use cases.
On the other hand, if we go with the escaping approach, we will be able to DRY the code by extracting most of the generation into a single method, and just changing the escaping method of the input.

Copy link
Contributor

Choose a reason for hiding this comment

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

If you go the echo path, you need to accumulate the result somewhere

Well, you can always spawn a subshell just like I did in the previous suggestion.

On the other hand, if we go with the escaping approach, we will be able to DRY the code by extracting most of the generation into a single method, and just changing the escaping method of the input.

True, at the same time we lose the option to write out binary blobs

Copy link
Member Author

Choose a reason for hiding this comment

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

Reimplemented as base64 single string.

@@ -124,8 +124,12 @@ def save_to_file(filename, content, verbatim: false)
content = Base64.encode64(content)
"cat << #{delimiter} | base64 -d > #{filename}\n#{content}#{delimiter}"
Copy link
Member

Choose a reason for hiding this comment

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

This case will still have an issue with indent().

Copy link
Member Author

Choose a reason for hiding this comment

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

Do you think we will have \n symbols there? I can implement the same solution here 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.

Changed the verbatim option to stop using heredoc too

@ShimShtein
Copy link
Member Author

This should fix the indentation error mentioned in #10283

Copy link
Member

@ekohl ekohl left a comment

Choose a reason for hiding this comment

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

This is not a change I feel comfortable cherry picking so I'd still prefer to revert the offending change in the short term and then figure this out.

@ShimShtein
Copy link
Member Author

but then we will be left with broken cloud-init, unless we redesign it as you suggest. Additionally we will be left with save_to_file that is unusable if indented

@ShimShtein
Copy link
Member Author

ShimShtein commented Aug 22, 2024

So I tracked down the whole chain of events:

Now this PR tries to avoid reverting the previous two

@ekohl
Copy link
Member

ekohl commented Aug 22, 2024

* [Fixes #37433 - Set indentation when calling subscription_manager_setup #10200](https://github.com/theforeman/foreman/pull/10200) fixed the indentation problem, but exposed a lingering problem with `save_to_file`, since this helper method does not work well with indentation

I think you mean #10153

@ShimShtein
Copy link
Member Author

Updated the original comment

#!/bin/sh

echo "Calling Ansible AWX/Tower provisioning callback..."
/usr/bin/curl -v -k -s --data "host_config_key=" https:///api/v2/job_templates//callback/
Copy link
Member

Choose a reason for hiding this comment

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

This is missing some interpolated values. Can we somehow set up the environment so those are set?

Copy link
Member Author

Choose a reason for hiding this comment

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

Added another test with interpolated values.

@ShimShtein ShimShtein force-pushed the fix_save_to_file branch 12 times, most recently from 18101e9 to 7b9056c Compare August 27, 2024 18:25
@ShimShtein ShimShtein marked this pull request as ready for review August 27, 2024 18:45
@ShimShtein
Copy link
Member Author

I have replaced base64 implementation with shellescape one. The whole idea of verbatim flag is to place the exact strings in the file as opposed to parameter interpolation in verbatim=false use case.

I have implemented them both in a similar manner with the only difference what symbols should be escaped: in verbatim everything is escaped, in non-verbatim only " is escaped.

end.join("\n")

# prefix the append commands with a cleanup command
"> #{filename}\n#{content_echos}"
Copy link
Contributor

Choose a reason for hiding this comment

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

This relies on stdin being closed, right? Can we safely assume that stdin will be closed in all the scenarios where the save_to_file macro might be used?

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 the implementation does not rely on stdin.

content += "\n" unless content.end_with?("\n")
"cat << #{delimiter} > #{filename}\n#{content}#{delimiter}"
content_echos = content.split("\n").map do |content_line|
"echo #{Shellwords.shellescape(content_line)} >> #{filename}"
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't really see where this wouldn't work, but I don't have a good feeling about it.

As a counter-proposal, how about we keep base64, but on the ruby side we drop the newlines from it and then echo it? Something like

content = Base64.encode64(content).tr("\n", "")
"echo #{content} | base64 -d > #{filename}

@ShimShtein
Copy link
Member Author

@adamruzicka forgot to push the version. Now it should work on all OS-es. What do you think about this implementation?

Copy link
Contributor

@adamruzicka adamruzicka left a comment

Choose a reason for hiding this comment

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

One last question, otherwise lgtm

Comment on lines +123 to +124
base64 = Base64.encode64(content)
content_assignments = base64.split("\n").map { |l| "echo #{Shellwords.shellescape(l)}" }.join("\n")
Copy link
Contributor

Choose a reason for hiding this comment

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

Is the escaping even needed? Base64 shouldn't contain any problematic characters.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants