Skip to content

Commit

Permalink
Merge pull request #47 from PierreBeucher/novops-run
Browse files Browse the repository at this point in the history
feat: Novops run - load secrets into subprocess
  • Loading branch information
PierreBeucher authored Aug 21, 2023
2 parents dd5cdd5 + 804b0f3 commit 1994669
Show file tree
Hide file tree
Showing 8 changed files with 264 additions and 119 deletions.
10 changes: 7 additions & 3 deletions .github/workflows/build-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ jobs:
novops-build/novops
load_novops:
name: run Novops load on Github action
name: run Novops commands
runs-on: ubuntu-latest
needs: docker_build
steps:
Expand All @@ -70,11 +70,15 @@ jobs:
with:
name: novops-binary

- name: load novops
- name: novops load
run: |
chmod +x ./novops
./novops load -c tests/.novops.plain-strings.yml -s .envrc -e dev
cat .envrc >> "$GITHUB_ENV"
- name: check novops loaded values
run: env | grep MY_APP_HOST
run: env | grep MY_APP_HOST

- name: novops run and check var
run: |
./novops run -c tests/.novops.plain-strings.yml -e dev -- sh -c "env | grep DOG_PATH"
1 change: 1 addition & 0 deletions docs/src/examples/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Examples and Use Cases

- [CLI advanced usage](cli-usage.md)
- [Ansible setup](ansible.md)
- [Terraform setup](terraform.md)
- [Pulumi setup](pulumi.md)
Expand Down
15 changes: 15 additions & 0 deletions docs/src/examples/cli-usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

- [Advanced CLI usage](#advanced-cli-usage)
- [Override default config path](#override-default-config-path)
- [Run a sub-process](#run-a-sub-process)
- [Specify environment without prompt](#specify-environment-without-prompt)
- [Writing .env to secure directory](#writing-env-to-secure-directory)
- [Change working directory](#change-working-directory)
Expand All @@ -15,6 +16,20 @@ By default Novops uses `.novops.yml` to load secrets. Use `novops load -c PATH`
novops load -c /path/to/novops/config.yml
```

## Run a sub-process

Use `novops run`

```sh
novops run sh
```

Use `FLAG -- COMMAND...` to provide flags:

```sh
novops run -e dev -c /tmp/novops.yml -- run terraform apply
```

## Specify environment without prompt

Use `novops load -e ENV` to load environment without prompting
Expand Down
14 changes: 11 additions & 3 deletions docs/src/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,12 @@ source <(novops load)
# source <(novops load -e dev) to avoid prompting for environment
```

Or run sub-process:

```sh
novops run sh
```

You can now access variables:

```sh
Expand Down Expand Up @@ -117,7 +123,8 @@ environments:
Load config:
```sh
source <(novops load -e dev)
source <(novops load -e dev)
# or novops run -e dev sh

env | grep AWS
# AWS_ACCESS_KEY_ID=AKIA...
Expand All @@ -138,10 +145,11 @@ cat $TOKEN_FILE

Novops tries to be secure:

- Secrets are kept in-memory: directly sourced in shell via standard output
- Secrets are kept in-memory: directly sourced in shell via standard output or by running a sub-process with environment variables
- Secrets won't be written to disk unless specifically asked by user
- If secrets are to be written to disk, Novops try to use a secure directory ([`XDG_RUNTIME_DIR`, see Security](advanced/security.md)) so they are protected and not persisted.
- Secrets are loaded temporarily: being in memory, they'll disappear as soon as process finished using them. No secret is persisted. When possible, Novops generates temporary secrets (e.g. credentials)
- Secrets are loaded temporarily: being in memory, they'll disappear as soon as process finished using them. No secret is persisted
- Novops generates temporary secrets (e.g. credentials) when possible

## Next steps

Expand Down
97 changes: 64 additions & 33 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,17 @@ use std::path::PathBuf;
use std::env;
use std::collections::HashMap;
use schemars::schema_for;
use std::process::Command;
use std::os::unix::process::CommandExt;

#[derive(Debug)]
pub struct NovopsArgs {
pub struct NovopsLoadArgs {
pub config: String,

pub env: Option<String>,

pub format: String,

pub working_directory: Option<String>,

pub symlink: Option<String>,

pub dry_run: Option<bool>

}
Expand All @@ -47,27 +45,47 @@ pub struct NovopsOutputs {
pub files: HashMap<String, FileOutput>
}

// pub async fn parse_arg_and_run() -> Result<(), anyhow::Error> {
// let args = NovopsArgs::parse();
// run(args).await
// }
pub async fn load_environment_write_vars(args: &NovopsLoadArgs, symlink: &Option<String>, format: &str) -> Result<(), anyhow::Error> {

let outputs = load_environment_no_write_vars(&args).await?;

let voutputs: Vec<VariableOutput> = outputs.variables.clone().into_iter().map(|(_, v)| v).collect();
export_variable_outputs(format, symlink, &voutputs, &outputs.context.workdir)?;

info!("Novops environment loaded ! Export variables with:");
info!(" source {:?}", &outputs.context.env_var_filepath);

Ok(())
}

pub async fn load_environment_no_write_vars(args: &NovopsLoadArgs) -> Result<NovopsOutputs, anyhow::Error> {

pub async fn load_environment(args: NovopsArgs) -> Result<(), anyhow::Error> {
init_logger();

// Read config from args and resolve all inputs to their concrete outputs
let outputs = load_context_and_resolve(&args).await?;

// Export output to user as per input (stdout or file)
export_outputs(&args, &outputs).await?;

info!("Novops environment loaded ! Export variables with:");
info!(" source {:?}", &outputs.context.env_var_filepath);
let foutputs: Vec<FileOutput> = outputs.files.clone().into_iter().map(|(_, f)| f).collect();
export_file_outputs(&foutputs)?;

Ok(outputs)

}

pub async fn load_environment_and_exec(args: &NovopsLoadArgs, command_args: Vec<&String>) -> Result<(), anyhow::Error> {

let outputs = load_environment_no_write_vars(&args).await?;
let vars : Vec<VariableOutput> = outputs.variables.into_values().collect();

let mut cmd = prepare_exec_command(command_args, &vars);
exec_replace(&mut cmd)
.with_context(|| format!("Error running process {:?} {:?}", &cmd.get_program(), &cmd.get_args()))?;

Ok(())
}

pub async fn load_context_and_resolve(args: &NovopsArgs) -> Result<NovopsOutputs, anyhow::Error> {
pub async fn load_context_and_resolve(args: &NovopsLoadArgs) -> Result<NovopsOutputs, anyhow::Error> {

debug!("Loading context for {:?}", &args);

Expand All @@ -85,6 +103,35 @@ pub async fn load_context_and_resolve(args: &NovopsArgs) -> Result<NovopsOutputs
})
}

pub fn prepare_exec_command(mut command_args: Vec<&String>, variables: &Vec<VariableOutput>) -> Command{
// first element of command argument is passed as child program
// everything else is passed as arguments
let child_program = command_args.remove(0);
let child_args = command_args;

let mut command = Command::new(&child_program);
command.args(&child_args);


for var in variables {
command.env(&var.name, &var.value);
}

command
}

/**
* Run child process replacing current process
* Never returns () as child process should have replaced current process
*/
fn exec_replace(cmd: &mut Command) -> Result<(), anyhow::Error>{

info!("Running child command: {:?} {:?}", &cmd.get_program(), &cmd.get_args());
let error = cmd.exec();

Err(anyhow::Error::new(error))
}

/**
* Initialize logger. Ca be called more than once.
* Novops always logs to stderr as stdout is reserved from output environment variables.
Expand All @@ -105,7 +152,7 @@ pub fn init_logger() {
/**
* Generate Novops context from arguments, env vars and Novops config
*/
pub async fn make_context(args: &NovopsArgs) -> Result<NovopsContext, anyhow::Error> {
pub async fn make_context(args: &NovopsLoadArgs) -> Result<NovopsContext, anyhow::Error> {
// Read CLI args and load config
let config = read_config_file(&args.config)
.with_context(|| format!("Error reading config file '{:}'", &args.config))?;
Expand Down Expand Up @@ -207,22 +254,6 @@ pub async fn resolve_environment_inputs(ctx: &NovopsContext, inputs: NovopsEnvir

}

/**
* Export output variables and files
* Variables are either shown as stdout or written to disk
* Files are always written to disk
*/
pub async fn export_outputs(args: &NovopsArgs, outputs: &NovopsOutputs) -> Result<(), anyhow::Error> {

let foutputs: Vec<FileOutput> = outputs.files.clone().into_iter().map(|(_, f)| f).collect();
export_file_outputs(&foutputs)?;

let voutputs: Vec<VariableOutput> = outputs.variables.clone().into_iter().map(|(_, v)| v).collect();
export_variable_outputs(&args.format, &args.symlink, &voutputs, &outputs.context.workdir)?;

Ok(())
}

fn read_config_file(config_path: &str) -> Result<NovopsConfigFile, anyhow::Error> {
let f = std::fs::File::open(config_path)?;
let config: NovopsConfigFile = serde_yaml::from_reader(f)
Expand Down Expand Up @@ -253,7 +284,7 @@ fn read_environment_name(config: &NovopsConfigFile, flag: &Option<String>) -> Re
*
* Returns the absolute path to working directory
*/
fn prepare_working_directory(args: &NovopsArgs, app_name: &String, env_name: &String) -> Result<PathBuf, anyhow::Error> {
fn prepare_working_directory(args: &NovopsLoadArgs, app_name: &String, env_name: &String) -> Result<PathBuf, anyhow::Error> {

let workdir = match &args.working_directory {
Some(wd) => PathBuf::from(wd),
Expand Down
Loading

0 comments on commit 1994669

Please sign in to comment.