Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Orctl #418

Merged
merged 18 commits into from
Jun 21, 2024
Merged

Orctl #418

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,4 @@ nb-configuration.xml
secrets.env
/docker/dev/docker/
mi6/export
kubernetes/.idea
7 changes: 7 additions & 0 deletions kubernetes/orctl
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#!/bin/bash

if [[ -f ../orctl/target/debug/orctl ]]; then
../orctl/target/debug/orctl "$@"
else
echo "Please build orctl, consult the README.md in ../orctl"
fi
21 changes: 21 additions & 0 deletions orctl/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Generated by Cargo
# will have compiled files and executables
debug/
target/

# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
Cargo.lock

# These are backup files generated by rustfmt
**/*.rs.bk

# MSVC Windows builds of rustc generate these, which store debugging information
*.pdb

# RustRover
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
.idea/
12 changes: 12 additions & 0 deletions orctl/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[package]
name = "orctl"
version = "0.1.0"
edition = "2021"

[dependencies]
clap={version="4", features=["derive"]}
serde_yaml={version="0.9.34"}
base64={version="0.22.1"}
colored={version="2.1.0"}
thiserror = "1.0"
dotenv = "0.15.0"
19 changes: 19 additions & 0 deletions orctl/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# ORCTL
## Orchestration Control

### Installation

- visit https://www.rust-lang.org/tools/install for full instructions

TL;DR for Linux and macOS

```shell
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
```

### Build code

```shell
cargo build
```

48 changes: 48 additions & 0 deletions orctl/src/commands/commands.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
use clap::{Subcommand, ValueEnum};
use crate::models::secret_files::SecretFiles;

#[derive(Subcommand)]
pub(crate) enum Commands {
/// get the status of a deployment
Status {
#[arg(short, long)]
pod: bool,
#[arg(short, long)]
svc: bool,
},
/// get secret values for a deployment
Secrets {
#[command(subcommand)]
command: SecretCommand,
},
}

#[derive(Subcommand)]
pub(crate) enum SecretCommand {
/// Decrypt
Decrypt {
/// What secret file to operate on
#[arg(value_enum)]
file: SecretFiles,
},
/// Encrypt
Encrypt {
/// What secret file to operate on
#[arg(value_enum)]
file: SecretFiles,

/// What key to operate on
key: String,

/// If decrypt what value to encode
value: String,
},
}

#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
pub(crate) enum Operation {
/// Decrypt
Decrypt,
/// Encrypt
Encrypt,
}
3 changes: 3 additions & 0 deletions orctl/src/commands/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
pub(crate) mod commands;
pub(crate) mod status;
pub(crate) mod secrets;
51 changes: 51 additions & 0 deletions orctl/src/commands/secrets.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
use std::io;
use thiserror::Error;
use crate::commands::commands::SecretCommand;
use crate::config::verbosity::Verbosity;
use crate::models::deployment_environment::Environment;

mod common;
mod decrypt;
mod encrypt;

#[derive(Error, Debug)]
pub(crate) enum OperateOnSecretsError {
#[error("Failed to decrypt (file {file:?})")]
FailedToDecrypt { file: String, },
#[error("Failed to decrypt (file {file:?})")]
FailedToEncrypt { file: String, },
#[error("Failed to convert from utf8 (file {file:?})")]
FailedToConvertFileFromUtf8 { file: String, },
#[error("Failed to convert file contents to yaml")]
FailedToConvertFileToYaml(),

#[error("YAML file missing 'data' section")]
MissingDataSection(),
#[error("YAML file missing specified key: {key:?}")]
MissingKeySection { key: String },

#[error("File error: {error:?}")]
FileError{ error: io::Error },
#[error("Writing Yaml error: {error:?}")]
WriteYaml{ error: serde_yaml::Error },

#[error("Value missing")]
ValueMissing(),
#[error("Value failed to decode from base64)")]
ValueFailedToDecodeBase64(),
#[error("Value failed to decode from base64 (utf8 step)")]
ValueFailedToDecodeBase64Utf8(),
#[error("Key missing")]
KeyMissing(),
}

pub(crate) fn operate_on_secrets(commands: &SecretCommand, env: Environment, verbosity: Verbosity) -> Result<(), OperateOnSecretsError> {
match commands {
SecretCommand::Decrypt { file } => {
decrypt::get_secrets(env, verbosity, file)
}
SecretCommand::Encrypt { file, key, value } => {
encrypt::put_secret(env, verbosity, file, key, value)
}
}
}
94 changes: 94 additions & 0 deletions orctl/src/commands/secrets/common.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
use std::collections::HashMap;
use std::env::VarError;
use std::process::Command;
use std::{env, io};
use std::io::Write;
use std::string::ToString;
use serde_yaml::Value;
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
use base64::Engine;
use crate::commands::secrets::OperateOnSecretsError;
use crate::config::verbosity::Verbosity;
use crate::models::secret_files::SecretFile;

pub(crate) const AGE_PUBLIC_KEY: &str = "AGE_PUBLIC_KEY";
const SOPS_AGE_KEY_FILE: &str = "SOPS_AGE_KEY_FILE";

pub(crate) fn age_env_vars() -> HashMap<String, String> {
let mut env_vars: HashMap<String, String> = HashMap::new();
env_vars.insert(AGE_PUBLIC_KEY.to_string(), get_env_value(AGE_PUBLIC_KEY));
env_vars.insert(SOPS_AGE_KEY_FILE.to_string(), get_env_value(SOPS_AGE_KEY_FILE));
env_vars
}

pub(crate) fn get_env_value(key: &str) -> String {
match env::var(key) {
Ok(value) => value,
Err(VarError::NotPresent) | Err(VarError::NotUnicode(_)) => {
panic!("Environment Variable {} is missing", key);
},
}
}

fn decrypt_file(enc_file_name: String, env_vars: HashMap<String, String>, verbosity: Verbosity) -> Result<String, OperateOnSecretsError> {
if verbosity >= Verbosity::TRACE {
println!("decrypting file: {}", enc_file_name.clone());
}
let output = Command::new("sops")
.arg("--decrypt")
.arg(enc_file_name.clone())
.envs(&env_vars)
.output()
.expect("failed to execute");

if output.status.success() {
let file_contents = std::str::from_utf8(&output.stdout);
if file_contents.is_err() {
return Err(OperateOnSecretsError::FailedToConvertFileFromUtf8 { file: enc_file_name});
}

Ok(file_contents.unwrap().to_owned())
} else {
io::stderr().write_all(&output.stderr).unwrap();
return Err(OperateOnSecretsError::FailedToDecrypt { file: enc_file_name});
}
}

pub(crate) fn decode_row(key: &Value, value: &Value) -> Result<(String, String), OperateOnSecretsError> {
let val_base64 = value.as_str();
if val_base64.is_none() { return Err(OperateOnSecretsError::ValueMissing()) }
let val_base64 = val_base64.unwrap().to_string();

let val_decoded_from_base_64 =
BASE64_STANDARD.decode(val_base64);
if val_decoded_from_base_64.is_err() { return Err(OperateOnSecretsError::ValueFailedToDecodeBase64()) }
let val_decoded_from_base_64 = &val_decoded_from_base_64.unwrap();

let val_decoded = std::str::from_utf8(val_decoded_from_base_64);
if val_decoded.is_err() { return Err(OperateOnSecretsError::ValueFailedToDecodeBase64Utf8()) }
let value = val_decoded.unwrap().to_string();

let key = key.as_str();
if key.is_none() { return Err(OperateOnSecretsError::KeyMissing()) }
let key = key.unwrap().to_string();

Ok((key, value))
}

fn read_yaml(file_contents: String, verbosity: Verbosity) -> Result<Value, OperateOnSecretsError> {
if verbosity >= Verbosity::TRACE {
println!("Reading yaml");
}
let secret_contents: Result<serde_yaml::Value, serde_yaml::Error> =
serde_yaml::from_str(&*file_contents);
if secret_contents.is_err() {
return Err(OperateOnSecretsError::FailedToConvertFileToYaml());
}
Ok(secret_contents.unwrap())
}

pub(crate) fn get_yaml_contents_from_file(path: &str, file: SecretFile, verbosity: Verbosity) -> Result<Value, OperateOnSecretsError> {
let enc_file_name = format!("{0}{1}{2}", path, file.folder, file.enc_name);
let file_contents = decrypt_file(enc_file_name, age_env_vars(), verbosity)?;
read_yaml(file_contents, verbosity)
}
45 changes: 45 additions & 0 deletions orctl/src/commands/secrets/decrypt.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
use serde_yaml::Value;
use colored::Colorize;
use crate::commands::secrets::{common, OperateOnSecretsError};
use crate::config::verbosity::Verbosity;
use crate::models::deployment_environment::Environment;
use crate::models::secret_files::{SecretFile, SecretFiles};

pub(crate) fn get_secrets(env: Environment, verbosity: Verbosity, file_type: &SecretFiles) -> Result<(), OperateOnSecretsError> {
let file = SecretFile::by_type(file_type);
if verbosity >= Verbosity::INFO {
println!("Secrets op:decrypt, file:{}", file.dec_name);
}
let yaml_content = common::get_yaml_contents_from_file(env.secrets_path, file, verbosity)?;

if verbosity >= Verbosity::TRACE {
println!("Getting data object");
}
match yaml_content["data"].as_mapping() {
Some(data) => {
for (key, value) in data.iter() {
print_row(key, value);
}
}
None => {
return Err(OperateOnSecretsError::MissingDataSection());
}
}
Ok(())
}

fn print_row(key: &Value, value: &Value) {
match common::decode_row(key, value) {
Ok((key,value)) => {
println!(
"{}: {}",
key.yellow(),
value.purple(),
);
},
Err(error) => {
// catch and continue - attempt to process as much as possible
println!("Error: {}", error);
}
}
}
Loading
Loading