Make CDH great (...not again) #3

Merged
allanger merged 7 commits from refs/pull/3/head into main 2023-01-16 20:05:07 +00:00
14 changed files with 460 additions and 194 deletions

61
.github/workflows/build-version.yaml vendored Normal file
View File

@ -0,0 +1,61 @@
---
name: "Version build"
on:
push:
tags:
- "v*.*.*"
jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
- os: macos-latest
target: x86_64-apple-darwin
- os: macos-latest
target: aarch64-apple-darwin
steps:
- name: Checkout
uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
target: ${{ matrix.target }}
- uses: actions-rs/cargo@v1
with:
command: build
args: --release --all-features --target=${{ matrix.target }}
- name: Archive build artifacts
uses: actions/upload-artifact@v2
with:
name: build-${{matrix.target}}
path: ${{ github.workspace }}/target/${{ matrix.target }}/release/cdh
release:
needs: build
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Download artifact
uses: actions/download-artifact@v2
- name: Set version variable
run: echo "CDH_VERSION=${GITHUB_REF##*/}" >> $GITHUB_ENV
- name: Rename release to avoid name conflict
run: ./scripts/rename_releases.sh
- name: Release
uses: softprops/action-gh-release@v1
with:
files: release/*

53
.github/workflows/container-stable.yaml vendored Normal file
View File

@ -0,0 +1,53 @@
---
name: "Stable container"
on:
push:
branches:
- main
paths:
- "src/**"
jobs:
containerization:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Set action link variable
run: echo "LINK=$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" >> $GITHUB_ENV
- name: Set up QEMU
uses: docker/setup-qemu-action@master
with:
platforms: all
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@master
- name: Login to GitHub Container Registry
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.CR_PAT }}
- name: Build
uses: docker/build-push-action@v2
with:
builder: ${{ steps.buildx.outputs.name }}
context: .
file: ./Dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: |
ghcr.io/allanger/${{ env.GITHUB_REPOSITORY }}:stable
ghcr.io/allanger/${{ env.GITHUB_REPOSITORY }}:latest
labels: |
action_id=${{ github.action }}
action_link=${{ env.LINK }}
actor=${{ github.actor }}
sha=${{ github.sha }}
ref=${{ github.ref }}

View File

@ -0,0 +1,53 @@
---
name: "Version container"
on:
push:
tags:
- "v*.*.*"
jobs:
containerization:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Set version variable
run: echo "TAG=${GITHUB_REF##*/}" >> $GITHUB_ENV
- name: Set action link variable
run: echo "LINK=$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" >> $GITHUB_ENV
- name: Set up QEMU
uses: docker/setup-qemu-action@master
with:
platforms: all
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@master
- name: Login to GitHub Container Registry
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.CR_PAT }}
- name: Build
uses: docker/build-push-action@v2
with:
builder: ${{ steps.buildx.outputs.name }}
context: .
file: ./Dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: |
ghcr.io/allanger/${{ env.GITHUB_REPOSITORY }}:${{ env.TAG }}
labels: |
action_id=${{ github.action }}
action_link=${{ env.LINK }}
actor=${{ github.actor }}
sha=${{ github.sha }}
ref=${{ github.ref }}

57
.github/workflows/tests.yaml vendored Normal file
View File

@ -0,0 +1,57 @@
---
name: "Tests"
on:
pull_request:
branches:
- main
paths:
- "src/**"
jobs:
cargo_udeps:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
toolchain: nightly
- name: Install cargo-udeps
run: cargo install cargo-udeps --locked
- name: Check dependencies
run: cargo +nightly udeps
cargo_test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
- uses: actions-rs/cargo@v1
with:
command: build
args: --release --all-features
- run: cargo test
cargo_clippy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
- uses: actions-rs/cargo@v1
with:
command: build
args: --release --all-features
- run: cargo clippy

17
Cargo.lock generated
View File

@ -32,12 +32,6 @@ dependencies = [
"generic-array", "generic-array",
] ]
[[package]]
name = "build_html"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa8ffb62af7b0911893e2d6126891b2a018e387078fa5d855cb42d0d90d88075"
[[package]] [[package]]
name = "bytecount" name = "bytecount"
version = "0.6.3" version = "0.6.3"
@ -54,8 +48,8 @@ checksum = "e9f73505338f7d905b19d18738976aae232eb46b8efc15554ffc56deb5d9ebe4"
name = "cdh" name = "cdh"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"build_html",
"clap", "clap",
"clap_complete",
"env_logger", "env_logger",
"handlebars", "handlebars",
"log", "log",
@ -87,6 +81,15 @@ dependencies = [
"termcolor", "termcolor",
] ]
[[package]]
name = "clap_complete"
version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce8955d4e8cd4f28f9a01c93a050194c4d131e73ca02f6636bcddbed867014d7"
dependencies = [
"clap",
]
[[package]] [[package]]
name = "clap_derive" name = "clap_derive"
version = "4.1.0" version = "4.1.0"

View File

@ -14,5 +14,6 @@ version-compare = "0.1.0"
clap = { version = "4.1.1", features = ["derive", "env"] } clap = { version = "4.1.1", features = ["derive", "env"] }
serde_yaml = "0.9.16" serde_yaml = "0.9.16"
tabled = "0.10.0" tabled = "0.10.0"
build_html = "2.1.0"
handlebars = "4.3.1" handlebars = "4.3.1"
clap_complete = "4.0.6"

View File

@ -1,2 +1,33 @@
# Check Da Helm # Check Da Helm
> Your helm releases are outdated, aren't they? Now you can check! > Your helm releases are outdated, aren't they? Now you can check!
[![Version build](https://github.com/allanger/check-da-helm/actions/workflows/build-version.yaml/badge.svg)](https://github.com/allanger/check-da-helm/actions/workflows/build-version.yaml)
[![Version container](https://github.com/allanger/check-da-helm/actions/workflows/container-version.yaml/badge.svg)](https://github.com/allanger/check-da-helm/actions/workflows/container-version.yaml)
[![Stable container](https://github.com/allanger/check-da-helm/actions/workflows/container-stable.yaml/badge.svg)](https://github.com/allanger/check-da-helm/actions/workflows/container-stable.yaml)
## What's this?
It's a simple command line tool that lets you check whether your helm releases (currently installed by helmfile or argo) are outdated or not. Why it's created? But the main reason why it's created, is a necessity to check if helm releases that you have installed in your cluster still exist in repos. Once `Bitnami` removed old charts from their main repo and, I believe, everybody needed then some time to understand what happened. So I decided to write this tool. I was checking helmfiles and testing if chart were still in repos. And in case something is broken, I would be notified in the morning. Of course, broken helm charts are something you'll eventually know about, but it just feels better to know about them with this simple cli.
## Install
### Dependencies
Depending on the tool you want to use `cdm` with, you must have either `helmfile` or `argocd` installed. And in any case you need to have `helm`
### Download
Get executable from github releases
Prebuilt binaries exist for **Linux x86_64** and **MacOS arm64** and **x86_64**
Don't forget to add the binary to $PATH
```BASH
$ curl https://raw.githubusercontent.com/allanger/check-da-helm/main/scripts/download_cdm.sh | bash
$ cdm -h
```
### Build from source
1. Build binary
```BASH
$ cargo build --release
```
2. Run `gum help`

50
scripts/download_cdh.sh Executable file
View File

@ -0,0 +1,50 @@
#!/bin/bash
case "$(uname)" in
"Darwin")
SYSTEM="apple-darwin"
case $(uname -m) in
"arm64")
TARGET="aarch64-$SYSTEM"
;;
"x86_64")
TARGET="x86_64-$SYSTEM"
;;
*)
echo "Unsuported target"
exit 1
;;
esac
;;
"Linux")
SYSTEM="unknown-linux-gnu"
case $(uname -m) in
"x86_64")
TARGET="x86_64-$SYSTEM"
;;
*)
echo "Unsuported target"
exit 1
;;
esac
;;
*)
echo "Signal number $1 is not processed"
exit 1
;;
esac
LATEST_VERSION="v$(curl -s https://raw.githubusercontent.com/allanger/check-da-helm/main/Cargo.toml | awk -F ' = ' '$1 ~ /version/ { gsub(/[\"]/, "", $2); printf("%s",$2) }')"
echo "Downloading $LATEST_VERSION"
RELEASE_NAME=cdh-$LATEST_VERSION-$TARGET
RELEASE_URL="https://github.com/allanger/check-da-helm/releases/download/$LATEST_VERSION/$RELEASE_NAME"
echo "Link for downloading: $RELEASE_URL"
curl -LJO $RELEASE_URL
mv $RELEASE_NAME cdh
chmod +x cdh
echo 'Make sure that cdh is in your $PATH'
echo 'Try: '
echo ' $ export PATH=$PATH:$PWD'
echo ' $ cdh -h'

10
scripts/rename_releases.sh Executable file
View File

@ -0,0 +1,10 @@
#!/bin/bash
echo 'renaming cdh to cdh-$VERSION-$SYSTEM format'
mkdir -p release
echo "version - $CDH_VERSION"
for BUILD in build*; do
SYSTEM=$(echo $BUILD | sed -e 's/build-//g')
echo "system - $SYSTEM"
cp $BUILD/cdh release/cdh-$CDH_VERSION-$SYSTEM
done
ls release

View File

@ -1,10 +1,13 @@
use clap::Arg;
use log::{debug, info, error};
use serde_json::from_str;
use crate::types::{self, HelmRepo}; use crate::types::{self, HelmRepo};
use log::{debug, error, info};
use serde_json::from_str;
use super::Connector; use super::Connector;
use std::{borrow::Borrow, io::{Result, Error, ErrorKind}, process::Command}; use std::{
borrow::Borrow,
io::{Error, ErrorKind, Result},
process::Command,
};
pub(crate) struct Argo; pub(crate) struct Argo;
@ -104,9 +107,7 @@ impl Connector for Argo {
String::from_utf8_lossy(&output.stderr), String::from_utf8_lossy(&output.stderr),
)) ))
} }
} }
} }
impl Argo { impl Argo {
pub(crate) fn init() -> Argo { pub(crate) fn init() -> Argo {

View File

@ -4,7 +4,7 @@ use serde_json::from_str;
use crate::types; use crate::types;
use super::Connector; use super::Connector;
use std::{borrow::Borrow, fmt::format, io::Result, process::Command}; use std::{borrow::Borrow, io::Result, process::Command};
pub(crate) struct Helmfile { pub(crate) struct Helmfile {
path: String, path: String,

View File

@ -1,22 +1,21 @@
mod connectors; mod connectors;
mod output;
mod types; mod types;
use clap::{arg, command, Parser, Subcommand, ValueEnum};
use clap::{Parser, ValueEnum};
use connectors::{Argo, Connector, Helm, Helmfile}; use connectors::{Argo, Connector, Helm, Helmfile};
use handlebars::Handlebars;
use log::{debug, error, info, warn}; use log::{debug, error, info, warn};
use output::Output;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::from_str; use serde_json::from_str;
use std::{ use std::{
borrow::Borrow, borrow::Borrow,
fmt::{self, format}, io::Result,
io::{Error, ErrorKind, Result},
process::{exit, Command}, process::{exit, Command},
}; };
use tabled::Tabled; use types::ExecResult;
use version_compare::{Cmp, Version}; use version_compare::{Cmp, Version};
use crate::types::HelmChart; use crate::types::{HelmChart, Status};
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)] #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
enum Kinds { enum Kinds {
@ -25,13 +24,22 @@ enum Kinds {
Helmfile, Helmfile,
} }
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
enum Outputs {
Yaml,
HTML,
}
/// Check you helm releaseas managed by Argo /// Check you helm releaseas managed by Argo
#[derive(Parser)] #[derive(Parser)]
#[clap(author, version, about, long_about = None)] #[clap(author, version, about, long_about = None)]
struct Args { struct Args {
/// Type of the /// How do you install your helm charts
#[clap(long, value_enum)] #[clap(long, value_enum)]
kind: Kinds, kind: Kinds,
/// What kind of output would you like to receive?
#[clap(long, value_enum, default_value = "yaml")]
output: Outputs,
/// Path to the helmfile /// Path to the helmfile
#[clap(short, long, value_parser, default_value = "./")] #[clap(short, long, value_parser, default_value = "./")]
path: String, path: String,
@ -43,6 +51,14 @@ struct Args {
no_sync: bool, no_sync: bool,
} }
#[derive(Debug, Subcommand)]
enum Commands {
#[command(arg_required_else_help = true)]
Generate {
#[arg(value_name = "SHELL", default_missing_value = "zsh")]
shell: clap_complete::shells::Shell,
},
}
/// A struct to write helm repo description to /// A struct to write helm repo description to
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] #[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
struct Repo { struct Repo {
@ -58,42 +74,7 @@ struct LocalCharts {
version: Option<String>, version: Option<String>,
} }
/// Three possible statuses of versions comparison
#[derive(Clone, Serialize)]
enum Status {
Uptodate,
Outdated,
Missing,
}
impl fmt::Display for Status {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
Status::Uptodate => write!(f, "Up-to-date"),
Status::Outdated => write!(f, "Outdated"),
Status::Missing => write!(f, "Missing"),
}
}
}
#[derive(Clone, Tabled, Serialize)]
struct ExecResult {
name: String,
latest_version: String,
current_version: String,
status: Status,
}
// Implementation for the ExecResult struct // Implementation for the ExecResult struct
impl ExecResult {
fn new(name: String, latest_version: String, current_version: String, status: Status) -> Self {
Self {
name,
latest_version,
current_version,
status,
}
}
}
fn main() { fn main() {
// Preparations step // Preparations step
@ -122,12 +103,12 @@ fn main() {
} }
charts.iter().for_each(|a| { charts.iter().for_each(|a| {
let err = check_chart(&mut result, a); check_chart(&mut result, a).unwrap();
}); });
// Parse the helmfile // Parse the helmfile
// Handling the result // Handling the result
match handle_result(&result, args.outdated_fail) { match handle_result(&result, args.outdated_fail, args.output) {
Ok(result) => { Ok(result) => {
if result { if result {
exit(1); exit(1);
@ -141,7 +122,7 @@ fn main() {
} }
fn check_chart(result: &mut Vec<ExecResult>, local_chart: &types::HelmChart) -> Result<()> { fn check_chart(result: &mut Vec<ExecResult>, local_chart: &types::HelmChart) -> Result<()> {
if local_chart.clone().name.is_some() { if local_chart.name.is_some() {
let version = local_chart.version.clone().unwrap(); let version = local_chart.version.clone().unwrap();
let chart = local_chart.name.clone().unwrap(); let chart = local_chart.name.clone().unwrap();
return match version.is_empty() { return match version.is_empty() {
@ -215,7 +196,11 @@ fn check_chart(result: &mut Vec<ExecResult>, local_chart: &types::HelmChart) ->
} }
/// Handle result /// Handle result
fn handle_result(result: &Vec<ExecResult>, outdated_fail: bool) -> Result<bool> { fn handle_result(
result: &Vec<ExecResult>,
outdated_fail: bool,
output_kind: Outputs,
) -> Result<bool> {
let mut failed = false; let mut failed = false;
for r in result.clone() { for r in result.clone() {
match r.status { match r.status {
@ -238,135 +223,15 @@ fn handle_result(result: &Vec<ExecResult>, outdated_fail: bool) -> Result<bool>
} }
} }
} }
let template = r#"
<table>
<tr>
<th>Chart Name</th>
<th>Current Version</th>
<th>Latest Version</th>
<th>Status</th>
</tr>
{{#each this as |tr|}}
<tr>
<th>{{tr.name}}</th>
<th>{{tr.current_version}}</th>
<th>{{tr.latest_version}}</th>
<th>{{tr.status}}</th>
</tr>
{{/each}}
</table>
"#;
let mut reg = Handlebars::new();
// TODO: Handle this error match output_kind {
reg.register_template_string("html_table", template) Outputs::Yaml => print!("{}", output::YAML::print(result)?),
.unwrap(); Outputs::HTML => print!("{}", output::HTML::print(result)?),
match reg.render("html_table", &result) {
Ok(res) => println!("{}", res),
Err(err) => error!("{}", err),
}; };
Ok(failed) Ok(failed)
} }
/// Downloading repos from repositories
fn repo_sync() -> Result<()> {
info!("syncing helm repos");
let cmd: String = "argocd app list -o json | jq '[ .[] | {name: .spec.source.chart, url: .spec.source.repoURL} ]'".to_string();
let output = Command::new("bash")
.arg("-c")
.arg(cmd)
.output()
.expect("helmfile is failed");
info!("{:?}", output.clone());
if output.status.success() {
let repos: Vec<Repo> = serde_json::from_slice(&output.stdout).unwrap();
info!("adding repositories");
for repo in repos.iter() {
let name = repo.name.clone();
if name.is_some() {
info!(
"syncing {} with the origin {}",
name.clone().unwrap(),
repo.url
);
let cmd = format!(
"helm repo add {} {}",
name.clone().unwrap(),
repo.url.clone()
);
debug!("running {}", cmd);
let output = Command::new("bash")
.arg("-c")
.arg(cmd)
.output()
.expect("helm repo sync is failed");
match output.status.success() {
true => {
info!(
"{} with the origin {} is synced successfully",
name.unwrap(),
repo.url
);
}
false => {
error!(
"{} with the origin {} can't be synced",
name.unwrap(),
repo.url
)
}
}
}
}
let cmd = "helm repo update";
let output = Command::new("bash")
.arg("-c")
.arg(cmd)
.output()
.expect("helm repo sync is failed");
match output.status.success() {
true => {
info!("repositories are updated successfully");
}
false => {
error!(
"repositories can't be updated, {}",
String::from_utf8_lossy(&output.stderr)
);
}
}
Ok(())
} else {
Err(Error::new(
ErrorKind::Other,
String::from_utf8_lossy(&output.stderr),
))
}
}
/// Run helmfile list and write the result into struct
fn parse_argo_apps() -> Result<Vec<LocalCharts>> {
let cmd: String = "argocd app list -o json | jq '[.[] | {chart: .spec.source.chart, version: .spec.source.targetRevision}]'".to_string();
debug!("executing '${}'", cmd);
let output = Command::new("bash")
.arg("-c")
.arg(cmd)
.output()
.expect("helmfile is failed");
let helm_stdout = String::from_utf8_lossy(&output.stdout);
match from_str::<Vec<LocalCharts>>(Borrow::borrow(&helm_stdout)) {
Ok(mut charts) => {
charts.dedup();
Ok(charts)
}
Err(err) => Err(err.into()),
}
}
/// Takes two version and returns the newer one. /// Takes two version and returns the newer one.
fn get_newer_version(v1: String, v2: String) -> String { fn get_newer_version(v1: String, v2: String) -> String {
match Version::from(&v1.replace('v', "")) match Version::from(&v1.replace('v', ""))

61
src/output/mod.rs Normal file
View File

@ -0,0 +1,61 @@
use std::io::{Result, Error, ErrorKind};
use handlebars::Handlebars;
use log::error;
use crate::types::ExecResult;
pub(crate) trait Output {
fn print(data: &Vec<ExecResult>) -> Result<String>;
}
pub(crate) struct HTML;
impl Output for HTML {
fn print(data: &Vec<ExecResult>) -> Result<String> {
// To generate htlm output, I have to use templates because I haven't found any other good
// solution
let template = r#"
<table>
<tr>
<th>Chart Name</th>
<th>Current Version</th>
<th>Latest Version</th>
<th>Status</th>
</tr>
{{#each this as |tr|}}
<tr>
<th>{{tr.name}}</th>
<th>{{tr.current_version}}</th>
<th>{{tr.latest_version}}</th>
<th>{{tr.status}}</th>
</tr>
{{/each}}
</table>
"#;
let mut reg = Handlebars::new();
// TODO: Handle this error
reg.register_template_string("html_table", template)
.unwrap();
match reg.render("html_table", &data) {
Ok(res) => Ok(res),
Err(err) => {
error!("{}", err);
return Err(Error::new(ErrorKind::InvalidInput, err.to_string()));
}
}
}
}
pub(crate) struct YAML;
impl Output for YAML {
fn print(data: &Vec<ExecResult>) -> Result<String> {
match serde_yaml::to_string(&data) {
Ok(res) => return Ok(res),
Err(err) => return Err(Error::new(ErrorKind::InvalidData, err.to_string())),
}
}
}

View File

@ -1,5 +1,6 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::fmt; use std::fmt;
use tabled::Tabled;
/// Struct for parsing charts info from helmfile /// Struct for parsing charts info from helmfile
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] #[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
@ -15,8 +16,8 @@ pub(crate) struct HelmRepo {
pub(crate) url: String, pub(crate) url: String,
} }
#[derive(Clone, Serialize)] #[derive(Clone, Serialize, Deserialize)]
enum Status { pub(crate) enum Status {
Uptodate, Uptodate,
Outdated, Outdated,
Missing, Missing,
@ -31,3 +32,22 @@ impl fmt::Display for Status {
} }
} }
} }
#[derive(Clone, Tabled, Serialize, Deserialize)]
pub(crate) struct ExecResult {
pub(crate) name: String,
pub(crate) latest_version: String,
pub(crate) current_version: String,
pub(crate) status: Status,
}
impl ExecResult {
pub(crate) fn new(name: String, latest_version: String, current_version: String, status: Status) -> Self {
Self {
name,
latest_version,
current_version,
status,
}
}
}