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

Implement a builder-style provisioning API #91

Merged
merged 1 commit into from
Jul 11, 2024

Conversation

jeremycline
Copy link
Member

This is based on top of PR #86 so I'd recommend just looking at the last commit in the series. It is a first draft of a builder-style interface to allow library users to select the backend for tools when provisioning.

It's not ready to be merged since there's no tests, documentation, or
implementation of default behavior when no backend is explicitly
selected. Additionally, I've not gated backends behind feature flags,
either. I'd love feedback on the general approach and what to do by
default.

My personal opinion is that the default option should iterate through
all available backends until one succeeds.

@dongsupark
Copy link
Collaborator

Code looks good in general. 👍

My personal opinion is that the default option should iterate through
all available backends until one succeeds.

Looks good to me.

@anhvoms
Copy link
Contributor

anhvoms commented Jun 26, 2024

+1 on having the default iterate through all available backend and letting user pass in their own backend

@jeremycline jeremycline force-pushed the provision-builder-api branch 2 times, most recently from 841044a to 8849b20 Compare June 28, 2024 18:41
@jeremycline
Copy link
Member Author

Okay, I've made a few changes and this should have all the things implemented (but not documented yet). At this point I probably could break some of these changes out into smaller PRs, like the ssh authorized_key tests/changes.

After a bunch of fiddling I decided I didn't love the "iterate through all the backends" approach and decided that the user needs to specify which ones they want to use. It makes the interface more verbose, and to be honest I don't really like what I've built, but my experience writing library APIs in Rust is not large.

However, what I've got is, I think, service-able and can do the things we want it to. I've added some feature flags to show how a user could restrict which backends get used.

Copy link
Collaborator

@dongsupark dongsupark left a comment

Choose a reason for hiding this comment

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

I like the approach in general.
While I am in the middle of reading code, I left comments below:

pub struct HostnameBackend;
#[derive(Default)]
pub struct PasswordBackend;
#[derive(Default)]
Copy link
Collaborator

Choose a reason for hiding this comment

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

Maybe UseraddBackend is missing?

libazureinit/src/provision/mod.rs Show resolved Hide resolved
libazureinit/src/provision/mod.rs Show resolved Hide resolved
libazureinit/src/provision/user.rs Outdated Show resolved Hide resolved
libazureinit/src/provision/mod.rs Outdated Show resolved Hide resolved
"Provisioning agent created this user based on username provided in IMDS",
)
.arg("--groups")
.arg("adm,audio,cdrom,dialout,dip,floppy,lxd,netdev,plugdev,sudo,video")
Copy link

@SeanDougherty SeanDougherty Jul 5, 2024

Choose a reason for hiding this comment

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

For Azure Linux, we will want to specify an alternative list of default groups for the user (example).

There's a few different ways to specify this, but perhaps an environment variable is the most logical? As that is how we are handling distro-specific knowledge in the other situations (i.e., binary paths).

To illustrate current behavior, here is a test trying to call path_useradd with this --groups list, and there are a few groups that aren't recognized on AzL. (See photo)
image
*Note: this test was built off of #100 so there may be some additional differences in the code, but the result of calling useradd should be the same as, at the time of this writing, both PRs use the same --groups list

Copy link
Member Author

Choose a reason for hiding this comment

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

Hm, yes. I added a commit to this PR that sets it via environment variable, but I'll mull on it more over the weekend.

I do worry about the discoverability of this approach. I think it's a little weird to have a library accept configuration this way instead of exposing an API and letting the application using the library handle it (either via build-time environment variables, or CLI options, or what have you).

Copy link
Member Author

Choose a reason for hiding this comment

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

Okay, I added a User type in commit 3b4b94b and threaded it through the API. It bundles all the user-related configuration together and allows you to optionally provide a list of supplementary groups. How does that look to you?

Choose a reason for hiding this comment

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

Reran the functional tests on AzL 3.0 using this branch

This has done the trick, thank you Jeremy

image

@jeremycline jeremycline force-pushed the provision-builder-api branch 2 times, most recently from bd29ce6 to 56ddd3f Compare July 5, 2024 21:09
hostname: impl Into<String>,
username: impl Into<String>,
ssh_keys: impl Into<Vec<PublicKeys>>,
) -> Self {
Copy link
Collaborator

Choose a reason for hiding this comment

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

The code looks okay as long as we have only 3 parameters for new().

Just a thought. I am wondering how much scalable it could be, for example when we would have 10 - 20 backends.
Then callers need to pass 10 - 20 parameters to new(), which could be tricky to follow and maintain.
However, as far as I understand, Rust does not have a native way of dealing variable args, right?
So we might need to split those into separate structs in the future.

Copy link
Member Author

@jeremycline jeremycline Jul 9, 2024

Choose a reason for hiding this comment

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

Yeah, new() is just for required arguments and any change there would be an API breakage. I felt like these three would always be required, but we could also accept no arguments at all to new() and if you don't want to create a user for whatever reason, you don't have to. At that point, though, I don't know why you'd use the API.

Copy link
Member Author

Choose a reason for hiding this comment

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

When adding the group I added a dedicated type for user configuration so it reduces the number of parameters to two.


/// A list of supplemental group names to add the user to.
///
/// If any of the groups do not exist on the host, provisioning will fail.
Copy link
Member Author

Choose a reason for hiding this comment

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

We could, instead, probe for groups that exist and only add the user to those groups. I don't love that because it feels spooky and unexpected ("I asked it to add the user to these groups and it didn't, but also didn't fail"), but on the other hand you wouldn't have to hand-tune the list for each distribution.

@jeremycline jeremycline changed the title RFC: Implement a builder-style provisioning API Implement a builder-style provisioning API Jul 11, 2024
@jeremycline jeremycline marked this pull request as ready for review July 11, 2024 15:39
This provides a unified interface to provision the host with the option
to select the tool used for setting the hostname, creating the user, and
so on.

By default, the library will try all the provisioning methods it knows
of until one succeeds. Users of the library can optionally specify a
subset to attempt when provisioning.

This allows users to decide which tool or tools to use when
provisioning. Some feature flags have been added to `azure-init` which
enable provisioning with a tool, letting you build binaries for a
particular platform relatively easily.
@jeremycline
Copy link
Member Author

Okay, I cleaned up the Rust docs and ensured all the publicly exported types have documentation, I think this is now ready.

Copy link
Collaborator

@dongsupark dongsupark left a comment

Choose a reason for hiding this comment

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

Thanks

@dongsupark dongsupark merged commit bc83b93 into Azure:main Jul 11, 2024
5 checks passed
@peytonr18
Copy link
Contributor

Thank you Jeremy!!!

@jeremycline jeremycline deleted the provision-builder-api branch July 11, 2024 16:56
balakreddy pushed a commit to balakreddy/azure-init that referenced this pull request Jul 19, 2024
Implement a single `Provision` interface

This provides a unified interface to provision the host with the option
to select the tool used for setting the hostname, creating the user, and
so on.

By default, the library will try all the provisioning methods it knows
of until one succeeds. Users of the library can optionally specify a
subset to attempt when provisioning.

This allows users to decide which tool or tools to use when
provisioning. Some feature flags have been added to `azure-init` which
enable provisioning with a tool, letting you build binaries for a
particular platform relatively easily.
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.

5 participants