This commit is contained in:
nora 2024-04-07 15:26:15 +02:00
commit bac720c512
11 changed files with 2115 additions and 0 deletions

1
.envrc Normal file
View file

@ -0,0 +1 @@
use nix

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/target

1275
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

14
Cargo.toml Normal file
View file

@ -0,0 +1,14 @@
[package]
name = "terraform-provider-terustform"
version = "0.1.0"
edition = "2021"
[dependencies]
eyre = "0.6.12"
prost = "0.12.4"
rustls = { version = "0.23.4", default-features = false, features = ["ring", "logging", "std", "tls12"] }
tokio = { version = "1.37.0", features = ["full"] }
tonic = "0.11.0"
[build-dependencies]
tonic-build = "0.11.0"

4
build.rs Normal file
View file

@ -0,0 +1,4 @@
fn main() -> Result<(), Box<dyn std::error::Error>> {
tonic_build::compile_protos("proto/tfplugin6.6.proto")?;
Ok(())
}

8
init-nix Normal file
View file

@ -0,0 +1,8 @@
#!/usr/bin/env bash
echo "use nix" > .envrc
cat > shell.nix <<EOF
{ pkgs ? import <nixpkgs> {} }: pkgs.mkShell {
packages = [];
}
EOF

604
proto/tfplugin6.6.proto Normal file
View file

@ -0,0 +1,604 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
// Terraform Plugin RPC protocol version 6.6
//
// This file defines version 6.6 of the RPC protocol. To implement a plugin
// against this protocol, copy this definition into your own codebase and
// use protoc to generate stubs for your target language.
//
// This file will not be updated. Any minor versions of protocol 6 to follow
// should copy this file and modify the copy while maintaing backwards
// compatibility. Breaking changes, if any are required, will come
// in a subsequent major version with its own separate proto definition.
//
// Note that only the proto files included in a release tag of Terraform are
// official protocol releases. Proto files taken from other commits may include
// incomplete changes or features that did not make it into a final release.
// In all reasonable cases, plugin developers should take the proto file from
// the tag of the most recent release of Terraform, and not from the main
// branch or any other development branch.
//
syntax = "proto3";
option go_package = "github.com/hashicorp/terraform/internal/tfplugin6";
package tfplugin6;
// DynamicValue is an opaque encoding of terraform data, with the field name
// indicating the encoding scheme used.
message DynamicValue {
bytes msgpack = 1;
bytes json = 2;
}
message Diagnostic {
enum Severity {
INVALID = 0;
ERROR = 1;
WARNING = 2;
}
Severity severity = 1;
string summary = 2;
string detail = 3;
AttributePath attribute = 4;
}
message FunctionError {
string text = 1;
// The optional function_argument records the index position of the
// argument which caused the error.
optional int64 function_argument = 2;
}
message AttributePath {
message Step {
oneof selector {
// Set "attribute_name" to represent looking up an attribute
// in the current object value.
string attribute_name = 1;
// Set "element_key_*" to represent looking up an element in
// an indexable collection type.
string element_key_string = 2;
int64 element_key_int = 3;
}
}
repeated Step steps = 1;
}
message StopProvider {
message Request {
}
message Response {
string Error = 1;
}
}
// RawState holds the stored state for a resource to be upgraded by the
// provider. It can be in one of two formats, the current json encoded format
// in bytes, or the legacy flatmap format as a map of strings.
message RawState {
bytes json = 1;
map<string, string> flatmap = 2;
}
enum StringKind {
PLAIN = 0;
MARKDOWN = 1;
}
// Schema is the configuration schema for a Resource or Provider.
message Schema {
message Block {
int64 version = 1;
repeated Attribute attributes = 2;
repeated NestedBlock block_types = 3;
string description = 4;
StringKind description_kind = 5;
bool deprecated = 6;
}
message Attribute {
string name = 1;
bytes type = 2;
Object nested_type = 10;
string description = 3;
bool required = 4;
bool optional = 5;
bool computed = 6;
bool sensitive = 7;
StringKind description_kind = 8;
bool deprecated = 9;
}
message NestedBlock {
enum NestingMode {
INVALID = 0;
SINGLE = 1;
LIST = 2;
SET = 3;
MAP = 4;
GROUP = 5;
}
string type_name = 1;
Block block = 2;
NestingMode nesting = 3;
int64 min_items = 4;
int64 max_items = 5;
}
message Object {
enum NestingMode {
INVALID = 0;
SINGLE = 1;
LIST = 2;
SET = 3;
MAP = 4;
}
repeated Attribute attributes = 1;
NestingMode nesting = 3;
// MinItems and MaxItems were never used in the protocol, and have no
// effect on validation.
int64 min_items = 4 [deprecated = true];
int64 max_items = 5 [deprecated = true];
}
// The version of the schema.
// Schemas are versioned, so that providers can upgrade a saved resource
// state when the schema is changed.
int64 version = 1;
// Block is the top level configuration block for this schema.
Block block = 2;
}
message Function {
// parameters is the ordered list of positional function parameters.
repeated Parameter parameters = 1;
// variadic_parameter is an optional final parameter which accepts
// zero or more argument values, in which Terraform will send an
// ordered list of the parameter type.
Parameter variadic_parameter = 2;
// Return is the function return parameter.
Return return = 3;
// summary is the human-readable shortened documentation for the function.
string summary = 4;
// description is human-readable documentation for the function.
string description = 5;
// description_kind is the formatting of the description.
StringKind description_kind = 6;
// deprecation_message is human-readable documentation if the
// function is deprecated.
string deprecation_message = 7;
message Parameter {
// name is the human-readable display name for the parameter.
string name = 1;
// type is the type constraint for the parameter.
bytes type = 2;
// allow_null_value when enabled denotes that a null argument value can
// be passed to the provider. When disabled, Terraform returns an error
// if the argument value is null.
bool allow_null_value = 3;
// allow_unknown_values when enabled denotes that only wholly known
// argument values will be passed to the provider. When disabled,
// Terraform skips the function call entirely and assumes an unknown
// value result from the function.
bool allow_unknown_values = 4;
// description is human-readable documentation for the parameter.
string description = 5;
// description_kind is the formatting of the description.
StringKind description_kind = 6;
}
message Return {
// type is the type constraint for the function result.
bytes type = 1;
}
}
// ServerCapabilities allows providers to communicate extra information
// regarding supported protocol features. This is used to indicate
// availability of certain forward-compatible changes which may be optional
// in a major protocol version, but cannot be tested for directly.
message ServerCapabilities {
// The plan_destroy capability signals that a provider expects a call
// to PlanResourceChange when a resource is going to be destroyed.
bool plan_destroy = 1;
// The get_provider_schema_optional capability indicates that this
// provider does not require calling GetProviderSchema to operate
// normally, and the caller can used a cached copy of the provider's
// schema.
bool get_provider_schema_optional = 2;
// The move_resource_state capability signals that a provider supports the
// MoveResourceState RPC.
bool move_resource_state = 3;
}
// Deferred is a message that indicates that change is deferred for a reason.
message Deferred {
// Reason is the reason for deferring the change.
enum Reason {
// UNKNOWN is the default value, and should not be used.
UNKNOWN = 0;
// RESOURCE_CONFIG_UNKNOWN is used when the config is partially unknown and the real
// values need to be known before the change can be planned.
RESOURCE_CONFIG_UNKNOWN = 1;
// PROVIDER_CONFIG_UNKNOWN is used when parts of the provider configuration
// are unknown, e.g. the provider configuration is only known after the apply is done.
PROVIDER_CONFIG_UNKNOWN = 2;
// ABSENT_PREREQ is used when a hard dependency has not been satisfied.
ABSENT_PREREQ = 3;
}
// reason is the reason for deferring the change.
Reason reason = 1;
}
service Provider {
//////// Information about what a provider supports/expects
// GetMetadata returns upfront information about server capabilities and
// supported resource types without requiring the server to instantiate all
// schema information, which may be memory intensive. This RPC is optional,
// where clients may receive an unimplemented RPC error. Clients should
// ignore the error and call the GetProviderSchema RPC as a fallback.
rpc GetMetadata(GetMetadata.Request) returns (GetMetadata.Response);
// GetSchema returns schema information for the provider, data resources,
// and managed resources.
rpc GetProviderSchema(GetProviderSchema.Request) returns (GetProviderSchema.Response);
rpc ValidateProviderConfig(ValidateProviderConfig.Request) returns (ValidateProviderConfig.Response);
rpc ValidateResourceConfig(ValidateResourceConfig.Request) returns (ValidateResourceConfig.Response);
rpc ValidateDataResourceConfig(ValidateDataResourceConfig.Request) returns (ValidateDataResourceConfig.Response);
rpc UpgradeResourceState(UpgradeResourceState.Request) returns (UpgradeResourceState.Response);
//////// One-time initialization, called before other functions below
rpc ConfigureProvider(ConfigureProvider.Request) returns (ConfigureProvider.Response);
//////// Managed Resource Lifecycle
rpc ReadResource(ReadResource.Request) returns (ReadResource.Response);
rpc PlanResourceChange(PlanResourceChange.Request) returns (PlanResourceChange.Response);
rpc ApplyResourceChange(ApplyResourceChange.Request) returns (ApplyResourceChange.Response);
rpc ImportResourceState(ImportResourceState.Request) returns (ImportResourceState.Response);
rpc MoveResourceState(MoveResourceState.Request) returns (MoveResourceState.Response);
rpc ReadDataSource(ReadDataSource.Request) returns (ReadDataSource.Response);
// GetFunctions returns the definitions of all functions.
rpc GetFunctions(GetFunctions.Request) returns (GetFunctions.Response);
//////// Provider-contributed Functions
rpc CallFunction(CallFunction.Request) returns (CallFunction.Response);
//////// Graceful Shutdown
rpc StopProvider(StopProvider.Request) returns (StopProvider.Response);
}
message GetMetadata {
message Request {
}
message Response {
ServerCapabilities server_capabilities = 1;
repeated Diagnostic diagnostics = 2;
repeated DataSourceMetadata data_sources = 3;
repeated ResourceMetadata resources = 4;
// functions returns metadata for any functions.
repeated FunctionMetadata functions = 5;
}
message FunctionMetadata {
// name is the function name.
string name = 1;
}
message DataSourceMetadata {
string type_name = 1;
}
message ResourceMetadata {
string type_name = 1;
}
}
message GetProviderSchema {
message Request {
}
message Response {
Schema provider = 1;
map<string, Schema> resource_schemas = 2;
map<string, Schema> data_source_schemas = 3;
map<string, Function> functions = 7;
repeated Diagnostic diagnostics = 4;
Schema provider_meta = 5;
ServerCapabilities server_capabilities = 6;
}
}
message ValidateProviderConfig {
message Request {
DynamicValue config = 1;
}
message Response {
repeated Diagnostic diagnostics = 2;
}
}
message UpgradeResourceState {
// Request is the message that is sent to the provider during the
// UpgradeResourceState RPC.
//
// This message intentionally does not include configuration data as any
// configuration-based or configuration-conditional changes should occur
// during the PlanResourceChange RPC. Additionally, the configuration is
// not guaranteed to exist (in the case of resource destruction), be wholly
// known, nor match the given prior state, which could lead to unexpected
// provider behaviors for practitioners.
message Request {
string type_name = 1;
// version is the schema_version number recorded in the state file
int64 version = 2;
// raw_state is the raw states as stored for the resource. Core does
// not have access to the schema of prior_version, so it's the
// provider's responsibility to interpret this value using the
// appropriate older schema. The raw_state will be the json encoded
// state, or a legacy flat-mapped format.
RawState raw_state = 3;
}
message Response {
// new_state is a msgpack-encoded data structure that, when interpreted with
// the _current_ schema for this resource type, is functionally equivalent to
// that which was given in prior_state_raw.
DynamicValue upgraded_state = 1;
// diagnostics describes any errors encountered during migration that could not
// be safely resolved, and warnings about any possibly-risky assumptions made
// in the upgrade process.
repeated Diagnostic diagnostics = 2;
}
}
message ValidateResourceConfig {
message Request {
string type_name = 1;
DynamicValue config = 2;
}
message Response {
repeated Diagnostic diagnostics = 1;
}
}
message ValidateDataResourceConfig {
message Request {
string type_name = 1;
DynamicValue config = 2;
}
message Response {
repeated Diagnostic diagnostics = 1;
}
}
message ConfigureProvider {
message Request {
string terraform_version = 1;
DynamicValue config = 2;
}
message Response {
repeated Diagnostic diagnostics = 1;
}
}
message ReadResource {
// Request is the message that is sent to the provider during the
// ReadResource RPC.
//
// This message intentionally does not include configuration data as any
// configuration-based or configuration-conditional changes should occur
// during the PlanResourceChange RPC. Additionally, the configuration is
// not guaranteed to be wholly known nor match the given prior state, which
// could lead to unexpected provider behaviors for practitioners.
message Request {
string type_name = 1;
DynamicValue current_state = 2;
bytes private = 3;
DynamicValue provider_meta = 4;
// deferral_allowed signals that the provider is allowed to defer the
// changes. If set the caller needs to handle the deferred response.
bool deferral_allowed = 5;
}
message Response {
DynamicValue new_state = 1;
repeated Diagnostic diagnostics = 2;
bytes private = 3;
// deferred is set if the provider is deferring the change. If set the caller
// needs to handle the deferral.
Deferred deferred = 4;
}
}
message PlanResourceChange {
message Request {
string type_name = 1;
DynamicValue prior_state = 2;
DynamicValue proposed_new_state = 3;
DynamicValue config = 4;
bytes prior_private = 5;
DynamicValue provider_meta = 6;
// deferral_allowed signals that the provider is allowed to defer the
// changes. If set the caller needs to handle the deferred response.
bool deferral_allowed = 7;
}
message Response {
DynamicValue planned_state = 1;
repeated AttributePath requires_replace = 2;
bytes planned_private = 3;
repeated Diagnostic diagnostics = 4;
// This may be set only by the helper/schema "SDK" in the main Terraform
// repository, to request that Terraform Core >=0.12 permit additional
// inconsistencies that can result from the legacy SDK type system
// and its imprecise mapping to the >=0.12 type system.
// The change in behavior implied by this flag makes sense only for the
// specific details of the legacy SDK type system, and are not a general
// mechanism to avoid proper type handling in providers.
//
// ==== DO NOT USE THIS ====
// ==== THIS MUST BE LEFT UNSET IN ALL OTHER SDKS ====
// ==== DO NOT USE THIS ====
bool legacy_type_system = 5;
// deferred is set if the provider is deferring the change. If set the caller
// needs to handle the deferral.
Deferred deferred = 6;
}
}
message ApplyResourceChange {
message Request {
string type_name = 1;
DynamicValue prior_state = 2;
DynamicValue planned_state = 3;
DynamicValue config = 4;
bytes planned_private = 5;
DynamicValue provider_meta = 6;
}
message Response {
DynamicValue new_state = 1;
bytes private = 2;
repeated Diagnostic diagnostics = 3;
// This may be set only by the helper/schema "SDK" in the main Terraform
// repository, to request that Terraform Core >=0.12 permit additional
// inconsistencies that can result from the legacy SDK type system
// and its imprecise mapping to the >=0.12 type system.
// The change in behavior implied by this flag makes sense only for the
// specific details of the legacy SDK type system, and are not a general
// mechanism to avoid proper type handling in providers.
//
// ==== DO NOT USE THIS ====
// ==== THIS MUST BE LEFT UNSET IN ALL OTHER SDKS ====
// ==== DO NOT USE THIS ====
bool legacy_type_system = 4;
}
}
message ImportResourceState {
message Request {
string type_name = 1;
string id = 2;
// deferral_allowed signals that the provider is allowed to defer the
// changes. If set the caller needs to handle the deferred response.
bool deferral_allowed = 3;
}
message ImportedResource {
string type_name = 1;
DynamicValue state = 2;
bytes private = 3;
}
message Response {
repeated ImportedResource imported_resources = 1;
repeated Diagnostic diagnostics = 2;
// deferred is set if the provider is deferring the change. If set the caller
// needs to handle the deferral.
Deferred deferred = 3;
}
}
message MoveResourceState {
message Request {
// The address of the provider the resource is being moved from.
string source_provider_address = 1;
// The resource type that the resource is being moved from.
string source_type_name = 2;
// The schema version of the resource type that the resource is being
// moved from.
int64 source_schema_version = 3;
// The raw state of the resource being moved. Only the json field is
// populated, as there should be no legacy providers using the flatmap
// format that support newly introduced RPCs.
RawState source_state = 4;
// The resource type that the resource is being moved to.
string target_type_name = 5;
// The private state of the resource being moved.
bytes source_private = 6;
}
message Response {
// The state of the resource after it has been moved.
DynamicValue target_state = 1;
// Any diagnostics that occurred during the move.
repeated Diagnostic diagnostics = 2;
// The private state of the resource after it has been moved.
bytes target_private = 3;
}
}
message ReadDataSource {
message Request {
string type_name = 1;
DynamicValue config = 2;
DynamicValue provider_meta = 3;
// deferral_allowed signals that the provider is allowed to defer the
// changes. If set the caller needs to handle the deferred response.
bool deferral_allowed = 4;
}
message Response {
DynamicValue state = 1;
repeated Diagnostic diagnostics = 2;
// deferred is set if the provider is deferring the change. If set the caller
// needs to handle the deferral.
Deferred deferred = 3;
}
}
message GetFunctions {
message Request {}
message Response {
// functions is a mapping of function names to definitions.
map<string, Function> functions = 1;
// diagnostics is any warnings or errors.
repeated Diagnostic diagnostics = 2;
}
}
message CallFunction {
message Request {
string name = 1;
repeated DynamicValue arguments = 2;
}
message Response {
DynamicValue result = 1;
FunctionError error = 2;
}
}

4
shell.nix Normal file
View file

@ -0,0 +1,4 @@
{ pkgs ? import <nixpkgs> { } }: pkgs.mkShell {
buildInputs = [ ];
packages = with pkgs; [ opentofu protobuf ];
}

59
src/main.rs Normal file
View file

@ -0,0 +1,59 @@
use std::{
env,
io::Write,
net::{IpAddr, Ipv4Addr, SocketAddr},
};
use eyre::{bail, ensure, Context, Result};
mod server;
#[tokio::main]
async fn main() -> eyre::Result<()> {
let addr = init_handshake();
let addr = match addr {
Ok(addr) => addr,
Err(err) => {
println!("{:?}", err);
bail!("init error");
}
};
let cert = std::env::var("PLUGIN_CLIENT_CERT").wrap_err("PLUGIN_CLIENT_CERT not found")?;
tonic::transport::Server::builder()
.add_service(server::tfplugin6::provider_server::ProviderServer::new(
server::MyProvider,
))
.serve(addr)
.await
.wrap_err("failed to start server")?;
Ok(())
}
fn init_handshake() -> Result<SocketAddr> {
// https://github.com/hashicorp/go-plugin/blob/8d2aaa458971cba97c3bfec1b0380322e024b514/docs/internals.md
let min_port = env::var("PLUGIN_MIN_PORT")
.wrap_err("PLUGIN_MIN_PORT not found")?
.parse::<u16>()
.wrap_err("PLUGIN_MIN_PORT not an int")?;
let max_port = env::var("PLUGIN_MAX_PORT")
.wrap_err("PLUGIN_MAX_PORT not found")?
.parse::<u16>()
.wrap_err("PLUGIN_MAX_PORT not an int")?;
let port = min_port + 15; // chosen by a d20, lol
ensure!(port < max_port);
let addr = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1));
let addr = SocketAddr::new(addr, port);
const VERSION: u8 = 6;
println!("1|{VERSION}|tcp|{addr}|grpc");
Ok(addr)
}

138
src/server.rs Normal file
View file

@ -0,0 +1,138 @@
pub mod tfplugin6 {
tonic::include_proto!("tfplugin6");
}
use tfplugin6::provider_server::{Provider, ProviderServer};
use tonic::{transport::Server, Request, Response, Result, Status};
#[derive(Debug, Default)]
pub struct MyProvider;
#[tonic::async_trait]
impl Provider for MyProvider {
/// GetMetadata returns upfront information about server capabilities and
/// supported resource types without requiring the server to instantiate all
/// schema information, which may be memory intensive. This RPC is optional,
/// where clients may receive an unimplemented RPC error. Clients should
/// ignore the error and call the GetProviderSchema RPC as a fallback.
async fn get_metadata(
&self,
request: tonic::Request<tfplugin6::get_metadata::Request>,
) -> std::result::Result<tonic::Response<tfplugin6::get_metadata::Response>, tonic::Status> {
todo!()
}
/// GetSchema returns schema information for the provider, data resources,
/// and managed resources.
async fn get_provider_schema(
&self,
request: tonic::Request<tfplugin6::get_provider_schema::Request>,
) -> std::result::Result<tonic::Response<tfplugin6::get_provider_schema::Response>, tonic::Status>
{
todo!()
}
async fn validate_provider_config(
&self,
request: tonic::Request<tfplugin6::validate_provider_config::Request>,
) -> std::result::Result<
tonic::Response<tfplugin6::validate_provider_config::Response>,
tonic::Status,
> {
todo!()
}
async fn validate_resource_config(
&self,
request: tonic::Request<tfplugin6::validate_resource_config::Request>,
) -> std::result::Result<
tonic::Response<tfplugin6::validate_resource_config::Response>,
tonic::Status,
> {
todo!()
}
async fn validate_data_resource_config(
&self,
request: tonic::Request<tfplugin6::validate_data_resource_config::Request>,
) -> std::result::Result<
tonic::Response<tfplugin6::validate_data_resource_config::Response>,
tonic::Status,
> {
todo!()
}
async fn upgrade_resource_state(
&self,
request: tonic::Request<tfplugin6::upgrade_resource_state::Request>,
) -> std::result::Result<tonic::Response<tfplugin6::upgrade_resource_state::Response>, tonic::Status>
{
todo!()
}
/// ////// One-time initialization, called before other functions below
async fn configure_provider(
&self,
request: tonic::Request<tfplugin6::configure_provider::Request>,
) -> std::result::Result<tonic::Response<tfplugin6::configure_provider::Response>, tonic::Status>
{
todo!()
}
/// ////// Managed Resource Lifecycle
async fn read_resource(
&self,
request: tonic::Request<tfplugin6::read_resource::Request>,
) -> std::result::Result<tonic::Response<tfplugin6::read_resource::Response>, tonic::Status> {
todo!()
}
async fn plan_resource_change(
&self,
request: tonic::Request<tfplugin6::plan_resource_change::Request>,
) -> std::result::Result<tonic::Response<tfplugin6::plan_resource_change::Response>, tonic::Status>
{
todo!()
}
async fn apply_resource_change(
&self,
request: tonic::Request<tfplugin6::apply_resource_change::Request>,
) -> std::result::Result<tonic::Response<tfplugin6::apply_resource_change::Response>, tonic::Status>
{
todo!()
}
async fn import_resource_state(
&self,
request: tonic::Request<tfplugin6::import_resource_state::Request>,
) -> std::result::Result<tonic::Response<tfplugin6::import_resource_state::Response>, tonic::Status>
{
todo!()
}
async fn move_resource_state(
&self,
request: tonic::Request<tfplugin6::move_resource_state::Request>,
) -> std::result::Result<tonic::Response<tfplugin6::move_resource_state::Response>, tonic::Status>
{
todo!()
}
async fn read_data_source(
&self,
request: tonic::Request<tfplugin6::read_data_source::Request>,
) -> std::result::Result<tonic::Response<tfplugin6::read_data_source::Response>, tonic::Status>
{
todo!()
}
/// GetFunctions returns the definitions of all functions.
async fn get_functions(
&self,
request: tonic::Request<tfplugin6::get_functions::Request>,
) -> std::result::Result<tonic::Response<tfplugin6::get_functions::Response>, tonic::Status> {
todo!()
}
/// ////// Provider-contributed Functions
async fn call_function(
&self,
request: tonic::Request<tfplugin6::call_function::Request>,
) -> std::result::Result<tonic::Response<tfplugin6::call_function::Response>, tonic::Status> {
todo!()
}
/// ////// Graceful Shutdown
async fn stop_provider(
&self,
request: tonic::Request<tfplugin6::stop_provider::Request>,
) -> std::result::Result<tonic::Response<tfplugin6::stop_provider::Response>, tonic::Status> {
todo!()
}
}

7
test/main.tf Normal file
View file

@ -0,0 +1,7 @@
terraform {
required_providers {
terustform = {
source = "github.com/Nilstrieb/terustform"
}
}
}