mirror of
https://github.com/geoffsee/predict-otron-9001.git
synced 2025-09-08 22:46:44 +00:00
520 lines
14 KiB
Rust
520 lines
14 KiB
Rust
use anyhow::{Context, Result};
|
|
use clap::{Arg, Command};
|
|
use serde::Deserialize;
|
|
use std::fs;
|
|
use std::path::Path;
|
|
use walkdir::WalkDir;
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
struct CargoToml {
|
|
package: Option<Package>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
struct Package {
|
|
name: String,
|
|
metadata: Option<Metadata>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
struct Metadata {
|
|
kube: Option<KubeMetadata>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
struct KubeMetadata {
|
|
image: String,
|
|
replicas: Option<u32>,
|
|
port: u16,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
struct ServiceInfo {
|
|
name: String,
|
|
image: String,
|
|
port: u16,
|
|
replicas: u32,
|
|
}
|
|
|
|
fn main() -> Result<()> {
|
|
let matches = Command::new("helm-chart-tool")
|
|
.about("Generate Helm charts from Cargo.toml metadata")
|
|
.arg(
|
|
Arg::new("workspace")
|
|
.short('w')
|
|
.long("workspace")
|
|
.value_name("PATH")
|
|
.help("Path to the workspace root")
|
|
.default_value("."),
|
|
)
|
|
.arg(
|
|
Arg::new("output")
|
|
.short('o')
|
|
.long("output")
|
|
.value_name("PATH")
|
|
.help("Output directory for the Helm chart")
|
|
.default_value("./helm-chart"),
|
|
)
|
|
.arg(
|
|
Arg::new("chart-name")
|
|
.short('n')
|
|
.long("name")
|
|
.value_name("NAME")
|
|
.help("Name of the Helm chart")
|
|
.default_value("predict-otron-9000"),
|
|
)
|
|
.get_matches();
|
|
|
|
let workspace_path = matches.get_one::<String>("workspace").unwrap();
|
|
let output_path = matches.get_one::<String>("output").unwrap();
|
|
let chart_name = matches.get_one::<String>("chart-name").unwrap();
|
|
|
|
println!("Parsing workspace at: {}", workspace_path);
|
|
println!("Output directory: {}", output_path);
|
|
println!("Chart name: {}", chart_name);
|
|
|
|
let services = discover_services(workspace_path)?;
|
|
println!("Found {} services:", services.len());
|
|
for service in &services {
|
|
println!(
|
|
" - {}: {} (port {})",
|
|
service.name, service.image, service.port
|
|
);
|
|
}
|
|
|
|
generate_helm_chart(output_path, chart_name, &services)?;
|
|
println!("Helm chart generated successfully!");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn discover_services(workspace_path: &str) -> Result<Vec<ServiceInfo>> {
|
|
let workspace_root = Path::new(workspace_path);
|
|
let mut services = Vec::new();
|
|
|
|
// Find all Cargo.toml files in the workspace
|
|
for entry in WalkDir::new(workspace_root)
|
|
.into_iter()
|
|
.filter_map(|e| e.ok())
|
|
{
|
|
if entry.file_name() == "Cargo.toml"
|
|
&& entry.path() != workspace_root.join("../../../Cargo.toml")
|
|
{
|
|
if let Ok(service_info) = parse_cargo_toml(entry.path()) {
|
|
services.push(service_info);
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(services)
|
|
}
|
|
|
|
fn parse_cargo_toml(path: &Path) -> Result<ServiceInfo> {
|
|
let content = fs::read_to_string(path)
|
|
.with_context(|| format!("Failed to read Cargo.toml at {:?}", path))?;
|
|
|
|
let cargo_toml: CargoToml = toml::from_str(&content)
|
|
.with_context(|| format!("Failed to parse Cargo.toml at {:?}", path))?;
|
|
|
|
let package = cargo_toml
|
|
.package
|
|
.ok_or_else(|| anyhow::anyhow!("No package section found in {:?}", path))?;
|
|
|
|
let metadata = package
|
|
.metadata
|
|
.ok_or_else(|| anyhow::anyhow!("No metadata section found in {:?}", path))?;
|
|
|
|
let kube_metadata = metadata
|
|
.kube
|
|
.ok_or_else(|| anyhow::anyhow!("No kube metadata found in {:?}", path))?;
|
|
|
|
Ok(ServiceInfo {
|
|
name: package.name,
|
|
image: kube_metadata.image,
|
|
port: kube_metadata.port,
|
|
replicas: kube_metadata.replicas.unwrap_or(1),
|
|
})
|
|
}
|
|
|
|
fn generate_helm_chart(
|
|
output_path: &str,
|
|
chart_name: &str,
|
|
services: &[ServiceInfo],
|
|
) -> Result<()> {
|
|
let chart_dir = Path::new(output_path);
|
|
let templates_dir = chart_dir.join("templates");
|
|
|
|
// Create directories
|
|
fs::create_dir_all(&templates_dir)?;
|
|
|
|
// Generate Chart.yaml
|
|
generate_chart_yaml(chart_dir, chart_name)?;
|
|
|
|
// Generate values.yaml
|
|
generate_values_yaml(chart_dir, services)?;
|
|
|
|
// Generate templates for each service
|
|
for service in services {
|
|
generate_deployment_template(&templates_dir, service)?;
|
|
generate_service_template(&templates_dir, service)?;
|
|
}
|
|
|
|
// Generate ingress template
|
|
generate_ingress_template(&templates_dir, services)?;
|
|
|
|
// Generate helper templates
|
|
generate_helpers_template(&templates_dir)?;
|
|
|
|
// Generate .helmignore
|
|
generate_helmignore(chart_dir)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn generate_chart_yaml(chart_dir: &Path, chart_name: &str) -> Result<()> {
|
|
let chart_yaml = format!(
|
|
r#"apiVersion: v2
|
|
name: {}
|
|
description: A Helm chart for the predict-otron-9000 AI platform
|
|
type: application
|
|
version: 0.1.0
|
|
appVersion: "0.1.0"
|
|
keywords:
|
|
- ai
|
|
- llm
|
|
- inference
|
|
- embeddings
|
|
- chat
|
|
maintainers:
|
|
- name: predict-otron-9000-team
|
|
"#,
|
|
chart_name
|
|
);
|
|
|
|
fs::write(chart_dir.join("Chart.yaml"), chart_yaml)?;
|
|
Ok(())
|
|
}
|
|
|
|
fn generate_values_yaml(chart_dir: &Path, services: &[ServiceInfo]) -> Result<()> {
|
|
let mut values = String::from(
|
|
r#"# Default values for predict-otron-9000
|
|
# This is a YAML-formatted file.
|
|
|
|
global:
|
|
imagePullPolicy: IfNotPresent
|
|
serviceType: ClusterIP
|
|
|
|
# Ingress configuration
|
|
ingress:
|
|
enabled: false
|
|
className: ""
|
|
annotations: {}
|
|
hosts:
|
|
- host: predict-otron-9000.local
|
|
paths:
|
|
- path: /
|
|
pathType: Prefix
|
|
backend:
|
|
service:
|
|
name: predict-otron-9000
|
|
port:
|
|
number: 8080
|
|
tls: []
|
|
|
|
"#,
|
|
);
|
|
|
|
for service in services {
|
|
let service_config = format!(
|
|
r#"{}:
|
|
image:
|
|
repository: {}
|
|
tag: "latest"
|
|
pullPolicy: IfNotPresent
|
|
replicas: {}
|
|
service:
|
|
type: ClusterIP
|
|
port: {}
|
|
resources:
|
|
limits:
|
|
memory: "1Gi"
|
|
cpu: "1000m"
|
|
requests:
|
|
memory: "512Mi"
|
|
cpu: "250m"
|
|
nodeSelector: {{}}
|
|
tolerations: []
|
|
affinity: {{}}
|
|
|
|
"#,
|
|
service.name.replace("-", "_"),
|
|
service.image.split(':').next().unwrap_or(&service.image),
|
|
service.replicas,
|
|
service.port
|
|
);
|
|
values.push_str(&service_config);
|
|
}
|
|
|
|
fs::write(chart_dir.join("values.yaml"), values)?;
|
|
Ok(())
|
|
}
|
|
|
|
fn generate_deployment_template(templates_dir: &Path, service: &ServiceInfo) -> Result<()> {
|
|
let service_name_underscore = service.name.replace("-", "_");
|
|
let deployment_template = format!(
|
|
r#"apiVersion: apps/v1
|
|
kind: Deployment
|
|
metadata:
|
|
name: {{{{ include "predict-otron-9000.fullname" . }}}}-{}
|
|
labels:
|
|
{{{{- include "predict-otron-9000.labels" . | nindent 4 }}}}
|
|
app.kubernetes.io/component: {}
|
|
spec:
|
|
replicas: {{{{ .Values.{}.replicas }}}}
|
|
selector:
|
|
matchLabels:
|
|
{{{{- include "predict-otron-9000.selectorLabels" . | nindent 6 }}}}
|
|
app.kubernetes.io/component: {}
|
|
template:
|
|
metadata:
|
|
labels:
|
|
{{{{- include "predict-otron-9000.selectorLabels" . | nindent 8 }}}}
|
|
app.kubernetes.io/component: {}
|
|
spec:
|
|
containers:
|
|
- name: {}
|
|
image: "{{{{ .Values.{}.image.repository }}}}:{{{{ .Values.{}.image.tag }}}}"
|
|
imagePullPolicy: {{{{ .Values.{}.image.pullPolicy }}}}
|
|
ports:
|
|
- name: http
|
|
containerPort: {}
|
|
protocol: TCP
|
|
livenessProbe:
|
|
httpGet:
|
|
path: /health
|
|
port: http
|
|
initialDelaySeconds: 30
|
|
periodSeconds: 10
|
|
readinessProbe:
|
|
httpGet:
|
|
path: /health
|
|
port: http
|
|
initialDelaySeconds: 5
|
|
periodSeconds: 5
|
|
resources:
|
|
{{{{- toYaml .Values.{}.resources | nindent 12 }}}}
|
|
{{{{- with .Values.{}.nodeSelector }}}}
|
|
nodeSelector:
|
|
{{{{- toYaml . | nindent 8 }}}}
|
|
{{{{- end }}}}
|
|
{{{{- with .Values.{}.affinity }}}}
|
|
affinity:
|
|
{{{{- toYaml . | nindent 8 }}}}
|
|
{{{{- end }}}}
|
|
{{{{- with .Values.{}.tolerations }}}}
|
|
tolerations:
|
|
{{{{- toYaml . | nindent 8 }}}}
|
|
{{{{- end }}}}
|
|
"#,
|
|
service.name,
|
|
service.name,
|
|
service_name_underscore,
|
|
service.name,
|
|
service.name,
|
|
service.name,
|
|
service_name_underscore,
|
|
service_name_underscore,
|
|
service_name_underscore,
|
|
service.port,
|
|
service_name_underscore,
|
|
service_name_underscore,
|
|
service_name_underscore,
|
|
service_name_underscore
|
|
);
|
|
|
|
let filename = format!("{}-deployment.yaml", service.name);
|
|
fs::write(templates_dir.join(filename), deployment_template)?;
|
|
Ok(())
|
|
}
|
|
|
|
fn generate_service_template(templates_dir: &Path, service: &ServiceInfo) -> Result<()> {
|
|
let service_template = format!(
|
|
r#"apiVersion: v1
|
|
kind: Service
|
|
metadata:
|
|
name: {{{{ include "predict-otron-9000.fullname" . }}}}-{}
|
|
labels:
|
|
{{{{- include "predict-otron-9000.labels" . | nindent 4 }}}}
|
|
app.kubernetes.io/component: {}
|
|
spec:
|
|
type: {{{{ .Values.{}.service.type }}}}
|
|
ports:
|
|
- port: {{{{ .Values.{}.service.port }}}}
|
|
targetPort: http
|
|
protocol: TCP
|
|
name: http
|
|
selector:
|
|
{{{{- include "predict-otron-9000.selectorLabels" . | nindent 4 }}}}
|
|
app.kubernetes.io/component: {}
|
|
"#,
|
|
service.name,
|
|
service.name,
|
|
service.name.replace("-", "_"),
|
|
service.name.replace("-", "_"),
|
|
service.name
|
|
);
|
|
|
|
let filename = format!("{}-service.yaml", service.name);
|
|
fs::write(templates_dir.join(filename), service_template)?;
|
|
Ok(())
|
|
}
|
|
|
|
fn generate_ingress_template(templates_dir: &Path, _services: &[ServiceInfo]) -> Result<()> {
|
|
let ingress_template = r#"{{- if .Values.ingress.enabled -}}
|
|
apiVersion: networking.k8s.io/v1
|
|
kind: Ingress
|
|
metadata:
|
|
name: {{ include "predict-otron-9000.fullname" . }}
|
|
labels:
|
|
{{- include "predict-otron-9000.labels" . | nindent 4 }}
|
|
{{- with .Values.ingress.annotations }}
|
|
annotations:
|
|
{{- toYaml . | nindent 4 }}
|
|
{{- end }}
|
|
spec:
|
|
{{- if .Values.ingress.className }}
|
|
ingressClassName: {{ .Values.ingress.className }}
|
|
{{- end }}
|
|
{{- if .Values.ingress.tls }}
|
|
tls:
|
|
{{- range .Values.ingress.tls }}
|
|
- hosts:
|
|
{{- range .hosts }}
|
|
- {{ . | quote }}
|
|
{{- end }}
|
|
secretName: {{ .secretName }}
|
|
{{- end }}
|
|
{{- end }}
|
|
rules:
|
|
{{- range .Values.ingress.hosts }}
|
|
- host: {{ .host | quote }}
|
|
http:
|
|
paths:
|
|
{{- range .paths }}
|
|
- path: {{ .path }}
|
|
{{- if .pathType }}
|
|
pathType: {{ .pathType }}
|
|
{{- end }}
|
|
backend:
|
|
service:
|
|
name: {{ include "predict-otron-9000.fullname" $ }}-{{ .backend.service.name }}
|
|
port:
|
|
number: {{ .backend.service.port.number }}
|
|
{{- end }}
|
|
{{- end }}
|
|
{{- end }}
|
|
"#;
|
|
|
|
fs::write(templates_dir.join("ingress.yaml"), ingress_template)?;
|
|
Ok(())
|
|
}
|
|
|
|
fn generate_helpers_template(templates_dir: &Path) -> Result<()> {
|
|
let helpers_template = r#"{{/*
|
|
Expand the name of the chart.
|
|
*/}}
|
|
{{- define "predict-otron-9000.name" -}}
|
|
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
|
|
{{- end }}
|
|
|
|
{{/*
|
|
Create a default fully qualified app name.
|
|
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
|
|
If release name contains chart name it will be used as a full name.
|
|
*/}}
|
|
{{- define "predict-otron-9000.fullname" -}}
|
|
{{- if .Values.fullnameOverride }}
|
|
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
|
|
{{- else }}
|
|
{{- $name := default .Chart.Name .Values.nameOverride }}
|
|
{{- if contains $name .Release.Name }}
|
|
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
|
|
{{- else }}
|
|
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
|
|
{{- end }}
|
|
{{- end }}
|
|
{{- end }}
|
|
|
|
{{/*
|
|
Create chart name and version as used by the chart label.
|
|
*/}}
|
|
{{- define "predict-otron-9000.chart" -}}
|
|
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
|
|
{{- end }}
|
|
|
|
{{/*
|
|
Common labels
|
|
*/}}
|
|
{{- define "predict-otron-9000.labels" -}}
|
|
helm.sh/chart: {{ include "predict-otron-9000.chart" . }}
|
|
{{ include "predict-otron-9000.selectorLabels" . }}
|
|
{{- if .Chart.AppVersion }}
|
|
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
|
{{- end }}
|
|
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
|
{{- end }}
|
|
|
|
{{/*
|
|
Selector labels
|
|
*/}}
|
|
{{- define "predict-otron-9000.selectorLabels" -}}
|
|
app.kubernetes.io/name: {{ include "predict-otron-9000.name" . }}
|
|
app.kubernetes.io/instance: {{ .Release.Name }}
|
|
{{- end }}
|
|
|
|
{{/*
|
|
Create the name of the service account to use
|
|
*/}}
|
|
{{- define "predict-otron-9000.serviceAccountName" -}}
|
|
{{- if .Values.serviceAccount.create }}
|
|
{{- default (include "predict-otron-9000.fullname" .) .Values.serviceAccount.name }}
|
|
{{- else }}
|
|
{{- default "default" .Values.serviceAccount.name }}
|
|
{{- end }}
|
|
{{- end }}
|
|
"#;
|
|
|
|
fs::write(templates_dir.join("_helpers.tpl"), helpers_template)?;
|
|
Ok(())
|
|
}
|
|
|
|
fn generate_helmignore(chart_dir: &Path) -> Result<()> {
|
|
let helmignore_content = r#"# Patterns to ignore when building packages.
|
|
# This supports shell glob matching, relative path matching, and
|
|
# negation (prefixed with !). Only one pattern per line.
|
|
.DS_Store
|
|
# Common VCS dirs
|
|
.git/
|
|
.gitignore
|
|
.bzr/
|
|
.bzrignore
|
|
.hg/
|
|
.hgignore
|
|
.svn/
|
|
# Common backup files
|
|
*.swp
|
|
*.bak
|
|
*.tmp
|
|
*.orig
|
|
*~
|
|
# Various IDEs
|
|
.project
|
|
.idea/
|
|
*.tmproj
|
|
.vscode/
|
|
"#;
|
|
|
|
fs::write(chart_dir.join(".helmignore"), helmignore_content)?;
|
|
Ok(())
|
|
}
|