feat(ops/yandex-cloud-rs): generated gRPC clients for Yandex Cloud

This uses tonic to generate the full set of gRPC clients for Yandex
Cloud. Includes some utility functions like an authentication
interceptor to make these actually work.

Since the upstream protos are exported regularly I've decided that the
versioning will simply be date-based.

The point of this is journaldriver integration, of course, hence also
the log-centric example code.

Change-Id: I00a615dcba80030e7f9bcfd476b2cfdb298f130d
Reviewed-on: https://cl.tvl.fyi/c/depot/+/8525
Tested-by: BuildkiteCI
Reviewed-by: tazjin <tazjin@tvl.su>
This commit is contained in:
Vincent Ambo 2023-04-25 03:08:41 +03:00 committed by tazjin
parent f1ca5a3096
commit ea1383682d
7 changed files with 1613 additions and 0 deletions

5
ops/yandex-cloud-rs/.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
target/
result/
# Ignore everything under src (except for lib.rs)
src/*
!src/lib.rs

1394
ops/yandex-cloud-rs/Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,17 @@
[package]
name = "yandex-cloud"
description = "Generated gRPC clients for the Yandex Cloud API"
version = "2023.4.24"
edition = "2021"
[dependencies]
prost = "0.11"
prost-types = "0.11"
[dependencies.tonic]
version = "0.9"
features = [ "tls", "tls-roots", "gzip" ]
[build-dependencies]
tonic-build = "0.9"
walkdir = "2.3.3"

View file

@ -0,0 +1,43 @@
use std::path::PathBuf;
use walkdir::{DirEntry, WalkDir};
fn proto_files(proto_dir: &str) -> Vec<PathBuf> {
let mut out = vec![];
fn is_proto(entry: &DirEntry) -> bool {
entry.file_type().is_file()
&& entry
.path()
.extension()
.map(|e| e.to_string_lossy() == "proto")
.unwrap_or(false)
}
for entry in WalkDir::new(format!("{}/yandex", proto_dir)).into_iter() {
let entry = entry.expect("failed to list proto files");
if is_proto(&entry) {
out.push(entry.into_path())
}
}
out
}
fn main() {
if let Some(proto_dir) = option_env!("YANDEX_CLOUD_PROTOS") {
tonic_build::configure()
.build_client(true)
.build_server(false)
.out_dir("src/")
.include_file("includes.rs")
.compile(
&proto_files(proto_dir),
&[
format!("{}", proto_dir),
format!("{}/third_party/googleapis", proto_dir),
],
)
.expect("failed to generate gRPC clients for Yandex Cloud")
}
}

View file

@ -0,0 +1,22 @@
{ depot, lib, pkgs, ... }:
let
protoSrc = pkgs.fetchFromGitHub {
owner = "yandex-cloud";
repo = "cloudapi";
rev = "67589e7503bd8997b7aa27af86a81fb6b9eec655";
sha256 = "0d8w98h58xzzcjq9s3y949gv5b4z4r05vgpz3n22asyfrfx6x6kf";
};
in
pkgs.rustPlatform.buildRustPackage rec {
name = "yandex-cloud-rs";
src = depot.third_party.gitignoreSource ./.;
cargoLock.lockFile = ./Cargo.lock;
YANDEX_CLOUD_PROTOS = "${protoSrc}";
nativeBuildInputs = [ pkgs.protobuf ];
# The generated doc comments contain lots of things that rustc
# *thinks* are doctests, but are actually just garbage leading to
# compiler errors.
doCheck = false;
}

View file

@ -0,0 +1,37 @@
//! This example uses the Yandex Cloud Logging API to write a log entry.
use prost_types::Timestamp;
use tonic::transport::channel::Endpoint;
use yandex_cloud::yandex::cloud::logging::v1::destination::Destination;
use yandex_cloud::yandex::cloud::logging::v1::log_ingestion_service_client::LogIngestionServiceClient;
use yandex_cloud::yandex::cloud::logging::v1::Destination as OuterDestination;
use yandex_cloud::yandex::cloud::logging::v1::IncomingLogEntry;
use yandex_cloud::yandex::cloud::logging::v1::WriteRequest;
use yandex_cloud::AuthInterceptor;
#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let channel = Endpoint::from_static("https://ingester.logging.yandexcloud.net")
.connect()
.await?;
let mut client = LogIngestionServiceClient::with_interceptor(
channel,
AuthInterceptor::new("YOUR_TOKEN_HERE"),
);
let request = WriteRequest {
destination: Some(OuterDestination {
destination: Some(Destination::LogGroupId("YOUR_LOG_GROUP_ID".into())),
}),
entries: vec![IncomingLogEntry {
timestamp: Some(Timestamp::date_time(2023, 04, 24, 23, 44, 30).unwrap()),
message: "test log message".into(),
..Default::default()
}],
..Default::default()
};
client.write(request).await.unwrap();
Ok(())
}

View file

@ -0,0 +1,95 @@
//! This module provides low-level generated gRPC clients for the
//! Yandex Cloud APIs.
//!
//! The clients are generated using the [tonic][] and [prost][]
//! crates and have default configuration.
//!
//! Documentation present in the protos is retained into the generated
//! Rust types, but for detailed API information you should visit the
//! official Yandex Cloud Documentation pages:
//!
//! * [in English](https://cloud.yandex.com/en-ru/docs/overview/api)
//! * [in Russian](https://cloud.yandex.ru/docs/overview/api)
//!
//! The proto sources are available on the [Yandex Cloud GitHub][protos].
//!
//! [tonic]: https://docs.rs/tonic/latest/tonic/
//! [prost]: https://docs.rs/prost/latest/prost/
//! [protos]: https://github.com/yandex-cloud/cloudapi
//!
//! The majority of user-facing structures can be found in the
//! [`yandex::cloud`] module.
//!
//! ## Usage
//!
//! Typically to use these APIs, you need to provide an authentication
//! credential and an endpoint to connect to. The full list of
//! Yandex's endpoints is [available online][endpoints] and you should
//! look up the service you plan to use and pick the correct endpoint
//! from the list.
//!
//! Authentication is done via an HTTP header using an IAM token,
//! which can be done in Tonic using [interceptors][]. The
//! [`AuthInterceptor`] provided by this crate can be used for that
//! purpose.
//!
//! [endpoints]: https://cloud.yandex.com/en/docs/api-design-guide/concepts/endpoints
//! [interceptors]: https://docs.rs/tonic/latest/tonic/service/trait.Interceptor.html
use tonic::metadata::{Ascii, MetadataValue};
use tonic::service::Interceptor;
/// Helper trait for types or closures that can provide authentication
/// tokens for Yandex Cloud.
pub trait TokenProvider {
/// Fetch a currently valid authentication token for Yandex Cloud.
fn get_token<'a>(&'a mut self) -> &'a str;
}
impl TokenProvider for String {
fn get_token<'a>(&'a mut self) -> &'a str {
self.as_str()
}
}
impl TokenProvider for &'static str {
fn get_token(&mut self) -> &'static str {
*self
}
}
/// Interceptor for adding authentication headers to gRPC requests.
/// This is constructed with a callable that returns authentication
/// tokens.
///
/// This callable is responsible for ensuring that the returned tokens
/// are valid at the given time, i.e. it should take care of
/// refreshing and so on.
pub struct AuthInterceptor<T: TokenProvider> {
token_provider: T,
}
impl<T: TokenProvider> AuthInterceptor<T> {
pub fn new(token_provider: T) -> Self {
Self { token_provider }
}
}
impl<T: TokenProvider> Interceptor for AuthInterceptor<T> {
fn call(
&mut self,
mut request: tonic::Request<()>,
) -> Result<tonic::Request<()>, tonic::Status> {
let token: MetadataValue<Ascii> =
self.token_provider.get_token().try_into().map_err(|_| {
tonic::Status::invalid_argument("authorization token contained invalid characters")
})?;
request.metadata_mut().insert("authorization", token);
Ok(request)
}
}
// The rest of this file is generated by the build script at ../build.rs.
include!("includes.rs");