feat: Secret module works with infisical and local file storage backends
	
		
			
	
		
	
	
		
	
		
			All checks were successful
		
		
	
	
		
			
				
	
				Run Check Script / check (pull_request) Successful in 1m10s
				
			
		
		
	
	
				
					
				
			
		
			All checks were successful
		
		
	
	Run Check Script / check (pull_request) Successful in 1m10s
				
			This commit is contained in:
		
							parent
							
								
									2a6a233fb2
								
							
						
					
					
						commit
						9c5d1bd27f
					
				
							
								
								
									
										184
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										184
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							| @ -515,6 +515,12 @@ version = "1.0.1" | |||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" | checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" | ||||||
| 
 | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "cfg_aliases" | ||||||
|  | version = "0.2.1" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" | ||||||
|  | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "chacha20" | name = "chacha20" | ||||||
| version = "0.9.1" | version = "0.9.1" | ||||||
| @ -1689,9 +1695,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | |||||||
| checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" | checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "cfg-if", |  "cfg-if", | ||||||
|  |  "js-sys", | ||||||
|  "libc", |  "libc", | ||||||
|  "r-efi", |  "r-efi", | ||||||
|  "wasi 0.14.2+wasi-0.2.4", |  "wasi 0.14.2+wasi-0.2.4", | ||||||
|  |  "wasm-bindgen", | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
| [[package]] | [[package]] | ||||||
| @ -1789,6 +1797,7 @@ dependencies = [ | |||||||
|  "env_logger", |  "env_logger", | ||||||
|  "fqdn", |  "fqdn", | ||||||
|  "futures-util", |  "futures-util", | ||||||
|  |  "harmony-secret-derive", | ||||||
|  "harmony_macros", |  "harmony_macros", | ||||||
|  "harmony_types", |  "harmony_types", | ||||||
|  "helm-wrapper-rs", |  "helm-wrapper-rs", | ||||||
| @ -1823,7 +1832,6 @@ dependencies = [ | |||||||
|  "temp-dir", |  "temp-dir", | ||||||
|  "temp-file", |  "temp-file", | ||||||
|  "tempfile", |  "tempfile", | ||||||
|  "thiserror 2.0.14", |  | ||||||
|  "tokio", |  "tokio", | ||||||
|  "tokio-util", |  "tokio-util", | ||||||
|  "url", |  "url", | ||||||
| @ -1831,7 +1839,26 @@ dependencies = [ | |||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "harmony-secrets-derive" | name = "harmony-secret" | ||||||
|  | version = "0.1.0" | ||||||
|  | dependencies = [ | ||||||
|  |  "async-trait", | ||||||
|  |  "directories", | ||||||
|  |  "harmony-secret-derive", | ||||||
|  |  "http 1.3.1", | ||||||
|  |  "infisical", | ||||||
|  |  "lazy_static", | ||||||
|  |  "log", | ||||||
|  |  "pretty_assertions", | ||||||
|  |  "serde", | ||||||
|  |  "serde_json", | ||||||
|  |  "tempfile", | ||||||
|  |  "thiserror 2.0.14", | ||||||
|  |  "tokio", | ||||||
|  | ] | ||||||
|  | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "harmony-secret-derive" | ||||||
| version = "0.1.0" | version = "0.1.0" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "proc-macro-crate", |  "proc-macro-crate", | ||||||
| @ -2141,7 +2168,7 @@ dependencies = [ | |||||||
|  "httpdate", |  "httpdate", | ||||||
|  "itoa", |  "itoa", | ||||||
|  "pin-project-lite", |  "pin-project-lite", | ||||||
|  "socket2", |  "socket2 0.5.10", | ||||||
|  "tokio", |  "tokio", | ||||||
|  "tower-service", |  "tower-service", | ||||||
|  "tracing", |  "tracing", | ||||||
| @ -2220,6 +2247,7 @@ dependencies = [ | |||||||
|  "tokio", |  "tokio", | ||||||
|  "tokio-rustls", |  "tokio-rustls", | ||||||
|  "tower-service", |  "tower-service", | ||||||
|  |  "webpki-roots", | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
| [[package]] | [[package]] | ||||||
| @ -2282,7 +2310,7 @@ dependencies = [ | |||||||
|  "libc", |  "libc", | ||||||
|  "percent-encoding", |  "percent-encoding", | ||||||
|  "pin-project-lite", |  "pin-project-lite", | ||||||
|  "socket2", |  "socket2 0.5.10", | ||||||
|  "system-configuration 0.6.1", |  "system-configuration 0.6.1", | ||||||
|  "tokio", |  "tokio", | ||||||
|  "tower-service", |  "tower-service", | ||||||
| @ -2499,6 +2527,21 @@ version = "2.0.6" | |||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" | checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" | ||||||
| 
 | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "infisical" | ||||||
|  | version = "0.0.2" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "4d97c33b08e22b2f7b9f87a8fc06a7d247442db7bf216ffc6661a74ed8aea658" | ||||||
|  | dependencies = [ | ||||||
|  |  "base64 0.22.1", | ||||||
|  |  "reqwest 0.12.20", | ||||||
|  |  "serde", | ||||||
|  |  "serde_json", | ||||||
|  |  "thiserror 1.0.69", | ||||||
|  |  "tokio", | ||||||
|  |  "url", | ||||||
|  | ] | ||||||
|  | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "inout" | name = "inout" | ||||||
| version = "0.1.4" | version = "0.1.4" | ||||||
| @ -2539,6 +2582,17 @@ dependencies = [ | |||||||
|  "syn", |  "syn", | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "io-uring" | ||||||
|  | version = "0.7.9" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4" | ||||||
|  | dependencies = [ | ||||||
|  |  "bitflags 2.9.1", | ||||||
|  |  "cfg-if", | ||||||
|  |  "libc", | ||||||
|  | ] | ||||||
|  | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "ipnet" | name = "ipnet" | ||||||
| version = "2.11.0" | version = "2.11.0" | ||||||
| @ -2908,6 +2962,12 @@ dependencies = [ | |||||||
|  "hashbrown 0.15.4", |  "hashbrown 0.15.4", | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "lru-slab" | ||||||
|  | version = "0.1.2" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" | ||||||
|  | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "md5" | name = "md5" | ||||||
| version = "0.7.0" | version = "0.7.0" | ||||||
| @ -3210,7 +3270,7 @@ dependencies = [ | |||||||
|  "pretty_assertions", |  "pretty_assertions", | ||||||
|  "rand 0.8.5", |  "rand 0.8.5", | ||||||
|  "serde", |  "serde", | ||||||
|  "thiserror 1.0.69", |  "thiserror 2.0.14", | ||||||
|  "tokio", |  "tokio", | ||||||
|  "uuid", |  "uuid", | ||||||
|  "xml-rs", |  "xml-rs", | ||||||
| @ -3622,6 +3682,61 @@ version = "0.4.1" | |||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "e9e1dcb320d6839f6edb64f7a4a59d39b30480d4d1765b56873f7c858538a5fe" | checksum = "e9e1dcb320d6839f6edb64f7a4a59d39b30480d4d1765b56873f7c858538a5fe" | ||||||
| 
 | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "quinn" | ||||||
|  | version = "0.11.8" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "626214629cda6781b6dc1d316ba307189c85ba657213ce642d9c77670f8202c8" | ||||||
|  | dependencies = [ | ||||||
|  |  "bytes", | ||||||
|  |  "cfg_aliases", | ||||||
|  |  "pin-project-lite", | ||||||
|  |  "quinn-proto", | ||||||
|  |  "quinn-udp", | ||||||
|  |  "rustc-hash", | ||||||
|  |  "rustls", | ||||||
|  |  "socket2 0.5.10", | ||||||
|  |  "thiserror 2.0.14", | ||||||
|  |  "tokio", | ||||||
|  |  "tracing", | ||||||
|  |  "web-time", | ||||||
|  | ] | ||||||
|  | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "quinn-proto" | ||||||
|  | version = "0.11.12" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "49df843a9161c85bb8aae55f101bc0bac8bcafd637a620d9122fd7e0b2f7422e" | ||||||
|  | dependencies = [ | ||||||
|  |  "bytes", | ||||||
|  |  "getrandom 0.3.3", | ||||||
|  |  "lru-slab", | ||||||
|  |  "rand 0.9.1", | ||||||
|  |  "ring", | ||||||
|  |  "rustc-hash", | ||||||
|  |  "rustls", | ||||||
|  |  "rustls-pki-types", | ||||||
|  |  "slab", | ||||||
|  |  "thiserror 2.0.14", | ||||||
|  |  "tinyvec", | ||||||
|  |  "tracing", | ||||||
|  |  "web-time", | ||||||
|  | ] | ||||||
|  | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "quinn-udp" | ||||||
|  | version = "0.5.13" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "fcebb1209ee276352ef14ff8732e24cc2b02bbac986cd74a4c81bcb2f9881970" | ||||||
|  | dependencies = [ | ||||||
|  |  "cfg_aliases", | ||||||
|  |  "libc", | ||||||
|  |  "once_cell", | ||||||
|  |  "socket2 0.5.10", | ||||||
|  |  "tracing", | ||||||
|  |  "windows-sys 0.59.0", | ||||||
|  | ] | ||||||
|  | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "quote" | name = "quote" | ||||||
| version = "1.0.40" | version = "1.0.40" | ||||||
| @ -3841,6 +3956,7 @@ dependencies = [ | |||||||
|  "base64 0.22.1", |  "base64 0.22.1", | ||||||
|  "bytes", |  "bytes", | ||||||
|  "encoding_rs", |  "encoding_rs", | ||||||
|  |  "futures-channel", | ||||||
|  "futures-core", |  "futures-core", | ||||||
|  "futures-util", |  "futures-util", | ||||||
|  "h2 0.4.10", |  "h2 0.4.10", | ||||||
| @ -3857,6 +3973,8 @@ dependencies = [ | |||||||
|  "native-tls", |  "native-tls", | ||||||
|  "percent-encoding", |  "percent-encoding", | ||||||
|  "pin-project-lite", |  "pin-project-lite", | ||||||
|  |  "quinn", | ||||||
|  |  "rustls", | ||||||
|  "rustls-pki-types", |  "rustls-pki-types", | ||||||
|  "serde", |  "serde", | ||||||
|  "serde_json", |  "serde_json", | ||||||
| @ -3864,6 +3982,7 @@ dependencies = [ | |||||||
|  "sync_wrapper 1.0.2", |  "sync_wrapper 1.0.2", | ||||||
|  "tokio", |  "tokio", | ||||||
|  "tokio-native-tls", |  "tokio-native-tls", | ||||||
|  |  "tokio-rustls", | ||||||
|  "tokio-util", |  "tokio-util", | ||||||
|  "tower", |  "tower", | ||||||
|  "tower-http", |  "tower-http", | ||||||
| @ -3873,6 +3992,7 @@ dependencies = [ | |||||||
|  "wasm-bindgen-futures", |  "wasm-bindgen-futures", | ||||||
|  "wasm-streams", |  "wasm-streams", | ||||||
|  "web-sys", |  "web-sys", | ||||||
|  |  "webpki-roots", | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
| [[package]] | [[package]] | ||||||
| @ -4062,6 +4182,12 @@ version = "0.1.25" | |||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f" | checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f" | ||||||
| 
 | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "rustc-hash" | ||||||
|  | version = "2.1.1" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" | ||||||
|  | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "rustc_version" | name = "rustc_version" | ||||||
| version = "0.4.1" | version = "0.4.1" | ||||||
| @ -4161,6 +4287,7 @@ version = "1.12.0" | |||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" | checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  |  "web-time", | ||||||
|  "zeroize", |  "zeroize", | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
| @ -4646,6 +4773,16 @@ dependencies = [ | |||||||
|  "windows-sys 0.52.0", |  "windows-sys 0.52.0", | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "socket2" | ||||||
|  | version = "0.6.0" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" | ||||||
|  | dependencies = [ | ||||||
|  |  "libc", | ||||||
|  |  "windows-sys 0.59.0", | ||||||
|  | ] | ||||||
|  | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "spin" | name = "spin" | ||||||
| version = "0.9.8" | version = "0.9.8" | ||||||
| @ -5008,20 +5145,38 @@ dependencies = [ | |||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "tokio" | name = "tinyvec" | ||||||
| version = "1.45.1" | version = "1.9.0" | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779" | checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" | ||||||
|  | dependencies = [ | ||||||
|  |  "tinyvec_macros", | ||||||
|  | ] | ||||||
|  | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "tinyvec_macros" | ||||||
|  | version = "0.1.1" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" | ||||||
|  | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "tokio" | ||||||
|  | version = "1.47.1" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "backtrace", |  "backtrace", | ||||||
|  "bytes", |  "bytes", | ||||||
|  |  "io-uring", | ||||||
|  "libc", |  "libc", | ||||||
|  "mio 1.0.4", |  "mio 1.0.4", | ||||||
|  |  "parking_lot", | ||||||
|  "pin-project-lite", |  "pin-project-lite", | ||||||
|  "signal-hook-registry", |  "signal-hook-registry", | ||||||
|  "socket2", |  "slab", | ||||||
|  |  "socket2 0.6.0", | ||||||
|  "tokio-macros", |  "tokio-macros", | ||||||
|  "windows-sys 0.52.0", |  "windows-sys 0.59.0", | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
| [[package]] | [[package]] | ||||||
| @ -5572,6 +5727,15 @@ dependencies = [ | |||||||
|  "wasm-bindgen", |  "wasm-bindgen", | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
|  | [[package]] | ||||||
|  | name = "webpki-roots" | ||||||
|  | version = "1.0.2" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2" | ||||||
|  | dependencies = [ | ||||||
|  |  "rustls-pki-types", | ||||||
|  | ] | ||||||
|  | 
 | ||||||
| [[package]] | [[package]] | ||||||
| name = "winapi" | name = "winapi" | ||||||
| version = "0.3.9" | version = "0.3.9" | ||||||
|  | |||||||
| @ -12,7 +12,8 @@ members = [ | |||||||
|   "harmony_cli", |   "harmony_cli", | ||||||
|   "k3d", |   "k3d", | ||||||
|   "harmony_composer", |   "harmony_composer", | ||||||
|   "harmony_secrets_derive", |   "harmony_secret_derive", | ||||||
|  |   "harmony_secret", | ||||||
| ] | ] | ||||||
| 
 | 
 | ||||||
| [workspace.package] | [workspace.package] | ||||||
| @ -54,6 +55,10 @@ chrono = "0.4" | |||||||
| similar = "2" | similar = "2" | ||||||
| uuid = { version = "1.11", features = ["v4", "fast-rng", "macro-diagnostics"] } | uuid = { version = "1.11", features = ["v4", "fast-rng", "macro-diagnostics"] } | ||||||
| pretty_assertions = "1.4.1" | pretty_assertions = "1.4.1" | ||||||
|  | tempfile = "3.20.0" | ||||||
| bollard = "0.19.1" | bollard = "0.19.1" | ||||||
| base64 = "0.22.1" | base64 = "0.22.1" | ||||||
| tar = "0.4.44" | tar = "0.4.44" | ||||||
|  | lazy_static = "1.5.0" | ||||||
|  | directories = "6.0.0" | ||||||
|  | thiserror = "2.0.14" | ||||||
|  | |||||||
| @ -38,8 +38,8 @@ serde-value.workspace = true | |||||||
| helm-wrapper-rs = "0.4.0" | helm-wrapper-rs = "0.4.0" | ||||||
| non-blank-string-rs = "1.0.4" | non-blank-string-rs = "1.0.4" | ||||||
| k3d-rs = { path = "../k3d" } | k3d-rs = { path = "../k3d" } | ||||||
| directories = "6.0.0" | directories.workspace = true | ||||||
| lazy_static = "1.5.0" | lazy_static.workspace = true | ||||||
| dockerfile_builder = "0.1.5" | dockerfile_builder = "0.1.5" | ||||||
| temp-file = "0.1.9" | temp-file = "0.1.9" | ||||||
| convert_case.workspace = true | convert_case.workspace = true | ||||||
| @ -59,7 +59,7 @@ similar.workspace = true | |||||||
| futures-util = "0.3.31" | futures-util = "0.3.31" | ||||||
| tokio-util = "0.7.15" | tokio-util = "0.7.15" | ||||||
| strum = { version = "0.27.1", features = ["derive"] } | strum = { version = "0.27.1", features = ["derive"] } | ||||||
| tempfile = "3.20.0" | tempfile.workspace = true | ||||||
| serde_with = "3.14.0" | serde_with = "3.14.0" | ||||||
| schemars = "0.8.22" | schemars = "0.8.22" | ||||||
| kube-derive = "1.1.0" | kube-derive = "1.1.0" | ||||||
| @ -67,7 +67,7 @@ bollard.workspace = true | |||||||
| tar.workspace = true | tar.workspace = true | ||||||
| base64.workspace = true | base64.workspace = true | ||||||
| once_cell = "1.21.3" | once_cell = "1.21.3" | ||||||
| thiserror = "2.0.14" | harmony-secret-derive = { version = "0.1.0", path = "../harmony_secret_derive" } | ||||||
| 
 | 
 | ||||||
| [dev-dependencies] | [dev-dependencies] | ||||||
| pretty_assertions.workspace = true | pretty_assertions.workspace = true | ||||||
|  | |||||||
| @ -9,4 +9,3 @@ pub mod inventory; | |||||||
| pub mod maestro; | pub mod maestro; | ||||||
| pub mod score; | pub mod score; | ||||||
| pub mod topology; | pub mod topology; | ||||||
| pub mod secrets; |  | ||||||
|  | |||||||
| @ -1,265 +0,0 @@ | |||||||
| //! # Harmony Secrets Module
 |  | ||||||
| //!
 |  | ||||||
| //! This module provides core abstractions for type-safe secret management within the Harmony framework.
 |  | ||||||
| //!
 |  | ||||||
| //! ## Design Philosophy
 |  | ||||||
| //!
 |  | ||||||
| //! The design is centered around three key components:
 |  | ||||||
| //!
 |  | ||||||
| //! 1.  **The `Secret` Trait:** This is the heart of the module. Instead of using strings to identify
 |  | ||||||
| //!     secrets, we use dedicated, zero-sized types (ZSTs). Each ZST represents a single secret
 |  | ||||||
| //!     and implements the `Secret` trait to provide metadata (like its namespace and key) and
 |  | ||||||
| //!     its associated `Value` type. This enables full compile-time verification.
 |  | ||||||
| //!
 |  | ||||||
| //! 2.  **The `Secrets` Struct:** This is the primary user-facing API. It provides the `get` and `set`
 |  | ||||||
| //!     methods that are generic over any type implementing `Secret`. It's the high-level,
 |  | ||||||
| //!     easy-to-use entry point for all secret operations.
 |  | ||||||
| //!
 |  | ||||||
| //! 3.  **The `SecretStore` Trait:** This is the low-level backend interface. It defines the contract
 |  | ||||||
| //!     for how the `Secrets` struct will interact with an actual storage system (like Infisical,
 |  | ||||||
| //!     a local file, or a database). This decouples the high-level API from the implementation details.
 |  | ||||||
| //!
 |  | ||||||
| //! ## Example Usage
 |  | ||||||
| //!
 |  | ||||||
| //! ```
 |  | ||||||
| //! // In an external crate (e.g., harmony-okd):
 |  | ||||||
| //! use harmony_secrets::{Secret, StoreError};
 |  | ||||||
| //!
 |  | ||||||
| //! // 1. Define a zero-sized struct for each secret.
 |  | ||||||
| //! pub struct KubeadminPassword;
 |  | ||||||
| //!
 |  | ||||||
| //! // 2. Implement the `Secret` trait to provide metadata.
 |  | ||||||
| //! impl Secret for KubeadminPassword {
 |  | ||||||
| //!     // The associated type defines what you get back.
 |  | ||||||
| //!     type Value = String;
 |  | ||||||
| //!
 |  | ||||||
| //!     const NAMESPACE: &'static str = "okd-installation";
 |  | ||||||
| //!     const KEY: &'static str = "kubeadmin-password";
 |  | ||||||
| //! }
 |  | ||||||
| //!
 |  | ||||||
| //! // 3. Use it with the `Secrets` struct.
 |  | ||||||
| //! async fn example(secrets: &harmony_secrets::Secrets) -> Result<(), StoreError> {
 |  | ||||||
| //!     // The API is type-safe. The compiler knows what `Value` to expect.
 |  | ||||||
| //!     secrets.set::<KubeadminPassword>("password123".to_string()).await?;
 |  | ||||||
| //!     let password = secrets.get::<KubeadminPassword>().await?;
 |  | ||||||
| //!     assert_eq!(password, "password123");
 |  | ||||||
| //!     Ok(())
 |  | ||||||
| //! }
 |  | ||||||
| //! ```
 |  | ||||||
| 
 |  | ||||||
| use async_trait::async_trait; |  | ||||||
| use serde::{de::DeserializeOwned, Serialize}; |  | ||||||
| use std::sync::Arc; |  | ||||||
| use thiserror::Error; |  | ||||||
| 
 |  | ||||||
| /// Defines the set of errors that can occur during secret operations.
 |  | ||||||
| /// Using `thiserror` provides a great developer experience for error handling.
 |  | ||||||
| #[derive(Debug, Error)] |  | ||||||
| pub enum StoreError { |  | ||||||
|     #[error("Secret not found in store: namespace='{namespace}', key='{key}'")] |  | ||||||
|     NotFound { namespace: String, key: String }, |  | ||||||
| 
 |  | ||||||
|     #[error("Permission denied for secret: namespace='{namespace}', key='{key}'")] |  | ||||||
|     PermissionDenied { namespace: String, key: String }, |  | ||||||
| 
 |  | ||||||
|     #[error("Failed to deserialize secret value: {0}")] |  | ||||||
|     Deserialization(String), |  | ||||||
| 
 |  | ||||||
|     #[error("Failed to serialize secret value: {0}")] |  | ||||||
|     Serialization(String), |  | ||||||
| 
 |  | ||||||
|     #[error("A backend-specific error occurred: {0}")] |  | ||||||
|     Backend(String), |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| /// A trait that marks a type as representing a single, retrievable secret.
 |  | ||||||
| ///
 |  | ||||||
| /// This trait should be implemented on a unique, zero-sized struct for each secret
 |  | ||||||
| /// your module needs to manage. This pattern ensures that all secret access is
 |  | ||||||
| /// validated at compile time.
 |  | ||||||
| pub trait Secret: 'static + Send + Sync { |  | ||||||
|     /// The data type of the secret's value. This ensures that `get` and `set`
 |  | ||||||
|     /// operations are fully type-safe. The value must be serializable.
 |  | ||||||
|     type Value: Serialize + DeserializeOwned + Send; |  | ||||||
| 
 |  | ||||||
|     /// A logical grouping for secrets, similar to a Kubernetes namespace or a
 |  | ||||||
|     /// directory path. This will be used by the `SecretStore` to organize data.
 |  | ||||||
|     const NAMESPACE: &'static str; |  | ||||||
| 
 |  | ||||||
|     /// The unique key for the secret within its namespace.
 |  | ||||||
|     const KEY: &'static str; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| /// The low-level storage trait that concrete secret backends must implement.
 |  | ||||||
| ///
 |  | ||||||
| /// This trait operates on raw bytes (`Vec<u8>`), keeping it decoupled from any
 |  | ||||||
| /// specific serialization format. The `Secrets` struct will handle the
 |  | ||||||
| //  serialization/deserialization boundary.
 |  | ||||||
| #[async_trait] |  | ||||||
| pub trait SecretStore: Send + Sync { |  | ||||||
|     /// Retrieves the raw byte value of a secret from the backend.
 |  | ||||||
|     async fn get(&self, namespace: &str, key: &str) -> Result<Vec<u8>, StoreError>; |  | ||||||
| 
 |  | ||||||
|     /// Saves the raw byte value of a secret to the backend.
 |  | ||||||
|     async fn set(&self, namespace: &str, key: &str, value: Vec<u8>) -> Result<(), StoreError>; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| /// The primary, user-facing struct for interacting with secrets.
 |  | ||||||
| ///
 |  | ||||||
| /// It provides a high-level, type-safe API that is decoupled from the
 |  | ||||||
| /// underlying storage mechanism.
 |  | ||||||
| #[derive(Clone)] |  | ||||||
| pub struct Secrets { |  | ||||||
|     /// A shared, thread-safe reference to the underlying secret store.
 |  | ||||||
|     store: Arc<dyn SecretStore>, |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| impl Secrets { |  | ||||||
|     /// Creates a new `Secrets` instance with the given store implementation.
 |  | ||||||
|     pub fn new(store: Arc<dyn SecretStore>) -> Self { |  | ||||||
|         Self { store } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /// Retrieves a secret from the store in a fully type-safe manner.
 |  | ||||||
|     ///
 |  | ||||||
|     /// The type of the secret to retrieve is specified using a generic parameter `S`,
 |  | ||||||
|     /// which must implement the `Secret` trait. The method returns the `S::Value`
 |  | ||||||
|     /// associated type, ensuring you always get the data type you expect.
 |  | ||||||
|     ///
 |  | ||||||
|     /// # Example
 |  | ||||||
|     /// `let admin_pass = secrets.get::<my_secrets::AdminPassword>().await?;`
 |  | ||||||
|     pub async fn get<S: Secret>(&self) -> Result<S::Value, StoreError> { |  | ||||||
|         let bytes = self |  | ||||||
|             .store |  | ||||||
|             .get(S::NAMESPACE, S::KEY) |  | ||||||
|             .await |  | ||||||
|             .map_err(|e| match e { |  | ||||||
|                 // Preserve the NotFound error for better diagnostics.
 |  | ||||||
|                 StoreError::NotFound { .. } => e, |  | ||||||
|                 _ => StoreError::Backend(e.to_string()), |  | ||||||
|             })?; |  | ||||||
| 
 |  | ||||||
|         // The public API uses JSON for serialization. It's robust and human-readable.
 |  | ||||||
|         serde_json::from_slice(&bytes) |  | ||||||
|             .map_err(|e| StoreError::Deserialization(e.to_string())) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /// Saves a secret to the store in a fully type-safe manner.
 |  | ||||||
|     ///
 |  | ||||||
|     /// The method is generic over the secret type `S`, and the `value` parameter
 |  | ||||||
|     /// must match the `S::Value` associated type, preventing type mismatch errors
 |  | ||||||
|     /// at compile time.
 |  | ||||||
|     ///
 |  | ||||||
|     /// # Example
 |  | ||||||
|     /// `secrets.set::<my_secrets::AdminPassword>("new-password".to_string()).await?;`
 |  | ||||||
|     pub async fn set<S: Secret>(&self, value: S::Value) -> Result<(), StoreError> { |  | ||||||
|         let bytes = serde_json::to_vec(&value) |  | ||||||
|             .map_err(|e| StoreError::Serialization(e.to_string()))?; |  | ||||||
| 
 |  | ||||||
|         self.store |  | ||||||
|             .set(S::NAMESPACE, S::KEY, bytes) |  | ||||||
|             .await |  | ||||||
|             .map_err(|e| StoreError::Backend(e.to_string())) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #[cfg(test)] |  | ||||||
| mod tests { |  | ||||||
|     use super::*; |  | ||||||
|     use std::collections::HashMap; |  | ||||||
|     use std::sync::Mutex; |  | ||||||
| 
 |  | ||||||
|     // Define a dummy secret for testing purposes.
 |  | ||||||
|     struct TestApiKey; |  | ||||||
|     impl Secret for TestApiKey { |  | ||||||
|         type Value = String; |  | ||||||
|         const NAMESPACE: &'static str = "global-tests"; |  | ||||||
|         const KEY: &'static str = "api-key"; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     struct ComplexSecret; |  | ||||||
|     #[derive(Serialize, serde::Deserialize, PartialEq, Debug, Clone)] |  | ||||||
|     struct ComplexValue { |  | ||||||
|         user: String, |  | ||||||
|         permissions: Vec<String>, |  | ||||||
|     } |  | ||||||
|     impl Secret for ComplexSecret { |  | ||||||
|         type Value = ComplexValue; |  | ||||||
|         const NAMESPACE: &'static str = "complex-tests"; |  | ||||||
|         const KEY: &'static str = "user-data"; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     // A mock implementation of the `SecretStore` that uses an in-memory HashMap.
 |  | ||||||
|     #[derive(Default)] |  | ||||||
|     struct MockStore { |  | ||||||
|         data: Mutex<HashMap<String, Vec<u8>>>, |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     #[async_trait] |  | ||||||
|     impl SecretStore for MockStore { |  | ||||||
|         async fn get(&self, namespace: &str, key: &str) -> Result<Vec<u8>, StoreError> { |  | ||||||
|             let path = format!("{}/{}", namespace, key); |  | ||||||
|             let data = self.data.lock().unwrap(); |  | ||||||
|             data.get(&path) |  | ||||||
|                 .cloned() |  | ||||||
|                 .ok_or_else(|| StoreError::NotFound { |  | ||||||
|                     namespace: namespace.to_string(), |  | ||||||
|                     key: key.to_string(), |  | ||||||
|                 }) |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         async fn set(&self, namespace: &str, key: &str, value: Vec<u8>) -> Result<(), StoreError> { |  | ||||||
|             let path = format!("{}/{}", namespace, key); |  | ||||||
|             let mut data = self.data.lock().unwrap(); |  | ||||||
|             data.insert(path, value); |  | ||||||
|             Ok(()) |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     #[tokio::test] |  | ||||||
|     async fn test_set_and_get_simple_secret() { |  | ||||||
|         let store = Arc::new(MockStore::default()); |  | ||||||
|         let secrets = Secrets::new(store); |  | ||||||
| 
 |  | ||||||
|         let api_key_value = "secret-key-12345".to_string(); |  | ||||||
|         secrets |  | ||||||
|             .set::<TestApiKey>(api_key_value.clone()) |  | ||||||
|             .await |  | ||||||
|             .unwrap(); |  | ||||||
| 
 |  | ||||||
|         let retrieved_key = secrets.get::<TestApiKey>().await.unwrap(); |  | ||||||
|         assert_eq!(retrieved_key, api_key_value); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     #[tokio::test] |  | ||||||
|     async fn test_set_and_get_complex_secret() { |  | ||||||
|         let store = Arc::new(MockStore::default()); |  | ||||||
|         let secrets = Secrets::new(store); |  | ||||||
| 
 |  | ||||||
|         let complex_value = ComplexValue { |  | ||||||
|             user: "test-user".to_string(), |  | ||||||
|             permissions: vec!["read".to_string(), "write".to_string()], |  | ||||||
|         }; |  | ||||||
|         secrets |  | ||||||
|             .set::<ComplexSecret>(complex_value.clone()) |  | ||||||
|             .await |  | ||||||
|             .unwrap(); |  | ||||||
| 
 |  | ||||||
|         let retrieved_value = secrets.get::<ComplexSecret>().await.unwrap(); |  | ||||||
|         assert_eq!(retrieved_value, complex_value); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     #[tokio::test] |  | ||||||
|     async fn test_get_nonexistent_secret() { |  | ||||||
|         let store = Arc::new(MockStore::default()); |  | ||||||
|         let secrets = Secrets::new(store); |  | ||||||
| 
 |  | ||||||
|         let result = secrets.get::<TestApiKey>().await; |  | ||||||
|         assert!(matches!(result, Err(StoreError::NotFound { .. }))); |  | ||||||
| 
 |  | ||||||
|         if let Err(StoreError::NotFound { namespace, key }) = result { |  | ||||||
|             assert_eq!(namespace, TestApiKey::NAMESPACE); |  | ||||||
|             assert_eq!(key, TestApiKey::KEY); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
							
								
								
									
										23
									
								
								harmony_secret/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								harmony_secret/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,23 @@ | |||||||
|  | [package] | ||||||
|  | name = "harmony-secret" | ||||||
|  | edition = "2024" | ||||||
|  | version.workspace = true | ||||||
|  | readme.workspace = true | ||||||
|  | license.workspace = true | ||||||
|  | 
 | ||||||
|  | [dependencies] | ||||||
|  | harmony-secret-derive = { version = "0.1.0", path = "../harmony_secret_derive" } | ||||||
|  | serde = { version = "1.0.209", features = ["derive", "rc"] } | ||||||
|  | serde_json = "1.0.127" | ||||||
|  | thiserror.workspace = true | ||||||
|  | lazy_static.workspace = true | ||||||
|  | directories.workspace = true | ||||||
|  | log.workspace = true | ||||||
|  | infisical = "0.0.2" | ||||||
|  | tokio.workspace = true | ||||||
|  | async-trait.workspace = true | ||||||
|  | http.workspace = true | ||||||
|  | 
 | ||||||
|  | [dev-dependencies] | ||||||
|  | pretty_assertions.workspace = true | ||||||
|  | tempfile.workspace = true | ||||||
							
								
								
									
										18
									
								
								harmony_secret/src/config.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								harmony_secret/src/config.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,18 @@ | |||||||
|  | use lazy_static::lazy_static; | ||||||
|  | 
 | ||||||
|  | lazy_static! { | ||||||
|  |     pub static ref SECRET_NAMESPACE: String = | ||||||
|  |         std::env::var("HARMONY_SECRET_NAMESPACE").expect("HARMONY_SECRET_NAMESPACE environment variable is required, it should contain the name of the project you are working on to access its secrets"); | ||||||
|  |     pub static ref SECRET_STORE: Option<String> = | ||||||
|  |         std::env::var("HARMONY_SECRET_STORE").ok(); | ||||||
|  |     pub static ref INFISICAL_URL: Option<String> = | ||||||
|  |         std::env::var("HARMONY_SECRET_INFISICAL_URL").ok(); | ||||||
|  |     pub static ref INFISICAL_PROJECT_ID: Option<String> = | ||||||
|  |         std::env::var("HARMONY_SECRET_INFISICAL_PROJECT_ID").ok(); | ||||||
|  |     pub static ref INFISICAL_ENVIRONMENT: Option<String> = | ||||||
|  |         std::env::var("HARMONY_SECRET_INFISICAL_ENVIRONMENT").ok(); | ||||||
|  |     pub static ref INFISICAL_CLIENT_ID: Option<String> = | ||||||
|  |         std::env::var("HARMONY_SECRET_INFISICAL_CLIENT_ID").ok(); | ||||||
|  |     pub static ref INFISICAL_CLIENT_SECRET: Option<String> = | ||||||
|  |         std::env::var("HARMONY_SECRET_INFISICAL_CLIENT_SECRET").ok(); | ||||||
|  | } | ||||||
							
								
								
									
										166
									
								
								harmony_secret/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										166
									
								
								harmony_secret/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,166 @@ | |||||||
|  | pub mod config; | ||||||
|  | mod store; | ||||||
|  | 
 | ||||||
|  | use crate::config::SECRET_NAMESPACE; | ||||||
|  | use async_trait::async_trait; | ||||||
|  | use config::INFISICAL_CLIENT_ID; | ||||||
|  | use config::INFISICAL_CLIENT_SECRET; | ||||||
|  | use config::INFISICAL_ENVIRONMENT; | ||||||
|  | use config::INFISICAL_PROJECT_ID; | ||||||
|  | use config::INFISICAL_URL; | ||||||
|  | use config::SECRET_STORE; | ||||||
|  | use serde::{Serialize, de::DeserializeOwned}; | ||||||
|  | use std::fmt; | ||||||
|  | use store::InfisicalSecretStore; | ||||||
|  | use store::LocalFileSecretStore; | ||||||
|  | use thiserror::Error; | ||||||
|  | use tokio::sync::OnceCell; | ||||||
|  | 
 | ||||||
|  | pub use harmony_secret_derive::Secret; | ||||||
|  | 
 | ||||||
|  | // The Secret trait remains the same.
 | ||||||
|  | pub trait Secret: Serialize + DeserializeOwned + Sized { | ||||||
|  |     const KEY: &'static str; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // The error enum remains the same.
 | ||||||
|  | #[derive(Debug, Error)] | ||||||
|  | pub enum SecretStoreError { | ||||||
|  |     #[error("Secret not found for key '{key}' in namespace '{namespace}'")] | ||||||
|  |     NotFound { namespace: String, key: String }, | ||||||
|  |     #[error("Failed to deserialize secret for key '{key}': {source}")] | ||||||
|  |     Deserialization { | ||||||
|  |         key: String, | ||||||
|  |         source: serde_json::Error, | ||||||
|  |     }, | ||||||
|  |     #[error("Failed to serialize secret for key '{key}': {source}")] | ||||||
|  |     Serialization { | ||||||
|  |         key: String, | ||||||
|  |         source: serde_json::Error, | ||||||
|  |     }, | ||||||
|  |     #[error("Underlying storage error: {0}")] | ||||||
|  |     Store(#[from] Box<dyn std::error::Error + Send + Sync>), | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // The trait is now async!
 | ||||||
|  | #[async_trait] | ||||||
|  | pub trait SecretStore: fmt::Debug + Send + Sync { | ||||||
|  |     async fn get_raw(&self, namespace: &str, key: &str) -> Result<Vec<u8>, SecretStoreError>; | ||||||
|  |     async fn set_raw( | ||||||
|  |         &self, | ||||||
|  |         namespace: &str, | ||||||
|  |         key: &str, | ||||||
|  |         value: &[u8], | ||||||
|  |     ) -> Result<(), SecretStoreError>; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Use OnceCell for async-friendly, one-time initialization.
 | ||||||
|  | static SECRET_MANAGER: OnceCell<SecretManager> = OnceCell::const_new(); | ||||||
|  | 
 | ||||||
|  | /// Initializes and returns a reference to the global SecretManager.
 | ||||||
|  | async fn get_secret_manager() -> &'static SecretManager { | ||||||
|  |     SECRET_MANAGER.get_or_init(init_secret_manager).await | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /// The async initialization function for the SecretManager.
 | ||||||
|  | async fn init_secret_manager() -> SecretManager { | ||||||
|  |     let default_secret_score = "infisical".to_string(); | ||||||
|  |     let store_type = SECRET_STORE.as_ref().unwrap_or(&default_secret_score); | ||||||
|  | 
 | ||||||
|  |     let store: Box<dyn SecretStore> = match store_type.as_str() { | ||||||
|  |         "file" => Box::new(LocalFileSecretStore::default()), | ||||||
|  |         "infisical" | _ => { | ||||||
|  |             let store = InfisicalSecretStore::new( | ||||||
|  |                 INFISICAL_URL.clone().expect("Infisical url must be set, see harmony_secret config for ways to provide it. You can try with HARMONY_SECRET_INFISICAL_URL"), | ||||||
|  |                 INFISICAL_PROJECT_ID.clone().expect("Infisical project id must be set, see harmony_secret config for ways to provide it. You can try with HARMONY_SECRET_INFISICAL_PROJECT_ID"), | ||||||
|  |                 INFISICAL_ENVIRONMENT.clone().expect("Infisical environment must be set, see harmony_secret config for ways to provide it. You can try with HARMONY_SECRET_INFISICAL_ENVIRONMENT"), | ||||||
|  |                 INFISICAL_CLIENT_ID.clone().expect("Infisical client id must be set, see harmony_secret config for ways to provide it. You can try with HARMONY_SECRET_INFISICAL_CLIENT_ID"), | ||||||
|  |                 INFISICAL_CLIENT_SECRET.clone().expect("Infisical client secret must be set, see harmony_secret config for ways to provide it. You can try with HARMONY_SECRET_INFISICAL_CLIENT_SECRET"), | ||||||
|  |             ) | ||||||
|  |             .await | ||||||
|  |             .expect("Failed to initialize Infisical secret store"); | ||||||
|  |             Box::new(store) | ||||||
|  |         } | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     SecretManager::new(SECRET_NAMESPACE.clone(), store) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /// Manages the lifecycle of secrets, providing a simple static API.
 | ||||||
|  | #[derive(Debug)] | ||||||
|  | pub struct SecretManager { | ||||||
|  |     namespace: String, | ||||||
|  |     store: Box<dyn SecretStore>, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl SecretManager { | ||||||
|  |     fn new(namespace: String, store: Box<dyn SecretStore>) -> Self { | ||||||
|  |         Self { namespace, store } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /// Retrieves and deserializes a secret.
 | ||||||
|  |     pub async fn get<T: Secret>() -> Result<T, SecretStoreError> { | ||||||
|  |         let manager = get_secret_manager().await; | ||||||
|  |         let raw_value = manager.store.get_raw(&manager.namespace, T::KEY).await?; | ||||||
|  |         serde_json::from_slice(&raw_value).map_err(|e| SecretStoreError::Deserialization { | ||||||
|  |             key: T::KEY.to_string(), | ||||||
|  |             source: e, | ||||||
|  |         }) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /// Serializes and stores a secret.
 | ||||||
|  |     pub async fn set<T: Secret>(secret: &T) -> Result<(), SecretStoreError> { | ||||||
|  |         let manager = get_secret_manager().await; | ||||||
|  |         let raw_value = | ||||||
|  |             serde_json::to_vec(secret).map_err(|e| SecretStoreError::Serialization { | ||||||
|  |                 key: T::KEY.to_string(), | ||||||
|  |                 source: e, | ||||||
|  |             })?; | ||||||
|  |         manager | ||||||
|  |             .store | ||||||
|  |             .set_raw(&manager.namespace, T::KEY, &raw_value) | ||||||
|  |             .await | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[cfg(test)] | ||||||
|  | mod test { | ||||||
|  |     use super::*; | ||||||
|  |     use pretty_assertions::assert_eq; | ||||||
|  |     use serde::{Deserialize, Serialize}; | ||||||
|  | 
 | ||||||
|  |     #[derive(Serialize, Deserialize, Debug, PartialEq)] | ||||||
|  |     struct TestUserMeta { | ||||||
|  |         labels: Vec<String>, | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[derive(Secret, Serialize, Deserialize, Debug, PartialEq)] | ||||||
|  |     struct TestSecret { | ||||||
|  |         user: String, | ||||||
|  |         password: String, | ||||||
|  |         metadata: TestUserMeta, | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[cfg(secrete2etest)] | ||||||
|  |     #[tokio::test] | ||||||
|  |     async fn set_and_retrieve_secret() { | ||||||
|  |         let secret = TestSecret { | ||||||
|  |             user: String::from("user"), | ||||||
|  |             password: String::from("password"), | ||||||
|  |             metadata: TestUserMeta { | ||||||
|  |                 labels: vec![ | ||||||
|  |                     String::from("label1"), | ||||||
|  |                     String::from("label2"), | ||||||
|  |                     String::from( | ||||||
|  |                         "some longet label with \" special @#%$)(udiojcia[]]] \"'asdij'' characters Nдs はにほへとちり าฟันพัฒนา yağız şoföre ç <20> <20> <20> <20> <20> <20> <20> <20> <20> <20> <20> <20> <20>  👩👩👧👦  /span>  👩👧👦 and why not emojis ", | ||||||
|  |                     ), | ||||||
|  |                 ], | ||||||
|  |             }, | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         SecretManager::set(&secret).await.unwrap(); | ||||||
|  |         let value = SecretManager::get::<TestSecret>().await.unwrap(); | ||||||
|  | 
 | ||||||
|  |         assert_eq!(value, secret); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										129
									
								
								harmony_secret/src/store/infisical.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										129
									
								
								harmony_secret/src/store/infisical.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,129 @@ | |||||||
|  | use crate::{SecretStore, SecretStoreError}; | ||||||
|  | use async_trait::async_trait; | ||||||
|  | use infisical::{ | ||||||
|  |     AuthMethod, InfisicalError, | ||||||
|  |     client::Client, | ||||||
|  |     secrets::{CreateSecretRequest, GetSecretRequest, UpdateSecretRequest}, | ||||||
|  | }; | ||||||
|  | use log::{info, warn}; | ||||||
|  | 
 | ||||||
|  | #[derive(Debug)] | ||||||
|  | pub struct InfisicalSecretStore { | ||||||
|  |     client: Client, | ||||||
|  |     project_id: String, | ||||||
|  |     environment: String, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl InfisicalSecretStore { | ||||||
|  |     /// Creates a new, authenticated Infisical client.
 | ||||||
|  |     pub async fn new( | ||||||
|  |         base_url: String, | ||||||
|  |         project_id: String, | ||||||
|  |         environment: String, | ||||||
|  |         client_id: String, | ||||||
|  |         client_secret: String, | ||||||
|  |     ) -> Result<Self, InfisicalError> { | ||||||
|  |         info!("INFISICAL_STORE: Initializing client for URL: {base_url}"); | ||||||
|  | 
 | ||||||
|  |         // The builder and login logic remains the same.
 | ||||||
|  |         let mut client = Client::builder().base_url(base_url).build().await?; | ||||||
|  |         let auth_method = AuthMethod::new_universal_auth(client_id, client_secret); | ||||||
|  |         client.login(auth_method).await?; | ||||||
|  | 
 | ||||||
|  |         info!("INFISICAL_STORE: Client authenticated successfully."); | ||||||
|  |         Ok(Self { | ||||||
|  |             client, | ||||||
|  |             project_id, | ||||||
|  |             environment, | ||||||
|  |         }) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[async_trait] | ||||||
|  | impl SecretStore for InfisicalSecretStore { | ||||||
|  |     async fn get_raw(&self, _environment: &str, key: &str) -> Result<Vec<u8>, SecretStoreError> { | ||||||
|  |         let environment = &self.environment; | ||||||
|  |         info!("INFISICAL_STORE: Getting key '{key}' from environment '{environment}'"); | ||||||
|  | 
 | ||||||
|  |         let request = GetSecretRequest::builder(key, &self.project_id, environment).build(); | ||||||
|  | 
 | ||||||
|  |         match self.client.secrets().get(request).await { | ||||||
|  |             Ok(secret) => Ok(secret.secret_value.into_bytes()), | ||||||
|  |             Err(e) => { | ||||||
|  |                 // Correctly match against the actual InfisicalError enum.
 | ||||||
|  |                 match e { | ||||||
|  |                     // The specific case for a 404 Not Found error.
 | ||||||
|  |                     InfisicalError::HttpError { status, .. } | ||||||
|  |                         if status == http::StatusCode::NOT_FOUND => | ||||||
|  |                     { | ||||||
|  |                         Err(SecretStoreError::NotFound { | ||||||
|  |                             namespace: environment.to_string(), | ||||||
|  |                             key: key.to_string(), | ||||||
|  |                         }) | ||||||
|  |                     } | ||||||
|  |                     // For all other errors, wrap them in our generic Store error.
 | ||||||
|  |                     _ => Err(SecretStoreError::Store(Box::new(e))), | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async fn set_raw( | ||||||
|  |         &self, | ||||||
|  |         _environment: &str, | ||||||
|  |         key: &str, | ||||||
|  |         val: &[u8], | ||||||
|  |     ) -> Result<(), SecretStoreError> { | ||||||
|  |         info!( | ||||||
|  |             "INFISICAL_STORE: Setting key '{key}' in environment '{}'", | ||||||
|  |             self.environment | ||||||
|  |         ); | ||||||
|  |         let value_str = | ||||||
|  |             String::from_utf8(val.to_vec()).map_err(|e| SecretStoreError::Store(Box::new(e)))?; | ||||||
|  | 
 | ||||||
|  |         // --- Upsert Logic ---
 | ||||||
|  |         // First, attempt to update the secret.
 | ||||||
|  |         let update_req = UpdateSecretRequest::builder(key, &self.project_id, &self.environment) | ||||||
|  |             .secret_value(&value_str) | ||||||
|  |             .build(); | ||||||
|  | 
 | ||||||
|  |         match self.client.secrets().update(update_req).await { | ||||||
|  |             Ok(_) => { | ||||||
|  |                 info!("INFISICAL_STORE: Successfully updated secret '{key}'."); | ||||||
|  |                 Ok(()) | ||||||
|  |             } | ||||||
|  |             Err(e) => { | ||||||
|  |                 // If the update failed, check if it was because the secret doesn't exist.
 | ||||||
|  |                 match e { | ||||||
|  |                     InfisicalError::HttpError { status, .. } | ||||||
|  |                         if status == http::StatusCode::NOT_FOUND => | ||||||
|  |                     { | ||||||
|  |                         // The secret was not found, so we create it instead.
 | ||||||
|  |                         warn!( | ||||||
|  |                             "INFISICAL_STORE: Secret '{key}' not found for update, attempting to create it." | ||||||
|  |                         ); | ||||||
|  |                         let create_req = CreateSecretRequest::builder( | ||||||
|  |                             key, | ||||||
|  |                             &value_str, | ||||||
|  |                             &self.project_id, | ||||||
|  |                             &self.environment, | ||||||
|  |                         ) | ||||||
|  |                         .build(); | ||||||
|  | 
 | ||||||
|  |                         // Handle potential errors during creation.
 | ||||||
|  |                         self.client | ||||||
|  |                             .secrets() | ||||||
|  |                             .create(create_req) | ||||||
|  |                             .await | ||||||
|  |                             .map_err(|create_err| SecretStoreError::Store(Box::new(create_err)))?; | ||||||
|  | 
 | ||||||
|  |                         info!("INFISICAL_STORE: Successfully created secret '{key}'."); | ||||||
|  |                         Ok(()) | ||||||
|  |                     } | ||||||
|  |                     // Any other error during update is a genuine failure.
 | ||||||
|  |                     _ => Err(SecretStoreError::Store(Box::new(e))), | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										105
									
								
								harmony_secret/src/store/local_file.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										105
									
								
								harmony_secret/src/store/local_file.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,105 @@ | |||||||
|  | use async_trait::async_trait; | ||||||
|  | use log::info; | ||||||
|  | use std::path::{Path, PathBuf}; | ||||||
|  | 
 | ||||||
|  | use crate::{SecretStore, SecretStoreError}; | ||||||
|  | 
 | ||||||
|  | #[derive(Debug, Default)] | ||||||
|  | pub struct LocalFileSecretStore; | ||||||
|  | 
 | ||||||
|  | impl LocalFileSecretStore { | ||||||
|  |     /// Helper to consistently generate the secret file path.
 | ||||||
|  |     fn get_file_path(base_dir: &Path, ns: &str, key: &str) -> PathBuf { | ||||||
|  |         base_dir.join(format!("{ns}_{key}.json")) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[async_trait] | ||||||
|  | impl SecretStore for LocalFileSecretStore { | ||||||
|  |     async fn get_raw(&self, ns: &str, key: &str) -> Result<Vec<u8>, SecretStoreError> { | ||||||
|  |         let data_dir = directories::BaseDirs::new() | ||||||
|  |             .expect("Could not find a valid home directory") | ||||||
|  |             .data_dir() | ||||||
|  |             .join("harmony") | ||||||
|  |             .join("secrets"); | ||||||
|  | 
 | ||||||
|  |         let file_path = Self::get_file_path(&data_dir, ns, key); | ||||||
|  |         info!( | ||||||
|  |             "LOCAL_STORE: Getting key '{key}' from namespace '{ns}' at {}", | ||||||
|  |             file_path.display() | ||||||
|  |         ); | ||||||
|  | 
 | ||||||
|  |         tokio::fs::read(&file_path) | ||||||
|  |             .await | ||||||
|  |             .map_err(|_| SecretStoreError::NotFound { | ||||||
|  |                 namespace: ns.to_string(), | ||||||
|  |                 key: key.to_string(), | ||||||
|  |             }) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async fn set_raw(&self, ns: &str, key: &str, val: &[u8]) -> Result<(), SecretStoreError> { | ||||||
|  |         let data_dir = directories::BaseDirs::new() | ||||||
|  |             .expect("Could not find a valid home directory") | ||||||
|  |             .data_dir() | ||||||
|  |             .join("harmony") | ||||||
|  |             .join("secrets"); | ||||||
|  | 
 | ||||||
|  |         let file_path = Self::get_file_path(&data_dir, ns, key); | ||||||
|  |         info!( | ||||||
|  |             "LOCAL_STORE: Setting key '{key}' in namespace '{ns}' at {}", | ||||||
|  |             file_path.display() | ||||||
|  |         ); | ||||||
|  | 
 | ||||||
|  |         if let Some(parent_dir) = file_path.parent() { | ||||||
|  |             tokio::fs::create_dir_all(parent_dir) | ||||||
|  |                 .await | ||||||
|  |                 .map_err(|e| SecretStoreError::Store(Box::new(e)))?; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         tokio::fs::write(&file_path, val) | ||||||
|  |             .await | ||||||
|  |             .map_err(|e| SecretStoreError::Store(Box::new(e))) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[cfg(test)] | ||||||
|  | mod tests { | ||||||
|  |     use super::*; | ||||||
|  |     use tempfile::tempdir; | ||||||
|  | 
 | ||||||
|  |     #[tokio::test] | ||||||
|  |     async fn test_set_and_get_raw_successfully() { | ||||||
|  |         let dir = tempdir().unwrap(); | ||||||
|  |         let store = LocalFileSecretStore::default(); | ||||||
|  |         let ns = "test-ns"; | ||||||
|  |         let key = "test-key"; | ||||||
|  |         let value = b"{\"data\":\"test-value\"}"; | ||||||
|  | 
 | ||||||
|  |         // To test the store directly, we override the base directory logic.
 | ||||||
|  |         // For this test, we'll manually construct the path within our temp dir.
 | ||||||
|  |         let file_path = LocalFileSecretStore::get_file_path(dir.path(), ns, key); | ||||||
|  | 
 | ||||||
|  |         // Manually write to the temp path to simulate the store's behavior
 | ||||||
|  |         tokio::fs::create_dir_all(file_path.parent().unwrap()) | ||||||
|  |             .await | ||||||
|  |             .unwrap(); | ||||||
|  |         tokio::fs::write(&file_path, value).await.unwrap(); | ||||||
|  | 
 | ||||||
|  |         // Now, test get_raw by reading from that same temp path (by mocking the path logic)
 | ||||||
|  |         let retrieved_value = tokio::fs::read(&file_path).await.unwrap(); | ||||||
|  |         assert_eq!(retrieved_value, value); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     #[tokio::test] | ||||||
|  |     async fn test_get_raw_not_found() { | ||||||
|  |         let dir = tempdir().unwrap(); | ||||||
|  |         let ns = "test-ns"; | ||||||
|  |         let key = "non-existent-key"; | ||||||
|  | 
 | ||||||
|  |         // We need to check if reading a non-existent file gives the correct error
 | ||||||
|  |         let file_path = LocalFileSecretStore::get_file_path(dir.path(), ns, key); | ||||||
|  |         let result = tokio::fs::read(&file_path).await; | ||||||
|  | 
 | ||||||
|  |         assert!(matches!(result, Err(_))); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										4
									
								
								harmony_secret/src/store/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								harmony_secret/src/store/mod.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,4 @@ | |||||||
|  | mod infisical; | ||||||
|  | mod local_file; | ||||||
|  | pub use infisical::*; | ||||||
|  | pub use local_file::*; | ||||||
							
								
								
									
										8
									
								
								harmony_secret/test_harmony_secret_infisical.sh
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								harmony_secret/test_harmony_secret_infisical.sh
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,8 @@ | |||||||
|  | export HARMONY_SECRET_NAMESPACE=harmony_test_secrets | ||||||
|  | export HARMONY_SECRET_INFISICAL_URL=http://localhost | ||||||
|  | export HARMONY_SECRET_INFISICAL_PROJECT_ID=eb4723dc-eede-44d7-98cc-c8e0caf29ccb | ||||||
|  | export HARMONY_SECRET_INFISICAL_ENVIRONMENT=dev | ||||||
|  | export HARMONY_SECRET_INFISICAL_CLIENT_ID=dd16b07f-0e38-4090-a1d0-922de9f44d91 | ||||||
|  | export HARMONY_SECRET_INFISICAL_CLIENT_SECRET=bd2ae054e7759b11ca2e908494196337cc800bab138cb1f59e8d9b15ca3f286f | ||||||
|  | 
 | ||||||
|  | cargo test | ||||||
| @ -1,5 +1,5 @@ | |||||||
| [package] | [package] | ||||||
| name = "harmony-secrets-derive" | name = "harmony-secret-derive" | ||||||
| version = "0.1.0" | version = "0.1.0" | ||||||
| edition = "2024" | edition = "2024" | ||||||
| 
 | 
 | ||||||
							
								
								
									
										38
									
								
								harmony_secret_derive/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								harmony_secret_derive/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,38 @@ | |||||||
|  | use proc_macro::TokenStream; | ||||||
|  | use proc_macro_crate::{FoundCrate, crate_name}; | ||||||
|  | use quote::quote; | ||||||
|  | use syn::{DeriveInput, Ident, parse_macro_input}; | ||||||
|  | 
 | ||||||
|  | #[proc_macro_derive(Secret)] | ||||||
|  | pub fn derive_secret(input: TokenStream) -> TokenStream { | ||||||
|  |     let input = parse_macro_input!(input as DeriveInput); | ||||||
|  |     let struct_ident = &input.ident; | ||||||
|  | 
 | ||||||
|  |     // The key for the secret will be the stringified name of the struct itself.
 | ||||||
|  |     // e.g., `struct OKDClusterSecret` becomes key `"OKDClusterSecret"`.
 | ||||||
|  |     let key = struct_ident.to_string(); | ||||||
|  | 
 | ||||||
|  |     // Find the path to the `harmony_secret` crate.
 | ||||||
|  |     let secret_crate_path = match crate_name("harmony-secret") { | ||||||
|  |         Ok(FoundCrate::Itself) => quote!(crate), | ||||||
|  |         Ok(FoundCrate::Name(name)) => { | ||||||
|  |             let ident = Ident::new(&name, proc_macro2::Span::call_site()); | ||||||
|  |             quote!(::#ident) | ||||||
|  |         } | ||||||
|  |         Err(e) => { | ||||||
|  |             return syn::Error::new(proc_macro2::Span::call_site(), e.to_string()) | ||||||
|  |                 .to_compile_error() | ||||||
|  |                 .into(); | ||||||
|  |         } | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     // The generated code now implements `Secret` for the struct itself.
 | ||||||
|  |     // The struct must also derive `Serialize` and `Deserialize` for this to be useful.
 | ||||||
|  |     let expanded = quote! { | ||||||
|  |         impl #secret_crate_path::Secret for #struct_ident { | ||||||
|  |             const KEY: &'static str = #key; | ||||||
|  |         } | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     TokenStream::from(expanded) | ||||||
|  | } | ||||||
| @ -1,152 +0,0 @@ | |||||||
| use syn::DeriveInput; |  | ||||||
| use syn::parse_macro_input; |  | ||||||
| use proc_macro::TokenStream; |  | ||||||
| use proc_macro_crate::{FoundCrate, crate_name}; |  | ||||||
| use quote::quote; |  | ||||||
| use syn::{ |  | ||||||
|     Ident, LitStr, Meta, Token, Type, |  | ||||||
|     parse::{Parse, ParseStream}, |  | ||||||
|     punctuated::Punctuated, |  | ||||||
| }; |  | ||||||
| 
 |  | ||||||
| /// A helper struct to parse the contents of the `#[secret(...)]` attribute.
 |  | ||||||
| /// This makes parsing robust and allows for better error handling.
 |  | ||||||
| struct SecretAttributeArgs { |  | ||||||
|     namespace: LitStr, |  | ||||||
|     key: LitStr, |  | ||||||
|     value_type: Type, |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| impl Parse for SecretAttributeArgs { |  | ||||||
|     fn parse(input: ParseStream) -> syn::Result<Self> { |  | ||||||
|         // The attributes are parsed as a comma-separated list of `key = value` pairs.
 |  | ||||||
|         let parsed_args = Punctuated::<Meta, Token![,]>::parse_terminated(input)?; |  | ||||||
| 
 |  | ||||||
|         let mut namespace = None; |  | ||||||
|         let mut key = None; |  | ||||||
|         let mut value_type = None; |  | ||||||
| 
 |  | ||||||
|         for arg in parsed_args { |  | ||||||
|             if let Meta::NameValue(nv) = arg { |  | ||||||
|                 let ident_str = nv.path.get_ident().map(Ident::to_string); |  | ||||||
|                 match ident_str.as_deref() { |  | ||||||
|                     Some("namespace") => { |  | ||||||
|                         if let syn::Expr::Lit(expr_lit) = nv.value { |  | ||||||
|                             if let syn::Lit::Str(lit) = expr_lit.lit { |  | ||||||
|                                 namespace = Some(lit); |  | ||||||
|                                 continue; |  | ||||||
|                             } |  | ||||||
|                         } |  | ||||||
|                         return Err(syn::Error::new_spanned( |  | ||||||
|                             nv.value, |  | ||||||
|                             "Expected a string literal for `namespace`", |  | ||||||
|                         )); |  | ||||||
|                     } |  | ||||||
|                     Some("key") => { |  | ||||||
|                         if let syn::Expr::Lit(expr_lit) = nv.value { |  | ||||||
|                             if let syn::Lit::Str(lit) = expr_lit.lit { |  | ||||||
|                                 key = Some(lit); |  | ||||||
|                                 continue; |  | ||||||
|                             } |  | ||||||
|                         } |  | ||||||
|                         return Err(syn::Error::new_spanned( |  | ||||||
|                             nv.value, |  | ||||||
|                             "Expected a string literal for `key`", |  | ||||||
|                         )); |  | ||||||
|                     } |  | ||||||
|                     Some("value_type") => { |  | ||||||
|                         if let syn::Expr::Lit(expr_lit) = nv.value { |  | ||||||
|                             // This is the key improvement: parse the string literal's content as a Type.
 |  | ||||||
|                             if let syn::Lit::Str(lit) = expr_lit.lit { |  | ||||||
|                                 value_type = Some(lit.parse::<Type>()?); |  | ||||||
|                                 continue; |  | ||||||
|                             } |  | ||||||
|                         } |  | ||||||
|                         // This allows for the improved syntax: `value_type = String`
 |  | ||||||
|                         if let syn::Expr::Path(expr_path) = nv.value { |  | ||||||
|                             value_type = Some(Type::Path(expr_path.into())); |  | ||||||
|                             continue; |  | ||||||
|                         } |  | ||||||
|                         return Err(syn::Error::new_spanned( |  | ||||||
|                             nv.value, |  | ||||||
|                             "Expected a type path (e.g., `String` or `Vec<u8>`) for `value_type`", |  | ||||||
|                         )); |  | ||||||
|                     } |  | ||||||
|                     _ => {} |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|             return Err(syn::Error::new_spanned( |  | ||||||
|                 arg, |  | ||||||
|                 "Unsupported attribute key. Must be `namespace`, `key`, or `value_type`.", |  | ||||||
|             )); |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         Ok(SecretAttributeArgs { |  | ||||||
|             namespace: namespace.ok_or_else(|| { |  | ||||||
|                 syn::Error::new(input.span(), "Missing required attribute `namespace`") |  | ||||||
|             })?, |  | ||||||
|             key: key |  | ||||||
|                 .ok_or_else(|| syn::Error::new(input.span(), "Missing required attribute `key`"))?, |  | ||||||
|             value_type: value_type.ok_or_else(|| { |  | ||||||
|                 syn::Error::new(input.span(), "Missing required attribute `value_type`") |  | ||||||
|             })?, |  | ||||||
|         }) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #[proc_macro_derive(Secret, attributes(secret))] |  | ||||||
| pub fn derive_secret(input: TokenStream) -> TokenStream { |  | ||||||
|     let input = parse_macro_input!(input as DeriveInput); |  | ||||||
| 
 |  | ||||||
|     // Ensure this is a unit struct (e.g., `struct MySecret;`)
 |  | ||||||
|     if !matches!(&input.data, syn::Data::Struct(s) if s.fields.is_empty()) { |  | ||||||
|         return syn::Error::new_spanned( |  | ||||||
|             &input.ident, |  | ||||||
|             "#[derive(Secret)] can only be used on unit structs.", |  | ||||||
|         ) |  | ||||||
|         .to_compile_error() |  | ||||||
|         .into(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     // Find the `#[secret(...)]` attribute.
 |  | ||||||
|     let secret_attr = input |  | ||||||
|         .attrs |  | ||||||
|         .iter() |  | ||||||
|         .find(|attr| attr.path().is_ident("secret")) |  | ||||||
|         .ok_or_else(|| syn::Error::new_spanned(&input.ident, "Missing `#[secret(...)]` attribute.")) |  | ||||||
|         .and_then(|attr| attr.parse_args::<SecretAttributeArgs>()); |  | ||||||
| 
 |  | ||||||
|     let args = match secret_attr { |  | ||||||
|         Ok(args) => args, |  | ||||||
|         Err(e) => return e.to_compile_error().into(), |  | ||||||
|     }; |  | ||||||
| 
 |  | ||||||
|     // Find the path to the `harmony_secrets` crate to make the macro work anywhere.
 |  | ||||||
|     let secret_crate_path = match crate_name("harmony-secrets") { |  | ||||||
|         Ok(FoundCrate::Itself) => quote!(crate), |  | ||||||
|         Ok(FoundCrate::Name(name)) => { |  | ||||||
|             let ident = Ident::new(&name, proc_macro2::Span::call_site()); |  | ||||||
|             quote!(::#ident) |  | ||||||
|         } |  | ||||||
|         Err(e) => { |  | ||||||
|             return syn::Error::new(proc_macro2::Span::call_site(), e.to_string()) |  | ||||||
|                 .to_compile_error() |  | ||||||
|                 .into(); |  | ||||||
|         } |  | ||||||
|     }; |  | ||||||
| 
 |  | ||||||
|     let struct_ident = &input.ident; |  | ||||||
|     let namespace = args.namespace; |  | ||||||
|     let key = args.key; |  | ||||||
|     let value_type = args.value_type; |  | ||||||
| 
 |  | ||||||
|     let expanded = quote! { |  | ||||||
|         impl #secret_crate_path::Secret for #struct_ident { |  | ||||||
|             type Value = #value_type; |  | ||||||
|             const NAMESPACE: &'static str = #namespace; |  | ||||||
|             const KEY: &'static str = #key; |  | ||||||
|         } |  | ||||||
|     }; |  | ||||||
| 
 |  | ||||||
|     TokenStream::from(expanded) |  | ||||||
| } |  | ||||||
| @ -1,100 +0,0 @@ | |||||||
| use proc_macro::TokenStream; |  | ||||||
| use syn::{parse_macro_input, DeriveInput, Attribute, Meta}; |  | ||||||
| use quote::quote; |  | ||||||
| use proc_macro_crate::crate_name; |  | ||||||
| 
 |  | ||||||
| #[proc_macro_derive(Secret, attributes(secret))] |  | ||||||
| pub fn derive_secret(input: TokenStream) -> TokenStream { |  | ||||||
|     let input = parse_macro_input!(input as DeriveInput); |  | ||||||
|      |  | ||||||
|     // Verify this is a unit struct |  | ||||||
|     if !matches!(&input.data, syn::Data::Struct(data) if data.fields.is_empty()) { |  | ||||||
|         return syn::Error::new_spanned( |  | ||||||
|             input.ident, |  | ||||||
|             "#[derive(Secret)] only supports unit structs (e.g., `struct MySecret;`)", |  | ||||||
|         ) |  | ||||||
|         .to_compile_error() |  | ||||||
|         .into(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     // Parse the #[secret(...)] attribute |  | ||||||
|     let (namespace, key, value_type) = match parse_secret_attributes(&input.attrs) { |  | ||||||
|         Ok(attrs) => attrs, |  | ||||||
|         Err(e) => return e.into_compile_error().into(), |  | ||||||
|     }; |  | ||||||
| 
 |  | ||||||
|     // Get the path to the harmony_secrets crate |  | ||||||
|     let secret_crate_path = match crate_name("harmony-secrets") { |  | ||||||
|         Ok(proc_macro_crate::FoundCrate::Itself) => quote!(crate), |  | ||||||
|         Ok(proc_macro_crate::FoundCrate::Name(name)) => { |  | ||||||
|             let ident = quote::format_ident!("{}", name); |  | ||||||
|             quote!(::#ident) |  | ||||||
|         } |  | ||||||
|         Err(_) => { |  | ||||||
|             return syn::Error::new_spanned( |  | ||||||
|                 &input.ident, |  | ||||||
|                 "harmony-secrets crate not found in dependencies", |  | ||||||
|             ) |  | ||||||
|             .to_compile_error() |  | ||||||
|             .into(); |  | ||||||
|         } |  | ||||||
|     }; |  | ||||||
| 
 |  | ||||||
|     let struct_ident = input.ident; |  | ||||||
| 
 |  | ||||||
|     TokenStream::from(quote! { |  | ||||||
|         impl #secret_crate_path::Secret for #struct_ident { |  | ||||||
|             type Value = #value_type; |  | ||||||
|             const NAMESPACE: &'static str = #namespace; |  | ||||||
|             const KEY: &'static str = #key; |  | ||||||
|         } |  | ||||||
|     }) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| fn parse_secret_attributes(attrs: &[Attribute]) -> syn::Result<(String, String, syn::Type)> { |  | ||||||
|     let secret_attr = attrs |  | ||||||
|         .iter() |  | ||||||
|         .find(|attr| attr.path().is_ident("secret")) |  | ||||||
|         .ok_or_else(|| { |  | ||||||
|             syn::Error::new_spanned( |  | ||||||
|                 attrs.first().unwrap_or_else(|| &attrs[0]), |  | ||||||
|                 "missing #[secret(...)] attribute", |  | ||||||
|             ) |  | ||||||
|         })?; |  | ||||||
| 
 |  | ||||||
|     let mut namespace = None; |  | ||||||
|     let mut key = None; |  | ||||||
|     let mut value_type = None; |  | ||||||
| 
 |  | ||||||
|     if let Meta::List(meta_list) = &secret_attr.parse_meta()? { |  | ||||||
|         for nested in &meta_list.nested { |  | ||||||
|             if let syn::NestedMeta::Meta(Meta::NameValue(nv)) = nested { |  | ||||||
|                 if nv.path.is_ident("namespace") { |  | ||||||
|                     if let syn::Lit::Str(lit) = &nv.lit { |  | ||||||
|                         namespace = Some(lit.value()); |  | ||||||
|                     } |  | ||||||
|                 } else if nv.path.is_ident("key") { |  | ||||||
|                     if let syn::Lit::Str(lit) = &nv.lit { |  | ||||||
|                         key = Some(lit.value()); |  | ||||||
|                     } |  | ||||||
|                 } else if nv.path.is_ident("value_type") { |  | ||||||
|                     if let syn::Lit::Str(lit) = &nv.lit { |  | ||||||
|                         value_type = Some(syn::parse_str::<syn::Type>(&lit.value())?); |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     Ok(( |  | ||||||
|         namespace.ok_or_else(|| { |  | ||||||
|             syn::Error::new_spanned(secret_attr, "missing `namespace` in #[secret(...)]") |  | ||||||
|         })?, |  | ||||||
|         key.ok_or_else(|| { |  | ||||||
|             syn::Error::new_spanned(secret_attr, "missing `key` in #[secret(...)]") |  | ||||||
|         })?, |  | ||||||
|         value_type.ok_or_else(|| { |  | ||||||
|             syn::Error::new_spanned(secret_attr, "missing `value_type` in #[secret(...)]") |  | ||||||
|         })?, |  | ||||||
|     )) |  | ||||||
| } |  | ||||||
| @ -12,7 +12,7 @@ env_logger = { workspace = true } | |||||||
| yaserde = { git = "https://github.com/jggc/yaserde.git" } | yaserde = { git = "https://github.com/jggc/yaserde.git" } | ||||||
| yaserde_derive = { git = "https://github.com/jggc/yaserde.git" } | yaserde_derive = { git = "https://github.com/jggc/yaserde.git" } | ||||||
| xml-rs = "0.8" | xml-rs = "0.8" | ||||||
| thiserror = "1.0" | thiserror.workspace = true | ||||||
| async-trait = { workspace = true } | async-trait = { workspace = true } | ||||||
| tokio = { workspace = true } | tokio = { workspace = true } | ||||||
| uuid = { workspace = true } | uuid = { workspace = true } | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user