Skip to content
Merged
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
892 changes: 453 additions & 439 deletions Cargo.lock

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,14 @@ repository = "https://github.com/stackabletech/stackable-cockpit/"
async-trait = "0.1"
axum = { version = "0.6", features = ["http2", "headers"] }
bcrypt = "0.15"
bindgen = "0.68.1"
cc = "1.0.83"
clap = { version = "4.2.1", features = ["derive", "env"] }
clap_complete = "4.2"
comfy-table = { version = "7.0", features = ["custom_styling"] }
directories = "5.0"
dotenvy = "0.15"
futures = "0.3"
gobuild = "0.1.0-alpha.2"
indexmap = { version = "2.0", features = ["serde"] }
k8s-openapi = { version = "0.19", default-features = false, features = ["v1_27"] }
kube = { version = "0.85", default-features = false, features = ["client", "rustls-tls"] }
Expand Down
2 changes: 1 addition & 1 deletion extra/completions/stackablectl.bash

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

28 changes: 14 additions & 14 deletions extra/completions/stackablectl.fish

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion rust/helm-sys/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,6 @@ publish = false
links = "helm"

[build-dependencies]
gobuild.workspace = true
cc.workspace = true
bindgen.workspace = true
snafu.workspace = true
110 changes: 89 additions & 21 deletions rust/helm-sys/build.rs
Original file line number Diff line number Diff line change
@@ -1,27 +1,95 @@
use std::env;
use std::{
env::{self, VarError},
path::PathBuf,
process::Command,
};

use gobuild::BuildMode;
use snafu::{ResultExt, Snafu};

const ENV_GO_HELM_WRAPPER: &str = "GO_HELM_WRAPPER";
#[derive(Debug, Snafu)]
enum Error {
#[snafu(display("Failed to find env var"))]
EnvVarNotFound { source: VarError },

#[snafu(display("Unsupported GOARCH: {arch}"))]
UnsupportedGoArch { arch: String },

#[snafu(display("Unsupported GOOS: {os}"))]
UnsupportedGoOs { os: String },
}

fn main() {
// cgo requires an explicit dependency on libresolv on some platforms (such as Red Hat Enterprise Linux 8 and derivatives)
let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());

println!("cargo:rerun-if-changed=go-helm-wrapper/main.go");

let cc = cc::Build::new().try_get_compiler().unwrap();
let goarch = get_goarch().unwrap();
let goos = get_goos().unwrap();

let mut cmd = Command::new("go");
cmd.arg("build")
.args(["-buildmode", "c-archive"])
.arg("-o")
.arg(out_path.join("libgo-helm-wrapper.a"))
.arg("go-helm-wrapper/main.go")
.env("CGO_ENABLED", "1")
.env("GOARCH", goarch)
.env("GOOS", goos)
.env("CC", format!("'{}'", cc.path().display()));

cmd.status().expect("Failed to build go-helm-wrapper");

let bindings = bindgen::builder()
.header(out_path.join("libgo-helm-wrapper.h").to_str().unwrap())
.parse_callbacks(Box::new(bindgen::CargoCallbacks))
.generate()
.expect("Failed to generate Rust bindings from Go header file");

bindings
.write_to_file(out_path.join("bindings.rs"))
.expect("Failed to write bindings");

println!("cargo:rustc-link-lib=resolv");
println!("cargo:rerun-if-env-changed={ENV_GO_HELM_WRAPPER}");
match env::var(ENV_GO_HELM_WRAPPER) {
Ok(go_helm_wrapper) => {
// Reuse pre-built helm wrapper if possible
eprintln!("Reusing pre-built go-helm-wrapper ({go_helm_wrapper:?})");
println!("cargo:rustc-link-lib=static:+verbatim={go_helm_wrapper}");
}
Err(env::VarError::NotPresent) => {
gobuild::Build::new()
.file("go-helm-wrapper/main.go")
.buildmode(BuildMode::CArchive)
.compile("go-helm-wrapper");
}
Err(err @ env::VarError::NotUnicode(..)) => {
panic!("{ENV_GO_HELM_WRAPPER} must be valid unicode: {err}");
}
}
println!("cargo:rustc-link-lib=static=go-helm-wrapper");
println!(
"cargo:rustc-link-search=native={}",
out_path.to_str().unwrap()
);
}

fn get_goarch() -> Result<String, Error> {
let arch = env::var("CARGO_CFG_TARGET_ARCH").context(EnvVarNotFoundSnafu)?;

let arch = match arch.as_str() {
"x86" => "386",
"x86_64" => "amd64",
"mips" => "mips",
"powerpc" => "ppc",
"powerpc64" => "ppc64",
"arm" => "arm",
"aarch64" => "arm64",
_ => return UnsupportedGoArchSnafu { arch }.fail(),
};

Ok(arch.into())
}

fn get_goos() -> Result<String, Error> {
let os = env::var("CARGO_CFG_TARGET_OS").context(EnvVarNotFoundSnafu)?;

let os = match os.as_str() {
"windows" => "windows",
"macos" => "darwin",
"ios" => "darwin",
"linux" => "linux",
"android" => "android",
"freebsd" => "freebsd",
"dragonfly" => "dragonfly",
"openbsd" => "openbsd",
"netbsd" => "netbsd",
_ => return UnsupportedGoOsSnafu { os }.fail(),
};

Ok(os.into())
}
46 changes: 28 additions & 18 deletions rust/helm-sys/go-helm-wrapper/main.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
package main

/*
#include <stdlib.h>
*/
import "C"

import (
"C"
"context"
"encoding/json"
"fmt"
"time"
"unsafe"

gohelm "github.com/mittwald/go-helm-client"
"helm.sh/helm/v3/pkg/action"
Expand All @@ -31,16 +36,16 @@ func main() {
}

//export go_install_helm_release
func go_install_helm_release(releaseName string, chartName string, chartVersion string, valuesYaml string, namespace string, suppressOutput bool) *C.char {
func go_install_helm_release(releaseName *C.char, chartName *C.char, chartVersion *C.char, valuesYaml *C.char, namespace *C.char, suppressOutput bool) *C.char {
helmClient := getHelmClient(namespace, suppressOutput)

timeout, _ := time.ParseDuration("10m")
chartSpec := gohelm.ChartSpec{
ReleaseName: releaseName,
ChartName: chartName,
Version: chartVersion,
ValuesYaml: valuesYaml,
Namespace: namespace,
ReleaseName: C.GoString(releaseName),
ChartName: C.GoString(chartName),
Version: C.GoString(chartVersion),
ValuesYaml: C.GoString(valuesYaml),
Namespace: C.GoString(namespace),
UpgradeCRDs: true,
Wait: true,
Timeout: timeout,
Expand All @@ -54,21 +59,21 @@ func go_install_helm_release(releaseName string, chartName string, chartVersion
}

//export go_uninstall_helm_release
func go_uninstall_helm_release(releaseName string, namespace string, suppressOutput bool) *C.char {
func go_uninstall_helm_release(releaseName *C.char, namespace *C.char, suppressOutput bool) *C.char {
helmClient := getHelmClient(namespace, suppressOutput)

if err := helmClient.UninstallReleaseByName(releaseName); err != nil {
if err := helmClient.UninstallReleaseByName(C.GoString(releaseName)); err != nil {
return C.CString(fmt.Sprintf("%s%s", HELM_ERROR_PREFIX, err))
}

return C.CString("")
}

//export go_helm_release_exists
func go_helm_release_exists(releaseName string, namespace string) bool {
func go_helm_release_exists(releaseName *C.char, namespace *C.char) bool {
helmClient := getHelmClient(namespace, true)

release, _ := helmClient.GetRelease(releaseName)
release, _ := helmClient.GetRelease(C.GoString(releaseName))
return release != nil
}

Expand All @@ -78,7 +83,7 @@ func go_helm_release_exists(releaseName string, namespace string) bool {
// by the Rust code and it will abort operations.
//
//export go_helm_list_releases
func go_helm_list_releases(namespace string) *C.char {
func go_helm_list_releases(namespace *C.char) *C.char {
helmClient := getHelmClient(namespace, true)

// List all releases, not only the deployed ones (e.g. include pending installations)
Expand Down Expand Up @@ -112,12 +117,12 @@ func go_helm_list_releases(namespace string) *C.char {
// operations.
//
//export go_add_helm_repo
func go_add_helm_repo(name string, url string) *C.char {
helmClient := getHelmClient("default", true) // Namespace doesn't matter
func go_add_helm_repo(name *C.char, url *C.char) *C.char {
helmClient := getHelmClient(C.CString("default"), true) // Namespace doesn't matter

chartRepo := repo.Entry{
Name: name,
URL: url,
Name: C.GoString(name),
URL: C.GoString(url),
}

if err := helmClient.AddOrUpdateChartRepo(chartRepo); err != nil {
Expand All @@ -127,9 +132,14 @@ func go_add_helm_repo(name string, url string) *C.char {
return C.CString("")
}

func getHelmClient(namespace string, suppressOutput bool) gohelm.Client {
//export free_go_string
func free_go_string(ptr *C.char) {
C.free(unsafe.Pointer(ptr))
}

func getHelmClient(namespace *C.char, suppressOutput bool) gohelm.Client {
options := gohelm.Options{
Namespace: namespace,
Namespace: C.GoString(namespace),
Debug: false,
}

Expand Down
135 changes: 105 additions & 30 deletions rust/helm-sys/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,37 +1,112 @@
use std::{marker::PhantomData, os::raw::c_char};
#![allow(non_upper_case_globals)]
#![allow(non_camel_case_types)]
#![allow(improper_ctypes)]
#![allow(non_snake_case)]

#[repr(C)]
pub struct GoString<'a> {
p: *const u8,
n: i64,
_lifetime: PhantomData<&'a str>,
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));

use std::ffi::{c_char, CStr, CString};

pub const HELM_ERROR_PREFIX: &str = "ERROR:";

pub fn install_helm_release(
release_name: &str,
chart_name: &str,
chart_version: &str,
values_yaml: &str,
namespace: &str,
suppress_output: bool,
) -> String {
let release_name = CString::new(release_name).unwrap();
let chart_name = CString::new(chart_name).unwrap();
let chart_version = CString::new(chart_version).unwrap();
let values_yaml = CString::new(values_yaml).unwrap();
let namespace = CString::new(namespace).unwrap();

unsafe {
let c = go_install_helm_release(
release_name.as_ptr() as *mut c_char,
chart_name.as_ptr() as *mut c_char,
chart_version.as_ptr() as *mut c_char,
values_yaml.as_ptr() as *mut c_char,
namespace.as_ptr() as *mut c_char,
suppress_output as u8,
);

cstr_ptr_to_string(c)
}
}

pub fn uninstall_helm_release(
release_name: &str,
namespace: &str,
suppress_output: bool,
) -> String {
let release_name = CString::new(release_name).unwrap();
let namespace = CString::new(namespace).unwrap();

unsafe {
let c = go_uninstall_helm_release(
release_name.as_ptr() as *mut c_char,
namespace.as_ptr() as *mut c_char,
suppress_output as u8,
);

cstr_ptr_to_string(c)
}
}

pub fn check_helm_release_exists(release_name: &str, namespace: &str) -> bool {
let release_name = CString::new(release_name).unwrap();
let namespace = CString::new(namespace).unwrap();

unsafe {
go_helm_release_exists(
release_name.as_ptr() as *mut c_char,
namespace.as_ptr() as *mut c_char,
) != 0
}
}

impl<'a> From<&'a str> for GoString<'a> {
fn from(str: &'a str) -> Self {
GoString {
p: str.as_ptr(),
n: str.len() as i64,
_lifetime: PhantomData,
}
pub fn list_helm_releases(namespace: &str) -> String {
let namespace = CString::new(namespace).unwrap();

unsafe {
let c = go_helm_list_releases(namespace.as_ptr() as *mut c_char);
cstr_ptr_to_string(c)
}
}

extern "C" {
pub fn go_install_helm_release(
release_name: GoString,
chart_name: GoString,
chart_version: GoString,
values_yaml: GoString,
namespace: GoString,
suppress_output: bool,
) -> *const c_char;
pub fn go_uninstall_helm_release(
release_name: GoString,
namespace: GoString,
suppress_output: bool,
) -> *const c_char;
pub fn go_helm_release_exists(release_name: GoString, namespace: GoString) -> bool;
pub fn go_helm_list_releases(namespace: GoString) -> *const c_char;
pub fn go_add_helm_repo(name: GoString, url: GoString) -> *const c_char;
pub fn add_helm_repository(repository_name: &str, repository_url: &str) -> String {
let repository_name = CString::new(repository_name).unwrap();
let repository_url = CString::new(repository_url).unwrap();

unsafe {
let c = go_add_helm_repo(
repository_name.as_ptr() as *mut c_char,
repository_url.as_ptr() as *mut c_char,
);

cstr_ptr_to_string(c)
}
}

/// Checks if the result string is an error, and if so, returns the error message as a string.
pub fn to_helm_error(result: &str) -> Option<String> {
if !result.is_empty() && result.starts_with(HELM_ERROR_PREFIX) {
return Some(result.replace(HELM_ERROR_PREFIX, ""));
}

None
}

/// Converts a raw C string pointer into an owned Rust [`String`]. This function
/// also makes sure, that the pointer (and underlying memory) of the Go string is
/// freed. The pointer **cannot** be used afterwards.
unsafe fn cstr_ptr_to_string(c: *mut c_char) -> String {
Comment thread
Techassi marked this conversation as resolved.
let cstr = CStr::from_ptr(c);
let s = String::from_utf8_lossy(cstr.to_bytes()).to_string();
free_go_string(cstr.as_ptr() as *mut c_char);

s
}
Loading