Skip to content

Commit

Permalink
working for statefulset, but not tested well
Browse files Browse the repository at this point in the history
  • Loading branch information
slackspace-io committed Mar 23, 2024
1 parent bd2ffcc commit 8dcb3c1
Show file tree
Hide file tree
Showing 8 changed files with 230 additions and 45 deletions.
52 changes: 52 additions & 0 deletions backend/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions backend/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ config = "0.14.0"
serde_derive = "1.0.197"
cron = "0.12.1"
git2 = "0.18.3"
serde_yaml = "0.9.33"
walkdir = "2.5.0"

[features]
default = ["kube/runtime"]
Expand Down
132 changes: 97 additions & 35 deletions backend/src/gitops/gitops.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,27 @@
use crate::config::Settings;
use crate::models::models::Workload;
use futures::FutureExt;
use git2::{
Commit, Cred, ErrorCode, IndexAddOption, PushOptions, RemoteCallbacks, Repository, Signature,
};
use walkdir::WalkDir;

use crate::web::exweb::update_workload;
use k8s_openapi::api::apps::v1::{Deployment, StatefulSet};
use std::error::Error;
use std::fs::OpenOptions;
use std::io::Write;
use std::fs::{File, OpenOptions};
use std::io::{Read, Write};
use std::path::Path;

fn delete_local_repo() -> Result<(), std::io::Error> {
let local_path = Path::new("/tmp/repos/");
std::fs::remove_dir_all(local_path)?;
//delete if exists
if local_path.exists() {
std::fs::remove_dir_all(local_path)?;
}
Ok(())
}
/// Clone or open a repository based on the provided URL and local path.

fn clone_or_open_repo(
repo_url: &str,
repo_path: &Path,
Expand Down Expand Up @@ -40,25 +49,79 @@ fn clone_or_open_repo(
}
}

/// Perform file modifications needed for your workload.
/// This is a placeholder function; implement your file editing logic here.
fn edit_files(local_path: &Path) -> Result<(), Box<dyn Error>> {
// Example: Append a line to a file in the repository.
let file_path = local_path.join("file_to_edit.txt");
let mut file = OpenOptions::new().append(true).open(file_path)?;
writeln!(file, "New line added by gitops operation.")?;
Ok(())
fn edit_files(local_path: &Path, workload: &Workload) {
let name = &workload.name;
let search_path = local_path.join(name);
let image = Some(workload.image.clone());
let current_version = Some(workload.current_version.clone());
let latest_version = Some(workload.latest_version.clone());
//split image to get base image
let image_copy = image.clone().unwrap();
//use latest_version tag to make new image name
let base_image = image_copy.split(":").collect::<Vec<&str>>()[0];
let new_image = format!("{}:{}", base_image, latest_version.unwrap());
log::info!("Base image: {}", &base_image);
log::info!("New image: {}", &new_image);
//list files
for entry in WalkDir::new(search_path).into_iter().filter_map(|e| e.ok()) {
log::info!("Entry: {:?}", entry.path());
if entry.path().extension().unwrap_or_default() == "yaml" {
log::info!("YAML file found: {:?}", entry.path());
let mut file = File::open(entry.path()).unwrap();
let mut contents = String::new();
file.read_to_string(&mut contents).unwrap();
let mut image_updated = false; // Flag to track if the image was updated

let deployment: Result<Deployment, _> = serde_yaml::from_str(&contents);
if let Ok(deployment) = deployment {
log::info!("File: {:?}", entry.path());
log::info!("Deployment: {:?}", &deployment);
} else {
log::info!("Not a deployment {:?}", entry.path());
}
let statefulset_result: Result<StatefulSet, _> = serde_yaml::from_str(&contents);
if let Ok(mut statefulset) = statefulset_result {
if let Some(spec) = statefulset.spec.as_mut() {
if let Some(template_spec) = spec.template.spec.as_mut() {
for container in &mut template_spec.containers {
// Replace image in StatefulSet
if container.image.as_ref().unwrap().contains(&base_image) {
log::info!("Found target image in file: {:?}", entry.path());
container.image = Some(new_image.clone());
image_updated = true; // Image has been updated

//Set flag that image has been updated
}
log::info!("Found target image in file: {:?}", entry.path());
}
}
}
log::info!("New StatefulSet: {:?}", &mut statefulset);
if image_updated {
log::info!("Updating image in file: {:?}", entry.path());
let mut file = OpenOptions::new()
.write(true)
.truncate(true)
.open(entry.path())
.unwrap();
file.write_all(serde_yaml::to_string(&statefulset).unwrap().as_bytes())
.unwrap();
}
} else {
log::info!("Not a statefulset {:?}", entry.path());
// Handle non-statefulset scenario
}
}
}
}

/// Stage all changes in the repository.
fn stage_changes(repo: &Repository) -> Result<(), git2::Error> {
let mut index = repo.index()?;
index.add_all(["*"].iter(), IndexAddOption::DEFAULT, None)?;
index.write()?;
Ok(())
}

/// Commit the staged changes to the repository.
fn commit_changes<'a>(repo: &'a Repository, message: &str) -> Result<Commit<'a>, git2::Error> {
let sig = Signature::now("Your Name", "your_email@example.com")?;
let oid = repo.index()?.write_tree()?;
Expand All @@ -68,14 +131,12 @@ fn commit_changes<'a>(repo: &'a Repository, message: &str) -> Result<Commit<'a>,
Ok(repo.find_commit(commit)?)
}

/// Find the last commit in the repository.
fn find_last_commit(repo: &Repository) -> Result<Commit<'_>, git2::Error> {
let obj = repo.head()?.resolve()?.peel(git2::ObjectType::Commit)?;
obj.into_commit()
.map_err(|_| git2::Error::from_str("Couldn't find commit"))
}

/// Push the changes to the remote repository.
fn push_changes(repo: &Repository, access_token: &str) -> Result<(), git2::Error> {
let mut cb = RemoteCallbacks::new();
cb.credentials(|_url, username_from_url, _allowed_types| {
Expand All @@ -90,37 +151,38 @@ fn push_changes(repo: &Repository, access_token: &str) -> Result<(), git2::Error
Ok(())
}

/// Main function to run git operations.
pub fn run_git_operations(settings: Settings) -> Result<(), Box<dyn Error>> {
pub fn run_git_operations(workload: Workload) -> Result<(), Box<dyn Error>> {
let settings = Settings::new().unwrap_or_else(|err| {
log::error!("Failed to load settings: {}", err);
panic!("Failed to load settings: {}", err);
});
for gitops_config in settings.gitops {
log::info!("Gitops config: {:?}", gitops_config);
log::info!("Workload: {:?}", workload);
if gitops_config.name.as_str() != workload.git_ops_repo.clone().unwrap_or_default().as_str()
{
log::info!(
"Skipping gitops operation for repository: {}",
gitops_config.name
);
continue;
}
let repo_url = gitops_config.repository_url;
let branch = gitops_config.branch;
let name = gitops_config.name;
let access_token_env_name = gitops_config.access_token_env_name;
//try to get environment variable otherwise default
let access_token = std::env::var(access_token_env_name).unwrap_or_default();
log::info!("Access token: {}", access_token);
let local_path = Path::new("/tmp/repos/").join(name);
log::info!("Running git operations for repository: {}", repo_url);
log::info!("Local path: {:?}", local_path);
delete_local_repo()?;
//let repo = clone_or_open_repo(&repo_url, &local_path, &access_token)?;

// edit_files(&local_path)?;
// stage_changes(&repo)?;
// commit_changes(&repo, "Automated commit by gitops.rs")?;
// push_changes(&repo, &access_token)?;
let repo = clone_or_open_repo(&repo_url, &local_path, &access_token)?;
edit_files(&local_path, &workload);
//stage_changes(&repo)?;
//commit_changes(&repo, "Automated commit by gitops.rs")?;
//push_changes(&repo, &access_token)?;
}

Ok(())
}
//) -> Result<(), Box<dyn Error>> {
// let repo = clone_or_open_repo(repo_url, local_path)?;
//
// //edit_files(local_path)?;
// //stage_changes(&repo)?;
// //commit_changes(&repo, "Automated commit by gitops.rs")?;
// //push_changes(&repo, access_token)?;
//
// Ok(())
//}
30 changes: 30 additions & 0 deletions backend/src/models/gitops_models.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
use serde_derive::{Deserialize, Serialize};

// Define a structure to deserialize relevant parts of the YAML.
#[derive(Debug, Serialize, Deserialize)]
pub struct Deployment {
apiVersion: String,
kind: String,
// You might need to adjust the structure based on the actual content of your YAML files
spec: Spec,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct Spec {
template: Template,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct Template {
spec: PodSpec,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct PodSpec {
containers: Vec<Container>,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct Container {
image: String,
}
1 change: 1 addition & 0 deletions backend/src/models/mod.rs
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
pub mod gitops_models;
pub mod models;
6 changes: 6 additions & 0 deletions backend/src/models/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,9 @@ pub enum UpdateStatus {
Available,
NotAvailable,
}

#[derive(Serialize)]
pub struct ApiResponse {
pub(crate) status: String,
pub(crate) message: String,
}
47 changes: 38 additions & 9 deletions backend/src/web/exweb.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
use crate::database;
use crate::database::client::get_latest_scan_id;
use crate::gitops::gitops::run_git_operations;
use crate::models::models;
use crate::models::models::{ApiResponse, Workload};
use crate::services;
use crate::services::workloads;
use actix_web::{get, web, App, HttpRequest, HttpResponse, HttpServer, Responder};
Expand Down Expand Up @@ -28,14 +31,18 @@ async fn fetch_all_workloads() -> impl Responder {

#[get("/api/workloads/refresh")]
async fn refresh_workloads() -> impl Responder {
//fetch and update all workloads return if successful or error
if let Ok(_) = services::workloads::fetch_and_update_all_watched().await {
HttpResponse::Ok().body("Workloads refreshed")
} else if let Err(e) = services::workloads::fetch_and_update_all_watched().await {
HttpResponse::Ok().json(e.to_string())
} else {
HttpResponse::Ok().body("Workload not found")
}
tokio::spawn(async move {
// This is the background task.
// Since we're not waiting on it, we won't hold up the HTTP response.
match services::workloads::fetch_and_update_all_watched().await {
Ok(_) => println!("Workloads refreshed successfully."),
Err(e) => eprintln!("Failed to refresh workloads: {}", e),
}
});
HttpResponse::Ok().json(ApiResponse {
status: "success".to_string(),
message: "Workloads refreshed".to_string(),
})
}

#[get("/api/workloads/{name}/{namespace}")]
Expand All @@ -57,7 +64,29 @@ async fn fetch_workload(path: web::Path<(String, String)>) -> impl Responder {
async fn update_workload(req: HttpRequest) -> impl Responder {
let query_params =
web::Query::<HashMap<String, String>>::from_query(req.query_string()).unwrap();
log::info!("Query params: {:?}", query_params);
//create workload struct
let name = query_params.get("name").unwrap();
let namespace = query_params.get("namespace").unwrap();
let image = query_params.get("image").unwrap();
let current_version = query_params.get("current_version").unwrap();
let latest_version = query_params.get("latest_version").unwrap();
let git_ops_repo = query_params.get("git_ops_repo").unwrap();
let workload = Workload {
name: name.clone(),
exclude_pattern: None,
git_ops_repo: Some(git_ops_repo.clone()),
include_pattern: None,
update_available: models::UpdateStatus::Available,
image: image.clone(),
last_scanned: "2021-08-01".to_string(),
namespace: namespace.clone(),
current_version: current_version.clone(),
latest_version: latest_version.clone(),
};

log::info!("name = {}, namespace = {}, image = {}, current_version = {}, latest_version = {}, git_ops_repo = {}", name, namespace, image, current_version, latest_version, git_ops_repo);
log::info!("Workload: {:?}", workload);
run_git_operations(workload).unwrap();
HttpResponse::Ok().body("Workload updated")
}

Expand Down
Loading

0 comments on commit 8dcb3c1

Please sign in to comment.