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

Feature/registry alias #273

Open
wants to merge 89 commits into
base: main
Choose a base branch
from

Conversation

dgehriger
Copy link

@dgehriger dgehriger commented Nov 21, 2024

This PR introduces a whole set of features which are all related to implement a comprehensive build workflow.

Important

This PR bumps the Proto.toml edition to 0.10.

buffrs --insecure ...

The -k / --insecure general flag skips SSL validation during operations. Useful for environments with self-signed certificates or testing scenarios, such a Docker containers.

buffrs install --only-dependencies

Adding this flag during buffrs install (e.g., buffrs install --only-dependencies) installs only the vendor, while leaving a package's own proto files untouched.

buffrs install --generate-buf-yaml <file> [<prefix>]

Adding this flag during buffrs install (e.g., buffrs install --generate-buf-yaml) generates a buf.yaml file in the proto directory matching installed dependencies during dependency resolution. This enables seamless use of buf for linting and generating proxies/stubs from proto files.

The flag will either create a new buf.yaml file from scratch or update an existing one with the latest dependencies.
IDE extensions, such as the Buf Linter for Visual Studio Code, will readily pick up the buf.yaml file and provide linting capabilities. A typical buf.yaml file looks like this:

version: v2
modules:
  - path: "."
    excludes:
      - vendor
  - path: vendor/api-movie-player
  - path: vendor/api-robot-arm-motion-control
  - path: vendor/lib-geom
  - path: vendor/lib-waypoints
deps:
  - buf.build/googleapis/googleapis
  - buf.build/grpc/grpc
lint:
  use:
    - DEFAULT
  except:
    - PACKAGE_VERSION_SUFFIX
breaking:
  use:
    - FILE

Repository Configuration File (repo-root/.buffrs/config.toml)

A new configuration file, .buffrs/config.toml, can be added to a repository to specify various options for the buffrs tool. This file is optional and can be used to set default values for commands, such as buffrs install, or to specify registry aliases and a default registry.

Note

There was a lot of discussion about the location and name of the configuration file. In particular, it was suggested that it should be placed at $HOME/.config/buffrs.toml. It is true that there is a general move to collect configuration files in $HOME/.config/, but this does not apply to this case, because the purpose of the configuration file is to specify options for a specific repository. Therefore, it makes sense to place the configuration file in the root of the repository.

Further research provided no indication that a common configuration folder is used for repositories in a similar fashion as for $HOME directory. This was confirmed by Mara Schulke. Therefore, this PR keeps the file at its original location.

The configuration has the following structure:

edition = "0.10"

[registries]
acme = "https://conan.acme.com/artifactory"

[registry]
default = "acme"

[command]
default_args = ["--insecure"]

[commands.install]
default_args = ["--generate-buf-yaml"]
  • edition: Specifies the edition of the configuration file. This field is mandatory and must be set to the current edition of the configuration file, which mirrors the edition of the Proto.toml file.
  • registries: Specifies registry aliases. The key is the alias, and the value is the actual registry URL.
  • registry: Specifies the default registry to use when no registry is specified on the command line.
  • command: Specifies default arguments for the buffrs tool using the default_args field. The default_args field is an array of default arguments.
  • commands.<command>: Specifies default arguments for a specific command. The default_args field is an array of default arguments.

Registry Aliases

If configured in the configuration file, registry aliases can be used anywhere a registry URL is expected. This simplifies the use of registries by providing a human-readable alias instead of a full URL.

Aliases on the Command Line

Aliases can be used on the command line by specifying the --registry flag with the alias name. For example:

buffrs publish --registry acme --repository grpc

Aliases in Proto.toml

The registry field in Proto.toml now supports aliases. For instance:

edition = "0.10"

[package]
type = "api"
name = "grpc-test"
version = "0.1.0"

[dependencies.api-examples-hub]
version = "=0.2.0"
registry = "acme" # Alias for "https://conan.acme.com/artifactory"
repository = "grpc"

Alias resolution during package and publish

Buffrs resolves registry aliases when packing or publishing to an upstream repository. To ensure compatibility, the original Proto.toml is preserved as Proto.toml.orig, while the normalized version is used during operations. This mirrors Cargo's behavior.

Example:

Original Proto.toml:

edition = "0.10"

[package]
type = "api"
name = "api-examples-test"
version = "0.3.0"

[dependencies.lib-geom]
version = "=0.1.0"
registry = "acme"
repository = "grpc"

Generated Proto.toml:

# THIS FILE IS AUTOMATICALLY GENERATED BY BUFFRS
#
# When uploading packages to the registry buffrs will automatically
# "normalize" Proto.toml files for maximal compatibility
# with all versions of buffrs and also rewrite `path` dependencies
# to registry dependencies.
#
# If you are reading this file be aware that the original Proto.toml
# will likely look very different (and much more reasonable).
# See Proto.toml.orig for the original contents.
edition = "0.10"

[package]
type = "api"
name = "api-examples-test"
version = "0.3.0"

[dependencies.lib-geom]
version = "=0.1.0"
registry = "https://conan.acme.com/artifactory"
repository = "grpc"

Improved Path Dependency Handling

buffrs already supports path dependencies, e.g.

edition = "0.10"

[dependencies]
lib-geom = { path = "../lib-geom" }

This PR adds the following new features:

Nested Path Dependencies

Proto packages may now specify a path dependency that itself has path dependencies, improving fast turnaround times during development.

Path Dependency and Registry Dependency

Similar to Cargo, Proto packages may now specify a path dependency and a registry dependency for the same package. This is useful for development scenarios where a package references a local package, but, once published, needs to reference a registry package.

edition = "0.10"

[dependencies]
lib-geom = { path = "../lib-geom", version = "=0.1.0", repository = "grpc", registry = "acme" }

When publishing, the path dependency is dropped, and only the registry dependency is maintained during alias resolution. The original Proto.toml is preserved as Proto.toml.orig.

Even during development, the version constraint will be checked against version found at the local path.

Code Refactoring

CLI code moved to cli.rs

The CLI code formerly in main.rs has been moved to cli.rs, and and is invoked from main.rs through a cli::run() function. The main purpose of this change is to allow buffrs to be invoked from build.rs scripts simply by declaring buffrs as a dev-dependency in Cargo.toml, without having to install the binary itself.

dgehriger and others added 30 commits June 25, 2024 21:43
Add .buffrs/config.toml registries and hierarchical packages
Bumps [gix-path](https://github.com/Byron/gitoxide) from 0.10.7 to 0.10.10.
- [Release notes](https://github.com/Byron/gitoxide/releases)
- [Changelog](https://github.com/Byron/gitoxide/blob/main/CHANGELOG.md)
- [Commits](GitoxideLabs/gitoxide@gix-path-v0.10.7...gix-path-v0.10.10)

---
updated-dependencies:
- dependency-name: gix-path
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
# Conflicts:
#	Cargo.lock
#	Cargo.toml
#	src/main.rs
#	src/manifest.rs
#	src/registry/mod.rs
#	tests/cmd/publish/in
#	tests/cmd/publish/out
@dgehriger
Copy link
Author

@mara-schulke : some of the unit tests are failing due to the change in the manifest version (0.9 → 0.10). I'm fixing this now and will update.

@mara-schulke
Copy link
Contributor

Hi this is great thank you so much! One note before I'd start a detailed review: The proto rust module is not necessary as this is covered by Tonic itself:

https://docs.rs/tonic-build/latest/tonic_build/struct.Config.html#method.include_file

Could you maybe revert that command?

Once done I'm happy to review and merge this asap!

src/main.rs Show resolved Hide resolved
src/package/compressed.rs Outdated Show resolved Hide resolved
src/package/compressed.rs Outdated Show resolved Hide resolved
src/package/compressed.rs Outdated Show resolved Hide resolved
src/package/compressed.rs Outdated Show resolved Hide resolved
src/registry/artifactory.rs Outdated Show resolved Hide resolved
src/registry/artifactory.rs Outdated Show resolved Hide resolved
src/registry/artifactory.rs Outdated Show resolved Hide resolved
src/registry/mod.rs Outdated Show resolved Hide resolved
src/registry/mod.rs Show resolved Hide resolved
@mara-schulke
Copy link
Contributor

Im not entirely done with reviewing but I've commented some points of concern above so they can be already addressed

@mara-schulke
Copy link
Contributor

You should not invoke the CLI level code in a build.rs as doing network requests as part of your build process is going to yield you flaky builds

I'm not keen on re introducing this pattern (this was actually once possible with buffrs - it turned out to be causing all sorts of issues). Scheduling the installation step of your protos is better done by using (or better integrating buffrs into) a proper build system for your project like nix, bazel etc.

We have done this at Helsing and it works great, incl caching etc.

@dgehriger
Copy link
Author

dgehriger commented Dec 1, 2024

One note before I'd start a detailed review: The proto rust module is not necessary as this is covered by Tonic itself:

docs.rs/tonic-build/latest/tonic_build/struct.Config.html#method.include_file

Could you maybe revert that command?

What a joke... I wasn't aware of this. Of course. a5651c1

Copy link
Contributor

@mara-schulke mara-schulke 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 definitely going in the right direction although I feel like this is loosening the type safety and semantic guarantees we currently provide. I'm happy to have another chat about this to get it over the line!

src/command.rs Show resolved Hide resolved
src/config.rs Show resolved Hide resolved
config_path: Option<PathBuf>,

/// Default registry alias to use if none is specified
default_registry: Option<String>,
Copy link
Contributor

Choose a reason for hiding this comment

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

Last new type request: Could we use a registry name new type to ensure that this string is infact a valid registry name?

I would suggest using a similar convention as with package names

Copy link
Contributor

Choose a reason for hiding this comment

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

You could basically derive / share a lot of logic with the PackageName type

/// - None -> alias://<default>
pub fn parse_registry_arg(&self, registry: &Option<String>) -> miette::Result<RegistryRef> {
match registry {
Some(registry) => RegistryRef::from_str(registry),
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: .parse is more idiomatic imo

}

impl Config {
/// Create a new configuration with default values
Copy link
Contributor

Choose a reason for hiding this comment

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

This comment lies

Could you roughly follow the public api semantics from the manifest / cache?

Copy link
Author

Choose a reason for hiding this comment

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

Can you give me specifics?

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah yes sorry.

So this function will do IO although the comment says it will create a new one with default values!

With regards to the common pattern I was referring to the Manifest struct for example

/// A URL to a registry
Url(RegistryUri),
/// An alias to a registry
Alias(String),
Copy link
Contributor

Choose a reason for hiding this comment

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

In my understand you have two types:

RegistryRef (a reference, potentially aliased registry) and RegistryUri the uri version, independent of its origin

Copy link
Author

@dgehriger dgehriger Dec 6, 2024

Choose a reason for hiding this comment

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

You mean we could express the Alias as Alias(String, Option<RegistryUri>) ?

I found it safer to not use the option, but a third type. But for aliases, we need to remember the alias name and URL at all times, as, depending on the context, we do need to be able to access the alias, even if resolved.

What could be done is to split this enum into two types:

enum RawRegistryRef {
    Url(RegistryUri),
    Alias(String),
};

enum RegistryRef {
    Url(RegistryUri),
    Alias(String, RegistryUri),
};

The former is returned just after parsing the config files / command lines, and the latter is the result of resolving the aliases.

Copy link
Contributor

Choose a reason for hiding this comment

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

Well I'm talking about:

enum RegistryRef {
    Uri(Uri)
    Alias(RegistryName)
}

impl RegistryRef {
    fn resolve(config: &Config) -> Result<RegistryUri> {
        // 
    }
}

struct RegistryUri(Uri);

Copy link
Contributor

Choose a reason for hiding this comment

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

Why do you need to remember aliases here?

Like you can do the resolving as late as possible (and still keep the original registry ref around)

/// An alias to a registry
Alias(String),
/// A resolved alias to a registry
ResolvedAlias {
Copy link
Contributor

Choose a reason for hiding this comment

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

Thus this case seems both type unsafe and unnecessary to me as you would want to go from RegistryRef to RegistryUri during alias resolution

/// Get the raw URL of the registry with any alias resolved
///
/// # Arguments
/// * `config` - The configuration to use to resolve the alias
Copy link
Contributor

Choose a reason for hiding this comment

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

Ie this could be typesafe!

}

/// Serializer for resolved RegistryUris
pub fn serialize_resolved<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
Copy link
Contributor

Choose a reason for hiding this comment

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

This aswell.

///
/// # Arguments
/// * `url` - The URL to check
pub fn sanity_check_url(url: &Url) -> miette::Result<()> {
Copy link
Contributor

Choose a reason for hiding this comment

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

Please don't make this part of the public api

@mara-schulke
Copy link
Contributor

Okay, let's cut any refactorings or feature changes to get it merged asap. If you could tackle the code quality topics I've raised I would be glad. Once you are happy please give me another go and I'll have a look!

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.

2 participants