diff --git a/.gitignore b/.gitignore index e10bb0704..c7021dcb4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,12 @@ -target +build/ +target/ bin/ .DS_Store *.msix -# Node.js generated files for tree-sitter -build/ -node_modules/ +# Generated files for tree-sitter grammars/**/bindings/ grammars/**/src/ grammars/**/parser.* +tree-sitter-ssh-server-config/ +tree-sitter-dscexpression/ diff --git a/Cargo.lock b/Cargo.lock index 2f8011dcb..440ad54c4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -96,6 +96,12 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + [[package]] name = "arc-swap" version = "1.7.1" @@ -108,6 +114,28 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "async-trait" version = "0.1.89" @@ -131,6 +159,49 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "axum" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b098575ebe77cb6d14fc7f32749631a6e44edbef6b796f89b020e99ba20d425" +dependencies = [ + "axum-core", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "sync_wrapper", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-core" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59446ce19cd142f8833f856eb31f3eb097812d1479ab224f54d72428ca21ea22" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", +] + [[package]] name = "base32" version = "0.5.1" @@ -890,6 +961,24 @@ dependencies = [ "tree-sitter-ssh-server-config", ] +[[package]] +name = "dscbicep" +version = "0.1.0" +dependencies = [ + "async-stream", + "clap", + "dsc-lib", + "prost", + "tokio", + "tokio-stream", + "tonic", + "tonic-prost", + "tonic-prost-build", + "tonic-reflection", + "tracing", + "tracing-subscriber", +] + [[package]] name = "dsctest" version = "0.1.0" @@ -972,6 +1061,12 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0399f9d26e5191ce32c498bebd31e7a3ceabc2745f0ac54af3f335126c3f24b3" +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + [[package]] name = "flate2" version = "1.1.4" @@ -1186,6 +1281,25 @@ dependencies = [ "walkdir", ] +[[package]] +name = "h2" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hashbrown" version = "0.16.0" @@ -1243,6 +1357,12 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + [[package]] name = "hyper" version = "1.7.0" @@ -1253,9 +1373,11 @@ dependencies = [ "bytes", "futures-channel", "futures-core", + "h2", "http", "http-body", "httparse", + "httpdate", "itoa", "pin-project-lite", "pin-utils", @@ -1281,6 +1403,19 @@ dependencies = [ "webpki-roots", ] +[[package]] +name = "hyper-timeout" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" +dependencies = [ + "hyper", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", +] + [[package]] name = "hyper-util" version = "0.1.17" @@ -1637,12 +1772,24 @@ dependencies = [ "regex-automata", ] +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + [[package]] name = "memchr" version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -1665,6 +1812,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "multimap" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" + [[package]] name = "murmurhash64" version = "0.3.1" @@ -2074,6 +2227,36 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "petgraph" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" +dependencies = [ + "fixedbitset", + "indexmap", +] + +[[package]] +name = "pin-project" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -2139,6 +2322,16 @@ dependencies = [ "yansi", ] +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + [[package]] name = "proc-macro2" version = "1.0.103" @@ -2148,6 +2341,80 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "prost" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7231bd9b3d3d33c86b58adbac74b5ec0ad9f496b19d22801d773636feaa95f3d" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-build" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac6c3320f9abac597dcbc668774ef006702672474aad53c6d596b62e487b40b1" +dependencies = [ + "heck", + "itertools", + "log", + "multimap", + "once_cell", + "petgraph", + "prettyplease", + "prost", + "prost-types", + "pulldown-cmark", + "pulldown-cmark-to-cmark", + "regex", + "syn", + "tempfile", +] + +[[package]] +name = "prost-derive" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9120690fafc389a67ba3803df527d0ec9cbbc9cc45e4cc20b332996dfb672425" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "prost-types" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9b4db3d6da204ed77bb26ba83b6122a73aeb2e87e25fbf7ad2e84c4ccbf8f72" +dependencies = [ + "prost", +] + +[[package]] +name = "pulldown-cmark" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e8bbe1a966bd2f362681a44f6edce3c2310ac21e4d5067a6e7ec396297a6ea0" +dependencies = [ + "bitflags 2.9.4", + "memchr", + "unicase", +] + +[[package]] +name = "pulldown-cmark-to-cmark" +version = "21.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8246feae3db61428fd0bb94285c690b460e4517d83152377543ca802357785f1" +dependencies = [ + "pulldown-cmark", +] + [[package]] name = "quick-xml" version = "0.38.3" @@ -3135,6 +3402,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-stream" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.16" @@ -3189,6 +3467,88 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" +[[package]] +name = "tonic" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb7613188ce9f7df5bfe185db26c5814347d110db17920415cf2fbcad85e7203" +dependencies = [ + "async-trait", + "axum", + "base64", + "bytes", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-timeout", + "hyper-util", + "percent-encoding", + "pin-project", + "socket2", + "sync_wrapper", + "tokio", + "tokio-stream", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tonic-build" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c40aaccc9f9eccf2cd82ebc111adc13030d23e887244bc9cfa5d1d636049de3" +dependencies = [ + "prettyplease", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tonic-prost" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66bd50ad6ce1252d87ef024b3d64fe4c3cf54a86fb9ef4c631fdd0ded7aeaa67" +dependencies = [ + "bytes", + "prost", + "tonic", +] + +[[package]] +name = "tonic-prost-build" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4a16cba4043dc3ff43fcb3f96b4c5c154c64cbd18ca8dce2ab2c6a451d058a2" +dependencies = [ + "prettyplease", + "proc-macro2", + "prost-build", + "prost-types", + "quote", + "syn", + "tempfile", + "tonic-build", +] + +[[package]] +name = "tonic-reflection" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34da53e8387581d66db16ff01f98a70b426b091fdf76856e289d5c1bd386ed7b" +dependencies = [ + "prost", + "prost-types", + "tokio", + "tokio-stream", + "tonic", + "tonic-prost", +] + [[package]] name = "tower" version = "0.5.2" @@ -3197,11 +3557,15 @@ checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" dependencies = [ "futures-core", "futures-util", + "indexmap", "pin-project-lite", + "slab", "sync_wrapper", "tokio", + "tokio-util", "tower-layer", "tower-service", + "tracing", ] [[package]] @@ -3393,6 +3757,12 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +[[package]] +name = "unicase" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" + [[package]] name = "unicode-general-category" version = "1.1.0" diff --git a/Cargo.toml b/Cargo.toml index 106a4945b..dbebbc33d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ resolver = "2" # the path to a crate. members = [ "dsc", + "dscbicep", "lib/dsc-lib", "lib/dsc-lib-jsonschema", "lib/dsc-lib-jsonschema-macros", @@ -28,6 +29,7 @@ members = [ # avoid unintentionally modifying this value. default-members = [ "dsc", + "dscbicep", "lib/dsc-lib", "lib/dsc-lib-jsonschema", "lib/dsc-lib-jsonschema-macros", @@ -54,6 +56,7 @@ default-members = [ # current operating system to enable faster builds. Windows = [ "dsc", + "dscbicep", "lib/dsc-lib", "lib/dsc-lib-jsonschema", "lib/dsc-lib-jsonschema-macros", @@ -75,6 +78,7 @@ Windows = [ ] macOS = [ "dsc", + "dscbicep", "lib/dsc-lib", "lib/dsc-lib-jsonschema", "lib/dsc-lib-jsonschema-macros", @@ -93,6 +97,7 @@ macOS = [ ] Linux = [ "dsc", + "dscbicep", "lib/dsc-lib", "lib/dsc-lib-jsonschema", "lib/dsc-lib-jsonschema-macros", @@ -127,7 +132,7 @@ base32 = { version = "0.5" } base64 = { version = "0.22" } # dsc-lib, sshdconfig chrono = { version = "0.4" } -# dsc, dsc-lib, dscecho, registry, runcommandonset, sshdconfig, dsctest, test_group_resource +# dsc, dsc-lib, dscbicep, dscecho, registry, runcommandonset, sshdconfig, dsctest, test_group_resource clap = { version = "4.5", features = ["derive"] } # dsc clap_complete = { version = "4.5" } @@ -141,6 +146,8 @@ darling = { version = "0.23" } derive_builder = { version = "0.20" } # dsc, dsc-lib indicatif = { version = "0.18" } +# dscbicep +async-stream = { version = "0.3" } # dsc-lib-security_context::windows is_elevated = { version = "0.1" } # dsc, dsc-lib @@ -157,6 +164,8 @@ num-traits = { version = "0.2" } os_info = { version = "3.14" } # dsc, dsc-lib path-absolutize = { version = "3.1" } +# dscbicep +prost = { version = "0.14" } # dsc-lib-jsonschema-macros proc-macro2 = { version = "1.0" } # dsc-lib-jsonschema-macros @@ -191,15 +200,23 @@ sysinfo = { version = "0.37" } tempfile = { version = "3.23" } # dsc, dsc-lib, registry, dsc-lib-registry, sshdconfig thiserror = { version = "2.0" } -# dsc, dsc-lib +# dsc, dsc-lib, dscbicep tokio = { version = "1.48" } +# dscbicep +tokio-stream = { version = "0.1" } # dsc tokio-util = { version = "0.7" } -# dsc, dsc-lib, registry, dsc-lib-registry, runcommandonset, sshdconfig +# dscbicep +tonic = { version = "0.14" } +# dscbicep +tonic-prost = { version = "0.14" } +# dscbicep +tonic-reflection = { version = "0.14" } +# dsc, dsc-lib, dscbicep, registry, dsc-lib-registry, runcommandonset, sshdconfig tracing = { version = "0.1" } # dsc, dsc-lib tracing-indicatif = { version = "0.3" } -# dsc, registry, dsc-lib-registry, runcommandonset, sshdconfig +# dsc, dscbicep, registry, dsc-lib-registry, runcommandonset, sshdconfig tracing-subscriber = { version = "0.3", features = ["ansi", "env-filter", "json"] } # dsc-lib, sshdconfig, tree-sitter-dscexpression, tree-sitter-ssh-server-config tree-sitter = { version = "0.25" } @@ -223,6 +240,8 @@ ipnetwork = { version = "0.21" } # build-only dependencies # dsc-lib, dsc-lib-registry, sshdconfig, tree-sitter-dscexpression, tree-sitter-ssh-server-config cc = { version = "1.2" } +# dsc +tonic-prost-build = { version = "0.14" } # test-only dependencies # dsc-lib-jsonschema diff --git a/dsc/locales/en-us.toml b/dsc/locales/en-us.toml index 61d5946b6..62d3525b2 100644 --- a/dsc/locales/en-us.toml +++ b/dsc/locales/en-us.toml @@ -36,6 +36,7 @@ functionAbout = "Operations on DSC functions" listFunctionAbout = "List or find functions" version = "The version of the resource to invoke in semver format" mcpAbout = "Use DSC as a MCP server" +bicepAbout = "Use DSC as a Bicep server over gRPC" [main] ctrlCReceived = "Ctrl-C received" diff --git a/dsc/src/util.rs b/dsc/src/util.rs index 6e4ec31a0..049a886fa 100644 --- a/dsc/src/util.rs +++ b/dsc/src/util.rs @@ -73,6 +73,7 @@ pub const EXIT_CTRL_C: i32 = 6; pub const EXIT_DSC_RESOURCE_NOT_FOUND: i32 = 7; pub const EXIT_DSC_ASSERTION_FAILED: i32 = 8; pub const EXIT_MCP_FAILED: i32 = 9; +pub const EXIT_BICEP_FAILED: i32 = 10; pub const DSC_CONFIG_ROOT: &str = "DSC_CONFIG_ROOT"; pub const DSC_TRACE_LEVEL: &str = "DSC_TRACE_LEVEL"; diff --git a/dscbicep/.project.data.json b/dscbicep/.project.data.json new file mode 100644 index 000000000..1f0ee7c11 --- /dev/null +++ b/dscbicep/.project.data.json @@ -0,0 +1,6 @@ +{ + "Name": "dscbicep", + "Kind": "CLI", + "IsRust": true, + "Binaries": ["dscbicep"] +} diff --git a/dscbicep/Cargo.toml b/dscbicep/Cargo.toml new file mode 100644 index 000000000..2140e2900 --- /dev/null +++ b/dscbicep/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "dscbicep" +version = "0.1.0" +edition = "2021" + +[[bin]] +name = "dscbicep" +path = "src/main.rs" + +[dependencies] +dsc-lib = { workspace = true } + +async-stream = { workspace = true } +clap = { workspace = true } +prost = { workspace = true } +tokio = { workspace = true, features = ["rt-multi-thread", "macros", "signal", "io-std", "net"] } +tokio-stream = { workspace = true, features = ["net"] } +tonic = { workspace = true } +tonic-prost = { workspace = true } +tonic-reflection = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } + +[build-dependencies] +tonic-prost-build = { workspace = true } diff --git a/dscbicep/build.rs b/dscbicep/build.rs new file mode 100644 index 000000000..0a7482414 --- /dev/null +++ b/dscbicep/build.rs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use std::{env, path::PathBuf}; + +fn main() -> Result<(), Box> { + let descriptor_path = PathBuf::from(env::var("OUT_DIR").unwrap()).join("bicep.bin"); + + tonic_prost_build::configure() + .build_client(false) + .file_descriptor_set_path(&descriptor_path) + // TODO: Configure and commit the out_dir to avoid dependency on protoc + // .out_dir(out_dir) + .compile_protos(&["proto/bicep.proto"], &["proto"])?; + Ok(()) +} diff --git a/dscbicep/proto/bicep.proto b/dscbicep/proto/bicep.proto new file mode 100644 index 000000000..c02b28d90 --- /dev/null +++ b/dscbicep/proto/bicep.proto @@ -0,0 +1,66 @@ +syntax = "proto3"; + +option csharp_namespace = "Bicep.Local.Rpc"; + +package extension; + +service BicepExtension { + rpc CreateOrUpdate (ResourceSpecification) returns (LocalExtensibilityOperationResponse); + rpc Preview (ResourceSpecification) returns (LocalExtensibilityOperationResponse); + rpc Get (ResourceReference) returns (LocalExtensibilityOperationResponse); + rpc Delete (ResourceReference) returns (LocalExtensibilityOperationResponse); + rpc GetTypeFiles(Empty) returns (TypeFilesResponse); + rpc Ping(Empty) returns (Empty); +} + +message Empty {} + +message ResourceSpecification { + optional string config = 1; + string type = 2; + optional string apiVersion = 3; + string properties = 4; +} + +message ResourceReference { + string identifiers = 1; + optional string config = 2; + string type = 3; + optional string apiVersion = 4; +} + +message LocalExtensibilityOperationResponse { + optional Resource resource = 1; + optional ErrorData errorData = 2; +} + +message Resource { + string type = 1; + optional string apiVersion = 2; + string identifiers = 3; + string properties = 4; + optional string status = 5; +} + +message ErrorData { + Error error = 1; +} + +message Error { + string code = 1; + optional string target = 2; + string message = 3; + repeated ErrorDetail details = 4; + optional string innerError = 5; +} + +message ErrorDetail { + string code = 1; + optional string target = 2; + string message = 3; +} + +message TypeFilesResponse { + string indexFile = 1; + map typeFiles = 2; +} diff --git a/dscbicep/src/main.rs b/dscbicep/src/main.rs new file mode 100644 index 000000000..726dc6e13 --- /dev/null +++ b/dscbicep/src/main.rs @@ -0,0 +1,424 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use clap::Parser; +use dsc_lib::{ + configure::config_doc::ExecutionKind, + dscresources::{ + dscresource::Invoke, + invoke_result::{GetResult, SetResult}, + }, + DscManager, +}; +use std::{env, io, process}; +use tonic::{transport::Server, Request, Response, Status}; + +// Include the generated protobuf code +pub mod proto { + tonic::include_proto!("extension"); + pub(crate) const FILE_DESCRIPTOR_SET: &[u8] = tonic::include_file_descriptor_set!("bicep"); +} + +use proto::bicep_extension_server::{BicepExtension, BicepExtensionServer}; +use proto::{ + Empty, LocalExtensibilityOperationResponse, ResourceReference, ResourceSpecification, + TypeFilesResponse, +}; + +#[derive(Debug, Default)] +pub struct BicepExtensionService; + +#[tonic::async_trait] +impl BicepExtension for BicepExtensionService { + async fn create_or_update( + &self, + request: Request, + ) -> Result, Status> { + let spec = request.into_inner(); + let resource_type = spec.r#type; + let version = spec.api_version; + let properties = spec.properties; + + tracing::debug!("CreateOrUpdate called for {resource_type}@{version:?}: {properties}"); + + let mut dsc = DscManager::new(); + let Some(resource) = dsc.find_resource(&resource_type, version.as_deref()) else { + return Err(Status::not_found("Resource not found")); + }; + + let SetResult::Resource(result) = resource + .set(&properties, false, &ExecutionKind::Actual) + .map_err(|e| Status::aborted(e.to_string()))? + else { + return Err(Status::unimplemented("Group resources not supported")); + }; + + Ok(Response::new(LocalExtensibilityOperationResponse { + resource: Some(proto::Resource { + r#type: resource_type, + api_version: version, + identifiers: properties, + properties: result.after_state.to_string(), + status: None, + }), + error_data: None, + })) + } + + async fn preview( + &self, + request: Request, + ) -> Result, Status> { + let spec = request.into_inner(); + let resource_type = spec.r#type; + let version = spec.api_version; + let properties = spec.properties; + + tracing::debug!("Preview called for {resource_type}@{version:?}: {properties}"); + + let mut dsc = DscManager::new(); + let Some(resource) = dsc.find_resource(&resource_type, version.as_deref()) else { + return Err(Status::not_found("Resource not found")); + }; + + let SetResult::Resource(result) = resource + .set(&properties, false, &ExecutionKind::WhatIf) + .map_err(|e| Status::aborted(e.to_string()))? + else { + return Err(Status::unimplemented("Group resources not supported")); + }; + + Ok(Response::new(LocalExtensibilityOperationResponse { + resource: Some(proto::Resource { + r#type: resource_type, + api_version: version, + identifiers: properties, + properties: result.after_state.to_string(), + status: None, + }), + error_data: None, + })) + } + + async fn get( + &self, + request: Request, + ) -> Result, Status> { + let reference = request.into_inner(); + let resource_type = reference.r#type.clone(); + let version = reference.api_version.clone(); + let identifiers = reference.identifiers.clone(); + + tracing::debug!("Get called for {resource_type}@{version:?}: {identifiers}"); + + let mut dsc = DscManager::new(); + let Some(resource) = dsc.find_resource(&resource_type, version.as_deref()) else { + return Err(Status::not_found("Resource not found")); + }; + + // TODO: DSC asks for 'properties' here but we only have 'identifiers' from Bicep. + let GetResult::Resource(result) = resource + .get(&identifiers) + .map_err(|e| Status::aborted(e.to_string()))? + else { + return Err(Status::unimplemented("Group resources not supported")); + }; + + Ok(Response::new(LocalExtensibilityOperationResponse { + resource: Some(proto::Resource { + r#type: resource_type, + api_version: version, + identifiers: identifiers, + properties: result.actual_state.to_string(), + status: None, + }), + error_data: None, + })) + } + + async fn delete( + &self, + request: Request, + ) -> Result, Status> { + let reference = request.into_inner(); + let resource_type = reference.r#type.clone(); + let version = reference.api_version.clone(); + let identifiers = reference.identifiers.clone(); + + tracing::debug!( + "Delete called for {}@{:?}: {}", + resource_type, + version, + identifiers + ); + + let mut dsc = DscManager::new(); + let Some(resource) = dsc.find_resource(&resource_type, version.as_deref()) else { + return Err(Status::not_found("Resource not found")); + }; + + // TODO: DSC asks for 'properties' here but we only have 'identifiers' from Bicep. + resource + .delete(&identifiers) + .map_err(|e| Status::aborted(e.to_string()))?; + + Ok(Response::new(LocalExtensibilityOperationResponse { + resource: Some(proto::Resource { + r#type: resource_type, + api_version: version, + identifiers: identifiers, + properties: "{}".to_string(), + status: None, + }), + error_data: None, + })) + } + + async fn get_type_files( + &self, + _request: Request, + ) -> Result, Status> { + tracing::debug!("GetTypeFiles called"); + + // TODO: Return actual Bicep type definitions...yet the extension already has these? + // Perhaps this is where we can dynamically get them from the current system. + Err(Status::unimplemented("GetTypeFiles not yet implemented")) + } + + async fn ping(&self, _request: Request) -> Result, Status> { + tracing::debug!("Ping called"); + Ok(Response::new(Empty {})) + } +} + +#[derive(Parser, Debug)] +#[command(name = "dscbicep")] +#[command(about = "DSC Bicep Local Deploy Extension", long_about = None)] +struct Args { + /// The path to the domain socket to connect on (Unix-like systems) + #[arg(long)] + socket: Option, + + /// The named pipe to connect on (Windows) + #[arg(long)] + pipe: Option, + + /// The HTTP address to listen on (e.g., 127.0.0.1:50051) + #[arg(long)] + http: Option, + + /// Wait for debugger to attach before starting + #[arg(long)] + wait_for_debugger: bool, +} + +#[allow(unused_variables)] +async fn run_server( + socket: Option, + pipe: Option, + http: Option, +) -> Result<(), Box> { + let service = BicepExtensionService; + + #[cfg(unix)] + if let Some(socket_path) = socket { + use tokio::net::UnixListener; + use tokio_stream::wrappers::UnixListenerStream; + + tracing::info!("Starting Bicep gRPC server on Unix socket: {socket_path}"); + + // Remove the socket file if it exists + let _ = std::fs::remove_file(&socket_path); + + let uds = UnixListener::bind(&socket_path)?; + let uds_stream = UnixListenerStream::new(uds); + + Server::builder() + .add_service(BicepExtensionServer::new(service)) + .serve_with_incoming(uds_stream) + .await?; + + return Ok(()); + } + + #[cfg(windows)] + if let Some(pipe_name) = pipe { + // TODO: This named pipe code is messy and honestly mostly generated. It + // does work, but most of the problem lies in minimal Windows support + // inside the Tokio library (and no support for UDS). + use std::pin::Pin; + use std::task::{Context, Poll}; + use tokio::io::{AsyncRead, AsyncWrite}; + use tokio::net::windows::named_pipe::ServerOptions; + use tonic::transport::server::Connected; + + // Wrapper to implement Connected trait for NamedPipeServer + struct NamedPipeConnection(tokio::net::windows::named_pipe::NamedPipeServer); + + impl Connected for NamedPipeConnection { + type ConnectInfo = (); + + fn connect_info(&self) -> Self::ConnectInfo { + () + } + } + + impl AsyncRead for NamedPipeConnection { + fn poll_read( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut tokio::io::ReadBuf<'_>, + ) -> Poll> { + Pin::new(&mut self.0).poll_read(cx, buf) + } + } + + impl AsyncWrite for NamedPipeConnection { + fn poll_write( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + Pin::new(&mut self.0).poll_write(cx, buf) + } + + fn poll_flush( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll> { + Pin::new(&mut self.0).poll_flush(cx) + } + + fn poll_shutdown( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll> { + Pin::new(&mut self.0).poll_shutdown(cx) + } + } + + // Windows named pipes must be in the format \\.\pipe\{name} + let full_pipe_path = format!(r"\\.\pipe\{}", pipe_name); + tracing::info!("Starting Bicep gRPC server on named pipe: {full_pipe_path}"); + + // Create a stream that accepts connections on the named pipe + let incoming = async_stream::stream! { + // Track whether this is the first instance + let mut is_first = true; + + loop { + let pipe = if is_first { + ServerOptions::new() + .first_pipe_instance(true) + .create(&full_pipe_path) + } else { + ServerOptions::new() + .create(&full_pipe_path) + }; + + let server = match pipe { + Ok(server) => server, + Err(e) => { + tracing::error!("Failed to create named pipe: {}", e); + break; + } + }; + + is_first = false; + + tracing::debug!("Waiting for client to connect to named pipe..."); + match server.connect().await { + Ok(()) => { + tracing::info!("Client connected to named pipe"); + yield Ok::<_, std::io::Error>(NamedPipeConnection(server)); + } + Err(e) => { + tracing::error!("Failed to accept connection: {}", e); + break; + } + } + } + }; + + Server::builder() + .add_service(BicepExtensionServer::new(service)) + .serve_with_incoming(incoming) + .await?; + + return Ok(()); + } + + // Default to HTTP server on [::1]:50051 if no transport specified + let addr = http.unwrap_or_else(|| "[::1]:50051".to_string()).parse()?; + tracing::info!("Starting Bicep gRPC server on HTTP: {addr}"); + + let reflection_service = tonic_reflection::server::Builder::configure() + .register_encoded_file_descriptor_set(proto::FILE_DESCRIPTOR_SET) + .build_v1() + .unwrap(); + + Server::builder() + .add_service(reflection_service) + .add_service(BicepExtensionServer::new(service)) + .serve(addr) + .await?; + + Ok(()) +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + let trace_level = env::var("DSC_TRACE_LEVEL") + .ok() + .and_then(|level| match level.to_uppercase().as_str() { + "TRACE" => Some(tracing::Level::TRACE), + "DEBUG" => Some(tracing::Level::DEBUG), + "INFO" => Some(tracing::Level::INFO), + "WARN" => Some(tracing::Level::WARN), + "ERROR" => Some(tracing::Level::ERROR), + _ => None, + }) + .unwrap_or(tracing::Level::WARN); + + tracing_subscriber::fmt() + .with_target(false) + .with_level(true) + .with_max_level(trace_level) + .init(); + + let args = Args::parse(); + tracing::debug!("Args are {args:#?}"); + + if args.wait_for_debugger + || env::var_os("DSC_GRPC_DEBUG").is_some_and(|v| v.eq_ignore_ascii_case("true")) + { + tracing::warn!( + "Press any key to continue after attaching to PID: {}", + process::id() + ); + let mut input = String::new(); + io::stdin().read_line(&mut input)?; + } + + // Set up graceful shutdown on SIGTERM/SIGINT + let shutdown_signal = async { + tokio::signal::ctrl_c() + .await + .expect("Failed to listen for shutdown signal"); + tracing::info!("Received shutdown signal, terminating gracefully..."); + }; + + tokio::select! { + result = run_server(args.socket, args.pipe, args.http) => { + if let Err(e) = result { + tracing::error!("Server error: {e}"); + return Err(e); + } + } + _ = shutdown_signal => { + tracing::info!("Shutdown complete"); + } + } + + Ok(()) +} diff --git a/lib/dsc-lib/src/discovery/command_discovery.rs b/lib/dsc-lib/src/discovery/command_discovery.rs index 8479b8cb6..1234fe080 100644 --- a/lib/dsc-lib/src/discovery/command_discovery.rs +++ b/lib/dsc-lib/src/discovery/command_discovery.rs @@ -185,6 +185,20 @@ impl CommandDiscovery { } } } + + // if current working directory is not already in PATH env var then add it to env var and list of searched paths + if let Ok(cwd) = env::current_dir() { + if paths.contains(&cwd) { + trace!("Current working directory already in path: {}", cwd.to_string_lossy()); + } else { + trace!("Adding current working directory to path: {}", cwd.to_string_lossy()); + paths.push(cwd); + + if let Ok(new_path) = env::join_paths(paths.clone()) { + env::set_var("PATH", new_path); + } + } + } } if let Ok(final_resource_path) = env::join_paths(paths.clone()) { diff --git a/lib/dsc-lib/src/dscresources/command_resource.rs b/lib/dsc-lib/src/dscresources/command_resource.rs index 8d7001c03..7bf3b3c83 100644 --- a/lib/dsc-lib/src/dscresources/command_resource.rs +++ b/lib/dsc-lib/src/dscresources/command_resource.rs @@ -765,24 +765,41 @@ pub fn invoke_command(executable: &str, args: Option>, input: Option let exit_codes = convert_hashmap_string_keys_to_i32(exit_codes)?; let executable = canonicalize_which(executable, cwd)?; - tokio::runtime::Builder::new_multi_thread().enable_all().build().unwrap().block_on( - async { - trace!("{}", t!("dscresources.commandResource.commandInvoke", executable = executable, args = args : {:?})); - if let Some(cwd) = cwd { - trace!("{}", t!("dscresources.commandResource.commandCwd", cwd = cwd.display())); - } + let run_async = async { + trace!("{}", t!("dscresources.commandResource.commandInvoke", executable = executable, args = args : {:?})); + if let Some(cwd) = cwd { + trace!("{}", t!("dscresources.commandResource.commandCwd", cwd = cwd.display())); + } - match run_process_async(&executable, args, input, cwd, env, exit_codes.as_ref()).await { - Ok((code, stdout, stderr)) => { - Ok((code, stdout, stderr)) - }, - Err(err) => { - error!("{}", t!("dscresources.commandResource.runProcessError", executable = executable, error = err)); - Err(err) - } + match run_process_async(&executable, args, input, cwd, env, exit_codes.as_ref()).await { + Ok((code, stdout, stderr)) => { + Ok((code, stdout, stderr)) + }, + Err(err) => { + error!("{}", t!("dscresources.commandResource.runProcessError", executable = executable, error = err)); + Err(err) } } - ) + }; + + // Try to use existing runtime first (e.g. from gRPC or MCP server) + match tokio::runtime::Handle::try_current() { + Ok(handle) => { + std::thread::scope(|s| { + s.spawn(|| { + handle.block_on(run_async) + }).join().unwrap() + }) + }, + // Otherwise create a new runtime + Err(_) => { + tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .unwrap() + .block_on(run_async) + } + } } /// Process the arguments for a command resource.