-
Notifications
You must be signed in to change notification settings - Fork 519
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
Add support for static DNS mappings #2129
Conversation
LGTM so far! Once the migrations are out I will give it another look-see |
351f82a
to
920e6b2
Compare
|
I'm going to make a few changes to the |
A few changes:
|
c504f46
to
1b9f780
Compare
I keep ending up having strange conflicts with develop. Will fix the git tree. |
Fixed, as well as fixed some code clarity issues. |
sources/models/src/lib.rs
Outdated
// Ordering matters in /etc/hosts if multiple domains point to the same IP, so we use BTreeMap to preserve order. | ||
hosts: BTreeMap<IpAddr, Vec<ValidLinuxHostname>>, | ||
https_proxy: Url, |
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've been doing some extra reading about BTreeMap
and my assumption that it preserves iteration order based on insertion is false: BTreeMap preserves iteration order based on key order. The common solution in the Rust community is to use IndexMap for a map which preserves insertion order.
This also means that I would need to assert that every serializer and deserializer which touches settings objects respects ordering. Fortunately for serde_json
and toml
this can be enabled by adding a feature flag, but we have a few points e.g. in migrations
where helpers assume that a Map
can be safely represented via a HashMap.
I'm looking into solving this either by building in order preservation to settings (which could be extremely difficult to continuously enforce in the Bottlerocket repo, even with great testing in the core settings path) or by changing our model here to use an ordered data structure, although this adds some uncomfortable ergonomic questions over how to use the feature.
aa83b3b
to
ad263da
Compare
I reworked the static DNS feature to use a While it does make it a bit less ergonomic to specify mappings due to the use of lists to represent pairs, it safely models the behavior of |
Force-pushed to rebase atop develop. |
sources/api/schnauzer/src/helpers.rs
Outdated
// If our hostname isn't resolveable, add it to the alias list. | ||
if hostname.len() > 0 && !hostname_resolveable(hostname) { | ||
results.push(hostname.to_owned()); | ||
} |
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 think there's an edge case here where the hostname isn't initially resolvable, but should resolve to a non-localhost IP provided in settings.network.hosts
, after /etc/hosts
is populated.
In that case, we'd incorrectly write /etc/hosts
with the hostname as an alias for localhost.
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 catch! I'll fix that and see if I can't add a unit test to cover that edge case.
sources/api/schnauzer/src/helpers.rs
Outdated
// Cast aside anything that doesn't parse as an IP Address, or any references to localhost. | ||
*ip_address != IPV4_LOCALHOST && *ip_address != IPV6_LOCALHOST |
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.
Where are we casting aside anything that doesn't parse as an IP address?
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.
Whoops, holdover comment. This used to be much more "stringly typed" but I ended up routing all of the parsing through the models
-- much cleaner now.
Force-pushed to address comments by @bcressey. Added a unit test for the edge case. Additional testing of that change: https://gist.github.com/cbgbt/d0e7789fe3a9a12e2b59e59b6f2f0278 |
LGTM once |
Force-push to fix |
sources/api/schnauzer/src/helpers.rs
Outdated
let template_name = template_name(renderctx); | ||
trace!("Template name: {}", &template_name); | ||
|
||
// Check number of parameters, must be exactly one | ||
// Check number of parameters, must be exactly two (IP version, hostname, hosts overrides) |
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.
nit: should the number be three?
sources/api/schnauzer/src/helpers.rs
Outdated
// Check number of parameters, must be exactly two (IP version, hostname, hosts overrides) | ||
trace!("Number of params: {}", helper.params().len()); | ||
check_param_count(helper, template_name, 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.
nit: another inconsistent count?
sources/api/schnauzer/src/helpers.rs
Outdated
@@ -1128,6 +1267,41 @@ fn kube_cpu_helper(num_cores: usize) -> Result<String, TemplateHelperError> { | |||
)) | |||
} | |||
|
|||
/// Returns whether or not a hostname resolves to a non-loopback IP address. | |||
/// | |||
/// If `configured_hosts` is Some, the hostname will be considered resolvable if it is listed as an alias for any given IP address. |
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.
nit:
/// If `configured_hosts` is Some, the hostname will be considered resolvable if it is listed as an alias for any given IP address. | |
/// If `configured_hosts` is set, the hostname will be considered resolvable if it is listed as an alias for any given IP address. |
sources/api/schnauzer/src/helpers.rs
Outdated
#[test] | ||
fn hostname_resolves_to_static_mapping() { | ||
// Given a configured hostname that does not resolve in DNS | ||
// and an /etc/hosts configuration which points that contains that hostname as an alias to an IP address, |
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.
Nit: I think there are extra words here? Should the line be:
// and an /etc/hosts configuration which points that contains that hostname as an alias to an IP address, | |
// and an /etc/hosts configuration that contains that hostname as an alias to an IP address, |
|
||
#[test] | ||
fn test_valid_etc_hosts_entries() { | ||
assert!(serde_json::from_str::<EtcHostsEntries>( |
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.
why can't all these tests be part of the same assert? Same question similar tests below.
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've tried to logically separate different checks into multiple assert lines here, so if one fails it's easier to reason about what behavior the check was testing for.
README.md
Outdated
Repeated entries are merged (including loopback entries), with the first aliases listed taking precedence. e.g.: | ||
|
||
```toml | ||
[settings.network] | ||
hosts = [ | ||
["10.0.0.0", ["test.example.com", "test1.example.com"]], | ||
["10.1.1.1", ["test2.example.com"]], | ||
["10.1.1.0", ["test3.example.com"]], | ||
] | ||
``` | ||
Would result in `/etc/hosts` entries like so: | ||
``` | ||
10.0.0.0 test.example.com test1.example.com test3.example.com | ||
10.1.1.1 test2.example.com | ||
``` |
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.
Hi I'm trying to validate if the current logic will work for our use-case.
-
Why in the example test3.example.com (
10.1.1.0
) is merged with the10.0.0.0
line and thus effectively rewritten to10.0.0.0
? -
Is it possible with the current logic to have one hostname to resolve to multiple IPs? The input syntax seems to support it. Just want to make sure no de-duplication will happen in this case. We will require a final
/etc/hosts
file that looks something like this:
10.0.2.252 test1.example.com
10.0.2.236 test1.example.com
10.0.3.205 test1.example.com
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.
Hi, thanks for taking a look at this PR.
- This is a documentation mistake, I meant for
10.1.1.0
to be a repeat of10.0.0.0
. I'll fix that. - Yes, you can have one hostname resolve to multiple IPs. De-duplication will only occur for IPs, not hostnames. If you submit something like the following, you should end up with
/etc/hosts
entries like the ones you've given:
hosts = [
["10.0.2.252", ["test1.example.com"]],
["10.0.2.236", ["test1.example.com"]],
["10.0.3.205", ["test1.example.com]]
]
Force push to address comments from @arnaldo2792 |
Additional push to address README issue pointed out by @fgutmann. Thanks! |
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 cursed with the ability to always see a way to unit test. Otherwise this looks great.
README.md
Outdated
@@ -502,11 +502,27 @@ In addition to the container runtime daemons, these credential settings will als | |||
|
|||
* `settings.network.hostname`: The desired hostname of the system. | |||
**Important note for all Kubernetes variants:** Changing this setting at runtime (not via user data) can cause issues with kubelet registration, as hostname is closely tied to the identity of the system for both registration and certificates/authorization purposes. | |||
* `settings.network.hosts`: A mapping of IP addresses to domain names which should resolve to those IP addresses. | |||
This setting results in modifications to the `/etc/hosts` file for Bottlerocket. | |||
Note that this setting does not typically impact name resolution for containers, which usually rely on [orchestrator-specific](https://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_HostEntry.html) [mechanisms](https://kubernetes.io/docs/tasks/network/customize-hosts-file-for-pods/) for configuring static resolution. |
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.
Just a suggestion...
Note that this setting does not typically impact name resolution for containers, which usually rely on [orchestrator-specific](https://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_HostEntry.html) [mechanisms](https://kubernetes.io/docs/tasks/network/customize-hosts-file-for-pods/) for configuring static resolution. | |
Note that this setting does not typically impact name resolution for containers, which usually rely on orchestrator-specific mechanisms for configuring static resolution. | |
(See [ECS](https://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_HostEntry.html) and [Kubernetes](https://kubernetes.io/docs/tasks/network/customize-hosts-file-for-pods/) documentation for those mechanisms.) |
README.md
Outdated
Entries which point to the loopback address are merged as aliases with the lowest precedence on the existing loopback line. | ||
|
||
|
||
|
||
Most users don't need to change this setting as the following defaults work for the majority of use cases. | ||
If this setting isn't set we attempt to use DNS reverse lookup for the hostname. | ||
If the lookup is unsuccessful, the IP of the node is used. | ||
|
||
##### Proxy settings |
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.
nit: newline crimes were committed
Entries which point to the loopback address are merged as aliases with the lowest precedence on the existing loopback line.
Most users don't need to change this setting as the following defaults work for the majority of use cases.
If this setting isn't set we attempt to use DNS reverse lookup for the hostname.
If the lookup is unsuccessful, the IP of the node is used.
##### Proxy settings
sources/api/schnauzer/src/helpers.rs
Outdated
let template_name = template_name(renderctx); | ||
trace!("Template name: {}", &template_name); | ||
|
||
// Check number of parameters, must be exactly one | ||
// Check number of parameters, must be exactly two (IP version, hostname, hosts overrides) |
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.
// Check number of parameters, must be exactly two (IP version, hostname, hosts overrides) | |
// Check number of parameters, must be exactly three (IP version, hostname, hosts overrides) |
sources/api/schnauzer/src/helpers.rs
Outdated
.param(1) | ||
.map(|v| v.value()) | ||
.context(error::InternalSnafu { | ||
msg: "Missing params after confirming that they exist.", |
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.
Maybe since we use this same message all over we could add ParamUnwrapSnafu variant with this message baked in and eliminate the strings from the code.
msg: "Missing params after confirming that they exist.",
sources/api/schnauzer/src/helpers.rs
Outdated
if let Ok(ip_address) = ip_address.parse::<IpAddr>() { | ||
ip_address == localhost_comparator | ||
} else { | ||
false | ||
} |
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.
Golfable if desired
if let Ok(ip_address) = ip_address.parse::<IpAddr>() { | |
ip_address == localhost_comparator | |
} else { | |
false | |
} | |
ip_address.parse::<IpAddr>().map(|ip_address| ip_address == localhost_comparator).unwrap_or_default() |
sources/api/schnauzer/src/helpers.rs
Outdated
if hosts_value.is_null() { | ||
// If hosts aren't set, just exit. | ||
Ok(()) | ||
} else { |
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's a stylistic preference, but I would return here and unindent the "else" code.
if hosts_value.is_null() { | |
// If hosts aren't set, just exit. | |
Ok(()) | |
} else { | |
if hosts_value.is_null() { | |
// If hosts aren't set, just exit. | |
return Ok(()) | |
} |
trace!("Hosts from template: {:?}", hosts); | ||
|
||
// If our hostname isn't resolveable, add it to the alias list. | ||
if hostname.len() > 0 && !hostname_resolveable(hostname, hosts.as_ref()) { |
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 was trying to think of a way to remove this impurity from this otherwise testable (and complex) function. Alas, I don't see a way to inject this network dependency.
Edit, well... as usual I did think of a way:
localhost_aliases
could be a thin wrapper for localhost_aliases_impl
.
localhost_aliases_impl
takes a functor matching the signature of hostname_resolvable
, then localhost_aliases
becomes:
pub fn localhost_aliases(
helper: &Helper<'_, '_>,
h: &Handlebars,
c: &Context,
renderctx: &mut RenderContext<'_, '_>,
out: &mut dyn Output,
) -> Result<(), RenderError> {
localhost_aliases_impl(helper, h, c, renderctx, out, &hostname_resolvabe)
}
Then I think you could write tests for localhost_aliases_impl
that takes a mock of the hostname_resolvable
function.
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 love function purity. Thanks for the tip, I'll implement this.
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 toyed around with implementing something like this, but it ended up making it more challenging to understand and didn't necessarily improve testability, since there are a few tests here already which just use more apparently resolve-able or un-resolve-able hostnames.
.append(&mut (aliases.clone())); | ||
} | ||
|
||
merged.into_iter() |
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 rust.
fd7577f
to
d6ab9ae
Compare
Force push to make some stylistic and comment changes suggested by @webern |
If this setting isn't set we attempt to use DNS reverse lookup for the hostname. | ||
If the lookup is unsuccessful, the IP of the node is used. | ||
|
||
* `settings.network.hosts`: A mapping of IP addresses to domain names which should resolve to those IP addresses. |
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.
Should we explain what the default is when this setting is not set? Or maybe it's obvious enough, and it would be too verbose.
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 think it might be obvious enough to omit. I noticed that we don't always include the default value, for settings as well.
This allows users to populate /etc/hosts with static DNS entries via settings and the Bottlerocket API. Bottlerocket doesn't currently perform resolver caching, so there is no need to restart networked services after rendering a new /etc/hosts file.
Force push to rebase. |
Issue number: #2045
Description of changes:
According to
man hosts:
Today, this file is still used to configure a static lookup table for hostnames on Linux. The hosts file is a text file that associates IP addresses (v4 or v6) with hostnames, one line per IP address.
e.g.
Would ensure that
myhost.example.com
andmyhost
both resolve to10.0.0.1
. The man page explicitly calls out the first element as being thecanonical_hostname
, but there are no technical requirements that differentiate this to aliases — each element must:The modeled settings enforce these requirements.
Per #921, we don't support DNS caching on Bottlerocket, so we shouldn't need to restart any additional services when we modify static DNS mappings.
Testing done:
Example of using the feature:
Terms of contribution:
By submitting this pull request, I agree that this contribution is dual-licensed under the terms of both the Apache License, version 2.0, and the MIT license.