diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000000..f1219ac067 --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,73 @@ +# Architecture + +This codebase is set up as a [Cargo workspace](https://doc.rust-lang.org/book/ch14-03-cargo-workspaces.html). The `rover` binary is built from the root `bin` target, which is a thin wrapper around the `rover-client` library crate. + +## CLI Design + +Great thought and attention has been paid to Rover's design, and any new command surface must be considered holistically. + +### Command layout + +Rover commands are laid out as `rover [NOUN] [VERB]` to create clear separation of concerns for multiple areas of graph management. + +### Designing new commands + +Generally, we are hesitant to add a new `NOUN` to Rover's surface area, unless there is a clear and real need. + +An example of a clear need is the `graph` vs. `subgraph` vs. `supergraph` command structure. Each of these nouns has similar associated verbs. + +Let's look at the `fetch` commands as an example. `rover graph fetch` and `rover subgraph fetch` each take a positional required `` argument, and `subgraph fetch` also has a required `--subgraph` flag. It really looks like there doesn't need to be differentiation between these commands. We could have made this behavior implicit by making `--subgraph` optional, and only returning a subgraph schema if the `--subgraph` argument was provided. + +The problem with this approach is that having two different return types from the same command leads to unexpected results and makes it difficult to understand the mental model needed to work with the graph registry. Additionally, it would have made it difficult to design commands that _only_ exist for `subgraphs`, and vice versa (such as `rover subgraph check`). + +In general, it is best to keep related commands together, and to avoid cognitive complexity wherever possible. New commands should either be associated with an existing top-level noun, or a new noun should be proposed. + +### Error Handling + + + +### Project Structure + +- `Cargo.toml`: crate metadata, including definitions for both internal and external dependencies + +- `Cargo.lock`: an autogenerated lockfile for Rover's dependencies + +- `src`: the `rover` CLI + - `src/bin/rover.rs`: the entry point for the CLI executable + - `src/command`: logic for the CLI commands + - `src/command/output.rs`: Enum containing all possible `stdout` options for Rover commands + - `src/command/{command_name}/mod.rs`: Contains the definition of Rov + - `src/utils`: shared utility functions + - `src/error`: application-level error handling including suggestions and error codes + - `src/cli.rs`: Module containing definition for all top-level commands + - `src/lib.rs`: all the logic used by the CLI + +- `tests`: Integration tests + +- `crates` + - `crates/houston`: logic related to configuring rover + - `crates/robot-panic`: Fork of `human-panic` to create panic handlers that allows users to submit crash reports as GitHub issues + - `crates/rover-client`: logic for querying apollo services + - `crates/sputnik`: logic for capturing anonymous usage data + - `crates/timber`: output formatting and logging logic + - `crates/xtask`: logic for building and testing Rover + +- `.cargo`: Sets up `cargo xtask` commands in the workspace + +- `docs` + - `source/*.md`: Individual documentation pages + - `source/assets`: Images and other resources + - `static/_redirects`: [Netlify redirects](https://docs.netlify.com/routing/redirects/) + +- `netlify.toml`: Configuration for Rover's [docs](https://apollographql.com/docs/rover) + +- `installers` + - `binstall`: Rover's cross-platform installer that downloads and installs prebuilt binaries + - `npm`: Rover's npm installer that downloads and installs prebuilt binaries + +- `.github` + - `ISSUE_TEMPLATE`: Issues templates for our GitHub repository + - `workflows/lint.yml`: GitHub Action that checks for idiomatic code style, proper formatting, and broken markdown links + - `workflows/release.yml`: GitHub Action that builds cross-platform binaries and creates a GitHub release when a version is tagged + - `workflows/test.yml`: Runs integration and unit tests on each commit that is pushed to GitHub + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b2a6ad95d0..51f99698a4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,7 +4,7 @@ > external feature contributors, though some documentation contributions may be > accepted. -## Prerequisites +## Prerequisites Rover is written in [Rust]. In order to contribute, you'll need to have Rust installed. To install Rust, visit [https://www.rust-lang.org/tools/install]. @@ -19,6 +19,17 @@ To build the CLI: cargo build ``` +To build the CLI without `rover supergraph compose` (for Alpine Linux): +```bash +cargo build --no-default-features +``` + +To cross-compile Rover for different platforms, you can run the following, where `TARGET` is one of Rust's [supported platforms](https://doc.rust-lang.org/stable/rustc/platform-support.html): +```bash +rustup target add +cargo build --target +``` + To build and run the CLI with a set of arguments: ```bash cargo run -- @@ -43,11 +54,13 @@ cargo test --workspace To format your code: ```bash +rustup component add rustfmt cargo fmt --all ``` To lint your code: ```bash +rustup component add clippy cargo clippy ``` @@ -66,28 +79,35 @@ cargo xtask test [`cargo`]: https://doc.rust-lang.org/cargo/index.html [https://www.rust-lang.org/tools/install]: https://www.rust-lang.org/tools/install -## Project Structure +### IDEs + +The Rover team primarily uses [VS Code](https://code.visualstudio.com/) along with [rust-analyzer](https://rust-analyzer.github.io/manual.html) when developing Rover. `rust-analyzer` can also be used with [other IDEs](https://rust-analyzer.github.io/manual.html#installation) if you are more familiar with something else. + +## How to contribute + +### Using issues -- `src`: the `rover` CLI - - `src/bin/rover.rs`: the entry point for the CLI executable - - `src/command`: logic for the CLI commands - - `src/command/output.rs`: Enum containing all possible `stdout` options for Rover commands - - `src/utils`: shared utility functions - - `src/error`: application-level error handling including suggestions and error codes - - `src/cli.rs`: Module containing definition for all top-level commands - - `src/lib.rs`: all the logic used by the CLI +The Rover team works largely in public using GitHub [issues] to track work. To make sure contributions are aligned with the project's goals, keep the following issue etiquette in mind: -- `crates` - - `crates/houston`: logic related to configuring rover - - `crates/robot-panic`: Fork of robot-panic to create helpful panic handlers - - `crates/rover-client`: logic for querying apollo services - - `crates/sputnik`: logic for capturing anonymous usage data - - `crates/timber`: output formatting and logging logic +1. [Open an issue](https://github.com/apollographql/rover/issues/new/choose) for your contribution. If there is already an issue open, please ask if anyone is working on it or let us know you plan on working on it. This will let us know what to expect, help us to prioritize reviews, and ensure there is no duplication of work. +1. Use issue templates! These templates have been created to help minimize back-and-forth between creators and the Rover team. They include the necessary information to help the team triage your issue or question, as well as automatically applying the appropriate labels. +1. Issues with the `triage` label still applied have not yet been reviewed by the Rover team, and there are no guarantees that PRs fixing an untriaged issue will be accepted. It's best to wait for issues to be triaged before beginning work. +[issues]: https://github.com/apollographql/rover/issues -## Documentation +### Submitting a Pull Request -Documentation for using and contributing to rover is built using Gatsby and [Apollo's Docs Theme for Gatsby](https://github.com/apollographql/gatsby-theme-apollo/tree/master/packages/gatsby-theme-apollo-docs). +Pull requests (PRs) should only be opened after discussion and consensus has been reached in a related issue, and you have communicated your intentions to create a PR with the Rover team. + +1. When creating a PR, make sure to link it to an issue or use the `Fixes #123` syntax to make sure others know which issue(s) your PR is trying to address and to help us automatically close resolved issues. +1. Include a helpful description. It is important to provide context to reviewers that show _how_ your PR addresses an issue and any questions you still have unanswered, or portions of the code you think deserve some extra attention. +1. If your work is still in-progress and you're opening a PR to get early feedback, let us know by opening it as a draft PR and adding `wip:` prefix in the PR title. +1. Add tests for any logic changes in your code, especially if you are fixing a bug. Your PR should have no failing tests before merging. Please let us know if you need help writing tests, there are still some portions of the Rover codebase that do not have established testing patterns. +1. Add a changelog entry in [CHANGELOG.md](https://github.com/apollographql/rover/blob/main/CHANGELOG.md) under the `Unreleased` heading, following the pattern of previous entries. + +### Documentation + +Documentation for using and contributing to Rover is built using Gatsby and [Apollo's Docs Theme for Gatsby](https://github.com/apollographql/gatsby-theme-apollo/tree/master/packages/gatsby-theme-apollo-docs). To contribute to these docs, you can add or edit the markdown & MDX files in the `docs/source` directory. @@ -103,44 +123,355 @@ This will start up a development server with live reload enabled. You can see th To see how the sidebar is built and how pages are grouped and named, see [this section](https://github.com/apollographql/gatsby-theme-apollo/tree/master/packages/gatsby-theme-apollo-docs#sidebarcategories) of the gatsby-theme-apollo-docs docs. There is also a [creating pages section](https://github.com/apollographql/gatsby-theme-apollo/tree/master/packages/gatsby-theme-apollo-docs#creating-pages) if you're interested in adding new pages. -## Code of Conduct -The project has a [Code of Conduct] that *all* contributors are expected to -follow. This code describes the *minimum* behavior expectations for all -contributors. - -As a contributor, how you choose to act and interact towards your fellow -contributors, as well as to the community, will reflect back not only on -yourself but on the project as a whole. The Code of Conduct is designed and -intended, above all else, to help establish a culture within the project that -allows anyone and everyone who wants to contribute to feel safe doing so. - -Should any individual act in any way that is considered in violation of the -[Code of Conduct], corrective actions will be taken. It is possible, however, -for any individual to *act* in such a manner that is not in violation of the -strict letter of the Code of Conduct guidelines while still going completely -against the spirit of what that Code is intended to accomplish. - -Open, diverse, and inclusive communities live and die on the basis of trust. -Contributors can disagree with one another so long as they trust that those -disagreements are in good faith and everyone is working towards a common goal. - -## Bad Actors -All contributors to tacitly agree to abide by both the letter and spirit of the -[Code of Conduct]. Failure, or unwillingness, to do so will result in -contributions being respectfully declined. - -A *bad actor* is someone who repeatedly violates the *spirit* of the Code of -Conduct through consistent failure to self-regulate the way in which they -interact with other contributors in the project. In doing so, bad actors -alienate other contributors, discourage collaboration, and generally reflect -poorly on the project as a whole. - -Being a bad actor may be intentional or unintentional. Typically, unintentional -bad behavior can be easily corrected by being quick to apologize and correct -course *even if you are not entirely convinced you need to*. Giving other -contributors the benefit of the doubt and having a sincere willingness to admit -that you *might* be wrong is critical for any successful open collaboration. - -Don't be a bad actor. - -[Code of Conduct]: https://github.com/apollographql/.github/blob/main/CODE_OF_CONDUCT.md +### Adding a new command + +Prior to adding a new command to Rover, you should familiarize yourself with Rover's existing [architecture](./ARCHITECTURE.md) and to make sure that you have discussed the design of the new command in a [GitHub issue](#Using-issues) before submitting a pull request. + +#### `rover graph hello` + +Let's walk through what it would look like to add a new `hello` subcommand to the `rover graph` command namespace. + +##### Scaffold the new command + +The first thing we want to do when creating a new command is to create an entry point for it. The current project does not have a `graph hello` command as you can see here: + +```console +$ cargo run -- graph hello + Finished dev [unoptimized + debuginfo] target(s) in 0.09s + Running `target/debug/rover graph hello` +error: The subcommand 'hello' wasn't recognized + Did you mean 'help'? + +If you believe you received this message in error, try re-running with 'rover graph -- hello' + +USAGE: + rover graph [OPTIONS] + +For more information try --help +``` + +--- + +Each of Rover's "nouns" has their own module in `src/command`. The noun we are trying to add a verb command to is `graph`. If you open `src/command/graph/mod.rs`, you can see an example of how each of the `graph` commands are laid out. + +Each one has their own file, and is included with a `mod command;` statement at the top of the file. The entry for `rover graph publish` and its help text are laid out in `mod.rs` in a struct with the `StructOpt` trait automatically derived. (You can read more about StructOpt [here](https://docs.rs/structopt/latest/structopt/)). + +The actual logic for `rover graph publish` lives in `src/command/graph/publish.rs` + +Before we can add the command to Rover's API, allowing us to run it, we need to define the command and its possible arguments, along with providing a simple `run` function. We can do this under the `src/command` directory. + +Subcommands each have their own files or directories under `src/command`. Files directly in `src/command` are flat commands with no subcommands, like `rover info` in `src/command/info.rs`. Commands with subcommands include files for each of their subcommands, like `rover graph publish` in `src/command/graph/publish.rs`. Here, each argument is laid out in the `Publish` struct, and a `run` method is added to the struct. + +A minimal command in Rover would be laid out exactly like this: + +```rust +#[derive(Debug, Serialize, StructOpt)] +pub struct MyNewCommand { } + +impl MyNewCommand { + pub fn run(&self) -> Result { + Ok(RoverStdout::None) + } +} +``` + +For our `graph hello` command, we'll add a new `hello.rs` file under `src/command/graph` with the following contents: + +```rust +use serde::Serialize; +use structopt::StructOpt; + +use crate::command::RoverStdout; +use crate::Result; + +#[derive(Debug, Serialize, StructOpt)] +pub struct Hello { } + +impl Hello { + pub fn run(&self) -> Result { + eprintln!("Hello, world!"); + Ok(RoverStdout::None) + } +} +``` + +In this file, the `pub struct Hello` struct declaration is where we define the arguments and options available for our `Hello` command. + +In its current state, this file would not be compiled, as the module is not included in the parent module. + +To fix this, we will include the newly created `hello` module in `src/command/graph/mod.rs`: + +```rust +mod hello; +``` + +Then, we can add a `Hello` value to the `Command` enum like so: + +```rust +#[derive(Debug, Serialize, StructOpt)] +pub enum Command { + ... + /// Say hello to a graph! + Hello(hello::Hello), +} +``` + +`hello::Hello`, the value associated with the `Hello` variant of `Command`, is the struct that we created in the previous step. The doc comment here `/// Say hello to a graph` is also important, as that's the description for the command that will be shown when running `rover graph --help`. + +Running `cargo check` or an editor extension (like Rust Analyzer for VS Code) will warn you that `pattern &Hello not covered` for the `impl` block below the enum definition. This just means that for the `run` function in the `mod.rs` file we're in, we're not matching all possible variants of the `Command` enum. + +Add the following line to the `match` block. This tells StructOpt that when we encounter the `graph hello` command, we want to use the `Hello::run` function that we defined earlier to execute it: + +``` +Command::Hello(command) => command.run(), +``` + +After adding that, there should be no errors when running `cargo check` and we can run our basic command using `cargo run`: + +```shell +$ cargo run -- graph hello + Finished dev [unoptimized + debuginfo] target(s) in 0.08s + Running `target/debug/rover graph hello` +Hello, world! +``` + +##### Accepting required arguments and optional flags + +Rover uses a library called [StructOpt](https://docs.rs/structopt) to build commands. We apply the `StructOpt` trait using the `#[derive(StructOpt)]` syntax above it, to let `StructOpt` know that this is a command definition, and the values and implementations for this struct will be related to the command defined by `Hello`. + +All commands under the `graph` namespace accept a required, positional argument `` that describes the graph and variant a user is operating on. Additionally, it takes an optional `--profile` flag that can swap out the API token a user is using to interact with the graph registry. + +In order to add these to our new `graph hello` command, we should copy and paste the field from any other `graph` command like so: + +```rust +#[derive(Debug, Serialize, StructOpt)] +pub struct Hello { + /// @ of graph in Apollo Studio to publish to. + /// @ may be left off, defaulting to @current + #[structopt(name = "GRAPH_REF", parse(try_from_str = parse_graph_ref))] + #[serde(skip_serializing)] + graph: GraphRef + + /// Name of configuration profile to use + #[structopt(long = "profile", default_value = "default")] + #[serde(skip_serializing)] + profile_name: String, +} +``` + +We'll have to also add some import statements at the top of our file to support parsing this new argument: + +```rust +use crate::utils::parsers::{parse_graph_ref, GraphRef}; +``` + +Now, if we run the command again, it will complain if we don't provide a graph ref: + +```console +$ cargo run -- graph hello + Running `target/debug/rover graph hello` +error: The following required arguments were not provided: + + +USAGE: + rover graph hello [OPTIONS] + +For more information try --help +``` + +##### Setting up a command to work with `rover-client` + +Most of Rover's commands make requests to Apollo Studio's API. Rather than handling the request logic in the main package in the repository, Rover is structured so that logic lives in `crates/rover-client`. This is helpful for separation of concerns and testing. + +In order to access functionality from `rover-client` in our `rover graph hello` command, we'll need to pass down a client from the entry to our command in `src/command/graph/mod.rs`. + +You can do this by changing the `Command::Hello(command) => command.run(),` line to `Command::Hello(command) => command.run(client_config),`. + +Then you'll need to change `Hello::run` to accept a `client_config: StudioClientConfig` parameter in `src/command/graph/hello.rs`, and add a `use crate::utils::client::StudioClientConfig` import statement. Then, at the top of the run function, you can create a `StudioClient` by adding `let client = client_config.get_client(&self.profile_name)?;`. You can see examples of this in the other commands. + +##### Auto-generated help command + +Now that we've successfully scaffolded out a new `rover graph hello` command that is ready to start making requests to the Apollo Studio API, we can take a look at the help command. + +```console +$ cargo run -- graph hello --help +rover-graph-hello 0.1.5 +Hello, World! + +USAGE: + rover graph hello [OPTIONS] + +FLAGS: + -h, --help Prints help information + +OPTIONS: + -l, --log Specify Rover's log level [possible values: error, warn, info, + debug, trace] + --profile Name of configuration profile to use [default: default] + +ARGS: + @ of graph in Apollo Studio to publish to. @ may be left off, defaulting + to @current +``` + +`` and `--profile ` should look familiar to you, but `-h, --help`, and `-l --log ` might seem a bit magical. + +The `--help` flag is automatically created by `StructOpt`, and the `--log` flag is defined as a global flag in `src/cli.rs` on the top level `Rover` struct. + +##### Important note on telemetry + +Any time you are creating a new command, you need to make sure to add `#[serde(skip_serializing)]` to any flag or parameter that could contain personally identifiable information (PII), as commands and their parameters without this attribute are automatically sent to our telemetry endpoint. + +##### Adding a query to Apollo Studio + +The only piece of the `rover-client` crate that we need to be concerned with for now is the `src/query` directory. This is where all the queries to Apollo Studio live. This directory is roughly organized by the command names as well, but there may be some queries in these directories that are used by multiple commands. + +You can see in the `src/query/graph` directory a number of `.rs` files paired with `.graphql` files. The `.graphql` files are the files where the GraphQL operations live, and the matching `.rs` files contain the logic needed to execute those operations. + +##### Writing a GraphQL operation + +For our basic `graph hello` command, we're going to make a request for a specific graph to Apollo Studio, inquiring about the existence of a graph, and nothing else. For this, we can use the `Query.service` field. + +Create a `hello.graphql` file in `crates/rover-client/src/query/graph` and paste the following into it: + +```graphql +query GraphHello($graphId: ID!) { + service(id: $graphId) { + deletedAt + } +} +``` + +This simple GraphQL operation uses a graph's unique ID (which we get from the `GraphRef` we defined earlier), and fetches the graph from the registry, along with a field describing when it was deleted. Using this information, we can determine if a graph exists (if the `service` field is `null`) and if it was deleted and no longer usable. + +Note: It can be very helpful to use the Studio Explorer to navigate the Apollo Studio API when creating new queries. This can be found [here](https://studio-staging.apollographql.com/graph/engine/explorer?variant=prod), but is not open to the public at this time. + +##### Writing the request handler + +This project uses [graphql-client](https://docs.rs/graphql_client/latest/graphql_client/) to generate types for each raw `.graphql` query that we write. + +You'll want to create an empty file at `crates/rover-client/src/query/graph/hello.rs`. + +In order to start compiling this file, we need to export the module in `crates/rover-client/src/query/graph/mod.rs`: + +```rust +... +/// "Graph hello" command execution +pub mod hello; +``` + +Back in `hello.rs`, we'll import the following types: + +```rust +use crate::blocking::StudioClient; +use crate::RoverClientError; +use graphql_client::*; +``` + +Then, we'll create a new struct that will have auto-generated types for the `hello.graphql` file that we created earlier: + +```rust +#[derive(GraphQLQuery)] +// The paths are relative to the directory where your `Cargo.toml` is located. +// Both json and the GraphQL schema language are supported as sources for the schema +#[graphql( + query_path = "src/query/graph/hello.graphql", + schema_path = ".schema/schema.graphql", + response_derives = "PartialEq, Debug, Serialize, Deserialize", + deprecated = "warn" +)] +/// This struct is used to generate the module containing `Variables` and +/// `ResponseData` structs. +pub struct GraphHello; +``` + +Since the type we will be returning is autogenerated to be a `Timestamp`, we'll need to add the following line: + +``` +type Timestamp = String; +``` + +From here, we'll want an entrypoint to actually run the query. To do so, we'll create a public `run` function: + +```rust +pub fn run( + variables: graph_hello::Variables, + client: &StudioClient, +) -> Result { + Ok("stub".to_string()) +} +``` + +Before we go any further, lets make sure everything is set up properly. We're going back to `src/command/graph/hello.rs` to add a call to our newly created `run` function. + +It should look something like this (you should make sure you are following the style of other commands when creating new ones): + +```rust +pub fn run(&self, client_config: StudioClientConfig) -> Result { + let client = client_config.get_client(&self.profile_name)?; + let graph_ref = self.graph.to_string(); + eprintln!( + "Checking deletion of graph {} using credentials from the {} profile.", + Cyan.normal().paint(&graph_ref), + Yellow.normal().paint(&self.profile_name) + ); + let deleted_at = hello::run( + hello::graph_hello::Variables { + graph_id: self.graph.name.clone(), + }, + &client, + )?; + Ok(RoverStdout::PlainText(deleted_at)) +} +``` + +Since we've just stubbed out a fake response without actually executing the query, this command should just print out `stub` every time you run it with a valid graph ref. + +To actually execute the query, we'll modify our `rover-client` hello.rs to look like this: + +```rust +pub fn run( + variables: graph_hello::Variables, + client: &StudioClient, +) -> Result { + let graph = variables.graph_id.clone(); + let data = client.post::(variables)?; + build_response(data, graph) +} + +fn build_response( + data: graph_hello::ResponseData, + graph: String, +) -> Result { + let service = data.service.ok_or(RoverClientError::NoService { graph })?; + service.deleted_at.ok_or(RoverClientError::AdhocError { + msg: "Graph has never been deleted".to_string(), + }) +} +``` + +This should get you to the point where you can run `rover graph hello ` and see if and when the last graph was deleted. From here, you should be able to follow the examples of other commands to write out tests for the `build_response` function. This is left as an exercise for the reader. + +##### `RoverStdout` + +Now that you can actually execute the `hello::run` query and return its result, you should create a new variant of `RoverStdout` in `src/command/output.rs` that is not `PlainText`. Your new variant should print the descriptor using the `print_descriptor` function, and print the raw content using `print_content`. + +You'll want to change the line `Ok(RoverStdout::PlainText(deleted_at))` to `Ok(RoverStdout::DeletedAt(deleted_at))`, add a new `DeletedAt(String)` variant to `RoverStdout`, and then match on it in `pub fn print(&self)`: + +```rust +... +RoverStdout::DeletedAt(timestamp) => { + print_descriptor("Deleted At"); + print_content(×tamp); +} +``` + +##### Error handling + +Rover places a very strong emphasis on good error handling, with properly structured errors, accompanying error codes, and actionable suggestions to resolve errors. Each workspace crate uses `thiserror` to create a top level error enum in `error.rs` that defines all of the possible errors that can occur in that crate. + +Then, in Rover, we create a `RoverError` struct defined in `src/error/mod.rs` that formats each of these errors, and adds some extra metadata to them for end users. Each time a new error is added to any workspace crate, you'll receive a compiler error complaining about an unmatched variant in `src/error/metadata/mod.rs`. This new error type should then be mapped to either an existing variant of the `Suggestion` enum (`src/error/metadata/suggestion.rs`), or a new one should be created. Additionally, a new error code should likely be created in `code.rs`, along with a longer form description of that error code in a markdown file in `src/error/codes`. + +##### Environment Variables + +Most environment variables within Rover are preceded with `APOLLO_`. In order to support a new environment variable following this format, you'll want to head to `src/utils/env.rs`, and add a new variant to the enum there. It should be as easy as following the patterns set out there and passing the variable where you need it to go. The top level `Rover` struct has a global `RoverEnv` instance that will slurp up all of the system's environment variables into a `HashMap` that can then be accessed in any command. `RoverEnv` also provides the ability to mock specific environment variables for use in testing. diff --git a/crates/rover-client/src/query/graph/mod.rs b/crates/rover-client/src/query/graph/mod.rs index ecbc72219e..3c5fc20cf0 100644 --- a/crates/rover-client/src/query/graph/mod.rs +++ b/crates/rover-client/src/query/graph/mod.rs @@ -4,8 +4,8 @@ pub mod fetch; /// "graph publish" command execution pub mod publish; -/// "graph check" command exeuction +/// "graph check" command execution pub mod check; -/// "graph introspect" command exeuction +/// "graph introspect" command execution pub mod introspect; diff --git a/docs/source/contributing.md b/docs/source/contributing.md index 5c2bca63fe..f4e6262ae4 100644 --- a/docs/source/contributing.md +++ b/docs/source/contributing.md @@ -9,7 +9,7 @@ sidebar_title: "Contributing" > external feature contributors, though some documentation contributions may be > accepted. -## Prerequisites +## Prerequisites Rover is written in [Rust]. In order to contribute, you'll need to have Rust installed. To install Rust, visit [https://www.rust-lang.org/tools/install]. @@ -24,6 +24,17 @@ To build the CLI: cargo build ``` +To build the CLI without `rover supergraph compose` (for Alpine Linux): +```bash +cargo build --no-default-features +``` + +To cross-compile Rover for different platforms, you can run the following, where `TARGET` is one of Rust's [supported platforms](https://doc.rust-lang.org/stable/rustc/platform-support.html): +```bash +rustup target add +cargo build --target +``` + To build and run the CLI with a set of arguments: ```bash cargo run -- @@ -48,11 +59,13 @@ cargo test --workspace To format your code: ```bash +rustup component add rustfmt cargo fmt --all ``` To lint your code: ```bash +rustup component add clippy cargo clippy ``` @@ -71,28 +84,35 @@ cargo xtask test [`cargo`]: https://doc.rust-lang.org/cargo/index.html [https://www.rust-lang.org/tools/install]: https://www.rust-lang.org/tools/install -## Project Structure +### IDEs + +The Rover team primarily uses [VS Code](https://code.visualstudio.com/) along with [rust-analyzer](https://rust-analyzer.github.io/manual.html) when developing Rover. `rust-analyzer` can also be used with [other IDEs](https://rust-analyzer.github.io/manual.html#installation) if you are more familiar with something else. + +## How to contribute + +### Using issues -- `src`: the `rover` CLI - - `src/bin/rover.rs`: the entry point for the CLI executable - - `src/command`: logic for the CLI commands - - `src/command/output.rs`: Enum containing all possible `stdout` options for Rover commands - - `src/utils`: shared utility functions - - `src/error`: application-level error handling including suggestions and error codes - - `src/cli.rs`: Module containing definition for all top-level commands - - `src/lib.rs`: all the logic used by the CLI +The Rover team works largely in public using GitHub [issues] to track work. To make sure contributions are aligned with the project's goals, keep the following issue etiquette in mind: -- `crates` - - `crates/houston`: logic related to configuring rover - - `crates/robot-panic`: Fork of robot-panic to create helpful panic handlers - - `crates/rover-client`: logic for querying apollo services - - `crates/sputnik`: logic for capturing anonymous usage data - - `crates/timber`: output formatting and logging logic +1. [Open an issue](https://github.com/apollographql/rover/issues/new/choose) for your contribution. If there is already an issue open, please ask if anyone is working on it or let us know you plan on working on it. This will let us know what to expect, help us to prioritize reviews, and ensure there is no duplication of work. +1. Use issue templates! These templates have been created to help minimize back-and-forth between creators and the Rover team. They include the necessary information to help the team triage your issue or question, as well as automatically applying the appropriate labels. +1. Issues with the `triage` label still applied have not yet been reviewed by the Rover team, and there are no guarantees that PRs fixing an untriaged issue will be accepted. It's best to wait for issues to be triaged before beginning work. +[issues]: https://github.com/apollographql/rover/issues -## Documentation +### Submitting a Pull Request -Documentation for using and contributing to rover is built using Gatsby and [Apollo's Docs Theme for Gatsby](https://github.com/apollographql/gatsby-theme-apollo/tree/master/packages/gatsby-theme-apollo-docs). +Pull requests (PRs) should only be opened after discussion and consensus has been reached in a related issue, and you have communicated your intentions to create a PR with the Rover team. + +1. When creating a PR, make sure to link it to an issue or use the `Fixes #123` syntax to make sure others know which issue(s) your PR is trying to address and to help us automatically close resolved issues. +1. Include a helpful description. It is important to provide context to reviewers that show _how_ your PR addresses an issue and any questions you still have unanswered, or portions of the code you think deserve some extra attention. +1. If your work is still in-progress and you're opening a PR to get early feedback, let us know by opening it as a draft PR and adding `wip:` prefix in the PR title. +1. Add tests for any logic changes in your code, especially if you are fixing a bug. Your PR should have no failing tests before merging. Please let us know if you need help writing tests, there are still some portions of the Rover codebase that do not have established testing patterns. +1. Add a changelog entry in [CHANGELOG.md](https://github.com/apollographql/rover/blob/main/CHANGELOG.md) under the `Unreleased` heading, following the pattern of previous entries. + +### Documentation + +Documentation for using and contributing to Rover is built using Gatsby and [Apollo's Docs Theme for Gatsby](https://github.com/apollographql/gatsby-theme-apollo/tree/master/packages/gatsby-theme-apollo-docs). To contribute to these docs, you can add or edit the markdown & MDX files in the `docs/source` directory. @@ -108,44 +128,355 @@ This will start up a development server with live reload enabled. You can see th To see how the sidebar is built and how pages are grouped and named, see [this section](https://github.com/apollographql/gatsby-theme-apollo/tree/master/packages/gatsby-theme-apollo-docs#sidebarcategories) of the gatsby-theme-apollo-docs docs. There is also a [creating pages section](https://github.com/apollographql/gatsby-theme-apollo/tree/master/packages/gatsby-theme-apollo-docs#creating-pages) if you're interested in adding new pages. -## Code of Conduct -The project has a [Code of Conduct] that *all* contributors are expected to -follow. This code describes the *minimum* behavior expectations for all -contributors. - -As a contributor, how you choose to act and interact towards your fellow -contributors, as well as to the community, will reflect back not only on -yourself but on the project as a whole. The Code of Conduct is designed and -intended, above all else, to help establish a culture within the project that -allows anyone and everyone who wants to contribute to feel safe doing so. - -Should any individual act in any way that is considered in violation of the -[Code of Conduct], corrective actions will be taken. It is possible, however, -for any individual to *act* in such a manner that is not in violation of the -strict letter of the Code of Conduct guidelines while still going completely -against the spirit of what that Code is intended to accomplish. - -Open, diverse, and inclusive communities live and die on the basis of trust. -Contributors can disagree with one another so long as they trust that those -disagreements are in good faith and everyone is working towards a common goal. - -## Bad Actors -All contributors to tacitly agree to abide by both the letter and spirit of the -[Code of Conduct]. Failure, or unwillingness, to do so will result in -contributions being respectfully declined. - -A *bad actor* is someone who repeatedly violates the *spirit* of the Code of -Conduct through consistent failure to self-regulate the way in which they -interact with other contributors in the project. In doing so, bad actors -alienate other contributors, discourage collaboration, and generally reflect -poorly on the project as a whole. - -Being a bad actor may be intentional or unintentional. Typically, unintentional -bad behavior can be easily corrected by being quick to apologize and correct -course *even if you are not entirely convinced you need to*. Giving other -contributors the benefit of the doubt and having a sincere willingness to admit -that you *might* be wrong is critical for any successful open collaboration. - -Don't be a bad actor. - -[Code of Conduct]: https://github.com/apollographql/.github/blob/main/CODE_OF_CONDUCT.md +### Adding a new command + +Prior to adding a new command to Rover, you should familiarize yourself with Rover's existing [architecture](./ARCHITECTURE.md) and to make sure that you have discussed the design of the new command in a [GitHub issue](#Using-issues) before submitting a pull request. + +#### `rover graph hello` + +Let's walk through what it would look like to add a new `hello` subcommand to the `rover graph` command namespace. + +##### Scaffold the new command + +The first thing we want to do when creating a new command is to create an entry point for it. The current project does not have a `graph hello` command as you can see here: + +```console +$ cargo run -- graph hello + Finished dev [unoptimized + debuginfo] target(s) in 0.09s + Running `target/debug/rover graph hello` +error: The subcommand 'hello' wasn't recognized + Did you mean 'help'? + +If you believe you received this message in error, try re-running with 'rover graph -- hello' + +USAGE: + rover graph [OPTIONS] + +For more information try --help +``` + +--- + +Each of Rover's "nouns" has their own module in `src/command`. The noun we are trying to add a verb command to is `graph`. If you open `src/command/graph/mod.rs`, you can see an example of how each of the `graph` commands are laid out. + +Each one has their own file, and is included with a `mod command;` statement at the top of the file. The entry for `rover graph publish` and its help text are laid out in `mod.rs` in a struct with the `StructOpt` trait automatically derived. (You can read more about StructOpt [here](https://docs.rs/structopt/latest/structopt/)). + +The actual logic for `rover graph publish` lives in `src/command/graph/publish.rs` + +Before we can add the command to Rover's API, allowing us to run it, we need to define the command and its possible arguments, along with providing a simple `run` function. We can do this under the `src/command` directory. + +Subcommands each have their own files or directories under `src/command`. Files directly in `src/command` are flat commands with no subcommands, like `rover info` in `src/command/info.rs`. Commands with subcommands include files for each of their subcommands, like `rover graph publish` in `src/command/graph/publish.rs`. Here, each argument is laid out in the `Publish` struct, and a `run` method is added to the struct. + +A minimal command in Rover would be laid out exactly like this: + +```rust +#[derive(Debug, Serialize, StructOpt)] +pub struct MyNewCommand { } + +impl MyNewCommand { + pub fn run(&self) -> Result { + Ok(RoverStdout::None) + } +} +``` + +For our `graph hello` command, we'll add a new `hello.rs` file under `src/command/graph` with the following contents: + +```rust +use serde::Serialize; +use structopt::StructOpt; + +use crate::command::RoverStdout; +use crate::Result; + +#[derive(Debug, Serialize, StructOpt)] +pub struct Hello { } + +impl Hello { + pub fn run(&self) -> Result { + eprintln!("Hello, world!"); + Ok(RoverStdout::None) + } +} +``` + +In this file, the `pub struct Hello` struct declaration is where we define the arguments and options available for our `Hello` command. + +In its current state, this file would not be compiled, as the module is not included in the parent module. + +To fix this, we will include the newly created `hello` module in `src/command/graph/mod.rs`: + +```rust +mod hello; +``` + +Then, we can add a `Hello` value to the `Command` enum like so: + +```rust +#[derive(Debug, Serialize, StructOpt)] +pub enum Command { + ... + /// Say hello to a graph! + Hello(hello::Hello), +} +``` + +`hello::Hello`, the value associated with the `Hello` variant of `Command`, is the struct that we created in the previous step. The doc comment here `/// Say hello to a graph` is also important, as that's the description for the command that will be shown when running `rover graph --help`. + +Running `cargo check` or an editor extension (like Rust Analyzer for VS Code) will warn you that `pattern &Hello not covered` for the `impl` block below the enum definition. This just means that for the `run` function in the `mod.rs` file we're in, we're not matching all possible variants of the `Command` enum. + +Add the following line to the `match` block. This tells StructOpt that when we encounter the `graph hello` command, we want to use the `Hello::run` function that we defined earlier to execute it: + +``` +Command::Hello(command) => command.run(), +``` + +After adding that, there should be no errors when running `cargo check` and we can run our basic command using `cargo run`: + +```shell +$ cargo run -- graph hello + Finished dev [unoptimized + debuginfo] target(s) in 0.08s + Running `target/debug/rover graph hello` +Hello, world! +``` + +##### Accepting required arguments and optional flags + +Rover uses a library called [StructOpt](https://docs.rs/structopt) to build commands. We apply the `StructOpt` trait using the `#[derive(StructOpt)]` syntax above it, to let `StructOpt` know that this is a command definition, and the values and implementations for this struct will be related to the command defined by `Hello`. + +All commands under the `graph` namespace accept a required, positional argument `` that describes the graph and variant a user is operating on. Additionally, it takes an optional `--profile` flag that can swap out the API token a user is using to interact with the graph registry. + +In order to add these to our new `graph hello` command, we should copy and paste the field from any other `graph` command like so: + +```rust +#[derive(Debug, Serialize, StructOpt)] +pub struct Hello { + /// @ of graph in Apollo Studio to publish to. + /// @ may be left off, defaulting to @current + #[structopt(name = "GRAPH_REF", parse(try_from_str = parse_graph_ref))] + #[serde(skip_serializing)] + graph: GraphRef + + /// Name of configuration profile to use + #[structopt(long = "profile", default_value = "default")] + #[serde(skip_serializing)] + profile_name: String, +} +``` + +We'll have to also add some import statements at the top of our file to support parsing this new argument: + +```rust +use crate::utils::parsers::{parse_graph_ref, GraphRef}; +``` + +Now, if we run the command again, it will complain if we don't provide a graph ref: + +```console +$ cargo run -- graph hello + Running `target/debug/rover graph hello` +error: The following required arguments were not provided: + + +USAGE: + rover graph hello [OPTIONS] + +For more information try --help +``` + +##### Setting up a command to work with `rover-client` + +Most of Rover's commands make requests to Apollo Studio's API. Rather than handling the request logic in the main package in the repository, Rover is structured so that logic lives in `crates/rover-client`. This is helpful for separation of concerns and testing. + +In order to access functionality from `rover-client` in our `rover graph hello` command, we'll need to pass down a client from the entry to our command in `src/command/graph/mod.rs`. + +You can do this by changing the `Command::Hello(command) => command.run(),` line to `Command::Hello(command) => command.run(client_config),`. + +Then you'll need to change `Hello::run` to accept a `client_config: StudioClientConfig` parameter in `src/command/graph/hello.rs`, and add a `use crate::utils::client::StudioClientConfig` import statement. Then, at the top of the run function, you can create a `StudioClient` by adding `let client = client_config.get_client(&self.profile_name)?;`. You can see examples of this in the other commands. + +##### Auto-generated help command + +Now that we've successfully scaffolded out a new `rover graph hello` command that is ready to start making requests to the Apollo Studio API, we can take a look at the help command. + +```console +$ cargo run -- graph hello --help +rover-graph-hello 0.1.5 +Hello, World! + +USAGE: + rover graph hello [OPTIONS] + +FLAGS: + -h, --help Prints help information + +OPTIONS: + -l, --log Specify Rover's log level [possible values: error, warn, info, + debug, trace] + --profile Name of configuration profile to use [default: default] + +ARGS: + @ of graph in Apollo Studio to publish to. @ may be left off, defaulting + to @current +``` + +`` and `--profile ` should look familiar to you, but `-h, --help`, and `-l --log ` might seem a bit magical. + +The `--help` flag is automatically created by `StructOpt`, and the `--log` flag is defined as a global flag in `src/cli.rs` on the top level `Rover` struct. + +##### Important note on telemetry + +Any time you are creating a new command, you need to make sure to add `#[serde(skip_serializing)]` to any flag or parameter that could contain personally identifiable information (PII), as commands and their parameters without this attribute are automatically sent to our telemetry endpoint. + +##### Adding a query to Apollo Studio + +The only piece of the `rover-client` crate that we need to be concerned with for now is the `src/query` directory. This is where all the queries to Apollo Studio live. This directory is roughly organized by the command names as well, but there may be some queries in these directories that are used by multiple commands. + +You can see in the `src/query/graph` directory a number of `.rs` files paired with `.graphql` files. The `.graphql` files are the files where the GraphQL operations live, and the matching `.rs` files contain the logic needed to execute those operations. + +##### Writing a GraphQL operation + +For our basic `graph hello` command, we're going to make a request for a specific graph to Apollo Studio, inquiring about the existence of a graph, and nothing else. For this, we can use the `Query.service` field. + +Create a `hello.graphql` file in `crates/rover-client/src/query/graph` and paste the following into it: + +```graphql +query GraphHello($graphId: ID!) { + service(id: $graphId) { + deletedAt + } +} +``` + +This simple GraphQL operation uses a graph's unique ID (which we get from the `GraphRef` we defined earlier), and fetches the graph from the registry, along with a field describing when it was deleted. Using this information, we can determine if a graph exists (if the `service` field is `null`) and if it was deleted and no longer usable. + +Note: It can be very helpful to use the Studio Explorer to navigate the Apollo Studio API when creating new queries. This can be found [here](https://studio-staging.apollographql.com/graph/engine/explorer?variant=prod), but is not open to the public at this time. + +##### Writing the request handler + +This project uses [graphql-client](https://docs.rs/graphql_client/latest/graphql_client/) to generate types for each raw `.graphql` query that we write. + +You'll want to create an empty file at `crates/rover-client/src/query/graph/hello.rs`. + +In order to start compiling this file, we need to export the module in `crates/rover-client/src/query/graph/mod.rs`: + +```rust +... +/// "Graph hello" command execution +pub mod hello; +``` + +Back in `hello.rs`, we'll import the following types: + +```rust +use crate::blocking::StudioClient; +use crate::RoverClientError; +use graphql_client::*; +``` + +Then, we'll create a new struct that will have auto-generated types for the `hello.graphql` file that we created earlier: + +```rust +#[derive(GraphQLQuery)] +// The paths are relative to the directory where your `Cargo.toml` is located. +// Both json and the GraphQL schema language are supported as sources for the schema +#[graphql( + query_path = "src/query/graph/hello.graphql", + schema_path = ".schema/schema.graphql", + response_derives = "PartialEq, Debug, Serialize, Deserialize", + deprecated = "warn" +)] +/// This struct is used to generate the module containing `Variables` and +/// `ResponseData` structs. +pub struct GraphHello; +``` + +Since the type we will be returning is autogenerated to be a `Timestamp`, we'll need to add the following line: + +``` +type Timestamp = String; +``` + +From here, we'll want an entrypoint to actually run the query. To do so, we'll create a public `run` function: + +```rust +pub fn run( + variables: graph_hello::Variables, + client: &StudioClient, +) -> Result { + Ok("stub".to_string()) +} +``` + +Before we go any further, lets make sure everything is set up properly. We're going back to `src/command/graph/hello.rs` to add a call to our newly created `run` function. + +It should look something like this (you should make sure you are following the style of other commands when creating new ones): + +```rust +pub fn run(&self, client_config: StudioClientConfig) -> Result { + let client = client_config.get_client(&self.profile_name)?; + let graph_ref = self.graph.to_string(); + eprintln!( + "Checking deletion of graph {} using credentials from the {} profile.", + Cyan.normal().paint(&graph_ref), + Yellow.normal().paint(&self.profile_name) + ); + let deleted_at = hello::run( + hello::graph_hello::Variables { + graph_id: self.graph.name.clone(), + }, + &client, + )?; + Ok(RoverStdout::PlainText(deleted_at)) +} +``` + +Since we've just stubbed out a fake response without actually executing the query, this command should just print out `stub` every time you run it with a valid graph ref. + +To actually execute the query, we'll modify our `rover-client` hello.rs to look like this: + +```rust +pub fn run( + variables: graph_hello::Variables, + client: &StudioClient, +) -> Result { + let graph = variables.graph_id.clone(); + let data = client.post::(variables)?; + build_response(data, graph) +} + +fn build_response( + data: graph_hello::ResponseData, + graph: String, +) -> Result { + let service = data.service.ok_or(RoverClientError::NoService { graph })?; + service.deleted_at.ok_or(RoverClientError::AdhocError { + msg: "Graph has never been deleted".to_string(), + }) +} +``` + +This should get you to the point where you can run `rover graph hello ` and see if and when the last graph was deleted. From here, you should be able to follow the examples of other commands to write out tests for the `build_response` function. This is left as an exercise for the reader. + +##### `RoverStdout` + +Now that you can actually execute the `hello::run` query and return its result, you should create a new variant of `RoverStdout` in `src/command/output.rs` that is not `PlainText`. Your new variant should print the descriptor using the `print_descriptor` function, and print the raw content using `print_content`. + +You'll want to change the line `Ok(RoverStdout::PlainText(deleted_at))` to `Ok(RoverStdout::DeletedAt(deleted_at))`, add a new `DeletedAt(String)` variant to `RoverStdout`, and then match on it in `pub fn print(&self)`: + +```rust +... +RoverStdout::DeletedAt(timestamp) => { + print_descriptor("Deleted At"); + print_content(×tamp); +} +``` + +##### Error handling + +Rover places a very strong emphasis on good error handling, with properly structured errors, accompanying error codes, and actionable suggestions to resolve errors. Each workspace crate uses `thiserror` to create a top level error enum in `error.rs` that defines all of the possible errors that can occur in that crate. + +Then, in Rover, we create a `RoverError` struct defined in `src/error/mod.rs` that formats each of these errors, and adds some extra metadata to them for end users. Each time a new error is added to any workspace crate, you'll receive a compiler error complaining about an unmatched variant in `src/error/metadata/mod.rs`. This new error type should then be mapped to either an existing variant of the `Suggestion` enum (`src/error/metadata/suggestion.rs`), or a new one should be created. Additionally, a new error code should likely be created in `code.rs`, along with a longer form description of that error code in a markdown file in `src/error/codes`. + +##### Environment Variables + +Most environment variables within Rover are preceded with `APOLLO_`. In order to support a new environment variable following this format, you'll want to head to `src/utils/env.rs`, and add a new variant to the enum there. It should be as easy as following the patterns set out there and passing the variable where you need it to go. The top level `Rover` struct has a global `RoverEnv` instance that will slurp up all of the system's environment variables into a `HashMap` that can then be accessed in any command. `RoverEnv` also provides the ability to mock specific environment variables for use in testing. diff --git a/installers/npm/package-lock.json b/installers/npm/package-lock.json index fa4fe0d7fd..d29b526758 100644 --- a/installers/npm/package-lock.json +++ b/installers/npm/package-lock.json @@ -247,9 +247,9 @@ } }, "node_modules/prettier": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.3.0.tgz", - "integrity": "sha512-kXtO4s0Lz/DW/IJ9QdWhAf7/NmPWQXkFr/r/WkR3vyI+0v8amTDxiaQSLzs8NBlytfLWX/7uQUMIW677yLKl4w==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.3.1.tgz", + "integrity": "sha512-p+vNbgpLjif/+D+DwAZAbndtRrR0md0MwfmOVN9N+2RgyACMT+7tfaRnT+WDPkqnuVwleyuBIG2XBxKDme3hPA==", "dev": true, "bin": { "prettier": "bin-prettier.js" @@ -476,9 +476,9 @@ "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" }, "prettier": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.3.0.tgz", - "integrity": "sha512-kXtO4s0Lz/DW/IJ9QdWhAf7/NmPWQXkFr/r/WkR3vyI+0v8amTDxiaQSLzs8NBlytfLWX/7uQUMIW677yLKl4w==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.3.1.tgz", + "integrity": "sha512-p+vNbgpLjif/+D+DwAZAbndtRrR0md0MwfmOVN9N+2RgyACMT+7tfaRnT+WDPkqnuVwleyuBIG2XBxKDme3hPA==", "dev": true }, "rimraf": {