Skip to content

Commit

Permalink
Merge pull request #2 from Infisical/daniel/caching-and-process_env
Browse files Browse the repository at this point in the history
Feat: Cache and process env support
  • Loading branch information
DanielHougaard authored Dec 24, 2023
2 parents 5c387ce + 0242cb5 commit 4df097d
Show file tree
Hide file tree
Showing 14 changed files with 274 additions and 104 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ languages/node/**/lib
InfisicalClient.egg-info

__pycache__
crates/infisical-py/infisical_client/schemas.py


infisical_py.so
Expand All @@ -180,4 +181,4 @@ infisical_py.so
languages/java/src/main/java/com/infisical/sdk/schema/*.java
languages/java/build
languages/java/.gradle
languages/java/src/main/resources/*
languages/java/src/main/resources/*
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@
"./crates/infisical-c/Cargo.toml",
"./crates/infisical-py/Cargo.toml"
],
"cSpell.words": ["infisical", "libinfisical", "openapi", "openapitools", "quicktype"],
"cSpell.words": ["infisical", "libinfisical", "openapi", "openapitools", "quicktype", "reqwest"],
"java.configuration.updateBuildConfiguration": "interactive"
}
49 changes: 16 additions & 33 deletions crates/infisical-py/example.py
Original file line number Diff line number Diff line change
@@ -1,40 +1,23 @@
from infisical_client import InfisicalClient, GetSecretOptions, ClientSettings, DeleteSecretOptions, CreateSecretOptions, UpdateSecretOptions, ListSecretsOptions

import time
client = InfisicalClient(ClientSettings(
client_id="77719230-a0b6-4590-8fbd-376e8b0898a0",
client_secret="4c9730a338dc64222114c473e8895311e5d34a1547e111fc173a67e418aed3a0",
site_url="http://localhost:8080" # This is optional. If not provided, it will default to https://app.infisical.com
))

client.createSecret(options=CreateSecretOptions(
secret_name="API_KEY",
secret_value="Some API Key",
environment="dev",
project_id="658066938ffb84aa0aa507f6"
))

secret = client.getSecret(options=GetSecretOptions(
environment="dev",
project_id="658066938ffb84aa0aa507f6",
secret_name="API_KEY",
type="personal"
client_id="92e6dae7-38ab-485d-8625-945a4f72c899",
client_secret="082ca0e72bfb8391acb834a7471e52773fab90cb61a95d10764ade9327ef347e",
site_url="http://localhost:8080", # This is optional. If not provided, it will default to https://app.infisical.com
cache_ttl=5,
))


client.updateSecret(options=UpdateSecretOptions(
secret_name="API_KEY",
secret_value="new secret value!",
client.getSecret(options=GetSecretOptions(
environment="dev",
project_id="658066938ffb84aa0aa507f6"
project_id="6587ff06fe3abf0cb8bf1742",
secret_name="TEST"
))


client.listSecrets(options=ListSecretsOptions(
environment="dev",
project_id="658066938ffb84aa0aa507f6",
))
client.deleteSecret(options=DeleteSecretOptions(
environment="dev",
project_id="658066938ffb84aa0aa507f6",
secret_name="API_KEY"
))
# Test rust multi threaded cache (should be super fast)
while True:
secret = client.getSecret(options=GetSecretOptions(
environment="dev",
project_id="6587ff06fe3abf0cb8bf1742",
secret_name="TEST"
))
print(secret.secret_value)
9 changes: 8 additions & 1 deletion crates/infisical-py/infisical_client/infisical_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from .schemas import DeleteSecretOptions, ResponseForDeleteSecretResponse
from .schemas import CreateSecretOptions, ResponseForCreateSecretResponse
import infisical_py
import os

class InfisicalClient:
def __init__(self, settings: ClientSettings = None):
Expand Down Expand Up @@ -36,7 +37,13 @@ def getSecret(self, options: GetSecretOptions) -> SecretElement:
def listSecrets(self, options: ListSecretsOptions) -> List[SecretElement]:
result = self._run_command(Command(list_secrets=options))

return ResponseForListSecretsResponse.from_dict(result).data.secrets
secrets = ResponseForListSecretsResponse.from_dict(result).data.secrets

# Setting the env in Rust is not enough for Python apparently, so we have to do this as well.
for secret in secrets:
if(options.attach_to_process_env):
os.environ[secret.secret_key] = secret.secret_value


def updateSecret(self, options: UpdateSecretOptions) -> SecretElement:
result = self._run_command(Command(update_secret=options))
Expand Down
22 changes: 20 additions & 2 deletions crates/infisical/src/api/secrets/get_secret.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::cache::{add_to_cache, create_cache_key, get_secret_from_cache};
use crate::error::api_error_handler;
use crate::helper::{build_base_request, build_url};
use crate::manager::secrets::{GetSecretOptions, GetSecretResponse};
Expand All @@ -12,17 +13,32 @@ pub async fn get_secret_request(
let base_url = format!(
"{}/api/v3/secrets/raw/{}",
client.site_url.clone(),
input.secret_name
&input.secret_name
);

let json = &serde_json::json!({
let json: &serde_json::Value = &serde_json::json!({
"workspaceId": input.project_id,
"environment": input.environment,
"secretPath": input.path.as_ref().unwrap_or(&"/".to_string()), // default is "/"
"type": input.r#type.as_ref().unwrap_or(&"shared".to_string()), // default is shared
"include_imports": input.include_imports.as_ref().unwrap_or(&false), // default is false
});

let secret_type = match input.r#type.as_ref() {
Some(r#type) => r#type,
None => "shared",
};
let cached_secret = get_secret_from_cache(
client,
&create_cache_key(&input.secret_name, secret_type, &input.environment),
);

if cached_secret.is_some() {
return Ok(GetSecretResponse {
secret: cached_secret.unwrap(),
});
}

let url = build_url(base_url, json);

let base_request = build_base_request(client, &url, reqwest::Method::GET);
Expand All @@ -49,6 +65,8 @@ pub async fn get_secret_request(
if status == StatusCode::OK {
let response = response.json::<GetSecretResponse>().await?;

add_to_cache(client, &response.secret);

Ok(response)
} else {
let err =
Expand Down
8 changes: 8 additions & 0 deletions crates/infisical/src/api/secrets/list_secrets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,14 @@ pub async fn list_secrets_request(
if status == StatusCode::OK {
let response = response.json::<ListSecretsResponse>().await?;

if input.attach_to_process_env.unwrap_or(false) == true {
let secrets = response.secrets.clone();

for secret in secrets {
std::env::set_var(secret.secret_key, secret.secret_value);
}
}

Ok(response)
} else {
let err = api_error_handler(status, response, None, false).await?;
Expand Down
149 changes: 149 additions & 0 deletions crates/infisical/src/cache.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
use log::{debug, error};

use crate::{manager::secrets::Secret, Client};
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::Duration;
use std::time::{SystemTime, SystemTimeError};

#[derive(Clone)]
pub struct CachedSecret {
pub key: String,
pub secret: Secret,

// unix timestamp
pub expires_at: u64,
}

fn get_sys_time_in_ms() -> Result<u64, SystemTimeError> {
let sec = match SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) {
Ok(n) => n.as_secs(),
Err(e) => return Err(e),
};

return Ok(sec * 1000);
}

pub fn create_cache_key(secret_key: &str, secret_type: &str, environment: &str) -> String {
return format!("{}-{}-{}", secret_key, environment, secret_type);
}

pub fn add_to_cache(client: &mut Client, secret: &Secret) {
if client.cache_ttl == 0 {
debug!("[CACHE]: Cache TTL is set to 0, not adding secret to cache.");
return;
}

let key = create_cache_key(&secret.secret_key, &secret.r#type, &secret.environment);

let existing_secret = get_secret_from_cache(client, &key);

if existing_secret.is_some() {
debug!("[CACHE]: Secret already exists in cache, not adding it again.");
return;
}

let expires_at = match get_sys_time_in_ms() {
Ok(n) => n + (client.cache_ttl * 1000),
Err(e) => {
error!("[CACHE]: Error adding secret to cache: {}", e);
return;
}
};

let cached_secret = CachedSecret {
key,
expires_at,
secret: secret.clone(),
};

{
let mut cache = client.cache.lock().unwrap();
cache.push(cached_secret);
debug!(
"[CACHE]: Element added to cache, index: {:?}",
cache.len() - 1
);
} // Mutex lock guard is dropped here when it goes out of scope
}

// We only start this thread if the cache_ttl is greater than 0.
pub fn cache_thread(cache: Arc<Mutex<Vec<CachedSecret>>>) {
let cloned_cache = Arc::clone(&cache);

std::thread::spawn(move || loop {
// We scope it here so it doesn't stay locked while we sleep.
{
let mut locked_cache = cloned_cache.lock().unwrap();

let current_time = match get_sys_time_in_ms() {
Ok(n) => n,
Err(e) => {
error!("Error getting current time: {}", e);
return;
}
};

if let Some(index) = locked_cache
.iter()
.position(|x| x.expires_at < current_time)
{
locked_cache.remove(index);
debug!(
"[CACHE]: Element removed from cache, removed index: {:?}",
index
);
}
}
// Mutex guard dropped here, allowing other threads to access the cache
thread::sleep(Duration::from_secs(10)); // Check every 10 seconds, this is an arbitrary number.
});
}

pub fn get_secret_from_cache(client: &mut Client, key: &String) -> Option<Secret> {
if client.cache_ttl == 0 {
debug!("[CACHE]: Cache TTL is set to 0, not adding secret to cache.");
return None;
}

let mut locked_cache = client.cache.lock().unwrap();

// Get index of the secret
let index = match locked_cache
.iter()
.position(|cached_secret| &cached_secret.key == key)
{
Some(index) => index,
None => return None,
};

// Get the new expires at time, if it fails just return no secret.
let expires_at = match get_sys_time_in_ms() {
Ok(n) => n + (client.cache_ttl * 1000),
Err(e) => {
error!(
"[CACHE]: Error getting new expiry date for cache element: {}",
e
);
return None;
}
};

let secret = locked_cache[index].secret.clone();

// Create a new cached secret
let cached_secret = CachedSecret {
key: key.to_string(),
expires_at,
secret: secret.clone(),
};

locked_cache.remove(index); // Remove the old cached secret
locked_cache.push(cached_secret); // Add the new cached secret

debug!(
"[CACHE]: Found cached secret with cache key, and updated the expiry time on it: {}",
key
);
return Some(secret);
}
27 changes: 24 additions & 3 deletions crates/infisical/src/client/client.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,27 @@
use crate::client::client_settings::ClientSettings;

use crate::{
cache::{cache_thread, CachedSecret},
client::client_settings::ClientSettings,
};
use std::sync::{Arc, Mutex};
pub(crate) struct ClientAuth {
pub client_id: String,
pub client_secret: String,
pub access_token: Option<String>,
}
pub struct Client {
pub(crate) auth: ClientAuth,

pub(crate) cache: Arc<Mutex<Vec<CachedSecret>>>,
pub(crate) cache_ttl: u64, // No need for a mutex lock here, as we are only reading this value in the cache thread.

pub site_url: String,
}

impl Client {
pub fn new(settings_input: Option<ClientSettings>) -> Self {
let settings = settings_input.unwrap();

Self {
let c = Self {
auth: ClientAuth {
client_id: settings.client_id.unwrap_or("".to_string()),
client_secret: settings.client_secret.unwrap_or("".to_string()),
Expand All @@ -23,7 +30,21 @@ impl Client {
site_url: settings
.site_url
.unwrap_or("https://app.infisical.com".to_string()),

cache: Arc::new(Mutex::new(Vec::new())),
cache_ttl: settings.cache_ttl.unwrap_or(300),
};

if c.cache_ttl != 0 {
cache_thread(Arc::clone(&c.cache));
}
return c;
}

pub fn set_cache(&self, new_cache: &[CachedSecret]) {
let mut cache = self.cache.lock().unwrap();
cache.clear();
cache.extend(new_cache.iter().cloned());
}

pub fn set_access_token(&mut self, token: String) {
Expand Down
7 changes: 5 additions & 2 deletions crates/infisical/src/client/client_settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ pub struct ClientSettings {
// These are optional because the access token can be set directly as well
pub client_secret: Option<String>,
pub client_id: Option<String>,

// Access token is optional because the user can also provide maci
pub access_token: Option<String>,

// Access token is optional because the user can also provide a machine token.
pub site_url: Option<String>,

pub cache_ttl: Option<u64>, // This controls how often the cache should refresh, default is 300 seconds
}

impl Default for ClientSettings {
Expand All @@ -21,6 +23,7 @@ impl Default for ClientSettings {
client_id: None,
access_token: None,
site_url: None,
cache_ttl: None,
}
}
}
Loading

0 comments on commit 4df097d

Please sign in to comment.