Lots more updates
Also begin adding rust building capabilities to be able to write rust binaries for some commands.
This commit is contained in:
parent
624508dd14
commit
dd1cfa79e7
52 changed files with 2509 additions and 150 deletions
1
.direnv/flake-profile
Symbolic link
1
.direnv/flake-profile
Symbolic link
|
@ -0,0 +1 @@
|
|||
flake-profile-2-link
|
1
.direnv/flake-profile-2-link
Symbolic link
1
.direnv/flake-profile-2-link
Symbolic link
|
@ -0,0 +1 @@
|
|||
/nix/store/dqy98711cp1rqz8nj7qpxq6qj6vnyf0j-nix-shell-env
|
1
.envrc
Normal file
1
.envrc
Normal file
|
@ -0,0 +1 @@
|
|||
use flake
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -1,2 +1,5 @@
|
|||
result/
|
||||
.DS_Store
|
||||
.terraform-cache/*/*/config.tf.json
|
||||
.terraform-cache/*/*/.terraform
|
||||
rust/target
|
||||
|
|
54
flake.lock
generated
54
flake.lock
generated
|
@ -1,5 +1,21 @@
|
|||
{
|
||||
"nodes": {
|
||||
"advisory-db": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1750151065,
|
||||
"narHash": "sha256-il+CAqChFIB82xP6bO43dWlUVs+NlG7a4g8liIP5HcI=",
|
||||
"owner": "rustsec",
|
||||
"repo": "advisory-db",
|
||||
"rev": "7573f55ba337263f61167dbb0ea926cdc7c8eb5d",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "rustsec",
|
||||
"repo": "advisory-db",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"bats-assert": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
|
@ -32,6 +48,21 @@
|
|||
"type": "github"
|
||||
}
|
||||
},
|
||||
"crane": {
|
||||
"locked": {
|
||||
"lastModified": 1751562746,
|
||||
"narHash": "sha256-smpugNIkmDeicNz301Ll1bD7nFOty97T79m4GUMUczA=",
|
||||
"owner": "ipetkov",
|
||||
"repo": "crane",
|
||||
"rev": "aed2020fd3dc26e1e857d4107a5a67a33ab6c1fd",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "ipetkov",
|
||||
"repo": "crane",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"disko": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
|
@ -211,13 +242,36 @@
|
|||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"advisory-db": "advisory-db",
|
||||
"crane": "crane",
|
||||
"disko": "disko",
|
||||
"flake-base": "flake-base",
|
||||
"nixpkgs": "nixpkgs",
|
||||
"rust-overlay": "rust-overlay",
|
||||
"terranix": "terranix",
|
||||
"terranix-hcloud": "terranix-hcloud"
|
||||
}
|
||||
},
|
||||
"rust-overlay": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1751769931,
|
||||
"narHash": "sha256-QR2Rp/41NkA5YxcpvZEKD1S2QE1Pb9U415aK8M/4tJc=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "3ac4f630e375177ea8317e22f5c804156de177e8",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"snowfall-lib": {
|
||||
"inputs": {
|
||||
"flake-compat": "flake-compat",
|
||||
|
|
18
flake.nix
18
flake.nix
|
@ -19,6 +19,17 @@
|
|||
url = "github:terranix/terranix-hcloud";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
crane.url = "github:ipetkov/crane";
|
||||
advisory-db = {
|
||||
url = "github:rustsec/advisory-db";
|
||||
flake = false;
|
||||
};
|
||||
rust-overlay = {
|
||||
url = "github:oxalica/rust-overlay";
|
||||
inputs = {
|
||||
nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
outputs =
|
||||
|
@ -28,7 +39,7 @@
|
|||
path:
|
||||
let
|
||||
files = builtins.readDir path;
|
||||
dirs = builtins.filterAttrs (name: kind: kind == "directory") files;
|
||||
dirs = inputs.nixpkgs.lib.filterAttrs (name: kind: kind == "directory") files;
|
||||
in
|
||||
builtins.attrNames dirs;
|
||||
profileArgs = { inherit self; };
|
||||
|
@ -60,6 +71,7 @@
|
|||
}) profileNames
|
||||
));
|
||||
};
|
||||
overlays = [ inputs.rust-overlay.overlays.default ];
|
||||
})
|
||||
// {
|
||||
terranixModules.cloudflare = import ./nix/modules/terranix/cloudflare {
|
||||
|
@ -70,5 +82,9 @@
|
|||
inherit inputs;
|
||||
khscodesLib = inputs.self.lib;
|
||||
};
|
||||
terranixModules.openbao = import ./nix/modules/terranix/openbao {
|
||||
inherit inputs;
|
||||
khscodesLib = inputs.self.lib;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
7
nix/checks/rust-audit/default.nix
Normal file
7
nix/checks/rust-audit/default.nix
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
inputs,
|
||||
pkgs,
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
(lib.khscodes.mkRust pkgs "${inputs.self}/rust").checks.rust-audit
|
7
nix/checks/rust-clippy/default.nix
Normal file
7
nix/checks/rust-clippy/default.nix
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
inputs,
|
||||
pkgs,
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
(lib.khscodes.mkRust pkgs "${inputs.self}/rust").checks.rust-clippy
|
7
nix/checks/rust-doc/default.nix
Normal file
7
nix/checks/rust-doc/default.nix
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
inputs,
|
||||
pkgs,
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
(lib.khscodes.mkRust pkgs "${inputs.self}/rust").checks.rust-doc
|
7
nix/checks/rust-fmt/default.nix
Normal file
7
nix/checks/rust-fmt/default.nix
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
inputs,
|
||||
pkgs,
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
(lib.khscodes.mkRust pkgs "${inputs.self}/rust").checks.rust-fmt
|
7
nix/checks/rust-hakari/default.nix
Normal file
7
nix/checks/rust-hakari/default.nix
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
inputs,
|
||||
pkgs,
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
(lib.khscodes.mkRust pkgs "${inputs.self}/rust").checks.rust-hakari
|
14
nix/lib/rust/default.nix
Normal file
14
nix/lib/rust/default.nix
Normal file
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
inputs,
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
{
|
||||
mkRust =
|
||||
pkgs: src:
|
||||
import src {
|
||||
inherit lib pkgs;
|
||||
crane = inputs.crane;
|
||||
advisory-db = inputs.advisory-db;
|
||||
};
|
||||
}
|
|
@ -19,8 +19,8 @@ in
|
|||
domain = if hostname == cfg then null else (lib.strings.removePrefix "${hostname}." cfg);
|
||||
in
|
||||
{
|
||||
networking.hostName = hostname;
|
||||
networking.domain = domain;
|
||||
networking.hostName = lib.mkForce hostname;
|
||||
networking.domain = lib.mkForce domain;
|
||||
boot.kernel.sysctl = {
|
||||
"kernel.hostname" = cfg;
|
||||
};
|
||||
|
|
|
@ -6,12 +6,8 @@
|
|||
...
|
||||
}:
|
||||
let
|
||||
cfg = config.khscodes.terraform-hetzner;
|
||||
cfg = config.khscodes.hetzner-instance;
|
||||
fqdn = config.khscodes.fqdn;
|
||||
hostPkgs = import inputs.nixpkgs {
|
||||
system = pkgs.buildPlatform.system;
|
||||
overlays = [ inputs.self.overlays.bitwarden-cli ];
|
||||
};
|
||||
firewallTcpRules = lib.lists.map (p: {
|
||||
direction = "in";
|
||||
protocol = "tcp";
|
||||
|
@ -41,7 +37,6 @@ let
|
|||
};
|
||||
firewallRules = firewallTcpRules ++ firewallUdpRules ++ firewallIcmpRules ++ cfg.extraFirewallRules;
|
||||
firewallEnable = config.networking.firewall.enable;
|
||||
mapRdns = cfg.mapRdns;
|
||||
tldFromFqdn =
|
||||
fqdn:
|
||||
let
|
||||
|
@ -53,8 +48,8 @@ let
|
|||
lib.strings.removePrefix "${builtins.head split}." fqdn;
|
||||
in
|
||||
{
|
||||
options.khscodes.terraform-hetzner = {
|
||||
enable = lib.mkEnableOption "enables generating a terraform config";
|
||||
options.khscodes.hetzner-instance = {
|
||||
enable = lib.mkEnableOption "enables generating a opentofu config";
|
||||
dnsNames = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
description = "DNS names for the server";
|
||||
|
@ -72,7 +67,7 @@ in
|
|||
"bitwarden"
|
||||
"vault"
|
||||
];
|
||||
description = "Whether to load terraform secrets from Bitwarden or Vault";
|
||||
description = "Whether to load opentofu secrets from Bitwarden or Vault";
|
||||
default = "vault";
|
||||
};
|
||||
datacenter = lib.mkOption {
|
||||
|
@ -146,95 +141,102 @@ in
|
|||
labels = {
|
||||
app = fqdn;
|
||||
};
|
||||
config = inputs.terranix.lib.terranixConfiguration {
|
||||
system = pkgs.hostPlatform.system;
|
||||
modules = [
|
||||
(
|
||||
{ config, ... }:
|
||||
{
|
||||
imports = [
|
||||
inputs.self.terranixModules.cloudflare
|
||||
inputs.self.terranixModules.hcloud
|
||||
];
|
||||
config = {
|
||||
terraform.backend.s3 = {
|
||||
bucket = "bw-terraform";
|
||||
key = cfg.bucket.key;
|
||||
region = "auto";
|
||||
endpoints = {
|
||||
s3 = "https://477b394a6a545699445c40953e40f00b.r2.cloudflarestorage.com";
|
||||
};
|
||||
use_path_style = true;
|
||||
skip_credentials_validation = true;
|
||||
skip_region_validation = true;
|
||||
skip_metadata_api_check = true;
|
||||
skip_requesting_account_id = true;
|
||||
skip_s3_checksum = true;
|
||||
modules = [
|
||||
(
|
||||
{ config, ... }:
|
||||
{
|
||||
imports = [
|
||||
inputs.self.terranixModules.cloudflare
|
||||
inputs.self.terranixModules.hcloud
|
||||
];
|
||||
config = {
|
||||
terraform.backend.s3 = {
|
||||
bucket = "bw-terraform";
|
||||
key = cfg.bucket.key;
|
||||
region = "auto";
|
||||
endpoints = {
|
||||
s3 = "https://477b394a6a545699445c40953e40f00b.r2.cloudflarestorage.com";
|
||||
};
|
||||
use_path_style = true;
|
||||
skip_credentials_validation = true;
|
||||
skip_region_validation = true;
|
||||
skip_metadata_api_check = true;
|
||||
skip_requesting_account_id = true;
|
||||
skip_s3_checksum = true;
|
||||
};
|
||||
|
||||
khscodes.hcloud.data.ssh_key.khs = {
|
||||
name = "ca.kaareskovgaard.net";
|
||||
};
|
||||
khscodes.hcloud.enable = true;
|
||||
khscodes.hcloud.server.compute = {
|
||||
inherit (cfg) server_type datacenter;
|
||||
inherit labels;
|
||||
name = fqdn;
|
||||
initial_image = "debian-12";
|
||||
rdns = fqdn;
|
||||
ssh_keys = [ config.khscodes.hcloud.output.data.ssh_key.khs.id ];
|
||||
};
|
||||
khscodes.cloudflare = {
|
||||
khscodes.hcloud.data.ssh_key.khs = {
|
||||
name = "ca.kaareskovgaard.net";
|
||||
};
|
||||
khscodes.hcloud.enable = true;
|
||||
khscodes.hcloud.server.compute = {
|
||||
inherit (cfg) server_type datacenter;
|
||||
inherit labels;
|
||||
name = fqdn;
|
||||
initial_image = "debian-12";
|
||||
rdns = fqdn;
|
||||
ssh_keys = [ config.khscodes.hcloud.output.data.ssh_key.khs.id ];
|
||||
};
|
||||
khscodes.cloudflare = {
|
||||
enable = true;
|
||||
dns = {
|
||||
enable = true;
|
||||
dns = {
|
||||
enable = true;
|
||||
zone_name = tldFromFqdn fqdn;
|
||||
aRecords = [
|
||||
{
|
||||
inherit fqdn;
|
||||
content = config.khscodes.hcloud.output.server.compute.ipv4_address;
|
||||
}
|
||||
];
|
||||
aaaaRecords = [
|
||||
{
|
||||
inherit fqdn;
|
||||
content = config.khscodes.hcloud.output.server.compute.ipv6_address;
|
||||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
resource.hcloud_firewall.fw = lib.mkIf firewallEnable {
|
||||
inherit labels;
|
||||
name = fqdn;
|
||||
apply_to = {
|
||||
server = config.khscodes.hcloud.output.server.compute.id;
|
||||
};
|
||||
rule = firewallRules;
|
||||
};
|
||||
output.ipv4_address = {
|
||||
value = config.khscodes.hcloud.output.server.compute.ipv4_address;
|
||||
sensitive = false;
|
||||
};
|
||||
|
||||
output.ipv6_address = {
|
||||
value = config.khscodes.hcloud.output.server.compute.ipv6_address;
|
||||
sensitive = false;
|
||||
zone_name = tldFromFqdn fqdn;
|
||||
aRecords = [
|
||||
{
|
||||
inherit fqdn;
|
||||
content = config.khscodes.hcloud.output.server.compute.ipv4_address;
|
||||
}
|
||||
];
|
||||
aaaaRecords = [
|
||||
{
|
||||
inherit fqdn;
|
||||
content = config.khscodes.hcloud.output.server.compute.ipv6_address;
|
||||
}
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
||||
)
|
||||
];
|
||||
};
|
||||
resource.hcloud_firewall.fw = lib.mkIf firewallEnable {
|
||||
inherit labels;
|
||||
name = fqdn;
|
||||
apply_to = {
|
||||
server = config.khscodes.hcloud.output.server.compute.id;
|
||||
};
|
||||
rule = firewallRules;
|
||||
};
|
||||
output.ipv4_address = {
|
||||
value = config.khscodes.hcloud.output.server.compute.ipv4_address;
|
||||
sensitive = false;
|
||||
};
|
||||
|
||||
output.ipv6_address = {
|
||||
value = config.khscodes.hcloud.output.server.compute.ipv6_address;
|
||||
sensitive = false;
|
||||
};
|
||||
};
|
||||
}
|
||||
)
|
||||
];
|
||||
in
|
||||
{
|
||||
assertions = [
|
||||
{
|
||||
assertion = config.khscodes.fqdn != null;
|
||||
message = "Must set config.khscodes.fqdn when using terraform";
|
||||
message = "Must set config.khscodes.fqdn when using opentofu";
|
||||
}
|
||||
];
|
||||
|
||||
khscodes.terraform-hetzner.output = config;
|
||||
khscodes.provisioning.pre = {
|
||||
modules = modules;
|
||||
secretsSource = cfg.secretsSource;
|
||||
variablesNeeded = [
|
||||
"TF_VAR_cloudflare_token"
|
||||
"TF_VAR_cloudflare_email"
|
||||
"AWS_ACCESS_KEY_ID"
|
||||
"AWS_SECRET_ACCESS_KEY"
|
||||
"TF_VAR_hcloud_api_token"
|
||||
];
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
1
nix/modules/nixos/opentofu-openbao/default.nix
Normal file
1
nix/modules/nixos/opentofu-openbao/default.nix
Normal file
|
@ -0,0 +1 @@
|
|||
{ pkgs, ... }: { }
|
63
nix/modules/nixos/provisioning/default.nix
Normal file
63
nix/modules/nixos/provisioning/default.nix
Normal file
|
@ -0,0 +1,63 @@
|
|||
{
|
||||
config,
|
||||
lib,
|
||||
inputs,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
let
|
||||
cfg = config.khscodes.provisioning;
|
||||
provisioning = {
|
||||
modules = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.anything;
|
||||
description = "Modules used to bring up the needed resources";
|
||||
default = [ ];
|
||||
};
|
||||
secretsSource = lib.mkOption {
|
||||
type = lib.types.enum [
|
||||
"vault"
|
||||
"bitwarden"
|
||||
];
|
||||
description = "Where to get the secrets for the provisioning from";
|
||||
default = "vault";
|
||||
};
|
||||
variablesNeeded = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
description = "Needed environment variables for the provisioning";
|
||||
default = [ ];
|
||||
};
|
||||
};
|
||||
in
|
||||
{
|
||||
options.khscodes.provisioning = {
|
||||
pre = provisioning;
|
||||
post = provisioning;
|
||||
preConfig = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.path;
|
||||
description = "The generated config for the pre provisioning, if any was specified";
|
||||
};
|
||||
postConfig = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.path;
|
||||
description = "The generated config for the post provisioning, if any was specified";
|
||||
};
|
||||
};
|
||||
|
||||
config = {
|
||||
khscodes.provisioning.preConfig =
|
||||
if lib.lists.length cfg.pre.modules > 0 then
|
||||
inputs.terranix.lib.terranixConfiguration {
|
||||
system = pkgs.hostPlatform.system;
|
||||
modules = cfg.pre.modules;
|
||||
}
|
||||
else
|
||||
null;
|
||||
khscodes.provisioning.postConfig =
|
||||
if lib.lists.length cfg.post.modules > 0 then
|
||||
inputs.terranix.lib.terranixConfiguration {
|
||||
system = pkgs.hostPlatform.system;
|
||||
modules = cfg.post.modules;
|
||||
}
|
||||
else
|
||||
null;
|
||||
};
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
{ inputs, khscodesLib }:
|
||||
{ khscodesLib, ... }:
|
||||
{ config, lib, ... }:
|
||||
let
|
||||
cfg = config.khscodes.hcloud;
|
||||
|
@ -52,7 +52,7 @@ in
|
|||
{
|
||||
id = "\${ hcloud_server.${sanitizedName}.id }";
|
||||
ipv4_address = "\${ hcloud_server.${sanitizedName}.ipv4_address }";
|
||||
ipv6_address = "\${ hcloud_server.${sanitizedName}.ipv4_address }";
|
||||
ipv6_address = "\${ hcloud_server.${sanitizedName}.ipv6_address }";
|
||||
}
|
||||
)
|
||||
) cfg.server;
|
||||
|
|
32
nix/modules/terranix/openbao/default.nix
Normal file
32
nix/modules/terranix/openbao/default.nix
Normal file
|
@ -0,0 +1,32 @@
|
|||
{ khscodesLib, inputs }:
|
||||
{ lib, config, ... }:
|
||||
let
|
||||
cfg = config.khscodes.openbao;
|
||||
modules = [
|
||||
./output.nix
|
||||
./vault_mount.nix
|
||||
];
|
||||
in
|
||||
{
|
||||
options.khscodes.openbao = {
|
||||
enable = lib.mkEnableOption "Enables the openbao provider";
|
||||
};
|
||||
|
||||
imports = lib.lists.map (m: import m { inherit khscodesLib inputs; }) modules;
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
provider.vault = {
|
||||
address = "https://auth.kaareskovgaard.net";
|
||||
};
|
||||
terraform.required_providers.vault = {
|
||||
source = "hashicorp/vault";
|
||||
version = "5.0.0";
|
||||
};
|
||||
resource.vault_mount = lib.mapAttrs' (
|
||||
name: value: {
|
||||
name = khscodesLib.sanitize-terraform-name name;
|
||||
value = value;
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
10
nix/modules/terranix/openbao/output.nix
Normal file
10
nix/modules/terranix/openbao/output.nix
Normal file
|
@ -0,0 +1,10 @@
|
|||
{ khscodesLib, ... }:
|
||||
{ config, lib, ... }:
|
||||
let
|
||||
cfg = config.khscodes.openbao;
|
||||
in
|
||||
{
|
||||
options.khscodes.openbao = { };
|
||||
config = {
|
||||
};
|
||||
}
|
45
nix/modules/terranix/openbao/ssh_secret_backend_ca.nix
Normal file
45
nix/modules/terranix/openbao/ssh_secret_backend_ca.nix
Normal file
|
@ -0,0 +1,45 @@
|
|||
{ khscodesLib, ... }:
|
||||
{ lib, config, ... }:
|
||||
let
|
||||
cfg = config.khscodes.openbao;
|
||||
in
|
||||
{
|
||||
options.khscodes.openbao = {
|
||||
vault_ssh_secret_backend_ca = lib.mkOption {
|
||||
type = lib.types.attrsOf (
|
||||
khscodesLib.mkSubmodule {
|
||||
options = {
|
||||
backend = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
description = "Path of the backend mount";
|
||||
};
|
||||
generate_signing_key = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
description = "Generate a signing key on the server";
|
||||
};
|
||||
key_type = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
description = "The type of the signing key to use/generate";
|
||||
};
|
||||
};
|
||||
description = "vault_ssh_secret_backend_ca";
|
||||
}
|
||||
);
|
||||
};
|
||||
};
|
||||
config = lib.mkIf cfg.enable {
|
||||
provider.vault = {
|
||||
address = "https://auth.kaareskovgaard.net";
|
||||
};
|
||||
terraform.required_providers.vault = {
|
||||
source = "hashicorp/vault";
|
||||
version = "5.0.0";
|
||||
};
|
||||
resource.vault_ssh_secret_backend_ca = lib.mapAttrs' (
|
||||
name: value: {
|
||||
name = khscodesLib.sanitize-terraform-name name;
|
||||
value = value;
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
52
nix/modules/terranix/openbao/vault_mount.nix
Normal file
52
nix/modules/terranix/openbao/vault_mount.nix
Normal file
|
@ -0,0 +1,52 @@
|
|||
{ khscodesLib, ... }:
|
||||
{ lib, config, ... }:
|
||||
let
|
||||
cfg = config.khscodes.openbao;
|
||||
in
|
||||
{
|
||||
options.khscodes.openbao = {
|
||||
vault_mount = lib.mkOption {
|
||||
type = lib.types.attrsOf (
|
||||
khscodesLib.mkSubmodule {
|
||||
options = {
|
||||
type = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
description = "Type of mount";
|
||||
};
|
||||
path = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
description = "Path of the mount";
|
||||
default = null;
|
||||
};
|
||||
default_lease_ttl_seconds = lib.mkOption {
|
||||
type = lib.types.int;
|
||||
description = "Default lease ttl in seconds";
|
||||
default = null;
|
||||
};
|
||||
max_lease_ttl_seconds = lib.mkOption {
|
||||
type = lib.types.int;
|
||||
description = "Max lease ttl in seconds";
|
||||
default = null;
|
||||
};
|
||||
};
|
||||
description = "vault_mount";
|
||||
}
|
||||
);
|
||||
};
|
||||
};
|
||||
config = lib.mkIf cfg.enable {
|
||||
provider.vault = {
|
||||
address = "https://auth.kaareskovgaard.net";
|
||||
};
|
||||
terraform.required_providers.vault = {
|
||||
source = "hashicorp/vault";
|
||||
version = "5.0.0";
|
||||
};
|
||||
resource.vault_mount = lib.mapAttrs' (
|
||||
name: value: {
|
||||
name = khscodesLib.sanitize-terraform-name name;
|
||||
value = value;
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
|
@ -1,51 +1,52 @@
|
|||
{ pkgs, lib, ... }:
|
||||
let
|
||||
opentofu = pkgs.opentofu;
|
||||
bw-opentofu = lib.khscodes.mkBwEnv {
|
||||
inherit pkgs;
|
||||
name = "bw-opentofu";
|
||||
items = {
|
||||
"KHS Openstack" = {
|
||||
TF_VAR_openstack_username = "login.username";
|
||||
TF_VAR_openstack_password = "login.password";
|
||||
TF_VAR_openstack_tenant_name = "Project Name";
|
||||
TF_VAR_openstack_auth_url = "Auth URL";
|
||||
TF_VAR_openstack_endpoint_type = "Interface";
|
||||
TF_VAR_openstack_region = "Region Name";
|
||||
};
|
||||
"Cloudflare" = {
|
||||
TF_VAR_cloudflare_token = "DNS API Token";
|
||||
TF_VAR_cloudflare_email = "login.username";
|
||||
AWS_ACCESS_KEY_ID = "BW Terraform access key id";
|
||||
AWS_SECRET_ACCESS_KEY = "BW Terraform secret access key";
|
||||
};
|
||||
"Hetzner Cloud" = {
|
||||
TF_VAR_hcloud_api_token = "Terraform API Token";
|
||||
};
|
||||
# TODO: We should figure out a way of passing the secrets map at runtime instead of build time.
|
||||
# for now this map just needs to include every secret we could need, which also makes the reading of secrets take way longer than
|
||||
# needed.
|
||||
secrets = {
|
||||
"KHS Openstack" = {
|
||||
TF_VAR_openstack_username = "login.username";
|
||||
TF_VAR_openstack_password = "login.password";
|
||||
TF_VAR_openstack_tenant_name = "Project Name";
|
||||
TF_VAR_openstack_auth_url = "Auth URL";
|
||||
TF_VAR_openstack_endpoint_type = "Interface";
|
||||
TF_VAR_openstack_region = "Region Name";
|
||||
};
|
||||
exe = lib.getExe opentofu;
|
||||
"Cloudflare" = {
|
||||
TF_VAR_cloudflare_token = "DNS API Token";
|
||||
TF_VAR_cloudflare_email = "login.username";
|
||||
AWS_ACCESS_KEY_ID = "BW Terraform access key id";
|
||||
AWS_SECRET_ACCESS_KEY = "BW Terraform secret access key";
|
||||
};
|
||||
"Hetzner Cloud" = {
|
||||
TF_VAR_hcloud_api_token = "Terraform API Token";
|
||||
};
|
||||
};
|
||||
wrappedScript = pkgs.writeShellApplication {
|
||||
name = "bw-opentofu-wrapped";
|
||||
runtimeInputs = [
|
||||
pkgs.uutils-coreutils-noprefix
|
||||
pkgs.bitwarden-cli
|
||||
pkgs.khscodes.find-flake-root
|
||||
opentofu
|
||||
];
|
||||
text = ''
|
||||
fqdn="$1"
|
||||
config="$2"
|
||||
phase="$3"
|
||||
flakeRoot="$(find-flake-root)"
|
||||
dir="$flakeRoot/.terraform-cache/$fqdn/$phase"
|
||||
mkdir -p "$dir"
|
||||
cat "''${config}" > "$dir/config.tf.json"
|
||||
tofu -chdir="$dir" init
|
||||
tofu -chdir="$dir" apply
|
||||
'';
|
||||
};
|
||||
in
|
||||
pkgs.writeShellApplication {
|
||||
lib.khscodes.mkBwEnv {
|
||||
inherit pkgs;
|
||||
name = "bw-opentofu";
|
||||
runtimeInputs = [
|
||||
bw-opentofu
|
||||
pkgs.uutils-coreutils-noprefix
|
||||
pkgs.bitwarden-cli
|
||||
];
|
||||
text = ''
|
||||
fqdn="$1"
|
||||
config="$2"
|
||||
lockHcl="$3"
|
||||
dir="$(mktemp -d --tmpdir -t "terraform-hetzher-''${fqdn}.XXXXXXXXXX")"
|
||||
cp "$lockHcl" "$dir/.terraform.lock.hcl"
|
||||
cp "''${config}" "$dir/config.tf.json"
|
||||
if [ "''${BW_SESSION:-}" == "" ]; then
|
||||
BW_SESSION="$(bw unlock --raw)"
|
||||
export BW_SESSION
|
||||
trap "bw lock" EXIT
|
||||
fi
|
||||
bw-opentofu -chdir="$dir" init
|
||||
bw-opentofu -chdir="$dir" apply
|
||||
'';
|
||||
items = secrets;
|
||||
exe = lib.getExe wrappedScript;
|
||||
}
|
||||
|
|
16
nix/packages/find-flake-root/default.nix
Normal file
16
nix/packages/find-flake-root/default.nix
Normal file
|
@ -0,0 +1,16 @@
|
|||
{ pkgs, ... }:
|
||||
pkgs.writeShellApplication {
|
||||
name = "find-flake-root";
|
||||
runtimeInputs = [ pkgs.uutils-coreutils-noprefix ];
|
||||
text = ''
|
||||
while [[ ! -f "$(pwd)/flake.nix" ]]; do
|
||||
if [[ "$(pwd)" == "/" ]]; then
|
||||
echo "Could not find flake root" 1>&2
|
||||
exit 1
|
||||
fi
|
||||
cd ..
|
||||
done
|
||||
pwd
|
||||
exit 0
|
||||
'';
|
||||
}
|
6
nix/packages/hetzner-ipv6/default.nix
Normal file
6
nix/packages/hetzner-ipv6/default.nix
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
lib,
|
||||
pkgs,
|
||||
inputs,
|
||||
}:
|
||||
(lib.khscodes.mkRust pkgs "${inputs.self}/rust").buildRustPackage "hetzner-ipv6"
|
|
@ -1,16 +0,0 @@
|
|||
{
|
||||
inputs,
|
||||
pkgs,
|
||||
}:
|
||||
pkgs.writeShellApplication {
|
||||
name = "opentofu-hetzner";
|
||||
runtimeInputs = [
|
||||
pkgs.nix
|
||||
pkgs.khscodes.bw-opentofu
|
||||
];
|
||||
text = ''
|
||||
hostname="$1"
|
||||
config="$(nix build --no-link --print-out-paths '${inputs.self}#nixosConfigurations."'"$hostname"'".config.khscodes.terraform-hetzner.output')"
|
||||
bw-opentofu "$hostname" "$config" "${./terraform.lock.hcl}"
|
||||
'';
|
||||
}
|
27
nix/packages/pre-provisioning/default.nix
Normal file
27
nix/packages/pre-provisioning/default.nix
Normal file
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
inputs,
|
||||
pkgs,
|
||||
}:
|
||||
pkgs.writeShellApplication {
|
||||
name = "pre-provisioning";
|
||||
runtimeInputs = [
|
||||
pkgs.nix
|
||||
pkgs.khscodes.bw-opentofu
|
||||
];
|
||||
# TODO: Use secret source and required secrets to set up the correct env variables
|
||||
text = ''
|
||||
hostname="$1"
|
||||
baseAttr='${inputs.self}#nixosConfigurations."'"$hostname"'".config.khscodes.provisioning'
|
||||
config="$(nix eval --raw "''${baseAttr}.preConfig")"
|
||||
secretsSource="$(nix eval --raw "''${baseAttr}.pre.secretsSource")"
|
||||
if [[ "$config" == "null" ]]; then
|
||||
echo "No preprovisioning needed"
|
||||
exit 0
|
||||
fi
|
||||
if [[ "$secretsSource" == "vault" ]]; then
|
||||
>&2 echo "Provisioning using vault is not yet implemented"
|
||||
exit 1
|
||||
fi
|
||||
bw-opentofu "$hostname" "$config" "pre"
|
||||
'';
|
||||
}
|
12
nix/shells/default/default.nix
Normal file
12
nix/shells/default/default.nix
Normal file
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
lib,
|
||||
pkgs,
|
||||
inputs,
|
||||
mkShell,
|
||||
}:
|
||||
mkShell {
|
||||
packages = [
|
||||
pkgs.nixd
|
||||
pkgs.nixfmt-rfc-style
|
||||
] ++ (lib.khscodes.mkRust pkgs "${inputs.self}/rust").devDeps;
|
||||
}
|
|
@ -4,7 +4,7 @@
|
|||
}:
|
||||
{
|
||||
imports = [ "${inputs.self}/nix/profiles/hetzner-server.nix" ];
|
||||
khscodes.terraform-hetzner = {
|
||||
khscodes.hetzner-instance = {
|
||||
enable = true;
|
||||
mapRdns = true;
|
||||
server_type = "cax11";
|
27
rust/.config/hakari.toml
Normal file
27
rust/.config/hakari.toml
Normal file
|
@ -0,0 +1,27 @@
|
|||
# This file contains settings for `cargo hakari`.
|
||||
# See https://docs.rs/cargo-hakari/latest/cargo_hakari/config for a full list of options.
|
||||
|
||||
hakari-package = "hakari"
|
||||
|
||||
# Format version for hakari's output. Version 4 requires cargo-hakari 0.9.22 or above.
|
||||
dep-format-version = "4"
|
||||
|
||||
# Setting workspace.resolver = "2" or higher in the root Cargo.toml is HIGHLY recommended.
|
||||
# Hakari works much better with the v2 resolver. (The v2 and v3 resolvers are identical from
|
||||
# hakari's perspective, so you're welcome to set either.)
|
||||
#
|
||||
# For more about the new feature resolver, see:
|
||||
# https://blog.rust-lang.org/2021/03/25/Rust-1.51.0.html#cargos-new-feature-resolver
|
||||
resolver = "2"
|
||||
|
||||
# Add triples corresponding to platforms commonly used by developers here.
|
||||
# https://doc.rust-lang.org/rustc/platform-support.html
|
||||
platforms = [
|
||||
# "x86_64-unknown-linux-gnu",
|
||||
# "x86_64-apple-darwin",
|
||||
# "aarch64-apple-darwin",
|
||||
# "x86_64-pc-windows-msvc",
|
||||
]
|
||||
|
||||
# Write out exact versions rather than a semver range. (Defaults to false.)
|
||||
exact-versions = true
|
513
rust/Cargo.lock
generated
Normal file
513
rust/Cargo.lock
generated
Normal file
|
@ -0,0 +1,513 @@
|
|||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "0.6.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"anstyle-parse",
|
||||
"anstyle-query",
|
||||
"anstyle-wincon",
|
||||
"colorchoice",
|
||||
"is_terminal_polyfill",
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd"
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-parse"
|
||||
version = "0.2.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
|
||||
dependencies = [
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-query"
|
||||
version = "1.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-wincon"
|
||||
version = "3.0.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"once_cell_polyfill",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.98"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.22.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "40b6887a1d8685cebccf115538db5c0efe625ccac9696ad45c409d96566e910f"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e0c66c08ce9f0c698cbce5c0279d0bb6ac936d8674174fe48f736533b964f59e"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"clap_lex",
|
||||
"strsim",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.5.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2c7947ae4cc3d851207c1adb5b5e260ff0cca11446b1d6d1423788e442257ce"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "0.7.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675"
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
|
||||
|
||||
[[package]]
|
||||
name = "common"
|
||||
version = "1.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64",
|
||||
"env_logger",
|
||||
"hakari",
|
||||
"log",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_repr",
|
||||
"serde_yml",
|
||||
"shell-quote",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "env_filter"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0"
|
||||
dependencies = [
|
||||
"log",
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "env_logger"
|
||||
version = "0.11.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"env_filter",
|
||||
"jiff",
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "hakari"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "hetzner-ipv6"
|
||||
version = "1.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
"common",
|
||||
"hakari",
|
||||
"log",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
|
||||
|
||||
[[package]]
|
||||
name = "jiff"
|
||||
version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49"
|
||||
dependencies = [
|
||||
"jiff-static",
|
||||
"log",
|
||||
"portable-atomic",
|
||||
"portable-atomic-util",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jiff-static"
|
||||
version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libyml"
|
||||
version = "0.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3302702afa434ffa30847a83305f0a69d6abd74293b6554c18ec85c7ef30c980"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.7.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell_polyfill"
|
||||
version = "1.70.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad"
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic"
|
||||
version = "1.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483"
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic-util"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507"
|
||||
dependencies = [
|
||||
"portable-atomic",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.95"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-automata",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.219"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.219"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.140"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"ryu",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_repr"
|
||||
version = "0.1.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_yml"
|
||||
version = "0.0.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "59e2dd588bf1597a252c3b920e0143eb99b0f76e4e082f4c92ce34fbc9e71ddd"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"itoa",
|
||||
"libyml",
|
||||
"memchr",
|
||||
"ryu",
|
||||
"serde",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shell-quote"
|
||||
version = "0.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fb502615975ae2365825521fa1529ca7648fd03ce0b0746604e0683856ecd7e4"
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.104"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.59.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_gnullvm",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
32
rust/Cargo.toml
Normal file
32
rust/Cargo.toml
Normal file
|
@ -0,0 +1,32 @@
|
|||
[workspace]
|
||||
resolver = "3"
|
||||
members = ["lib/*", "program/*"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.1.0"
|
||||
|
||||
[workspace.metadata.crane]
|
||||
name = "nix-machines"
|
||||
|
||||
[workspace.dependencies]
|
||||
anyhow = { version = "1.0.98", default-features = false, features = ["std"] }
|
||||
base64 = { version = "0.22.1", default-features = false, features = ["std"] }
|
||||
clap = { version = "4.5.39", default-features = false, features = [
|
||||
"color",
|
||||
"error-context",
|
||||
"help",
|
||||
"std",
|
||||
"suggestions",
|
||||
"usage",
|
||||
"derive",
|
||||
] }
|
||||
log = { version = "0.4.27", default-features = false, features = ["std"] }
|
||||
serde = { version = "1.0.219", default-features = false, features = [
|
||||
"derive",
|
||||
"std",
|
||||
] }
|
||||
serde_json = { version = "1.0.140", default-features = false, features = [
|
||||
"std",
|
||||
] }
|
||||
serde_repr = { version = "0.1.20", default-features = false }
|
||||
serde_yml = { version = "0.0.12", default-features = false }
|
94
rust/default.nix
Normal file
94
rust/default.nix
Normal file
|
@ -0,0 +1,94 @@
|
|||
{
|
||||
pkgs,
|
||||
lib,
|
||||
crane,
|
||||
advisory-db,
|
||||
}:
|
||||
let
|
||||
rustToolchain = pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml;
|
||||
craneLib = (crane.mkLib pkgs).overrideToolchain rustToolchain;
|
||||
src = craneLib.cleanCargoSource ./.;
|
||||
commonArgs = {
|
||||
inherit src;
|
||||
strictDeps = true;
|
||||
buildInputs = [ ];
|
||||
};
|
||||
cargoArtifacts = craneLib.buildDepsOnly commonArgs;
|
||||
individualCrateArgs = commonArgs // {
|
||||
inherit cargoArtifacts;
|
||||
inherit (craneLib.crateNameFromCargoToml { inherit src; }) version;
|
||||
doCheck = false;
|
||||
};
|
||||
fileSetForCrate =
|
||||
crate:
|
||||
lib.fileset.toSource {
|
||||
root = ./.;
|
||||
fileset = lib.fileset.unions [
|
||||
./Cargo.lock
|
||||
./Cargo.toml
|
||||
(craneLib.fileset.commonCargoSources ./lib/common)
|
||||
(craneLib.fileset.commonCargoSources ./program/${crate})
|
||||
];
|
||||
};
|
||||
in
|
||||
{
|
||||
buildRustPackage =
|
||||
crateName:
|
||||
craneLib.buildPackage (
|
||||
individualCrateArgs
|
||||
// {
|
||||
pname = crateName;
|
||||
cargoExtraArgs = "-p ${crateName}";
|
||||
src = fileSetForCrate crateName;
|
||||
}
|
||||
);
|
||||
checks = {
|
||||
rust-clippy = craneLib.cargoClippy (
|
||||
commonArgs
|
||||
// {
|
||||
inherit cargoArtifacts;
|
||||
cargoClippyExtraArgs = "--all-targets -- --deny warnings";
|
||||
}
|
||||
);
|
||||
rust-doc = craneLib.cargoDoc (
|
||||
commonArgs
|
||||
// {
|
||||
inherit cargoArtifacts;
|
||||
}
|
||||
);
|
||||
rust-fmt = craneLib.cargoFmt (
|
||||
commonArgs
|
||||
// {
|
||||
inherit cargoArtifacts;
|
||||
}
|
||||
);
|
||||
# Not used currently, as I have some other formatter changing toml formatting
|
||||
# not sure where it comes from, but I need to find out and decide on a formatter
|
||||
rust-toml-fmt = craneLib.taploFmt {
|
||||
src = lib.sources.sourceFilesBySuffices src [ ".toml" ];
|
||||
};
|
||||
rust-audit = craneLib.cargoAudit {
|
||||
inherit src advisory-db;
|
||||
};
|
||||
rust-hakari = craneLib.mkCargoDerivation {
|
||||
inherit src;
|
||||
pname = "rust-hakari";
|
||||
cargoArtifacts = null;
|
||||
doInstallCargoArtifacts = false;
|
||||
|
||||
buildPhaseCargoCommand = ''
|
||||
cargo hakari generate --diff # workspace-hack Cargo.toml is up-to-date
|
||||
cargo hakari manage-deps --dry-run # all workspace crates depend on workspace-hack
|
||||
cargo hakari verify
|
||||
'';
|
||||
|
||||
nativeBuildInputs = [
|
||||
pkgs.cargo-hakari
|
||||
];
|
||||
};
|
||||
};
|
||||
devDeps = [
|
||||
pkgs.cargo-hakari
|
||||
rustToolchain
|
||||
];
|
||||
}
|
18
rust/lib/common/Cargo.toml
Normal file
18
rust/lib/common/Cargo.toml
Normal file
|
@ -0,0 +1,18 @@
|
|||
[package]
|
||||
name = "common"
|
||||
edition = "2024"
|
||||
version = "1.0.0"
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
base64 = { workspace = true }
|
||||
env_logger = "0.11.8"
|
||||
log = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
serde_repr.workspace = true
|
||||
serde_yml = { workspace = true }
|
||||
shell-quote = { version = "0.7.2", default-features = false, features = [
|
||||
"bash",
|
||||
] }
|
||||
hakari = { version = "0.1", path = "../hakari" }
|
5
rust/lib/common/src/base64.rs
Normal file
5
rust/lib/common/src/base64.rs
Normal file
|
@ -0,0 +1,5 @@
|
|||
use base64::Engine;
|
||||
|
||||
pub fn encode(bytes: &[u8]) -> String {
|
||||
base64::prelude::BASE64_STANDARD.encode(bytes)
|
||||
}
|
358
rust/lib/common/src/bitwarden.rs
Normal file
358
rust/lib/common/src/bitwarden.rs
Normal file
|
@ -0,0 +1,358 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::proc;
|
||||
|
||||
mod entry_serde;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct BitwardenEntry {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
#[serde(rename = "organizationId")]
|
||||
pub organization_id: Option<String>,
|
||||
#[serde(rename = "folderId")]
|
||||
pub folder_id: Option<String>,
|
||||
#[serde(rename = "collectionIds")]
|
||||
pub collection_ids: Vec<String>,
|
||||
|
||||
pub fields: Option<Vec<BitwardenEntryField>>,
|
||||
|
||||
pub notes: Option<String>,
|
||||
|
||||
#[serde(flatten)]
|
||||
pub data: BitwardenEntryTypeData,
|
||||
}
|
||||
|
||||
impl BitwardenEntry {
|
||||
pub fn id(&self) -> &str {
|
||||
&self.id
|
||||
}
|
||||
pub fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
}
|
||||
|
||||
impl BitwardenEntry {
|
||||
pub fn into_command_entry(&self) -> CommandBitwardenEntry {
|
||||
CommandBitwardenEntry {
|
||||
name: self.name.clone(),
|
||||
organization_id: self.organization_id.clone(),
|
||||
folder_id: self.folder_id.clone(),
|
||||
collection_ids: self.collection_ids.clone(),
|
||||
fields: self.fields.clone(),
|
||||
notes: self.notes.clone(),
|
||||
data: self.data.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Clone, PartialEq)]
|
||||
pub struct CommandBitwardenEntry {
|
||||
pub name: String,
|
||||
#[serde(rename = "organizationId")]
|
||||
pub organization_id: Option<String>,
|
||||
#[serde(rename = "folderId")]
|
||||
pub folder_id: Option<String>,
|
||||
#[serde(rename = "collectionIds")]
|
||||
pub collection_ids: Vec<String>,
|
||||
|
||||
pub fields: Option<Vec<BitwardenEntryField>>,
|
||||
|
||||
pub notes: Option<String>,
|
||||
|
||||
#[serde(flatten)]
|
||||
pub data: BitwardenEntryTypeData,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum BitwardenEntryTypeData {
|
||||
Login(BitwardenEntryTypeLogin),
|
||||
SecureNote(BitwardenEntryTypeSecureNote),
|
||||
Card(BitwardenEntryTypeCard),
|
||||
Identity(BitwardenEntryTypeIdentity),
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
|
||||
pub struct BitwardenEntryTypeLogin {
|
||||
pub username: Option<String>,
|
||||
pub password: Option<String>,
|
||||
pub totp: Option<String>,
|
||||
pub uris: Option<Vec<BitwardenEntryLoginUri>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
|
||||
pub struct BitwardenEntryLoginUri {
|
||||
pub uri: Option<String>,
|
||||
#[serde(rename = "match")]
|
||||
pub match_type: Option<BitwardenEntryUriMatchType>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
|
||||
pub struct BitwardenEntryTypeSecureNote {}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
|
||||
pub struct BitwardenEntryTypeCard {}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
|
||||
pub struct BitwardenEntryTypeIdentity {}
|
||||
|
||||
#[derive(
|
||||
Debug, serde_repr::Serialize_repr, serde_repr::Deserialize_repr, Clone, Copy, PartialEq,
|
||||
)]
|
||||
#[repr(u8)]
|
||||
pub enum BitwardenEntryFieldType {
|
||||
Text = 0,
|
||||
Hidden = 1,
|
||||
Boolean = 2,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
|
||||
pub struct BitwardenEntryField {
|
||||
pub name: String,
|
||||
pub value: Option<String>,
|
||||
#[serde(rename = "type")]
|
||||
pub field_type: BitwardenEntryFieldType,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde_repr::Serialize_repr, serde_repr::Deserialize_repr)]
|
||||
#[repr(u8)]
|
||||
pub enum BitwardenTwoStepLoginMethod {
|
||||
Authenticator = 0,
|
||||
Email = 1,
|
||||
YubiKey = 3,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde_repr::Serialize_repr, serde_repr::Deserialize_repr, Clone, PartialEq)]
|
||||
#[repr(u8)]
|
||||
pub enum BitwardenEntryUriMatchType {
|
||||
Domain = 0,
|
||||
Host = 1,
|
||||
StartsWith = 2,
|
||||
Exact = 3,
|
||||
RegularExpression = 4,
|
||||
Never = 5,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde_repr::Serialize_repr, serde_repr::Deserialize_repr)]
|
||||
#[repr(u8)]
|
||||
pub enum BitwardenOrganizationUserType {
|
||||
Owner = 0,
|
||||
Admin = 1,
|
||||
User = 2,
|
||||
Manager = 3,
|
||||
Custom = 4,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde_repr::Serialize_repr, serde_repr::Deserialize_repr)]
|
||||
#[repr(i8)]
|
||||
pub enum BitwardenOrganizationUserStatus {
|
||||
Invited = 0,
|
||||
Accepted = 1,
|
||||
Confirmed = 2,
|
||||
Revoked = -1,
|
||||
}
|
||||
|
||||
pub struct BitwardenSession {
|
||||
session_id: Option<String>,
|
||||
}
|
||||
|
||||
impl BitwardenSession {
|
||||
fn bw_command(&self) -> proc::Command {
|
||||
let mut cmd = proc::Command::new("bw");
|
||||
if let Some(id) = self.session_id.as_deref() {
|
||||
cmd.env_sensitive("BW_SESSION", id);
|
||||
}
|
||||
cmd
|
||||
}
|
||||
|
||||
pub fn sync(&self) -> anyhow::Result<()> {
|
||||
log::info!("Syncing Bitwarden...");
|
||||
let _ = self.bw_command().arg("sync").try_spawn_to_string()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn new_or_authenticate(
|
||||
username: Option<&str>,
|
||||
bw_unlock_purpose: &str,
|
||||
) -> anyhow::Result<Self> {
|
||||
let bw = BitwardenSession::new_if_authenticated(username, bw_unlock_purpose, true)?;
|
||||
if let Some(bw) = bw {
|
||||
bw.sync()?;
|
||||
Ok(bw)
|
||||
} else {
|
||||
log::info!("User is not logged in to bitwarden, initiating login...");
|
||||
proc::Command::new("bw")
|
||||
.arg("login")
|
||||
.stdin_inherit()
|
||||
.stderr_inherit()
|
||||
.try_spawn_to_string()?;
|
||||
// Just logged in, no point in syncing
|
||||
let bw = BitwardenSession::new_if_authenticated(username, bw_unlock_purpose, false)?;
|
||||
if let Some(bw) = bw {
|
||||
Ok(bw)
|
||||
} else {
|
||||
Err(anyhow::format_err!(
|
||||
"Still not logged in to bitwarden, exiting..."
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn new_if_authenticated(
|
||||
username: Option<&str>,
|
||||
bw_unlock_purpose: &str,
|
||||
sync: bool,
|
||||
) -> anyhow::Result<Option<Self>> {
|
||||
let status: BitwardenAuthenticationStatus = proc::Command::new("bw")
|
||||
.args(["--nointeraction", "status"])
|
||||
.try_spawn_to_json()?;
|
||||
|
||||
let Some(user) = status.user() else {
|
||||
return Ok(None);
|
||||
};
|
||||
if let Some(username) = username {
|
||||
if user.user_email != username {
|
||||
return Err(anyhow::format_err!(
|
||||
"Authenticated user in bitwarden does not match the expected user of {}, was {}",
|
||||
username,
|
||||
user.user_email
|
||||
));
|
||||
}
|
||||
}
|
||||
let is_unlocked: bool = matches!(status, BitwardenAuthenticationStatus::Unlocked(_));
|
||||
if sync && !is_unlocked {
|
||||
log::info!("Syncing Bitwarden...");
|
||||
let _ = proc::Command::new("bw").arg("sync").try_spawn_to_string()?;
|
||||
}
|
||||
log::info!("Unlocking bitwarden...");
|
||||
let session_id = if is_unlocked {
|
||||
None
|
||||
} else {
|
||||
Some(
|
||||
proc::Command::new("bitwarden-unlock")
|
||||
.args(["--purpose", bw_unlock_purpose])
|
||||
.try_spawn_to_string()?,
|
||||
)
|
||||
};
|
||||
Ok(Some(Self { session_id }))
|
||||
}
|
||||
|
||||
pub fn list_items(&self) -> anyhow::Result<Vec<BitwardenEntry>> {
|
||||
log::info!("Listing bitwarden items...");
|
||||
self.bw_command()
|
||||
// Pretty format for better error messages during json decoding issues
|
||||
.args(["--pretty", "list", "items"])
|
||||
.try_spawn_to_json()
|
||||
}
|
||||
|
||||
pub fn get_item(&self, name: &str) -> anyhow::Result<Option<BitwardenEntry>> {
|
||||
let mut items = self.list_items()?;
|
||||
let Some(idx) = items
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find_map(|(idx, e)| if e.name() == name { Some(idx) } else { None })
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
let item = items.swap_remove(idx);
|
||||
Ok(Some(item))
|
||||
}
|
||||
|
||||
pub fn get_attachment(&self, entry: &BitwardenEntry, name: &str) -> anyhow::Result<Vec<u8>> {
|
||||
self.bw_command()
|
||||
.args(["get", "attachment", name, "--itemid"])
|
||||
.arg(entry.id.as_str())
|
||||
.arg("--raw")
|
||||
.try_spawn_to_bytes()
|
||||
}
|
||||
|
||||
pub fn list_own_items(&self) -> anyhow::Result<Vec<BitwardenEntry>> {
|
||||
let mut items = self.list_items()?;
|
||||
items.retain(|i| i.organization_id.as_ref().is_none_or(|o| o.is_empty()));
|
||||
Ok(items)
|
||||
}
|
||||
|
||||
pub fn create_item(&self, item: &CommandBitwardenEntry) -> anyhow::Result<BitwardenEntry> {
|
||||
log::info!("Creating bitwarden entry {name}", name = item.name);
|
||||
self.bw_command()
|
||||
.args(["create", "item"])
|
||||
.stdin_json_base64(item)?
|
||||
.try_spawn_to_json()
|
||||
}
|
||||
|
||||
pub fn update_item(
|
||||
&self,
|
||||
to_update: &BitwardenEntry,
|
||||
update_with: &CommandBitwardenEntry,
|
||||
) -> anyhow::Result<()> {
|
||||
log::info!(
|
||||
"Updating bitwarden entry {name}, with id {id}",
|
||||
id = to_update.id,
|
||||
name = update_with.name
|
||||
);
|
||||
let _output = self
|
||||
.bw_command()
|
||||
.args(["edit", "item", &to_update.id])
|
||||
.stdin_json_base64(update_with)?
|
||||
.try_spawn_to_string()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn delete_item(&self, to_delete: &BitwardenEntry) -> anyhow::Result<()> {
|
||||
log::info!(
|
||||
"Deleting bitwarden entry {name}, with id: {id}",
|
||||
id = to_delete.id,
|
||||
name = to_delete.name
|
||||
);
|
||||
let _output = self
|
||||
.bw_command()
|
||||
.args(["delete", "item", &to_delete.id])
|
||||
.try_spawn_to_string()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for BitwardenSession {
|
||||
fn drop(&mut self) {
|
||||
log::info!("Locking bitwarden session...");
|
||||
if self.session_id.is_some() {
|
||||
if let Err(e) = self
|
||||
.bw_command()
|
||||
.args(["--nointeraction", "lock"])
|
||||
.try_spawn_to_string()
|
||||
{
|
||||
log::error!("Could not lock bitwarden session: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(tag = "status")]
|
||||
enum BitwardenAuthenticationStatus {
|
||||
#[serde(rename = "unlocked")]
|
||||
Unlocked(BitwardenAuthenticationUser),
|
||||
#[serde(rename = "locked")]
|
||||
Locked(BitwardenAuthenticationUser),
|
||||
#[serde(rename = "unauthenticated")]
|
||||
Unauthenticated,
|
||||
}
|
||||
|
||||
impl BitwardenAuthenticationStatus {
|
||||
pub fn user(&self) -> Option<&BitwardenAuthenticationUser> {
|
||||
match self {
|
||||
Self::Locked(user) | Self::Unlocked(user) => Some(user),
|
||||
Self::Unauthenticated => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct BitwardenAuthenticationUser {
|
||||
#[serde(rename = "userEmail")]
|
||||
user_email: String,
|
||||
|
||||
#[serde(rename = "userId")]
|
||||
#[allow(dead_code)]
|
||||
user_id: String,
|
||||
}
|
162
rust/lib/common/src/bitwarden/entry_serde.rs
Normal file
162
rust/lib/common/src/bitwarden/entry_serde.rs
Normal file
|
@ -0,0 +1,162 @@
|
|||
use serde::{Deserialize, Serialize, ser::SerializeMap};
|
||||
|
||||
use crate::bitwarden::{
|
||||
BitwardenEntryTypeCard, BitwardenEntryTypeData, BitwardenEntryTypeIdentity,
|
||||
BitwardenEntryTypeLogin, BitwardenEntryTypeSecureNote,
|
||||
};
|
||||
|
||||
impl Serialize for super::BitwardenEntryTypeData {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
let mut map = serializer.serialize_map(Some(2))?;
|
||||
match self {
|
||||
Self::Login(login) => {
|
||||
map.serialize_entry("type", &BitwardenEntryType::Login)?;
|
||||
map.serialize_entry("login", login)?;
|
||||
}
|
||||
Self::Card(card) => {
|
||||
map.serialize_entry("type", &BitwardenEntryType::Card)?;
|
||||
map.serialize_entry("card", &card)?;
|
||||
}
|
||||
Self::SecureNote(secure_note) => {
|
||||
map.serialize_entry("type", &BitwardenEntryType::SecureNote)?;
|
||||
map.serialize_entry("secureNote", &secure_note)?;
|
||||
}
|
||||
Self::Identity(identity) => {
|
||||
map.serialize_entry("type", &BitwardenEntryType::Identity)?;
|
||||
map.serialize_entry("identity", &identity)?;
|
||||
}
|
||||
}
|
||||
map.end()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for BitwardenEntryTypeData {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
deserializer.deserialize_struct(
|
||||
"BitwardenEntryTypeData",
|
||||
&["type", "login", "card", "secureNote", "identity"],
|
||||
DeserializeVisitor,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
struct DeserializeVisitor;
|
||||
|
||||
impl<'de> serde::de::Visitor<'de> for DeserializeVisitor {
|
||||
type Value = BitwardenEntryTypeData;
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
formatter.write_str("an object with type and tagged type property")
|
||||
}
|
||||
|
||||
fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
|
||||
where
|
||||
M: serde::de::MapAccess<'de>,
|
||||
{
|
||||
let mut entry_type: Option<BitwardenEntryType> = None;
|
||||
let mut login_data: Option<BitwardenEntryTypeLogin> = None;
|
||||
let mut secure_note_data: Option<BitwardenEntryTypeSecureNote> = None;
|
||||
let mut card_data: Option<BitwardenEntryTypeCard> = None;
|
||||
let mut identity_data: Option<BitwardenEntryTypeIdentity> = None;
|
||||
while let Some(key) = map.next_key::<&str>()? {
|
||||
match key {
|
||||
"type" => {
|
||||
if entry_type.is_some() {
|
||||
return Err(serde::de::Error::duplicate_field("type"));
|
||||
}
|
||||
entry_type = Some(map.next_value()?);
|
||||
}
|
||||
"login" => {
|
||||
if login_data.is_some() {
|
||||
return Err(serde::de::Error::duplicate_field("login"));
|
||||
}
|
||||
login_data = Some(map.next_value()?);
|
||||
}
|
||||
"card" => {
|
||||
if card_data.is_some() {
|
||||
return Err(serde::de::Error::duplicate_field("card"));
|
||||
}
|
||||
card_data = Some(map.next_value()?);
|
||||
}
|
||||
"identity" => {
|
||||
if identity_data.is_some() {
|
||||
return Err(serde::de::Error::duplicate_field("identity"));
|
||||
}
|
||||
identity_data = Some(map.next_value()?);
|
||||
}
|
||||
"secureNote" => {
|
||||
if secure_note_data.is_some() {
|
||||
return Err(serde::de::Error::duplicate_field("secureNote"));
|
||||
}
|
||||
secure_note_data = Some(map.next_value()?);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
match entry_type {
|
||||
Some(BitwardenEntryType::Login) => {
|
||||
let login = login_data.ok_or(serde::de::Error::missing_field("login"))?;
|
||||
Ok(BitwardenEntryTypeData::Login(login))
|
||||
}
|
||||
Some(BitwardenEntryType::Card) => {
|
||||
let card = card_data.ok_or(serde::de::Error::missing_field("card"))?;
|
||||
Ok(BitwardenEntryTypeData::Card(card))
|
||||
}
|
||||
Some(BitwardenEntryType::SecureNote) => {
|
||||
let secure_note =
|
||||
secure_note_data.ok_or(serde::de::Error::missing_field("secure_note"))?;
|
||||
Ok(BitwardenEntryTypeData::SecureNote(secure_note))
|
||||
}
|
||||
Some(BitwardenEntryType::Identity) => {
|
||||
let identity = identity_data.ok_or(serde::de::Error::missing_field("identity"))?;
|
||||
Ok(BitwardenEntryTypeData::Identity(identity))
|
||||
}
|
||||
None => Err(serde::de::Error::missing_field("type")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, serde_repr::Serialize_repr, serde_repr::Deserialize_repr)]
|
||||
#[repr(u8)]
|
||||
enum BitwardenEntryType {
|
||||
Login = 1,
|
||||
SecureNote = 2,
|
||||
Card = 3,
|
||||
Identity = 4,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::json;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
pub fn test_simple_serializing() {
|
||||
let d = BitwardenEntryTypeData::Login(BitwardenEntryTypeLogin {
|
||||
username: None,
|
||||
password: None,
|
||||
totp: None,
|
||||
uris: None,
|
||||
});
|
||||
let json = json::to_string(&d).unwrap();
|
||||
assert_eq!(
|
||||
json,
|
||||
r#"{"type":1,"login":{"username":null,"password":null,"totp":null,"uris":null}}"#
|
||||
);
|
||||
match json::from_str(&json).unwrap() {
|
||||
BitwardenEntryTypeData::Login(BitwardenEntryTypeLogin {
|
||||
username: None,
|
||||
password: None,
|
||||
totp: None,
|
||||
uris: None,
|
||||
}) => {}
|
||||
_ => panic!("Could not deserialize json to itself"),
|
||||
}
|
||||
}
|
||||
}
|
13
rust/lib/common/src/curl.rs
Normal file
13
rust/lib/common/src/curl.rs
Normal file
|
@ -0,0 +1,13 @@
|
|||
use crate::proc::Command;
|
||||
|
||||
pub fn read_json_as_string(url: &str) -> anyhow::Result<String> {
|
||||
let mut cmd = Command::new("curl");
|
||||
cmd.args(["-H", "Accept: application/json", url]);
|
||||
cmd.try_spawn_to_string()
|
||||
}
|
||||
|
||||
pub fn read_text_as_string(url: &str) -> anyhow::Result<String> {
|
||||
let mut cmd = Command::new("curl");
|
||||
cmd.arg(url);
|
||||
cmd.try_spawn_to_string()
|
||||
}
|
23
rust/lib/common/src/env.rs
Normal file
23
rust/lib/common/src/env.rs
Normal file
|
@ -0,0 +1,23 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::Context;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::json;
|
||||
|
||||
pub fn read_env(var: &'static str) -> anyhow::Result<String> {
|
||||
log::trace!("read_env: {var}");
|
||||
std::env::var(var)
|
||||
.map_err(|e| anyhow::format_err!("Could not read {var} environment variable: {e}"))
|
||||
}
|
||||
|
||||
pub fn read_path_env(var: &'static str) -> anyhow::Result<PathBuf> {
|
||||
Ok(read_env(var)?.into())
|
||||
}
|
||||
|
||||
pub fn read_env_json<T: for<'de> Deserialize<'de>>(var: &'static str) -> anyhow::Result<T> {
|
||||
let json_text = read_env(var)?;
|
||||
json::from_str(&json_text).with_context(|| {
|
||||
format!("Could not parse contents of env var {var} into the correct json format")
|
||||
})
|
||||
}
|
156
rust/lib/common/src/fs.rs
Normal file
156
rust/lib/common/src/fs.rs
Normal file
|
@ -0,0 +1,156 @@
|
|||
use std::{
|
||||
fs::{Permissions, ReadDir},
|
||||
io::{ErrorKind, Write},
|
||||
path::Path,
|
||||
};
|
||||
|
||||
use anyhow::Context;
|
||||
|
||||
use crate::proc;
|
||||
|
||||
pub fn is_dir(p: &Path) -> anyhow::Result<bool> {
|
||||
log::trace!("is_dir: {}", p.display());
|
||||
Ok(match std::fs::metadata(p) {
|
||||
Ok(m) if m.is_dir() => true,
|
||||
Ok(_) => false,
|
||||
Err(e) if e.kind() == ErrorKind::NotFound => false,
|
||||
Err(e) => return Err(e).with_context(|| format!("Could not stat {p:?}")),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn is_file(p: &Path) -> anyhow::Result<bool> {
|
||||
log::trace!("is_file: {}", p.display());
|
||||
Ok(match std::fs::metadata(p) {
|
||||
Ok(m) if m.is_file() => true,
|
||||
Ok(_) => false,
|
||||
Err(e) if e.kind() == ErrorKind::NotFound => false,
|
||||
Err(e) => return Err(e).with_context(|| format!("Could not stat {p:?}")),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn is_file_with_permissions(p: &Path, permissions: Permissions) -> anyhow::Result<bool> {
|
||||
log::trace!("is_file_with_permissions: {}", p.display());
|
||||
Ok(match std::fs::metadata(p) {
|
||||
Ok(m) if m.is_file() => {
|
||||
log::trace!("Verifying {:?} == {:?}", m.permissions(), permissions);
|
||||
m.permissions() == permissions
|
||||
}
|
||||
Ok(_) => false,
|
||||
Err(e) if e.kind() == ErrorKind::NotFound => false,
|
||||
Err(e) => return Err(e).with_context(|| format!("Could not stat {p:?}")),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn create_dir(p: &Path) -> anyhow::Result<()> {
|
||||
log::info!("Creating directory : {}", p.display());
|
||||
std::fs::create_dir(p).with_context(|| format!("Could not create directory {p:?}"))
|
||||
}
|
||||
|
||||
pub fn write_file_string(p: &Path, contents: &str, permissions: Permissions) -> anyhow::Result<()> {
|
||||
log::info!("Writing contents to {}", p.display());
|
||||
let mut file = std::fs::File::create(p).with_context(|| {
|
||||
format!(
|
||||
"Could not create (or open existing) file at {}",
|
||||
p.display()
|
||||
)
|
||||
})?;
|
||||
file.set_permissions(permissions)
|
||||
.with_context(|| format!("Could not set permissions on file {}", p.display()))?;
|
||||
file.write_all(contents.as_bytes())
|
||||
.with_context(|| format!("Could not write to file {}", p.display()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn root_create_dir(p: &Path) -> anyhow::Result<()> {
|
||||
let mut cmd = proc::Command::new("mkdir");
|
||||
cmd.arg(p.display().to_string());
|
||||
|
||||
let _ = cmd.sudo().try_spawn_to_string()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn remove_file(path: &Path) -> anyhow::Result<()> {
|
||||
log::info!("Deleting file {}", path.display());
|
||||
std::fs::remove_file(path).with_context(|| format!("Could not delete file {}", path.display()))
|
||||
}
|
||||
|
||||
pub fn root_create_dir_recursive(p: &Path) -> anyhow::Result<()> {
|
||||
let mut cmd = proc::Command::new("mkdir");
|
||||
cmd.arg("-p");
|
||||
cmd.arg(p.display().to_string());
|
||||
|
||||
let _ = cmd.sudo().try_spawn_to_string()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn create_dir_recursive(p: &Path) -> anyhow::Result<()> {
|
||||
let mut cmd = proc::Command::new("mkdir");
|
||||
cmd.arg("-p");
|
||||
cmd.arg(p.display().to_string());
|
||||
|
||||
let _ = cmd.try_spawn_to_string()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn remove_dir_recursive(p: &Path) -> anyhow::Result<()> {
|
||||
std::fs::remove_dir_all(p)
|
||||
.with_context(|| format!("Could not remove directory {}", p.display()))
|
||||
}
|
||||
|
||||
pub fn list_dir(p: &Path) -> anyhow::Result<ReadDir> {
|
||||
std::fs::read_dir(p).with_context(|| format!("Could not read directory {}", p.display()))
|
||||
}
|
||||
|
||||
pub fn set_permissions(p: &Path, permissions: Permissions) -> anyhow::Result<()> {
|
||||
log::trace!("set_permissions: {}", p.display());
|
||||
std::fs::set_permissions(p, permissions.clone()).with_context(|| {
|
||||
format!(
|
||||
"Could not set permissions on {} to {permissions:?}",
|
||||
p.display()
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn metadata(p: &Path) -> anyhow::Result<std::fs::Metadata> {
|
||||
log::trace!("get_metadata: {}", p.display());
|
||||
std::fs::metadata(p).with_context(|| format!("Could not get metadata for {}", p.display()))
|
||||
}
|
||||
|
||||
#[cfg(target_family = "unix")]
|
||||
pub fn user_only_dir_permissions() -> Permissions {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
PermissionsExt::from_mode(0o040700)
|
||||
}
|
||||
|
||||
#[cfg(target_family = "unix")]
|
||||
pub fn user_only_file_permissions() -> Permissions {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
PermissionsExt::from_mode(0o100600)
|
||||
}
|
||||
|
||||
pub fn read_to_string(path: &Path) -> anyhow::Result<String> {
|
||||
log::trace!("read_file: {}", path.display());
|
||||
std::fs::read_to_string(path)
|
||||
.with_context(|| format!("Could not read file: {}", path.display()))
|
||||
}
|
||||
|
||||
#[cfg(target_family = "unix")]
|
||||
pub fn create_link(from: &Path, to: &Path) -> anyhow::Result<()> {
|
||||
std::os::unix::fs::symlink(to, from).with_context(|| {
|
||||
format!(
|
||||
"Could not create symbolic link from {from} to {to}",
|
||||
from = from.display(),
|
||||
to = to.display()
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn rename(from: &Path, to: &Path) -> anyhow::Result<()> {
|
||||
std::fs::rename(from, to)
|
||||
.with_context(|| format!("Could not rename {} to {}", from.display(), to.display()))
|
||||
}
|
69
rust/lib/common/src/json.rs
Normal file
69
rust/lib/common/src/json.rs
Normal file
|
@ -0,0 +1,69 @@
|
|||
use anyhow::Context;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub fn from_str<D: for<'de> Deserialize<'de>>(s: &str) -> anyhow::Result<D> {
|
||||
serde_json::from_str(s).map_err(|e| anyhow::format_err!("{e}:\n{}", extract_context(&e, s)))
|
||||
}
|
||||
|
||||
pub fn to_string<S: Serialize>(data: &S) -> anyhow::Result<String> {
|
||||
serde_json::to_string(data).context("Could not serialize data to json")
|
||||
}
|
||||
|
||||
pub fn to_string_pretty<S: Serialize>(data: &S) -> anyhow::Result<String> {
|
||||
serde_json::to_string_pretty(data).context("Could not serialize data to json")
|
||||
}
|
||||
|
||||
pub fn to_vec<S: Serialize>(data: &S) -> anyhow::Result<Vec<u8>> {
|
||||
serde_json::to_vec(data).context("Could not serialize data to json")
|
||||
}
|
||||
|
||||
pub fn to_vec_pretty<S: Serialize>(data: &S) -> anyhow::Result<Vec<u8>> {
|
||||
serde_json::to_vec_pretty(data).context("Could not serialize data to json")
|
||||
}
|
||||
|
||||
pub fn string(v: &str) -> String {
|
||||
serde_json::to_string(v).expect("Could not encode json string")
|
||||
}
|
||||
|
||||
fn extract_context(serde_error: &serde_json::Error, s: &str) -> String {
|
||||
let lines: Vec<_> = s.lines().collect();
|
||||
if lines.len() == 1 {
|
||||
let (col_begin, highlight) = if serde_error.column() > 30 {
|
||||
(serde_error.column() - 30, 30)
|
||||
} else {
|
||||
(1, serde_error.column())
|
||||
};
|
||||
let col_end = if lines[0].len() + 31 < serde_error.column() {
|
||||
lines[0].len() + 1
|
||||
} else {
|
||||
serde_error.column() + 30
|
||||
};
|
||||
let mut line: String = lines[0]
|
||||
.chars()
|
||||
.skip(col_begin - 1)
|
||||
.take(col_end - col_begin)
|
||||
.collect();
|
||||
line.push('\n');
|
||||
line.extend(std::iter::repeat_n(' ', highlight - 1));
|
||||
line.push('^');
|
||||
line
|
||||
} else {
|
||||
let error_line = serde_error.line();
|
||||
let mut result = String::new();
|
||||
if error_line > 1 {
|
||||
result.push_str(&format!("{}: {}\n", error_line - 1, lines[error_line - 2]));
|
||||
}
|
||||
result.push_str(&format!("{}: {}\n", error_line, lines[error_line - 1]));
|
||||
result.push_str(&format!(
|
||||
"{} {}\n",
|
||||
" ".repeat(error_line.to_string().len()),
|
||||
std::iter::repeat_n(' ', serde_error.column() - 1)
|
||||
.chain(['^'].into_iter())
|
||||
.collect::<String>(),
|
||||
));
|
||||
if lines.len() > error_line {
|
||||
result.push_str(&format!("{}: {}\n", error_line + 1, lines[error_line]));
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
40
rust/lib/common/src/lib.rs
Normal file
40
rust/lib/common/src/lib.rs
Normal file
|
@ -0,0 +1,40 @@
|
|||
use std::str::FromStr;
|
||||
|
||||
use log::LevelFilter;
|
||||
|
||||
pub mod base64;
|
||||
pub mod bitwarden;
|
||||
pub mod curl;
|
||||
pub mod env;
|
||||
pub mod fs;
|
||||
pub mod json;
|
||||
pub mod proc;
|
||||
pub mod yaml;
|
||||
|
||||
pub fn read_level_filter() -> LevelFilter {
|
||||
let env = env::read_env("LOGLEVEL").unwrap_or(String::from("INFO"));
|
||||
let env_upper = env.to_uppercase();
|
||||
let level = match env_upper.as_str() {
|
||||
"VERBOSE" => "TRACE",
|
||||
"WARNING" => "WARN",
|
||||
l => l,
|
||||
};
|
||||
log::LevelFilter::from_str(level).unwrap_or(log::LevelFilter::Info)
|
||||
}
|
||||
|
||||
pub fn entrypoint(m: impl FnOnce() -> anyhow::Result<()>) {
|
||||
env_logger::builder()
|
||||
.filter_level(read_level_filter())
|
||||
.format_timestamp(None)
|
||||
.format_module_path(false)
|
||||
.format_file(false)
|
||||
.format_source_path(false)
|
||||
.format_target(false)
|
||||
.try_init()
|
||||
.expect("Could not set logger");
|
||||
let res = m();
|
||||
if let Err(err) = res {
|
||||
log::error!("{err:#}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
355
rust/lib/common/src/proc.rs
Normal file
355
rust/lib/common/src/proc.rs
Normal file
|
@ -0,0 +1,355 @@
|
|||
use std::{
|
||||
collections::BTreeMap,
|
||||
io::Write as _,
|
||||
ops::Deref,
|
||||
process::{Child, ExitStatus, Stdio},
|
||||
};
|
||||
|
||||
use anyhow::Context;
|
||||
use log::Level;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{json, proc::util::command_to_string};
|
||||
|
||||
mod util;
|
||||
|
||||
#[derive(Debug)]
|
||||
enum Stdin {
|
||||
Null,
|
||||
Pipe(Vec<u8>),
|
||||
Inherit,
|
||||
}
|
||||
|
||||
impl Stdin {
|
||||
fn into_data(self) -> Option<Vec<u8>> {
|
||||
match self {
|
||||
Self::Pipe(d) => Some(d),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn as_std_stdio(&self) -> Stdio {
|
||||
match self {
|
||||
Self::Inherit => Stdio::inherit(),
|
||||
Self::Pipe(_) => Stdio::piped(),
|
||||
Self::Null => Stdio::null(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum Stderr {
|
||||
Pipe,
|
||||
Inherit,
|
||||
}
|
||||
|
||||
impl Stderr {
|
||||
fn as_std_stdio(&self) -> Stdio {
|
||||
match self {
|
||||
Self::Inherit => Stdio::inherit(),
|
||||
Self::Pipe => Stdio::piped(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum EnvData {
|
||||
Insensitive(String),
|
||||
Sensitive(String),
|
||||
}
|
||||
|
||||
impl EnvData {
|
||||
const fn as_str(&self) -> &str {
|
||||
match self {
|
||||
Self::Insensitive(d) | Self::Sensitive(d) => d.as_str(),
|
||||
}
|
||||
}
|
||||
|
||||
fn as_potentially_redacted_str(&self) -> &str {
|
||||
match self {
|
||||
Self::Insensitive(s) => s.as_str(),
|
||||
Self::Sensitive(_) => "<REDACTED>",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Command {
|
||||
program: String,
|
||||
args: Vec<String>,
|
||||
env: BTreeMap<String, Option<EnvData>>,
|
||||
is_sudo: bool,
|
||||
show_command: bool,
|
||||
stdin: Stdin,
|
||||
stderr: Stderr,
|
||||
}
|
||||
|
||||
impl Command {
|
||||
pub fn new(program: impl Into<String>) -> Self {
|
||||
Self {
|
||||
program: program.into(),
|
||||
args: Vec::new(),
|
||||
env: BTreeMap::new(),
|
||||
is_sudo: false,
|
||||
show_command: false,
|
||||
stdin: Stdin::Null,
|
||||
stderr: Stderr::Pipe,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_program(&self) -> &str {
|
||||
if self.is_sudo { "sudo" } else { &self.program }
|
||||
}
|
||||
|
||||
pub fn get_args(&self) -> impl Iterator<Item = &str> {
|
||||
let prefix_vec = if self.is_sudo {
|
||||
Vec::from([self.program.as_str()])
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
prefix_vec
|
||||
.into_iter()
|
||||
.chain(self.args.iter().map(Deref::deref))
|
||||
}
|
||||
|
||||
pub fn get_envs(&self) -> impl Iterator<Item = (&str, Option<&EnvData>)> {
|
||||
self.env.iter().map(|(k, v)| (k.as_str(), v.as_ref()))
|
||||
}
|
||||
|
||||
/// Sudo will automatically set stdin and stderr to inherit, to allow the user to enter the sudo password
|
||||
pub fn sudo(&mut self) -> &mut Self {
|
||||
self.is_sudo = true;
|
||||
self.stdin = Stdin::Inherit;
|
||||
self.stderr = Stderr::Inherit;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn announce(&mut self) -> &mut Self {
|
||||
self.show_command = true;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn stdin_json<S: Serialize>(&mut self, stdin: &S) -> anyhow::Result<&mut Self> {
|
||||
self.stdin = Stdin::Pipe(json::to_vec(stdin).context("Could not convert stdin to json")?);
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
pub fn stdin_json_base64<S: Serialize>(&mut self, stdin: &S) -> anyhow::Result<&mut Self> {
|
||||
let json = json::to_string(stdin).context("Could not convert stdin to json")?;
|
||||
let base64 = crate::base64::encode(json.as_bytes());
|
||||
self.stdin = Stdin::Pipe(base64.into());
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
pub fn stdin_string(&mut self, stdin: impl Into<String>) -> &mut Self {
|
||||
self.stdin = Stdin::Pipe(Vec::from(stdin.into()));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn stdin_bytes(&mut self, stdin: impl Into<Vec<u8>>) -> &mut Self {
|
||||
self.stdin = Stdin::Pipe(stdin.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn stdin_inherit(&mut self) -> &mut Self {
|
||||
self.stdin = Stdin::Inherit;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn stderr_inherit(&mut self) -> &mut Self {
|
||||
self.stderr = Stderr::Inherit;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn arg(&mut self, arg: impl Into<String>) -> &mut Self {
|
||||
self.args.push(arg.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn args<A>(&mut self, args: impl IntoIterator<Item = A>) -> &mut Self
|
||||
where
|
||||
A: Into<String>,
|
||||
{
|
||||
self.args.extend(args.into_iter().map(Into::into));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn env(&mut self, key: impl Into<String>, value: impl Into<String>) -> &mut Self {
|
||||
let _ = self
|
||||
.env
|
||||
.insert(key.into(), Some(EnvData::Insensitive(value.into())));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn env_sensitive(&mut self, key: impl Into<String>, value: impl Into<String>) -> &mut Self {
|
||||
let _ = self
|
||||
.env
|
||||
.insert(key.into(), Some(EnvData::Sensitive(value.into())));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn env_remove(&mut self, key: impl Into<String>) -> &mut Self {
|
||||
self.env.insert(key.into(), None);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn env_clear(&mut self) -> &mut Self {
|
||||
self.env.clear();
|
||||
for k in std::env::args() {
|
||||
self.env.insert(k, None);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
fn as_command(&self) -> std::process::Command {
|
||||
let is_sudo = self.is_sudo;
|
||||
let show_command = self.show_command;
|
||||
let mut cmd = if self.is_sudo {
|
||||
let mut cmd = std::process::Command::new("sudo");
|
||||
cmd.arg(&self.program);
|
||||
cmd
|
||||
} else {
|
||||
std::process::Command::new(&self.program)
|
||||
};
|
||||
cmd.args(self.args.iter());
|
||||
for (k, v) in self.env.iter() {
|
||||
if let Some(v) = v {
|
||||
cmd.env(k, v.as_str());
|
||||
} else {
|
||||
cmd.env_remove(k);
|
||||
}
|
||||
}
|
||||
if !self.env.contains_key("LOGEVEL") {
|
||||
if let Ok(loglevel) = std::env::var("LOGLEVEL") {
|
||||
match loglevel.to_ascii_lowercase().as_str() {
|
||||
"trace" | "debug" => cmd.env("LOGLEVEL", "debug"),
|
||||
"verbose" => cmd.env("LOGLEVEL", "verbose"),
|
||||
"info" => cmd.env("LOGLEVEL", "info"),
|
||||
"warning" | "warn" => cmd.env("LOGLEVEL", "warning"),
|
||||
"error" => cmd.env("LOGLEVEL", "error"),
|
||||
_ => cmd.env_remove("LOGLEVEL"),
|
||||
};
|
||||
}
|
||||
}
|
||||
if is_sudo {
|
||||
log::info!("[SUDO] Running command {}", command_to_string(self));
|
||||
} else {
|
||||
let level = if show_command {
|
||||
Level::Info
|
||||
} else {
|
||||
Level::Trace
|
||||
};
|
||||
log::log!(level, "Running command {}", command_to_string(self));
|
||||
}
|
||||
cmd
|
||||
}
|
||||
|
||||
/// WARNING: Spawning a command consumes all of its stdin, as such spawning it twice may not yield the same result
|
||||
pub fn try_spawn_to_bytes(&mut self) -> anyhow::Result<Vec<u8>> {
|
||||
let mut cmd = self.as_command();
|
||||
let mut child = cmd
|
||||
.stderr(self.stderr.as_std_stdio())
|
||||
.stdin(self.stdin.as_std_stdio())
|
||||
.stdout(Stdio::piped())
|
||||
.spawn()
|
||||
.with_context(|| format!("Could not spawn command: {}", command_to_string(self)))?;
|
||||
let mut stdin = Stdin::Null;
|
||||
std::mem::swap(&mut self.stdin, &mut stdin);
|
||||
let join_handle = if let Some(data) = stdin.into_data() {
|
||||
let mut stdin_pipe = child.stdin.take().expect("Child has no stdin");
|
||||
Some(std::thread::spawn(move || {
|
||||
stdin_pipe
|
||||
.write_all(data.as_slice())
|
||||
.expect("Could not write to child");
|
||||
}))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let output = wait_with_output(child, || command_to_string(self));
|
||||
if let Some(join_handle) = join_handle {
|
||||
join_handle
|
||||
.join()
|
||||
.map_err(|e| anyhow::format_err!("Thread sending stdin panicked: {e:?}"))?;
|
||||
}
|
||||
output
|
||||
}
|
||||
|
||||
/// WARNING: Spawning a command consumes all of its stdin, as such spawning it twice may not yield the same result
|
||||
/// Using try_spawn_to_string will trim a single trailing newline, if you don't want this, use to_bytes and convert the string manually.
|
||||
pub fn try_spawn_to_string(&mut self) -> anyhow::Result<String> {
|
||||
let output = self.try_spawn_to_bytes()?;
|
||||
let mut output: String = output.try_into().map_err(|_| {
|
||||
anyhow::format_err!(
|
||||
"Command {} didn't produce valid utf-8 output",
|
||||
command_to_string(self)
|
||||
)
|
||||
})?;
|
||||
if output.ends_with("\n") {
|
||||
output.truncate(output.len() - 1);
|
||||
}
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
/// WARNING: Spawning a command consumes all of its stdin, as such spawning it twice may not yield the same result
|
||||
pub fn try_spawn_to_json<D: for<'de> Deserialize<'de>>(&mut self) -> anyhow::Result<D> {
|
||||
let output = self.try_spawn_to_string()?;
|
||||
json::from_str(&output).with_context(|| {
|
||||
format!(
|
||||
"Could not parse output of {} as json",
|
||||
command_to_string(self)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// WARNING: Spawning a command consumes all of its stdin, as such spawning it twice may not yield the same result
|
||||
pub fn try_spawn_stdout_inherit(&mut self) -> anyhow::Result<ExitStatus> {
|
||||
let mut cmd = self.as_command();
|
||||
cmd.stdout(Stdio::inherit());
|
||||
cmd.status()
|
||||
.with_context(|| format!("Could not spawn command: {}", command_to_string(self)))
|
||||
}
|
||||
}
|
||||
|
||||
fn wait_with_output(child: Child, cmd_str: impl Fn() -> String) -> anyhow::Result<Vec<u8>> {
|
||||
let output = child
|
||||
.wait_with_output()
|
||||
.with_context(|| format!("Could not wait for output of commnad: {}", cmd_str()))?;
|
||||
if !output.status.success() {
|
||||
return Err(anyhow::format_err!(
|
||||
"Command {}, exited unexpectedly: {:?}. With stderr: {}",
|
||||
cmd_str(),
|
||||
output.status,
|
||||
String::from_utf8_lossy(&output.stderr),
|
||||
));
|
||||
}
|
||||
Ok(output.stdout)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::Command;
|
||||
|
||||
#[test]
|
||||
fn test_spawn() {
|
||||
let mut echo = Command::new("echo");
|
||||
echo.args(["Hello", "World"]);
|
||||
|
||||
assert_eq!(
|
||||
"Hello World",
|
||||
echo.try_spawn_to_string()
|
||||
.expect("Should be able to echo Hello World")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_spawn_stdin() {
|
||||
let mut rev = Command::new("rev");
|
||||
|
||||
assert_eq!(
|
||||
"dlroW olleH",
|
||||
rev.stdin_string("Hello World".to_string())
|
||||
.try_spawn_to_string()
|
||||
.expect("Should be able to rev Hello World")
|
||||
);
|
||||
}
|
||||
}
|
31
rust/lib/common/src/proc/util.rs
Normal file
31
rust/lib/common/src/proc/util.rs
Normal file
|
@ -0,0 +1,31 @@
|
|||
use super::Command;
|
||||
use std::borrow::Cow;
|
||||
|
||||
use shell_quote::Quote;
|
||||
|
||||
pub fn command_to_string(cmd: &Command) -> String {
|
||||
let program = escape_cli(cmd.get_program());
|
||||
let env: Vec<_> = cmd
|
||||
.get_envs()
|
||||
.map(|(key, value)| match value {
|
||||
None => key.into(),
|
||||
Some(value) => Cow::Owned(format!(
|
||||
"{key}={value} ",
|
||||
value = escape_cli(value.as_potentially_redacted_str())
|
||||
)),
|
||||
})
|
||||
.collect();
|
||||
let args: Vec<_> = cmd.get_args().map(escape_cli).collect();
|
||||
let env = env.join("");
|
||||
let args = args.join(" ");
|
||||
format!(
|
||||
"{env_marker}{env}{program}{arg_separator}{args}",
|
||||
env_marker = if env.is_empty() { "" } else { "env " },
|
||||
arg_separator = if args.is_empty() { "" } else { " " }
|
||||
)
|
||||
}
|
||||
|
||||
fn escape_cli<A: AsRef<str>>(str: A) -> String {
|
||||
let str = str.as_ref();
|
||||
shell_quote::Bash::quote(str)
|
||||
}
|
10
rust/lib/common/src/yaml.rs
Normal file
10
rust/lib/common/src/yaml.rs
Normal file
|
@ -0,0 +1,10 @@
|
|||
use anyhow::Context;
|
||||
use serde::Serialize;
|
||||
|
||||
pub fn string(str: &str) -> String {
|
||||
serde_yml::to_string(str).expect("Should be able to encode string")
|
||||
}
|
||||
|
||||
pub fn to_string<T: ?Sized + Serialize>(value: &T) -> anyhow::Result<String> {
|
||||
serde_yml::to_string(value).context("Could not serialize to yaml")
|
||||
}
|
4
rust/lib/hakari/.gitattributes
vendored
Normal file
4
rust/lib/hakari/.gitattributes
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
# Avoid putting conflict markers in the generated Cargo.toml file, since their presence breaks
|
||||
# Cargo.
|
||||
# Also do not check out the file as CRLF on Windows, as that's what hakari needs.
|
||||
Cargo.toml merge=binary -crlf
|
25
rust/lib/hakari/Cargo.toml
Normal file
25
rust/lib/hakari/Cargo.toml
Normal file
|
@ -0,0 +1,25 @@
|
|||
# This file is generated by `cargo hakari`.
|
||||
# To regenerate, run:
|
||||
# cargo hakari generate
|
||||
|
||||
[package]
|
||||
name = "hakari"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "workspace-hack package, managed by hakari"
|
||||
# You can choose to publish this crate: see https://docs.rs/cargo-hakari/latest/cargo_hakari/publishing.
|
||||
publish = false
|
||||
|
||||
# The parts of the file between the BEGIN HAKARI SECTION and END HAKARI SECTION comments
|
||||
# are managed by hakari.
|
||||
|
||||
### BEGIN HAKARI SECTION
|
||||
[dependencies]
|
||||
anstream = { version = "0.6.19" }
|
||||
|
||||
[build-dependencies]
|
||||
proc-macro2 = { version = "1.0.95" }
|
||||
quote = { version = "1.0.40" }
|
||||
syn = { version = "2.0.104", features = ["full"] }
|
||||
|
||||
### END HAKARI SECTION
|
2
rust/lib/hakari/build.rs
Normal file
2
rust/lib/hakari/build.rs
Normal file
|
@ -0,0 +1,2 @@
|
|||
// A build script is required for cargo to consider build dependencies.
|
||||
fn main() {}
|
1
rust/lib/hakari/src/lib.rs
Normal file
1
rust/lib/hakari/src/lib.rs
Normal file
|
@ -0,0 +1 @@
|
|||
// This is a stub lib.rs.
|
13
rust/program/hetzner-ipv6/Cargo.toml
Normal file
13
rust/program/hetzner-ipv6/Cargo.toml
Normal file
|
@ -0,0 +1,13 @@
|
|||
[package]
|
||||
name = "hetzner-ipv6"
|
||||
edition = "2024"
|
||||
version = "1.0.0"
|
||||
metadata.crane.name = "hetzner-ipv6"
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
clap = { workspace = true }
|
||||
common = { path = "../../lib/common" }
|
||||
log = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
hakari = { version = "0.1", path = "../../lib/hakari" }
|
28
rust/program/hetzner-ipv6/src/main.rs
Normal file
28
rust/program/hetzner-ipv6/src/main.rs
Normal file
|
@ -0,0 +1,28 @@
|
|||
use clap::{Parser, Subcommand};
|
||||
fn main() {
|
||||
common::entrypoint(program);
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(version, about, long_about = None)]
|
||||
pub struct Args {
|
||||
#[command(subcommand)]
|
||||
pub command: Commands,
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum Commands {
|
||||
/// Configures the ipv6 address using instance metadata and iproute2
|
||||
Configure,
|
||||
}
|
||||
|
||||
fn program() -> anyhow::Result<()> {
|
||||
let args = Args::parse();
|
||||
match args.command {
|
||||
Commands::Configure => configure(),
|
||||
}
|
||||
}
|
||||
|
||||
fn configure() -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
4
rust/rust-toolchain.toml
Normal file
4
rust/rust-toolchain.toml
Normal file
|
@ -0,0 +1,4 @@
|
|||
[toolchain]
|
||||
channel = "1.88.0"
|
||||
components = ["rustfmt", "clippy", "cargo"]
|
||||
profile = "minimal"
|
Loading…
Add table
Add a link
Reference in a new issue