11 files changed
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -808,10 +808,12 @@
dependencies = [
"age",
"anyhow",
+ "base64 0.22.1",
"clap",
"ed25519-dalek",
"fleet-shared",
"rand",
+ "x25519-dalek",
]
[[package]]
--- a/cmds/fleet/src/cmds/secrets/mod.rs
+++ b/cmds/fleet/src/cmds/secrets/mod.rs
@@ -40,9 +40,6 @@
/// Secret public part
#[clap(long)]
public: Option<String>,
- /// How to name public secret part
- #[clap(long, default_value = "public")]
- public_name: String,
/// Load public part from specified file
#[clap(long)]
public_file: Option<PathBuf>,
@@ -55,14 +52,19 @@
#[clap(long)]
re_add: bool,
- #[clap(default_value = "secret")]
- part_name: String,
+ /// How to name public secret part
+ #[clap(long, short = 'p', default_value = "public")]
+ public_part: String,
+ /// How to name private secret part
+ #[clap(short = 's', long, default_value = "secret")]
+ part: String,
},
/// Add secret, data should be provided in stdin
Add {
/// Secret name
name: String,
- /// Secret owners
+ /// Secret owner
+ #[clap(short = 'm', long)]
machine: String,
/// Override secret if already present
#[clap(long)]
@@ -70,41 +72,41 @@
/// Secret public part
#[clap(long)]
public: Option<String>,
- /// How to name public secret part
- #[clap(long, default_value = "public")]
- public_name: String,
/// Load public part from specified file
#[clap(long)]
public_file: Option<PathBuf>,
- #[clap(default_value = "secret")]
- part_name: String,
+ /// How to name public secret part
+ #[clap(short = 'p', long, default_value = "public")]
+ public_part: String,
+ /// How to name private secret part
+ #[clap(short = 's', long, default_value = "secret")]
+ part: String,
},
/// Read secret from remote host, requires sudo on said host
Read {
name: String,
+ #[clap(short = 'm', long)]
machine: String,
- #[clap(default_value = "secret")]
- part_name: String,
+ /// Which private secret part to read
+ #[clap(short = 'p', long, default_value = "secret")]
+ part: String,
},
UpdateShared {
name: String,
- #[clap(long)]
- machines: Option<Vec<String>>,
+ #[clap(short = 'm', long)]
+ machine: Option<Vec<String>>,
#[clap(long)]
- add_machines: Vec<String>,
+ add_machine: Vec<String>,
#[clap(long)]
- remove_machines: Vec<String>,
+ remove_machine: Vec<String>,
/// Which host should we use to decrypt
#[clap(long)]
prefer_identities: Vec<String>,
-
- #[clap(default_value = "secret")]
- part_name: String,
},
Regenerate {
/// Which host should we use to decrypt, in case if reencryption is required, without
@@ -115,13 +117,15 @@
List {},
Edit {
name: String,
+ #[clap(short = 'm', long)]
machine: String,
-
- #[clap(default_value = "secret")]
- part: String,
#[clap(long)]
add: bool,
+
+ /// Which private secret part to read
+ #[clap(short = 'p', long, default_value = "secret")]
+ part: String,
},
}
@@ -220,21 +224,18 @@
};
let on_pkgs = host.pkgs().await?;
let call_package = nix_go!(on_pkgs.callPackage);
- let mk_encrypt_secret = nix_go!(on_pkgs.mkEncryptSecret);
+ let mk_secret_generators = nix_go!(on_pkgs.mkSecretGenerators);
let mut recipients = Vec::new();
for owner in owners {
let key = config.key(owner).await?;
recipients.push(key);
}
- let encrypt = nix_go!(mk_encrypt_secret(Obj {
+ let generators = nix_go!(mk_secret_generators(Obj {
recipients: { recipients },
}));
- let generator = nix_go!(call_package(generator)(Obj {
- encrypt,
- // rustfmt_please_newline
- }));
+ let generator = nix_go!(call_package(generator)(generators));
let generator = generator.build().await?;
let generator = generator
@@ -305,6 +306,7 @@
}
let default_pkgs = &config.default_pkgs;
let default_call_package = nix_go!(default_pkgs.callPackage);
+ let default_mk_secret_generators = nix_go!(default_pkgs.mkSecretGenerators);
// Generators provide additional information in passthru, to access
// passthru we should call generator, but information about where this generator is supposed to build
// is located in passthru... Thus evaluating generator on host.
@@ -313,10 +315,10 @@
//
// I don't want to make modules always responsible for additional secret data anyway,
// so it should be in derivation, and not in the secret data itself.
- let default_generator = nix_go!(default_call_package(generator)(Obj {
- encrypt: { "exit 1" },
- // rustfmt_please_newline
+ let generators = nix_go!(default_mk_secret_generators(Obj {
+ recipients: { <Vec<String>>::new() },
}));
+ let default_generator = nix_go!(default_call_package(generator)(generators));
let kind: GeneratorKind = nix_go_json!(default_generator.generatorKind);
@@ -442,11 +444,11 @@
name,
force,
public,
- public_name,
+ public_part: public_name,
public_file,
expires_at,
re_add,
- part_name,
+ part: part_name,
} => {
// TODO: Forbid updating secrets with set expectedOwners (= not user-managed).
@@ -500,9 +502,9 @@
name,
force,
public,
- public_name,
+ public_part: public_name,
public_file,
- part_name,
+ part: part_name,
} => {
if config.has_secret(&machine, &name) && !force {
bail!("secret already defined");
@@ -535,7 +537,7 @@
Secret::Read {
name,
machine,
- part_name,
+ part: part_name,
} => {
let secret = config.host_secret(&machine, &name)?;
let Some(secret) = secret.parts.get(&part_name) else {
@@ -552,25 +554,24 @@
}
Secret::UpdateShared {
name,
- machines,
- add_machines,
- remove_machines,
+ machine,
+ add_machine,
+ remove_machine,
prefer_identities,
- part_name,
} => {
// TODO: Forbid updating secrets with set expectedOwners (= not user-managed).
let secret = config.shared_secret(&name)?;
- if secret.secret.parts.get(&part_name).is_none() {
+ if secret.secret.parts.values().all(|v| !v.raw.encrypted) {
bail!("no secret");
}
let initial_machines = secret.owners.clone();
let target_machines = parse_machines(
initial_machines.clone(),
- machines,
- add_machines,
- remove_machines,
+ machine,
+ add_machine,
+ remove_machine,
)?;
if target_machines.is_empty() {
--- a/cmds/fleet/src/host.rs
+++ b/cmds/fleet/src/host.rs
@@ -95,7 +95,7 @@
let out = cmd.run_string().await?;
let mut lines = out.split('\n');
if let Some(last) = lines.next_back() {
- ensure!(last == "", "output of ls should end with newline");
+ ensure!(last.is_empty(), "output of ls should end with newline");
}
Ok(lines.map(ToOwned::to_owned).collect())
}
--- a/cmds/generator-helper/Cargo.toml
+++ b/cmds/generator-helper/Cargo.toml
@@ -6,7 +6,9 @@
[dependencies]
age.workspace = true
anyhow.workspace = true
+base64 = "0.22.1"
clap.workspace = true
ed25519-dalek = { version = "2.1", features = ["rand_core"] }
fleet-shared.workspace = true
rand = "0.8.5"
+x25519-dalek = "2.0.1"
--- a/cmds/generator-helper/src/main.rs
+++ b/cmds/generator-helper/src/main.rs
@@ -1,52 +1,161 @@
use std::{
- fs,
- io::{self, stdout, Cursor, Read, Write},
- path::PathBuf,
+ env,
+ fs::{File, OpenOptions},
+ io::{copy, Read, Write},
str::FromStr,
};
-use age::Recipient;
+use age::{
+ ssh::{ParseRecipientKeyError, Recipient as SshRecipient},
+ Encryptor, Recipient,
+};
use anyhow::{anyhow, bail, ensure, Context, Result};
-use clap::Parser;
-use ed25519_dalek::SigningKey;
+use clap::{Parser, ValueEnum};
use fleet_shared::SecretData;
use rand::{
distributions::{Alphanumeric, DistString, Distribution, Uniform},
- rngs::OsRng,
- thread_rng, Rng,
+ thread_rng,
};
-fn write_output(out: &str, data: impl AsRef<[u8]>, stdout_marker: &mut bool) -> Result<()> {
- let data = data.as_ref();
- if out == "-" {
- let mut stdout = stdout();
- if *stdout_marker {
- stdout.write_all(&[b'\n'])?;
+fn write_output_file(out: &str) -> Result<File> {
+ let file = OpenOptions::new()
+ .create_new(true)
+ .write(true)
+ .open(out)
+ .with_context(|| format!("failed to open output {out:?}"))?;
+ Ok(file)
+}
+fn write_public(out: &str, mut input: impl Read, encoding: OutputEncoding) -> Result<()> {
+ let mut output = write_output_file(out)?;
+
+ let mut data = Vec::new();
+ copy(&mut input, &mut wrap_encoder(&mut data, encoding))?;
+
+ output.write_all(
+ SecretData {
+ data,
+ encrypted: false,
}
- *stdout_marker = true;
- stdout.write_all(data)?;
- } else {
- fs::write(out, data)?;
+ .to_string()
+ .as_bytes(),
+ )?;
+ Ok(())
+}
+fn write_private(
+ identities: &Identities,
+ out: &str,
+ mut input: impl Read,
+ encoding: OutputEncoding,
+) -> Result<()> {
+ let mut output = write_output_file(out)?;
+ let encryptor = make_encryptor(identities)?;
+
+ let mut data = Vec::new();
+ {
+ let mut encrypted_writer = encryptor.wrap_output(&mut data)?;
+ copy(
+ &mut input,
+ &mut wrap_encoder(&mut encrypted_writer, encoding),
+ )?;
+ encrypted_writer.finish()?;
};
+
+ output.write_all(
+ SecretData {
+ data,
+ encrypted: true,
+ }
+ .to_string()
+ .as_bytes(),
+ )?;
Ok(())
}
+type Identities = Vec<SshRecipient>;
+fn load_identities() -> Result<Identities> {
+ let list = env::var("GENERATOR_HELPER_IDENTITIES");
+ let list = match list {
+ Ok(v) => v,
+ Err(env::VarError::NotPresent) => {
+ bail!("gh is only intended to be used from secret generator scripts, but if you really want to use it somewhere else - set GENERATOR_HELPER_IDENTITIES to list of newline-delimited ssh identities");
+ }
+ Err(e) => bail!("somehow, identities list is not utf-8: {e}"),
+ };
+ let list = list.trim();
+ ensure!(!list.is_empty(), "no identities passed, can't encrypt data");
+ list.lines()
+ .map(age::ssh::Recipient::from_str)
+ .collect::<Result<Identities, ParseRecipientKeyError>>()
+ .map_err(|e| anyhow!("parse recipients: {e:?}"))
+}
+fn make_encryptor(r: &Identities) -> Result<Encryptor> {
+ Ok(Encryptor::with_recipients(
+ r.iter()
+ .map(|v| {
+ let coerced: Box<dyn Recipient + Send> = Box::new(v.clone());
+ coerced
+ })
+ .collect(),
+ )
+ .expect("list is not empty"))
+}
+fn wrap_encoder<'t>(w: impl Write + 't, encoding: OutputEncoding) -> impl Write + 't {
+ fn coerce<'t>(w: impl Write + 't) -> Box<dyn Write + 't> {
+ Box::new(w)
+ }
+ match encoding {
+ OutputEncoding::Raw => coerce(w),
+ OutputEncoding::Base64 => {
+ use base64::engine::general_purpose::STANDARD;
+ let writer = base64::write::EncoderWriter::new(w, &STANDARD);
+ coerce(writer)
+ }
+ }
+}
+
+#[derive(Clone, Copy, ValueEnum, Default)]
+enum OutputEncoding {
+ /// Do not encode data, store as is.
+ #[default]
+ Raw,
+ /// Encode as base64 (with padding).
+ Base64,
+}
+
#[derive(Parser)]
enum Generate {
/// Generate public, private keys without wrapping, in standard ed25519 schema
/// (64 bytes private (due to merge with private), 32 bytes public)
Ed25519 {
+ #[arg(long, short = 'p')]
public: String,
+ #[arg(long, short = 's')]
private: String,
/// Private key should be just the private key (32 bytes), not standard private+public.
#[arg(long)]
no_embed_public: bool,
+ #[arg(long, short = 'e', value_enum, default_value_t)]
+ encoding: OutputEncoding,
+ },
+ /// Generate public, private keys without wrapping, in standard x25519 schema
+ /// (32 bytes private, 32 bytes public)
+ X25519 {
+ #[arg(long, short = 'p')]
+ public: String,
+ #[arg(long, short = 's')]
+ private: String,
+ #[arg(long, short = 'e', value_enum, default_value_t)]
+ encoding: OutputEncoding,
},
Password {
+ #[arg(long, short = 'o')]
output: String,
+ #[arg(long)]
size: usize,
#[arg(long, short = 'n')]
no_symbols: bool,
+ #[arg(long, short = 'e', value_enum, default_value_t)]
+ encoding: OutputEncoding,
},
}
@@ -54,15 +163,17 @@
enum Opts {
/// Encode public part from stdin.
Public {
- #[arg(long)]
- allow_empty: bool,
+ #[arg(long, short = 'o')]
+ output: String,
+ #[arg(long, short = 'e', value_enum, default_value_t)]
+ encoding: OutputEncoding,
},
/// Encrypt private part from stdin.
Private {
- #[arg(long)]
- allow_empty: bool,
- #[arg(short = 'r')]
- recipient: Vec<String>,
+ #[arg(long, short = 'o')]
+ output: String,
+ #[arg(long, short = 'e', value_enum, default_value_t)]
+ encoding: OutputEncoding,
},
/// Generate keys in well-known schemas.
///
@@ -70,99 +181,34 @@
/// otherwise you should ensure noone is able to read generated files, they don't have any mode set by default.
#[command(subcommand)]
Generate(Generate),
- // Generate {
- // kind: GenerateKind,
- // /// Different generators generate different number of files, you need to specify number of outputs corresponding to the generator.
- // #[arg(short = 'o')]
- // outputs: Vec<String>,
- // },
}
-fn parse_stdin() -> Result<Option<Vec<u8>>> {
- let mut input = vec![];
- io::stdin().read_to_end(&mut input)?;
- if input.is_empty() {
- Ok(None)
- } else {
- Ok(Some(input))
- }
-}
-pub fn encrypt_secret_data(
- recipients: impl IntoIterator<Item = impl Recipient + Send + 'static>,
- data: Vec<u8>,
-) -> Option<SecretData> {
- let mut encrypted = vec![];
- let recipients = recipients
- .into_iter()
- .map(|v| Box::new(v) as Box<dyn Recipient + Send>)
- .collect::<Vec<_>>();
- let mut encryptor = age::Encryptor::with_recipients(recipients)?
- .wrap_output(&mut encrypted)
- .expect("in memory write");
- io::copy(&mut Cursor::new(data), &mut encryptor).expect("in memory copy");
- encryptor.finish().expect("in memory flush");
- Some(SecretData {
- data: encrypted,
- encrypted: true,
- })
-}
-
fn main() -> Result<()> {
let opts = Opts::parse();
// Assumed to be secure, seeded from secure OsRng+reseeded.
let mut rng = thread_rng();
match opts {
- Opts::Public { allow_empty } => {
- let stdin = parse_stdin()?;
- if stdin.is_none() && !allow_empty {
- bail!("empty stdin input is not allowed unless --allow-empty is set");
- }
- let stdin = stdin.unwrap_or_default();
- io::stdout().write_all(
- SecretData {
- data: stdin,
- encrypted: false,
- }
- .to_string()
- .as_bytes(),
- )?;
+ Opts::Public { output, encoding } => {
+ write_public(&output, std::io::stdin(), encoding)?;
}
- Opts::Private {
- allow_empty,
- recipient,
- } => {
- let stdin = parse_stdin()?;
- if stdin.is_none() && !allow_empty {
- bail!("empty stdin input is not allowed unless --allow-empty is set");
- }
- let stdin = stdin.unwrap_or_default();
- if recipient.is_empty() {
- bail!("recipient list is empty");
- }
- let out = encrypt_secret_data(
- recipient
- .into_iter()
- .map(|r| age::ssh::Recipient::from_str(&r))
- .collect::<Result<Vec<age::ssh::Recipient>, age::ssh::ParseRecipientKeyError>>()
- .map_err(|e| anyhow!("parse recipients: {e:?}"))?,
- stdin,
- )
- .expect("got recipients");
- io::stdout().write_all(out.to_string().as_bytes())?;
+ Opts::Private { output, encoding } => {
+ let recipients = load_identities()?;
+ write_private(&recipients, &output, std::io::stdin(), encoding)?;
}
Opts::Generate(gen) => {
- let mut stdout_marker: bool = false;
match gen {
Generate::Ed25519 {
public,
private,
no_embed_public,
+ encoding,
} => {
- let key = SigningKey::generate(&mut rng).to_keypair_bytes();
-
- write_output(&public, &key[32..], &mut stdout_marker).context("public")?;
- write_output(
+ let recipients = load_identities()?;
+ let key = ed25519_dalek::SigningKey::generate(&mut rng).to_keypair_bytes();
+ write_public(&public, &key[32..], encoding)?;
+ write_private(
+ &recipients,
&private,
&key[..{
if no_embed_public {
@@ -171,19 +217,31 @@
64
}
}],
- &mut stdout_marker,
- )
- .context("private")?;
+ encoding,
+ )?;
+ }
+ Generate::X25519 {
+ public,
+ private,
+ encoding,
+ } => {
+ let recipients = load_identities()?;
+ let key = x25519_dalek::StaticSecret::random_from_rng(rng);
+ let public_key: x25519_dalek::PublicKey = (&key).into();
+ write_public(&public, public_key.as_bytes().as_slice(), encoding)?;
+ write_private(&recipients, &private, key.as_bytes().as_slice(), encoding)?;
}
Generate::Password {
size,
no_symbols,
output,
+ encoding,
} => {
ensure!(
size >= 6,
"misconfiguration? password is shorter than 6 chars"
);
+ let recipients = load_identities()?;
let out = if no_symbols {
Alphanumeric.sample_string(&mut rng, size)
} else {
@@ -195,7 +253,7 @@
.map(|i| GEN_ASCII_SYMBOLS[i] as char)
.collect::<String>()
};
- write_output(&output, out, &mut stdout_marker)?;
+ write_private(&recipients, &output, out.as_bytes(), encoding)?;
}
}
}
--- a/flake.nix
+++ b/flake.nix
@@ -67,6 +67,7 @@
perSystem = {
config,
system,
+ pkgs,
...
}: let
# Can also be built for darwin, through it is not usual to deploy nixos systems from macos machines.
@@ -75,14 +76,14 @@
# It is not possible to deploy any host from armv6/armv7 hardware, and I don't think it even makes sense.
deployerSystems = ["aarch64-linux" "x86_64-linux"];
deployerSystem = builtins.elem system deployerSystems;
- pkgs = import nixpkgs {
- inherit system;
- overlays = [(rust-overlay.overlays.default)];
- };
lib = pkgs.lib;
rust = pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml;
craneLib = (crane.mkLib pkgs).overrideToolchain rust;
in {
+ _module.args.pkgs = import nixpkgs {
+ inherit system;
+ overlays = [(rust-overlay.overlays.default)];
+ };
# Reference fleet package should be built with nightly rust, specified in rust-toolchain.toml.
packages = lib.mkIf deployerSystem (let
packages = import ./pkgs {
--- a/lib/fleetLib.nix
+++ b/lib/fleetLib.nix
@@ -42,23 +42,46 @@
mkPassword = {size ? 32}: {
coreutils,
- encrypt,
mkSecretGenerator,
+ ...
}:
mkSecretGenerator {
script = ''
mkdir $out
+ gh generate password -o $out/secret --size ${toString size}
+ '';
+ };
- ${coreutils}/bin/tr -dc 'A-Za-z0-9!?%=' < /dev/random \
- | ${coreutils}/bin/head -c ${toString size} \
- | ${encrypt} > $out/secret
+ mkEd25519 = {
+ noEmbedPublic ? false,
+ encoding ? null,
+ }: {mkSecretGenerator, ...}:
+ mkSecretGenerator {
+ script = ''
+ mkdir $out
+ gh generate ed25519 -p $out/public -s $out/secret \
+ ${lib.optionalString noEmbedPublic "--no-embed-public"} \
+ ${lib.optionalString (encoding != null) "--encoding=${encoding}"}
'';
};
+ mkGarage = {}: mkEd25519 {noEmbedPublic = true;};
+
+ mkX25519 = {encoding ? null}: {mkSecretGenerator, ...}:
+ mkSecretGenerator {
+ script = ''
+ mkdir $out
+ gh generate x25519 -p $out/public -s $out/secret \
+ ${lib.optionalString (encoding != null) "--encoding=${encoding}"}
+ '';
+ };
+
+ mkWireguard = {}: mkX25519 {encoding = "base64";};
+
mkRsa = {size ? 4096}: {
openssl,
- encrypt,
mkSecretGenerator,
+ ...
}:
mkSecretGenerator {
script = ''
@@ -67,8 +90,8 @@
${openssl}/bin/openssl genrsa -out rsa_private.key ${toString size}
${openssl}/bin/openssl rsa -in rsa_private.key -pubout -out rsa_public.key
- sudo cat rsa_private.key | ${encrypt} > $out/secret
- sudo cat rsa_public.key > $out/public
+ cat rsa_private.key | gh private -o $out/secret
+ cat rsa_public.key | gh public -o $out/public
'';
};
}
130 overlays = [130 overlays = [
131 (final: prev: let131 (final: prev: let
132 lib = final.lib;132 lib = final.lib;
133 inherit (lib) strings concatMap;133 inherit (lib) strings;
134 inherit (strings) escapeShellArgs;134 inherit (strings) concatStringsSep;
135 in {135 in {
136 mkEncryptSecret = {136 mkSecretGenerators = {recipients}: rec {
137 rage ? prev.rage,
138 recipients,
139 }:
140 prev.writeShellScript "encryptor" ''
141 #!/bin/sh
142 exec ${rage}/bin/rage ${escapeShellArgs (concatMap (r: ["-r" r]) recipients)} -e "$@"
143 '';
144
145 137
146 138
147 139
151 143
152 impureOn ? null,144 impureOn ? null,
153 }:145 }:
154 (prev.writeShellScript "impureGenerator.sh" ''146 (prev.writeShellScript "impureGenerator.sh" ''
155 #!/bin/sh147 #!/bin/sh
156 set -eu148 set -eu
157149
158 # TODO: Provide tempdir from outside, to make it securely erasurable as needed?150 export GENERATOR_HELPER_IDENTITIES="${concatStringsSep "\n" recipients}";
151 export PATH=${final.fleet-generator-helper}/bin:$PATH
152
153 # TODO: Provide tempdir from outside, to make it securely erasurable as needed?
159 tmp=$(mktemp -d)154 tmp=$(mktemp -d)
160 cd $tmp155 cd $tmp
161 # cd /var/empty156 # cd /var/empty
162157
163 created_at=$(date -u +"%Y-%m-%dT%H:%M:%S.%NZ")158 created_at=$(date -u +"%Y-%m-%dT%H:%M:%S.%NZ")
164159
165 ${script}160 ${script}
166161
167 if ! test -d $out; then162 if ! test -d $out; then
168 echo "impure generator script did not produce expected \$out output"163 echo "impure generator script did not produce expected \$out output"
169 exit 1164 exit 1
170 fi165 fi
171166
172 echo -n $created_at > $out/created_at167 echo -n $created_at > $out/created_at
173 echo -n SUCCESS > $out/marker168 echo -n SUCCESS > $out/marker
174 '')169 '')
175 .overrideAttrs (old: {170 .overrideAttrs (old: {
176 passthru = {171 passthru = {
177 inherit impureOn;172 inherit impureOn;
178 generatorKind = "impure";173 generatorKind = "impure";
179 };174 };
180 });175 });
181 176
182 mkSecretGenerator = {script}: final.mkImpureSecretGenerator {inherit script;};177 mkSecretGenerator = {script}: mkImpureSecretGenerator {inherit script;};
183178
184 179
185 180
186 181
187 182
188 183
189 184
190 185
191 186
192 187
193 188
194 189
195 190
196 191
197 192
198 193
199 194
200 195
201 196
202 197
203 198
204 199
205 200
206 201
207 202
208 203
209 204
210 205
211 206
207 };
212 })208 })
213 ];209 ];
214 };210 };
--- a/pkgs/default.nix
+++ b/pkgs/default.nix
@@ -2,6 +2,7 @@
callPackage,
craneLib,
}: {
+ fleet = callPackage ./fleet.nix {inherit craneLib;};
fleet-install-secrets = callPackage ./fleet-install-secrets.nix {inherit craneLib;};
- fleet = callPackage ./fleet.nix {inherit craneLib;};
+ fleet-generator-helper = callPackage ./fleet-generator-helper.nix {inherit craneLib;};
}
--- /dev/null
+++ b/pkgs/fleet-generator-helper.nix
@@ -0,0 +1,13 @@
+{craneLib}:
+craneLib.buildPackage rec {
+ pname = "fleet-generator-helper";
+
+ src = craneLib.cleanCargoSource (craneLib.path ../.);
+ strictDeps = true;
+
+ cargoExtraArgs = "--locked -p ${pname}";
+
+ postInstall = ''
+ ln -s $out/bin/${pname} $out/bin/gh
+ '';
+}
--- a/pkgs/generator-helper.nix
+++ /dev/null
@@ -1,13 +0,0 @@
-{craneLib}:
-craneLib.buildPackage rec {
- pname = "fleet-generator-helper";
-
- src = craneLib.cleanCargoSource (craneLib.path ../.);
- strictDeps = true;
-
- cargoExtraArgs = "--locked -p ${pname}";
-
- postInstall = ''
- mv bin/${pname} bin/genhelper
- '';
-}