Helmule MVP

Basic functionality is there, helmule can mirror helm chart with small
modifications
This commit is contained in:
Nikolai Rodionov 2024-01-22 08:52:11 +01:00
parent 2f8170cf95
commit aabcb21f3b
Signed by: allanger
GPG Key ID: 0AA46A90E25592AD
53 changed files with 4817 additions and 5 deletions

5
.gitignore vendored
View File

@ -4,13 +4,8 @@
debug/
target/
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
Cargo.lock
# These are backup files generated by rustfmt
**/*.rs.bk
# MSVC Windows builds of rustc generate these, which store debugging information
*.pdb

1168
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

15
Cargo.toml Normal file
View File

@ -0,0 +1,15 @@
[workspace]
resolver = "2"
members = [
"helmule",
"helmudi",
"lib",
]
[workspace.dependencies]
serde = { version = "1.0.193", features = ["derive"] }
serde_json = "1.0.110"
serde_yaml = "0.9.29"
clap = { version = "4.4.18", features = ["derive"] }
tempfile = "3.9.0"
base64 = "0.21.7"

10
helmudi/Cargo.toml Normal file
View File

@ -0,0 +1,10 @@
[package]
name = "helmudi"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
clap = { version = "4.4.18", features = ["derive"] }
helmzoo_lib = { path = "../lib" }

46
helmudi/src/main.rs Normal file
View File

@ -0,0 +1,46 @@
use std::{error::Error, process::exit};
use helmzoo_lib::{
self,
output::{message_empty, message_error},
};
use clap::Parser;
/// Simple program to greet a person
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Args {
/// Name of the working dir
#[arg(short, long)]
workdir: Option<String>,
/// Path to the configuration file
#[arg(short, long)]
config: String,
/// Dry run
#[arg(short, long, default_value = "false")]
dry_run: bool,
#[arg(long, default_value = "false")]
skip_prerequisites_check: bool,
/// Init git patch. Use it if you want to create git patch for a chart
/// It's going to pull a chart and init a git repo there, so you can
/// apply changes and create a patch file
/// It's not going to try mirroring changes, but will apply extensions
/// and patches that are already defined
#[arg(long)]
init_git_patch: Option<Vec<String>>,
}
fn exec(args: Args) -> Result<(), Box<dyn Error>> {
let workdir_path = helmzoo_lib::workdir::setup_workdir(args.workdir)?;
Ok(())
}
fn main() {
match exec(Args::parse()) {
Ok(()) => message_empty("Thanks for using helmule"),
Err(err) => {
message_error(err);
exit(1)
}
}
}

17
helmule/Cargo.toml Normal file
View File

@ -0,0 +1,17 @@
[package]
name = "helmule"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
clap = { workspace = true }
dircpy = "0.3.15"
helmzoo_lib = { path = "../lib" }
regex = "1.10.3"
serde = { workspace = true }
serde_json ={ workspace = true }
serde_yaml = { workspace = true }
tempfile = { workspace = true }
base64 = { workspace = true }

View File

@ -0,0 +1,28 @@
{{/*
We have to create individual configmaps for each CRD - they exceed the total
allowed length for a configmap if they are combined.
*/}}
{{ $currentScope := . }}
{{- if .Values.crds.install }}
{{- range $path, $_ := .Files.Glob "crd-base/**" }}
{{- with $currentScope }}
---
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "crdInstall" . }}-{{ $path | base | trimSuffix ".yaml" }}
namespace: {{ .Release.Namespace | quote }}
annotations:
# create hook dependencies in the right order
"helm.sh/hook-weight": "-5"
{{- include "crdInstallAnnotations" . | nindent 4 }}
labels:
app.kubernetes.io/component: {{ include "crdInstall" . | quote }}
{{- include "labels.selector" . | nindent 4 }}
role: {{ include "crdInstallSelector" . | quote }}
data:
content: |
{{ tpl (.Files.Get $path) . | indent 4 }}
{{- end }}
{{- end }}
{{- end }}

View File

@ -0,0 +1,80 @@
{{- if .Values.crds.install }}
apiVersion: batch/v1
kind: Job
metadata:
name: {{ include "crdInstallJob" . }}
namespace: {{ .Release.Namespace | quote }}
annotations:
# create hook dependencies in the right order
"helm.sh/hook-weight": "-1"
{{- include "crdInstallAnnotations" . | nindent 4 }}
labels:
app.kubernetes.io/component: {{ include "crdInstall" . | quote }}
{{- include "labels.selector" . | nindent 4 }}
role: {{ include "crdInstallSelector" . | quote }}
spec:
ttlSecondsAfterFinished: 3600
template:
metadata:
labels:
app.kubernetes.io/component: {{ include "crdInstall" . | quote }}
{{- include "labels.selector" . | nindent 8 }}
spec:
serviceAccountName: {{ include "crdInstall" . }}
securityContext:
runAsUser: 1000
runAsGroup: 2000
{{- if ge (int .Capabilities.KubeVersion.Minor) 19 }}
{{- with .Values.crds.podSeccompProfile }}
seccompProfile:
{{- . | toYaml | nindent 10 }}
{{- end }}
{{- end }}
tolerations:
- key: node-role.kubernetes.io/master
effect: NoSchedule
- key: node-role.kubernetes.io/control-plane
effect: NoSchedule
containers:
- name: kubectl
image: "{{ .Values.images.registry }}/giantswarm/docker-kubectl:1.23.6"
command:
- sh
- -c
- |
set -o errexit ; set -o xtrace ; set -o nounset
# piping stderr to stdout means kubectl's errors are surfaced
# in the pod's logs.
kubectl apply -f /data/ 2>&1
securityContext:
readOnlyRootFilesystem: true
{{- if ge (int .Capabilities.KubeVersion.Minor) 19 }}
{{- with .Values.crds.seccompProfile }}
seccompProfile:
{{- . | toYaml | nindent 12 }}
{{- end }}
{{- end }}
volumeMounts:
{{- range $path, $_ := .Files.Glob "crd-base/**" }}
- name: {{ $path | base | trimSuffix ".yaml" }}
mountPath: /data/{{ $path | base }}
subPath: {{ $path | base }}
{{- end }}
resources: {{- toYaml .Values.crds.resources | nindent 10 }}
volumes:
{{ $currentScope := . }}
{{- range $path, $_ := .Files.Glob "crd-base/**" }}
{{- with $currentScope }}
- name: {{ $path | base | trimSuffix ".yaml" }}
configMap:
name: {{ include "crdInstall" . }}-{{ $path | base | trimSuffix ".yaml" }}
items:
- key: content
path: {{ $path | base }}
{{- end }}
{{- end }}
restartPolicy: Never
backoffLimit: 4
{{- end }}

View File

@ -0,0 +1,59 @@
{{- if .Values.crds.install }}
{{- if .Capabilities.APIVersions.Has "cilium.io/v2/CiliumNetworkPolicy" }}
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: {{ include "crdInstall" . }}
namespace: {{ .Release.Namespace | quote }}
annotations:
# create hook dependencies in the right order
"helm.sh/hook-weight": "-7"
{{- include "crdInstallAnnotations" . | nindent 4 }}
labels:
app.kubernetes.io/component: {{ include "crdInstall" . | quote }}
{{- include "labels.selector" . | nindent 4 }}
role: {{ include "crdInstallSelector" . | quote }}
spec:
egress:
- toEntities:
- kube-apiserver
endpointSelector: {}
{{- else }}
kind: NetworkPolicy
apiVersion: networking.k8s.io/v1
metadata:
name: {{ include "crdInstall" . }}
namespace: {{ .Release.Namespace | quote }}
annotations:
# create hook dependencies in the right order
"helm.sh/hook-weight": "-7"
{{- include "crdInstallAnnotations" . | nindent 4 }}
labels:
app.kubernetes.io/component: {{ include "crdInstall" . | quote }}
{{- include "labels.selector" . | nindent 4 }}
role: {{ include "crdInstallSelector" . | quote }}
spec:
podSelector:
matchLabels:
app.kubernetes.io/component: {{ include "crdInstall" . | quote }}
{{- include "labels.selector" . | nindent 6 }}
# allow egress traffic to the Kubernetes API
egress:
- ports:
- port: 443
protocol: TCP
# legacy port kept for compatibility
- port: 6443
protocol: TCP
to:
{{- range tuple "10.0.0.0/8" "172.16.0.0/12" "192.168.0.0/16" "100.64.0.0/10" }}
- ipBlock:
cidr: {{ . }}
{{- end }}
# deny ingress traffic
ingress: []
policyTypes:
- Egress
- Ingress
{{- end }}
{{- end }}

View File

@ -0,0 +1,62 @@
{{- if .Values.crds.install }}
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: {{ include "crdInstall" . }}
namespace: {{ .Release.Namespace | quote }}
annotations:
# create hook dependencies in the right order
"helm.sh/hook-weight": "-3"
{{- include "crdInstallAnnotations" . | nindent 4 }}
labels:
app.kubernetes.io/component: {{ include "crdInstall" . | quote }}
{{- include "labels.selector" . | nindent 4 }}
role: {{ include "crdInstallSelector" . | quote }}
rules:
- apiGroups:
- ""
resources:
- jobs
verbs:
- create
- delete
- apiGroups:
- apiextensions.k8s.io
resources:
- customresourcedefinitions
verbs:
- create
- delete
- get
- patch
- apiGroups:
- policy
resources:
- podsecuritypolicies
resourceNames:
- {{ include "crdInstall" . }}
verbs:
- use
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: {{ include "crdInstall" . }}
namespace: {{ .Release.Namespace | quote }}
annotations:
# create hook dependencies in the right order
"helm.sh/hook-weight": "-2"
{{- include "crdInstallAnnotations" . | nindent 4 }}
labels:
app.kubernetes.io/component: {{ include "crdInstall" . | quote }}
{{- include "labels.common" . | nindent 4 }}
role: {{ include "crdInstallSelector" . | quote }}
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: {{ include "crdInstall" . }}
subjects:
- kind: ServiceAccount
name: {{ include "crdInstall" . }}
namespace: {{ .Release.Namespace | quote }}
{{- end }}

View File

@ -0,0 +1,15 @@
{{- if .Values.crds.install }}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "crdInstall" . }}
namespace: {{ .Release.Namespace }}
annotations:
# create hook dependencies in the right order
"helm.sh/hook-weight": "-4"
{{- include "crdInstallAnnotations" . | nindent 4 }}
labels:
app.kubernetes.io/component: {{ include "crdInstall" . | quote }}
{{- include "labels.selector" . | nindent 4 }}
role: {{ include "crdInstallSelector" . | quote }}
{{- end }}

View File

@ -0,0 +1,30 @@
{{- if .Values.virtualservice.enabled -}}
{{- $fullName := include "vaultwarden.fullname" . -}}
{{- $svcPort := .Values.service.port -}}
{{- if $.Capabilities.APIVersions.Has "networking.istio.io/v1beta1" }}
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: {{ $fullName }}
labels:
{{- include "vaultwarden.labels" . | nindent 4 }}
{{- with .Values.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
gateways:
- {{ .Values.virtaulservice.gatewayRef }}
hosts:
- ci.badhouseplants.ne
http:
- match:
- uri:
prefix: /
route:
- destination:
host: woodpecker-ci-server
port:
number: 80
{{- end }}
{{- end }}

View File

@ -0,0 +1,7 @@
name: external-secrets
repository: external-secrets
variables:
target_repo: app-external-secrets-operator
version: 0.8.3
mirrors:
- apps-git

View File

@ -0,0 +1,61 @@
# -------------------------------------------------------------------
# -- GitOps Server Application
# -------------------------------------------------------------------
- name: weave-gitops
repository: weave
version: 4.0.15
variables:
target_repo: app-gitops-server
mirrors:
- apps-git
extensions:
- name: Add VPA
source_dir: ../extensions/vpa-gitops-server
target_dir: templates/gs-vpa
patches:
- name: Git patch
git:
path: ../patches/git/gitops-server.patch
- name: Generate values.schema
custom_command:
commands:
- helm schema-gen values.yaml > values.schema.json
- name: Git patch for values schema
git:
path: ../patches/git/gitops-server-values-schema.patch
- name: Git patch for test-job security
git:
path: ../patches/git/gitops-server-test-job.patch
# -- Update Chart.ymal
#- name: Change the chart name
# yq:
# op: Replace
# file: Chart.yaml
# key: .name
# value: gitops-server
- name: Set the home URL
yq:
op: Add
file: Chart.yaml
key: .home
value: https://github.com/giantswarm/gitops-server-app
- name: set the icon url
yq:
op: Add
file: Chart.yaml
key: .icon
value: https://s.giantswarm.io/app-icons/weaveworks/1/icon_light.svg
- name: Add keywords
yq:
op: Add
file: Chart.yaml
key: .keywords
value: '["gitops", "flux"]'
- name: team annotation
- name: gs version
yq:
op: Add
key: .annotations."config.giantswarm.io/version"
value: 1.x.x
file: Chart.yaml
- name: yamlfmt

View File

@ -0,0 +1,24 @@
# -------------------------------------------------------------------
# -- Zot Application
# -------------------------------------------------------------------
name: zot
repository: zot-git
extensions:
- name: Add VPA
source_dir: ../extensions/vpa
target_dir: templates/gs-vpa
- name: Add values for CI
source_dir: ../extensions/ci-values
target_dir: ci
variables:
target_repo: zot-app
patches:
- name: team annotation
- name: set home
- name: set engine
- name: yamlfmt
- name: Git patch
git:
path: ../patches/git/zot.patch
mirrors:
- apps-git

View File

@ -0,0 +1,9 @@
resources:
requests:
memory: 100Mi
cpu: 70m
limits:
memory: 700Mi
cpu: 400m
vpa:
enabled: true

View File

@ -0,0 +1,27 @@
{{ if eq (include "resource.vpa.enabled" .) "true" }}
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: gitops-server
namespace: {{ .Release.Namespace }}
labels:
{{- include "chart.labels" . | nindent 4 }}
spec:
resourcePolicy:
containerPolicies:
- containerName: {{ .Chart.Name }}
controlledValues: RequestsAndLimits
minAllowed:
cpu: {{ .Values.giantswarm.resources.server.requests.cpu }}
memory: {{ .Values.giantswarm.resources.server.requests.memory }}
maxAllowed:
cpu: 1000m
memory: 1000Mi
mode: Auto
targetRef:
apiVersion: apps/v1
kind: Deployment
name: {{ include "chart.fullname" . }}
updatePolicy:
updateMode: Auto
{{ end }}

View File

@ -0,0 +1,51 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "chart.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 "chart.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 "chart.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "chart.labels" -}}
helm.sh/chart: {{ include "chart.chart" . }}
{{ include "chart.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "chart.selectorLabels" -}}
app.kubernetes.io/name: {{ include "chart.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}

View File

@ -0,0 +1,29 @@
{{ if (.Values.vpa).enabled }}
{{ if .Values.resources }}
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: {{ include "chart.fullname" . }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "chart.labels" . | nindent 4 }}
spec:
resourcePolicy:
containerPolicies:
- containerName: manager
controlledValues: RequestsAndLimits
minAllowed:
cpu: {{ .Values.resources.requests.cpu }}
memory: {{ .Values.resources.requests.memory }}
maxAllowed:
cpu: {{ .Values.resources.limits.cpu }}
memory: {{ .Values.resources.limits.memory }}
mode: Auto
targetRef:
apiVersion: apps/v1
kind: Deployment
name: {{ include "chart.fullname" . }}
updatePolicy:
updateMode: Auto
{{- end }}
{{- end }}

View File

@ -0,0 +1,58 @@
variables:
global: example
include:
- kind: Charts
path: ./charts/zot.yaml
- kind: Charts
path: ./charts/gitops-server.yaml
- kind: Charts
path: ./charts/external-secrets-operator.yaml
patches:
- name: yamlfmt
custom_command:
commands:
- "cat <<EOT >> .yamlfmt\n formatter:\n pad_line_comments: 2\nEOT"
- yamlfmt values.yaml --conf ./yamlfmt.yaml
- rm -f yamlfmt.yaml
- name: team annotation
yq:
op: Add
key: .annotations."application.giantswarm.io/team"
value: team-honeybadger
file: Chart.yaml
- name: set home
yq:
op: Add
key: .home
value: https://github.com/giantswarm/flux-app
file: Chart.yaml
- name: set engine
yq:
op: Add
key: .engine
value: gtpl
file: Chart.yaml
repositories:
# -- Because their helm repo seems not to be accessible
- name: zot-git
git:
url: https://github.com/project-zot/helm-charts.git
git_ref: zot-0.1.42
path: charts
- name: weave
helm:
url: https://helm.gitops.weave.works
- name: external-secrets
helm:
url: https://charts.external-secrets.io
mirrors:
- name: apps-git
git:
url: git@git.badhouseplants.net:allanger/{{ variables.target_repo }}.git
git_dir: app-{{ name }}-git
branch: upgrade-{{ name }}-to-{{ version }}
path: helm/{{ name }}
commit: |-
chore: mirror {{ name }}-{{ version }}
upstream_repo: {{ repo_url }}

View File

@ -0,0 +1,19 @@
diff --git a/templates/tests/test-connection.yaml b/templates/tests/test-connection.yaml
index 8dfed87..b4b98bc 100644
--- a/templates/tests/test-connection.yaml
+++ b/templates/tests/test-connection.yaml
@@ -9,7 +9,13 @@ metadata:
spec:
containers:
- name: wget
- image: busybox
+ image: "{{ .Values.image.registry }}/{{ .Values.giantswarm.images.test.image }}:{{ .Values.giantswarm.images.test.tag }}"
+ imagePullPolicy: {{ .Values.giantswarm.images.test.pullPolicy }}
command: ['wget']
args: ['{{ include "chart.fullname" . }}:{{ .Values.service.port }}']
+ securityContext:
+ readOnlyRootFilesystem: true
+ runAsUser: 1000
+ resources:
+ {{- toYaml .Values.giantswarm.resources.test | nindent 8 }}
restartPolicy: Never

View File

@ -0,0 +1,210 @@
diff --git a/values.schema.json b/values.schema.json
index f759f82..c0762fa 100644
--- a/values.schema.json
+++ b/values.schema.json
@@ -43,10 +43,51 @@
}
},
"extraVolumeMounts": {
- "type": "array"
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "mountPath": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ },
+ "readOnly": {
+ "type": "boolean"
+ }
+ }
+ }
},
"extraVolumes": {
- "type": "array"
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "csi": {
+ "type": "object",
+ "properties": {
+ "driver": {
+ "type": "string"
+ },
+ "readOnly": {
+ "type": "boolean"
+ },
+ "volumeAttributes": {
+ "type": "object",
+ "properties": {
+ "secretProviderClass": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "name": {
+ "type": "string"
+ }
+ }
+ }
},
"fullnameOverride": {
"type": "string"
@@ -91,7 +132,30 @@
"type": "object",
"properties": {
"additionalRules": {
- "type": "array"
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "apiGroups": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "resources": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "verbs": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ }
+ }
},
"create": {
"type": "boolean"
@@ -106,7 +170,10 @@
"type": "boolean"
},
"resourceNames": {
- "type": "array"
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
}
}
},
@@ -117,7 +184,10 @@
"type": "boolean"
},
"resourceNames": {
- "type": "array"
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
}
}
}
@@ -134,6 +204,14 @@
"resources": {
"type": "object",
"properties": {
+ "vpa":{
+ "type": "object",
+ "properties": {
+ "enabled": {
+ "type": "boolean"
+ }
+ }
+ },
"server": {
"type": "object",
"properties": {
@@ -187,14 +265,6 @@
}
}
}
- },
- "vpa": {
- "type": "object",
- "properties": {
- "enabled": {
- "type": "boolean"
- }
- }
}
}
}
@@ -209,7 +279,15 @@
}
},
"imagePullSecrets": {
- "type": "array"
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ }
+ }
+ }
},
"ingress": {
"type": "object",
@@ -224,10 +302,46 @@
"type": "boolean"
},
"hosts": {
- "type": "array"
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "host": {
+ "type": "string"
+ },
+ "paths": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "path": {
+ "type": "string"
+ },
+ "pathType": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ }
},
"tls": {
- "type": "array"
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "hosts": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "secretName": {
+ "type": "string"
+ }
+ }
+ }
}
}
},

View File

@ -0,0 +1,307 @@
diff --git a/templates/_helpers.tpl b/templates/_helpers.tpl
index af32c5b..1fdf723 100644
--- a/templates/_helpers.tpl
+++ b/templates/_helpers.tpl
@@ -39,6 +39,7 @@ helm.sh/chart: {{ include "chart.chart" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
+application.giantswarm.io/team: {{ index .Chart.Annotations "application.giantswarm.io/team" | quote }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
@@ -75,3 +76,16 @@ Return the target Kubernetes version
{{- default .Capabilities.KubeVersion.Version .Values.kubeVersion -}}
{{- end -}}
{{- end -}}
+
+{{- define "resource.vpa.enabled" -}}
+{{- if and (.Capabilities.APIVersions.Has "autoscaling.k8s.io/v1") (.Values.giantswarm.resources.vpa.enabled) }}true{{ else }}false{{ end }}
+{{- end -}}
+
+{{- define "deployment.resources" -}}
+requests:
+{{ toYaml .Values.giantswarm.resources.server.requests | indent 2 -}}
+{{ if eq (include "resource.vpa.enabled" .) "false" }}
+limits:
+{{ toYaml .Values.giantswarm.resources.server.limits | indent 2 -}}
+{{- end -}}
+{{- end -}}
diff --git a/templates/admin-user-roles.yaml b/templates/admin-user-roles.yaml
index 74a1844..c0fa72c 100644
--- a/templates/admin-user-roles.yaml
+++ b/templates/admin-user-roles.yaml
@@ -30,8 +30,8 @@ rules:
resources: ["terraforms"]
verbs: [ "get", "list", "watch", "patch" ]
-{{- if gt (len $.Values.rbac.additionalRules) 0 -}}
-{{- toYaml $.Values.rbac.additionalRules | nindent 2 -}}
+{{- if gt (len $.Values.giantswarm.rbac.additionalRules) 0 -}}
+{{- toYaml $.Values.giantswarm.rbac.additionalRules | nindent 2 -}}
{{- end }}
{{- if .Values.adminUser.createClusterRole }}
---
@@ -72,8 +72,8 @@ rules:
resources: [ "providers", "alerts" ]
verbs: [ "get", "list", "watch", "patch" ]
-{{- if gt (len $.Values.rbac.additionalRules) 0 -}}
-{{- toYaml $.Values.rbac.additionalRules | nindent 2 -}}
+{{- if gt (len $.Values.giantswarm.rbac.additionalRules) 0 -}}
+{{- toYaml $.Values.giantswarm.rbac.additionalRules | nindent 2 -}}
{{- end -}}
{{- end }}
{{- end }}
diff --git a/templates/deployment.yaml b/templates/deployment.yaml
index a54c37c..a498259 100644
--- a/templates/deployment.yaml
+++ b/templates/deployment.yaml
@@ -36,8 +36,8 @@ spec:
- name: {{ .Chart.Name }}
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
- image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
- imagePullPolicy: {{ .Values.image.pullPolicy }}
+ image: "{{ .Values.image.registry }}/{{ .Values.giantswarm.images.server.image }}:{{ .Values.giantswarm.images.server.tag | default .Chart.AppVersion }}"
+ imagePullPolicy: {{ .Values.giantswarm.images.server.pullPolicy }}
args:
- "--log-level"
- "{{ .Values.logLevel }}"
@@ -88,7 +88,7 @@ spec:
{{- end }}
{{- end }}
resources:
- {{- toYaml .Values.resources | nindent 12 }}
+ {{- include "deployment.resources" . | nindent 12 }}
{{- if or .Values.serverTLS.enable .Values.extraVolumeMounts }}
volumeMounts:
{{- end }}
diff --git a/templates/role.yaml b/templates/role.yaml
index b292176..5a55339 100644
--- a/templates/role.yaml
+++ b/templates/role.yaml
@@ -1,4 +1,4 @@
-{{- if .Values.rbac.create -}}
+{{- if .Values.giantswarm.rbac.create -}}
{{- if semverCompare "<1.17-0" (include "common.capabilities.kubeVersion" .) -}}
apiVersion: rbac.authorization.k8s.io/v1beta1
{{- else }}
@@ -6,32 +6,39 @@ apiVersion: rbac.authorization.k8s.io/v1
{{- end }}
kind: ClusterRole
metadata:
- name: {{ include "chart.fullname" . }}
+ name: {{ include "chart.fullname" . }}
rules:
# impersonation rules for ui calls
+ {{- if .Values.giantswarm.rbac.impersonation.users.enabled }}
- apiGroups: [""]
- resources: {{ .Values.rbac.impersonationResources | toJson }}
+ resources: ["users"]
verbs: [ "impersonate" ]
- {{- with .Values.rbac.impersonationResourceNames }}
+ {{- with .Values.giantswarm.rbac.impersonation.users.resourceNames }}
resourceNames: {{ . | toJson }}
{{- end }}
+ {{- end }}
+ {{- if .Values.giantswarm.rbac.impersonation.groups.enabled }}
+ {{- if and .Values.giantswarm.rbac.impersonation.groups.enabled (not .Values.giantswarm.rbac.impersonation.users.enabled) }}
+ {{- fail "Enabling impersonation for groups requires users impersonation permissions, see https://kubernetes.io/docs/reference/access-authn-authz/authentication/#user-impersonation" }}
+ {{- end }}
+ - apiGroups: [""]
+ resources: ["groups"]
+ verbs: [ "impersonate" ]
+ {{- with .Values.giantswarm.rbac.impersonation.groups.resourceNames }}
+ resourceNames: {{ . | toJson }}
+ {{- end }}
+ {{- end }}
# Access to enterprise entitlement
- apiGroups: [""]
resources: [ "secrets" ]
verbs: [ "get", "list" ]
- {{- if and .Values.rbac.viewSecrets .Values.rbac.viewSecretsResourceNames }}
- {{- fail "You've supplied both rbac.viewSecrets and rbac.viewSecretsResourceNames. Please only use rbac.viewSecretsResourceNames" }}
- {{- end }}
- # or should return the first non-falsy result
- {{- with (or .Values.rbac.viewSecretsResourceNames .Values.rbac.viewSecrets) }}
+ {{- with .Values.giantswarm.rbac.viewSecretsResourceNames }}
resourceNames: {{ . | toJson }}
{{- end }}
-
# The service account needs to read namespaces to know where it can query
- apiGroups: [ "" ]
resources: [ "namespaces" ]
verbs: [ "get", "list", "watch" ]
-
# The service account needs to list custom resources to query if given feature
# is available or not.
- apiGroups: [ "apiextensions.k8s.io" ]
diff --git a/templates/rolebinding.yaml b/templates/rolebinding.yaml
index b8756fe..df718ff 100644
--- a/templates/rolebinding.yaml
+++ b/templates/rolebinding.yaml
@@ -1,4 +1,4 @@
-{{- if .Values.rbac.create -}}
+{{- if .Values.giantswarm.rbac.create -}}
{{- if semverCompare "<1.17-0" (include "common.capabilities.kubeVersion" .) -}}
apiVersion: rbac.authorization.k8s.io/v1beta1
{{- else }}
@@ -9,7 +9,7 @@ metadata:
name: {{ include "chart.fullname" . }}
labels:
{{- include "chart.labels" . | nindent 4 }}
- {{- with .Values.rbac.annotations }}
+ {{- with .Values.giantswarm.rbac.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
diff --git a/values.yaml b/values.yaml
index 374ad32..7b3b35f 100644
--- a/values.yaml
+++ b/values.yaml
@@ -1,16 +1,57 @@
-# Default values for chart.
-# This is a YAML-formatted file.
-# Declare variables to be passed into your templates.
+giantswarm:
+ images:
+ server:
+ image: giantswarm/weaveworks-wego-app
+ pullPolicy: IfNotPresent
+ tag: v0.18.0
+ test:
+ image: giantswarm/busybox
+ pullPolicy: IfNotPresent
+ tag: 1.36.0
+ resources:
+ vpa:
+ enabled: true
+ server:
+ limits:
+ cpu: 200m
+ memory: 256Mi
+ requests:
+ cpu: 100m
+ memory: 128Mi
+ test:
+ requests:
+ cpu: 10m
+ memory: 2Mi
+ limits:
+ cpu: 10m
+ memory: 4Mi
+ rbac:
+ create: true
+ impersonation:
+ users:
+ enabled: true
+ # -- If non-empty, this limits the users names that the service account
+ # can impersonate, e.g. `['user1@corporation.com', 'user2@corporation.com']`
+ resourceNames: []
+ groups:
+ enabled: true
+ # -- If non-empty, this limits the groups names that the service account
+ # can impersonate, e.g. `['admins', 'operations', 'devops']`
+ resourceNames: []
+ # -- If non-empty, this limits the secrets that can be accessed by
+ # the service account to the specified ones, e.g. `['weave-gitops-enterprise-credentials']`
+ viewSecretsResourceNames: ["cluster-user-auth", "oidc-auth"]
+ # -- If non-empty, these additional rules will be appended to the RBAC role and the cluster role.
+ # for example,
+ # additionalRules:
+ # - apiGroups: ["infra.contrib.fluxcd.io"]
+ # resources: ["terraforms"]
+ # verbs: [ "get", "list", "patch" ]
+ additionalRules: []
-# Note: paragraphs starting with `# --` will end up in our manual -
-# see https://github.com/norwoodj/helm-docs
replicaCount: 1
image:
- # FIXME check the app name
- repository: ghcr.io/weaveworks/wego-app
- pullPolicy: IfNotPresent
- # Overrides the image tag whose default is the chart appVersion.
- tag: "v0.18.0"
+ registry: gsoci.azurecr.io
imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""
@@ -43,28 +84,9 @@ serviceAccount:
# -- The name of the service account to use.
# If not set and create is true, a name is generated using the fullname template
name: ""
-rbac:
- # -- Specifies whether the clusterRole & binding to the service account should be created
- create: true
- # -- If non-empty, this limits the resources that the service
- # account can impersonate. This applies to both users and groups, e.g.
- # `['user1@corporation.com', 'user2@corporation.com', 'operations']`
- impersonationResourceNames: []
- # -- Limit the type of principal that can be impersonated
- impersonationResources: ["users", "groups"]
- # -- If non-empty, this limits the secrets that can be accessed by
- # the service account to the specified ones, e.g. `['weave-gitops-enterprise-credentials']`
- viewSecretsResourceNames: ["cluster-user-auth", "oidc-auth"]
- # -- If non-empty, these additional rules will be appended to the RBAC role and the cluster role.
- # for example,
- # additionalRules:
- # - apiGroups: ["infra.contrib.fluxcd.io"]
- # resources: ["terraforms"]
- # verbs: [ "get", "list", "patch" ]
- additionalRules: []
adminUser:
# -- Whether the local admin user should be created.
- # If you use this make sure you add it to `rbac.impersonationResourceNames`.
+ # If you use this make sure you add it to `giantswarm.rbac.impersonation.users.resourceNames`.
create: false
# -- Specifies whether the clusterRole & binding to the admin user should be created.
# Will be created only if `adminUser.create` is enabled. Without this,
@@ -82,7 +104,7 @@ adminUser:
# -- (string) Set the password for local admin user. Requires `adminUser.create` and `adminUser.createSecret`
# This needs to have been hashed using bcrypt.
# You can do this via our CLI with `gitops get bcrypt-hash`.
- passwordHash:
+ passwordHash: ""
podAnnotations: {}
podLabels: {}
# aadpodidbinding: identity
@@ -111,7 +133,7 @@ ingress:
annotations: {}
# kubernetes.io/ingress.class: nginx
# kubernetes.io/tls-acme: "true"
- hosts:
+ hosts: []
# - host: chart-example.local
# paths:
# - path: /
@@ -123,8 +145,8 @@ ingress:
# - chart-example.local
extraVolumes: []
extraVolumeMounts: []
-# Example using extraVolumes and extraVolumeMounts to load 'oidc-auth' secret
-# with a secrets store CSI driver. Specify the secretName 'oidc-auth' in the
+# Example using extraVolumes and extraVolumeMounts to load 'oidc-auth' secret
+# with a secrets store CSI driver. Specify the secretName 'oidc-auth' in the
# secretProviderClass so this will be created by the secrets store CSI driver.
# See https://secrets-store-csi-driver.sigs.k8s.io/topics/sync-as-kubernetes-secret.html
# extraVolumeMounts:
@@ -138,17 +160,6 @@ extraVolumeMounts: []
# readOnly: true
# volumeAttributes:
# secretProviderClass: ww-gitops-oauth-provider
-resources: {}
-# We usually recommend not to specify default resources and to leave this as a conscious
-# choice for the user. This also increases chances charts run on environments with little
-# resources, such as Minikube. If you do want to specify resources, uncomment the following
-# lines, adjust them as necessary, and remove the curly braces after 'resources:'.
-# limits:
-# cpu: 100m
-# memory: 128Mi
-# requests:
-# cpu: 100m
-# memory: 128Mi
networkPolicy:
# -- Specifies whether default network policies should be created.

View File

@ -0,0 +1,121 @@
diff --git a/templates/deployment.yaml b/templates/deployment.yaml
index c48dda1..b6de3af 100644
--- a/templates/deployment.yaml
+++ b/templates/deployment.yaml
@@ -24,12 +24,28 @@ spec:
{{- end }}
serviceAccountName: {{ include "zot.serviceAccountName" . }}
securityContext:
- {{- toYaml .Values.podSecurityContext | nindent 8 }}
+ fsGroup: 1337
+ {{- if ge (int .Capabilities.KubeVersion.Minor) 19 }}
+ {{- with .Values.podSeccompProfile }}
+ seccompProfile:
+ {{- . | toYaml | nindent 10 }}
+ {{- end }}
+ {{- end }}
containers:
- name: {{ .Chart.Name }}
securityContext:
- {{- toYaml .Values.securityContext | nindent 12 }}
- image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
+ {{- with .Values.containerSecurityContext }}
+ {{- . | toYaml | nindent 12 }}
+ {{- end }}
+ readOnlyRootFilesystem: true
+ runAsUser: 100
+ {{- if ge (int .Capabilities.KubeVersion.Minor) 19 }}
+ {{- with .Values.seccompProfile }}
+ seccompProfile:
+ {{- . | toYaml | nindent 14 }}
+ {{- end }}
+ {{- end }}
+ image: "{{ .Values.image.registry }}/{{ .Values.image.image }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
env:
{{- toYaml .Values.env | nindent 12 }}
diff --git a/templates/tests/test-connection-fails.yaml b/templates/tests/test-connection-fails.yaml
index 0e7a059..6ec4916 100644
--- a/templates/tests/test-connection-fails.yaml
+++ b/templates/tests/test-connection-fails.yaml
@@ -8,8 +8,28 @@ metadata:
"helm.sh/hook": test
"helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded,hook-failed
spec:
+ securityContext:
+ fsGroup: 1337
+ {{- if ge (int .Capabilities.KubeVersion.Minor) 19 }}
+ {{- with .Values.podSeccompProfile }}
+ seccompProfile:
+ {{- . | toYaml | nindent 10 }}
+ {{- end }}
+ {{- end }}
containers:
- name: wget
+ securityContext:
+ {{- with .Values.containerSecurityContext }}
+ {{- . | toYaml | nindent 12 }}
+ {{- end }}
+ readOnlyRootFilesystem: true
+ runAsUser: 100
+ {{- if ge (int .Capabilities.KubeVersion.Minor) 19 }}
+ {{- with .Values.seccompProfile }}
+ seccompProfile:
+ {{- . | toYaml | nindent 14 }}
+ {{- end }}
+ {{- end }}
image: alpine:3.18
command:
- sh
diff --git a/templates/tests/test-connection.yaml b/templates/tests/test-connection.yaml
index 59c64b4..2ded317 100644
--- a/templates/tests/test-connection.yaml
+++ b/templates/tests/test-connection.yaml
@@ -8,8 +8,28 @@ metadata:
"helm.sh/hook": test
"helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded,hook-failed
spec:
+ securityContext:
+ fsGroup: 1337
+ {{- if ge (int .Capabilities.KubeVersion.Minor) 19 }}
+ {{- with .Values.podSeccompProfile }}
+ seccompProfile:
+ {{- . | toYaml | nindent 10 }}
+ {{- end }}
+ {{- end }}
containers:
- name: wget
+ securityContext:
+ {{- with .Values.containerSecurityContext }}
+ {{- . | toYaml | nindent 12 }}
+ {{- end }}
+ readOnlyRootFilesystem: true
+ runAsUser: 100
+ {{- if ge (int .Capabilities.KubeVersion.Minor) 19 }}
+ {{- with .Values.seccompProfile }}
+ seccompProfile:
+ {{- . | toYaml | nindent 14 }}
+ {{- end }}
+ {{- end }}
image: alpine:3.18
command:
- sh
diff --git a/values.yaml b/values.yaml
index ac7f0f0..9730e9c 100644
--- a/values.yaml
+++ b/values.yaml
@@ -3,10 +3,10 @@
# Declare variables to be passed into your templates.
replicaCount: 1
image:
- repository: ghcr.io/project-zot/zot-linux-amd64
- pullPolicy: IfNotPresent
- # Overrides the image tag whose default is the chart appVersion.
- tag: "v2.0.0"
+ registry: gsoci.azurecr.io
+ image: dummy/zot-linux
+ pullPolicy: Always
+ tag: ""
serviceAccount:
# Specifies whether a service account should be created
create: true

View File

@ -0,0 +1,89 @@
---
name: Replace image repository in values
targets:
- values.yaml
before: |-
image:
repository: ghcr.io/project-zot/zot-linux-amd64
pullPolicy: IfNotPresent
# Overrides the image tag whose default is the chart appVersion.
tag: "v2.0.0"
after: |-
image:
repository: gsoci/dummy/zot-linux
pullPolicy: Always
tag: ""
---
name: Fix security policies in the deployment
targets:
- templates/deployment.yaml
before: |-
\{\{- toYaml .Values.podSecurityContext .*
after: |-
fsGroup: 1337
{{- if ge (int .Capabilities.KubeVersion.Minor) 19 }}
{{- with .Values.podSeccompProfile }}
seccompProfile:
{{- . | toYaml | nindent 10 }}
{{- end }}
{{- end }}
---
name: Fix security policies in container
targets:
- templates/deployment.yaml
before: |-
\{\{- toYaml .Values.securityContext .*
after: |-
{{- with .Values.containerSecurityContext }}
{{- . | toYaml | nindent 12 }}
{{- end }}
readOnlyRootFilesystem: true
runAsUser: 100
{{- if ge (int .Capabilities.KubeVersion.Minor) 19 }}
{{- with .Values.seccompProfile }}
seccompProfile:
{{- . | toYaml | nindent 14 }}
{{- end }}
{{- end }}
---
name: Fix security policies in test jobs
targets:
- templates/tests/test-connection-fails.yaml
- templates/tests/test-connection.yaml
before: |-
spec:
containers:
after: |-
spec:
securityContext:
fsGroup: 1337
{{- if ge (int .Capabilities.KubeVersion.Minor) 19 }}
{{- with .Values.podSeccompProfile }}
seccompProfile:
{{- . | toYaml | nindent 10 }}
{{- end }}
{{- end }}
containers:
---
name: Fix security policies in test jobs containers
targets:
- templates/tests/test-connection-fails.yaml
- templates/tests/test-connection.yaml
before: |-
containers:
- name: wget
after: |-
containers:
- name: wget
securityContext:
{{- with .Values.containerSecurityContext }}
{{- . | toYaml | nindent 12 }}
{{- end }}
readOnlyRootFilesystem: true
runAsUser: 100
{{- if ge (int .Capabilities.KubeVersion.Minor) 19 }}
{{- with .Values.seccompProfile }}
seccompProfile:
{{- . | toYaml | nindent 14 }}
{{- end }}
{{- end }}

View File

@ -0,0 +1,51 @@
---
name: Remove CRDs leftovers from values
targets:
- values.yaml
before: |-
installCRDs: true
crds:
# -- Add annotations to all CRD resources, e.g. "helm.sh/resource-policy": keep
annotations: \{\}
after: |-
crds:
install: true
# Add seccomp to pod security context
podSeccompProfile:
type: RuntimeDefault
# Add seccomp to container security context
seccompProfile:
type: RuntimeDefault
resources:
requests:
memory: "128Mi"
cpu: "250m"
limits:
memory: "256Mi"
cpu: "500m"
---
name: Append crd install data to helpers
targets:
- templates/_helper.tpl
after: |-
{{- define "crdInstall" -}}
{{- printf "%s-%s" ( include "name" . ) "crd-install" | replace "+" "_" | trimSuffix "-" -}}
{{- end -}}
{{- define "crdInstallJob" -}}
{{- printf "%s-%s-%s" ( include "name" . ) "crd-install" .Chart.AppVersion | replace "+" "_" | replace "." "-" | trimSuffix "-" | trunc 63 -}}
{{- end -}}
{{- define "crdInstallAnnotations" -}}
"helm.sh/hook": "pre-install,pre-upgrade"
"helm.sh/hook-delete-policy": "before-hook-creation,hook-succeeded,hook-failed"
{{- end -}}
{{/* Create a label which can be used to select any orphaned crd-install hook resources */}}
{{- define "crdInstallSelector" -}}
{{- printf "%s" "crd-install-hook" -}}
{{- end -}}

View File

@ -0,0 +1,34 @@
diff --git a/values.yaml b/values.yaml
index 7ed6839..2b144ad 100644
--- a/values.yaml
+++ b/values.yaml
@@ -1,6 +1,6 @@
image:
repository: registry.hub.docker.com/vaultwarden/server
- pullPolicy: IfNotPresent
+ pullPolicy: Always
# Overrides the image tag whose default is the chart appVersion.
tag: ""
imagePullSecrets: []
@@ -10,13 +10,14 @@ podAnnotations: {}
podSecurityContext: {}
# fsGroup: 2000
-securityContext: {}
-# capabilities:
-# drop:
-# - ALL
-# readOnlyRootFilesystem: true
-# runAsNonRoot: true
-# runAsUser: 1000
+securityContext:
+capabilities:
+ drop:
+ - ALL
+
+readOnlyRootFilesystem: true
+runAsNonRoot: true
+runAsUser: 1000
service:
type: ClusterIP

View File

@ -0,0 +1,13 @@
diff --git a/Chart.yaml b/Chart.yaml
index d8995d5..0e5f5a5 100644
--- a/Chart.yaml
+++ b/Chart.yaml
@@ -8,7 +8,7 @@ keywords:
- bitwarden
- bitwarden_rs
maintainers:
-- email: allanger@badhouseplants.net
+- email: Somebody else
name: Nikolai Rodionov
url: https://badhouseplants.net
name: vaultwarden

View File

@ -0,0 +1,7 @@
---
name: Add spaces before comments
targets:
- values.yaml
before: |-
^.*[\S]+.*#
after: " #"

View File

@ -0,0 +1,27 @@
name: vaultwarden
repository: badhouseplants
version: latest
extensions:
- name: Add virtual service to the chartc
target_dir: templates/extensions
source_dir: ../../extensions/vaultwarden
patches:
- name: Git patch 1
git:
path: ../../patches/git/patch.diff
- name: Git patch 2
git:
path: ../../patches/git/patch-2.diff
- name: yaml-fmt
custom_command:
commands:
- |-
cat <<EOT >> .yamlfmt
formatter:
pad_line_comments: 2
EOT
- yamlfmt values.yaml --conf ./yamlfmt.yaml
- rm -f yamlfmt.yaml
mirrors:
- badhouseplants-git
- custom-command

View File

@ -0,0 +1,54 @@
- name: vaultwarden
repository: badhouseplants
version: latest
extensions:
- name: Add virtual service to the chartc
target_dir: templates/extensions
source_dir: ./examples/extensions/vaultwarden
patches:
- name: Git patch 1
git:
path: ./examples/patches/git/patch.diff
- name: Git patch 2
git:
path: ./examples/patches/git/patch-2.diff
- name: yaml-fmt
custom_command:
commands:
- |-
cat <<EOT >> .yamlfmt
formatter:
pad_line_comments: 2
EOT
- yamlfmt values.yaml --conf ./yamlfmt.yaml
- rm -f yamlfmt.yaml
mirrors:
- badhouseplants-git
- custom-command
- name: vaultwarden
repository: badhouseplants
version: latest
extensions:
- name: Add virtual service to the chartc
target_dir: templates/extensions
source_dir: ./examples/extensions/vaultwarden
patches:
- name: Git patch 1
git:
path: ./examples/patches/git/patch.diff
- name: Git patch 2
git:
path: ./examples/patches/git/patch-2.diff
- name: yaml-fmt
custom_command:
commands:
- |-
cat <<EOT >> .yamlfmt
formatter:
pad_line_comments: 2
EOT
- yamlfmt values.yaml --conf ./yamlfmt.yaml
- rm -f yamlfmt.yaml
mirrors:
- badhouseplants-git
- custom-command

View File

@ -0,0 +1 @@
pad_line_comments: 2

308
helmule/src/config.rs Normal file
View File

@ -0,0 +1,308 @@
use std::{collections::HashMap, error::Error};
use helmzoo_lib::{
cli::is_path_relative,
config::ConfigImpl,
helm::{chart::Chart, repository::Repository},
include::Include,
output::message_info,
};
use serde::{Deserialize, Serialize};
use crate::{extensions::Extension, mirror::Mirror, patches::Patch};
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone, PartialOrd, Ord, Eq)]
pub(crate) enum SupportedIncludes {
Repositories,
Mirrors,
Charts,
}
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone, Default)]
pub(crate) struct ChartExtended {
#[serde(flatten)]
pub(crate) chart: Chart,
pub(crate) extensions: Option<Vec<Extension>>,
pub(crate) patches: Option<Vec<Patch>>,
pub variables: Option<HashMap<String, String>>,
}
impl ChartExtended {
pub(crate) fn populate_variables(&mut self, global_variables: Option<HashMap<String, String>>) {
if let Some(global_vars) = global_variables {
self.variables = match self.variables.clone() {
Some(mut vars) => {
vars.extend(global_vars);
Some(vars)
}
None => Some(global_vars),
}
};
}
}
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone, Default)]
pub(crate) struct Config {
pub(crate) variables: Option<HashMap<String, String>>,
#[serde(default = "empty_vec")]
pub(crate) repositories: Vec<Repository>,
pub(crate) include: Option<Vec<Include<SupportedIncludes>>>,
#[serde(default = "empty_vec")]
pub(crate) charts: Vec<ChartExtended>,
pub(crate) patches: Option<Vec<Patch>>,
#[serde(default = "empty_vec")]
pub(crate) mirrors: Vec<Mirror>,
}
fn empty_vec<T>() -> Vec<T> {
vec![]
}
impl ConfigImpl for Config {
fn apply_includes(&mut self, config_path: String) -> Result<(), Box<dyn Error>> {
if let Some(mut include) = self.include.clone() {
include.sort_by_key(|f| f.kind.clone());
include
.iter()
.try_for_each(|i| -> Result<(), Box<dyn Error>> {
let include_path = match is_path_relative(i.path.clone()) {
true => format!("{}/{}", config_path, i.path),
false => i.path.clone(),
};
match i.kind {
SupportedIncludes::Charts => {
Ok(self.append_to_charts(include_charts(include_path)?))
}
SupportedIncludes::Repositories => {
Ok(self.append_to_repositories(include_repositories(include_path)?))
}
SupportedIncludes::Mirrors => todo!(),
}
})?
}
Ok(())
}
}
impl Config {
// TODO: Maybe it can be a generic function
fn append_to_repositories(&mut self, mut repositories: Vec<Repository>) {
self.repositories.append(&mut repositories);
}
fn append_to_charts(&mut self, mut charts: Vec<ChartExtended>) {
self.charts.append(&mut charts);
}
}
fn include_repositories(path: String) -> Result<Vec<Repository>, Box<dyn Error>> {
message_info(&format!(
"trying to include repositories from {}",
path.clone()
));
let file = std::fs::File::open(path.clone())?;
let repositories: Vec<Repository> = match serde_yaml::from_reader(file) {
Ok(res) => res,
Err(_) => {
let file = std::fs::File::open(path.clone())?;
let repo: Repository = serde_yaml::from_reader(file)?;
vec![repo]
}
};
Ok(repositories)
}
fn include_charts(path: String) -> Result<Vec<ChartExtended>, Box<dyn Error>> {
message_info(&format!("trying to include chart from {}", path.clone()));
let file = std::fs::File::open(path.clone())?;
let chart_dir = match std::path::Path::new(&path).parent() {
Some(dir) => match dir.to_str() {
Some(dir) => dir.to_string(),
None => {
return Err(Box::from(format!(
"chart parrent dir not found for {}",
path
)));
}
},
None => {
return Err(Box::from(format!(
"chart parrent dir not found for {}",
path
)));
}
};
let mut charts: Vec<ChartExtended> = match serde_yaml::from_reader(file) {
Ok(res) => res,
Err(_) => {
let file = std::fs::File::open(path.clone())?;
let chart: ChartExtended = serde_yaml::from_reader(file)?;
vec![chart]
}
};
charts.iter_mut().for_each(|chart| {
match chart.extensions {
Some(ref mut extensions) => extensions.iter_mut().for_each(|extension| {
if is_path_relative(extension.source_dir.clone()) {
let clean_path = match extension.source_dir.clone().starts_with("./") {
true => extension.source_dir.clone().replacen("./", "", 1),
false => extension.source_dir.clone(),
};
if is_path_relative(clean_path.clone()) {
let new_path = format!("{}/{}", chart_dir, clean_path);
extension.source_dir = new_path;
}
}
}),
None => message_info(&format!("no extensions set, nothing to update")),
};
match chart.patches {
Some(ref mut patches) => patches.iter_mut().for_each(|patch| {
if is_path_relative(patch.get_path().clone()) {
let clean_path = match patch.get_path().clone().starts_with("./") {
true => patch.get_path().clone().replacen("./", "", 1),
false => patch.get_path().clone(),
};
if is_path_relative(clean_path.clone()) {
let new_path = format!("{}/{}", chart_dir, clean_path);
patch.set_path(new_path);
}
}
}),
None => message_info(&format!("no patch set, nothing to update")),
};
});
Ok(charts)
}
#[cfg(test)]
mod tests {
use std::{error::Error, fs::File, io::Write};
use helmzoo_lib::{
config::ConfigImpl,
helm::{helm_repository::HelmRepo, repository::Repository},
include::Include,
};
use tempfile::tempdir;
use crate::config::SupportedIncludes;
use super::{include_repositories, Config};
fn prepare_test_file(name: &str, data: &str) -> Result<String, Box<dyn Error>> {
let dir = tempdir()?;
let file_path = dir.into_path().join(&name);
let mut file = File::create(file_path.clone())?;
file.write_all(data.as_bytes())?;
let path = file_path.into_os_string().to_str().unwrap().to_string();
Ok(path)
}
#[test]
fn test_cfg_apply_includes_repo() -> Result<(), Box<dyn Error>> {
let data = "---
- name: test
helm:
url: test.rocks
";
let path = prepare_test_file("repositories.yaml", data)?;
let repo_1: Repository = Repository {
name: "test".to_string(),
helm: None,
git: None,
};
let repos = vec![repo_1.clone()];
let includes: Vec<Include<SupportedIncludes>> = vec![Include {
path: path.clone(),
kind: SupportedIncludes::Repositories,
}];
let mut config = Config {
repositories: repos.clone(),
include: Some(includes),
charts: vec![],
variables: None,
patches: None,
mirrors: vec![],
};
let repo_2 = Repository {
name: "test".to_string(),
helm: Some(HelmRepo {
url: "test.rocks".to_string(),
}),
git: None,
};
config.apply_includes(path)?;
assert_eq!(config.repositories, vec!(repo_1, repo_2));
Ok(())
}
#[test]
fn test_cfg_append_to_repos() -> Result<(), Box<dyn Error>> {
let repo_1: Repository = Repository {
name: "test".to_string(),
helm: None,
git: None,
};
let repos = vec![repo_1.clone()];
let mut config = Config {
variables: None,
repositories: repos.clone(),
include: None,
mirrors: vec![],
charts: vec![],
patches: None,
};
let repo_2 = Repository {
name: "test_2".to_string(),
helm: None,
git: None,
};
let repo_2_vec = vec![repo_2.clone()];
config.append_to_repositories(repo_2_vec);
assert_eq!(config.repositories, vec!(repo_1, repo_2));
Ok(())
}
#[test]
fn test_include_repositories() -> Result<(), Box<dyn Error>> {
let data = "---
- name: test
helm:
url: test.rocks
";
let path = prepare_test_file("repositories.yaml", data)?;
let include = include_repositories(path)?;
let expected: Vec<Repository> = vec![Repository {
name: "test".to_string(),
git: None,
helm: Some(HelmRepo {
url: "test.rocks".to_string(),
}),
}];
assert_eq!(expected, include);
Ok(())
}
#[test]
fn test_include_repository() -> Result<(), Box<dyn Error>> {
let data = "---
name: test
helm:
url: test.rocks
";
let path = prepare_test_file("repositories.yaml", data)?;
let include = include_repositories(path)?;
let expected: Vec<Repository> = vec![Repository {
name: "test".to_string(),
git: None,
helm: Some(HelmRepo {
url: "test.rocks".to_string(),
}),
}];
assert_eq!(expected, include);
Ok(())
}
}

38
helmule/src/extensions.rs Normal file
View File

@ -0,0 +1,38 @@
use std::fs::create_dir;
use helmzoo_lib::{
cli::{copy_recursively, is_path_relative},
output::message_info,
};
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub(crate) struct Extension {
name: Option<String>,
target_dir: String,
pub(crate) source_dir: String,
}
impl Extension {
pub(crate) fn apply(
&self,
chart_path: String,
config_path: String,
) -> Result<(), Box<dyn std::error::Error>> {
let extension_path = match is_path_relative(self.source_dir.clone()) {
true => format!("{}/{}", config_path, self.source_dir),
false => self.source_dir.clone(),
};
let extension_name = match self.name.clone() {
Some(res) => res,
None => "Unnamed".to_string(),
};
message_info(&format!("applying extension: '{}'", extension_name));
let target_dir = format!("{}/{}", chart_path, self.target_dir);
message_info(&format!("trying to create a dir: {}", target_dir));
create_dir(target_dir.clone())?;
message_info(&format!("copying {} to {}", extension_path, target_dir));
copy_recursively(extension_path, target_dir)?;
Ok(())
}
}

103
helmule/src/main.rs Normal file
View File

@ -0,0 +1,103 @@
use clap::Parser;
use config::Config;
use helmzoo_lib::{
self,
cli::{check_prerequisites, get_full_path_dir},
config::{read_config, ConfigImpl},
helm::repository::RepositoryImpl,
output::{message_empty, message_error},
};
use std::{error::Error, process::exit};
use crate::mirror::mirror_from_mirror_obj;
mod config;
mod extensions;
mod mirror;
mod patches;
/// Simple program to greet a person
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Args {
/// Name of the working dir
#[arg(short, long)]
workdir: Option<String>,
/// Path to the configuration file
#[arg(short, long)]
config: String,
/// Dry run
#[arg(short, long, default_value = "false")]
dry_run: bool,
#[arg(long, default_value = "false")]
skip_prerequisites_check: bool,
/// Init git patch. Use it if you want to create git patch for a chart
/// It's going to pull a chart and init a git repo there, so you can
/// apply changes and create a patch file
/// It's not going to try mirroring changes, but will apply extensions
/// and patches that are already defined
#[arg(long)]
init_git_patch: Option<Vec<String>>,
#[arg(long, default_value = "helm")]
helm_bin: String,
#[arg(long, default_value = "git")]
git_bin: String,
#[arg(long, default_value = "yq")]
yq_bin: String,
}
fn exec(args: Args) -> Result<(), Box<dyn Error>> {
let prerequisites = vec![args.helm_bin, args.git_bin, args.yq_bin];
check_prerequisites(prerequisites)?;
let workdir_path = helmzoo_lib::workdir::setup_workdir(args.workdir)?;
let mut config: Config = read_config(args.config.clone())?;
let config_full_path = get_full_path_dir(args.config.clone())?;
config.apply_includes(config_full_path.clone())?;
config
.charts
.into_iter()
.try_for_each(|mut chart| -> Result<(), Box<dyn Error>> {
chart.populate_variables(config.variables.clone());
// First step is to pull the chart to the working dir
let current_repo = chart.chart.find_repo(config.repositories.clone())?;
let chart_path = current_repo.pull_chart(chart.chart.clone(), workdir_path.clone())?;
if let Some(extensions) = chart.extensions.clone() {
extensions
.into_iter()
.try_for_each(|extension| -> Result<(), Box<dyn Error>> {
extension.apply(chart_path.clone(), config_full_path.clone())
})?
}
if let Some(patches) = chart.patches.clone() {
patches
.into_iter()
.try_for_each(|patch| -> Result<(), Box<dyn Error>> {
patch.apply(chart_path.clone(), config.patches.clone())
})?
}
config
.mirrors
.iter()
.try_for_each(|mirror| -> Result<(), Box<dyn Error>> {
mirror_from_mirror_obj(mirror.clone())?.push(
workdir_path.clone(),
chart_path.clone(),
chart.clone(),
args.dry_run,
)
})?;
message_empty(&format!("{}", chart.chart.name));
Ok(())
})?;
todo!()
}
fn main() {
match exec(Args::parse()) {
Ok(()) => message_empty("Thanks for using helmule"),
Err(err) => {
message_error(err);
exit(1)
}
}
}

View File

@ -0,0 +1,126 @@
use helmzoo_lib::{cli::cli_exec_from_dir, template};
use crate::config::ChartExtended;
use super::Target;
pub(crate) struct CustomCommands {
pub(crate) package: Vec<String>,
pub(crate) upload: Vec<String>,
}
impl Target for CustomCommands {
fn push(
&self,
workdir_path: String,
chart_path: String,
chart_local: ChartExtended,
dry_run: bool,
) -> Result<(), Box<dyn std::error::Error>> {
for cmd_tmpl in self.package.clone() {
let mut reg = helmzoo_lib::template::register_handlebars();
reg.register_template_string("cmd", cmd_tmpl)?;
let cmd = reg.render("cmd", &chart_local)?;
cli_exec_from_dir(cmd, workdir_path.clone())?;
}
if !dry_run {
for cmd_tmpl in self.upload.clone() {
let mut reg = template::register_handlebars();
reg.register_template_string("cmd", cmd_tmpl)?;
let cmd = reg.render("cmd", &chart_local)?;
cli_exec_from_dir(cmd, workdir_path.clone())?;
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::CustomCommands;
use crate::{config::ChartExtended, mirror::Target};
use std::{collections::HashMap, fs::create_dir_all, path::Path};
use tempfile::TempDir;
fn get_chart_local() -> ChartExtended {
let mut vars: HashMap<String, String> = HashMap::new();
vars.insert("key".to_string(), "value".to_string());
ChartExtended {
name: "chart".to_string(),
version: "1.0.0".to_string(),
path: "chart-1.0.0".to_string(),
repo_url: "https:://helm.repo".to_string(),
vars,
}
}
fn prepare_test_workdir(chart_path: String) -> String {
let workdir = TempDir::new().unwrap().path().to_str().unwrap().to_string();
println!("test workdir is {}", workdir.clone());
create_dir_all(format!("{}/{}", workdir, chart_path)).unwrap();
workdir
}
#[test]
fn test_package_basic() {
let chart_local = get_chart_local();
let workdir = prepare_test_workdir(chart_local.path.clone());
let custom_commands = CustomCommands {
package: vec!["touch package".to_string()],
upload: vec!["touch upload".to_string()],
};
let cc_target: Box<dyn Target> = Box::from(custom_commands);
cc_target.push(workdir.clone(), chart_local, true).unwrap();
assert!(Path::new(&format!("{}/package", workdir)).exists());
assert!(!Path::new(&format!("{}/upload", workdir)).exists());
}
#[test]
fn test_upload_basic() {
let chart_local = get_chart_local();
let workdir = prepare_test_workdir(chart_local.path.clone());
let custom_commands = CustomCommands {
package: vec!["touch package".to_string()],
upload: vec!["touch upload".to_string()],
};
let cc_target: Box<dyn Target> = Box::from(custom_commands);
cc_target.push(workdir.clone(), chart_local, false).unwrap();
assert!(Path::new(&format!("{}/package", workdir)).exists());
assert!(Path::new(&format!("{}/upload", workdir)).exists());
}
#[test]
fn test_templates() {
let chart_local = get_chart_local();
let workdir = prepare_test_workdir(chart_local.path.clone());
let custom_commands = CustomCommands {
package: vec!["touch {{ name }}-{{ version }}".to_string()],
upload: vec!["touch {{ repo_url }}-{{ vars.key }}".to_string()],
};
let cc_target: Box<dyn Target> = Box::from(custom_commands);
cc_target
.push(workdir.clone(), chart_local.clone(), true)
.unwrap();
assert!(Path::new(&format!(
"{}/{}-{}",
workdir, chart_local.name, chart_local.version
))
.exists());
assert!(!Path::new(&format!(
"{}/{}-{}",
workdir,
chart_local.repo_url,
chart_local.vars.get("key").unwrap()
))
.exists());
}
}

93
helmule/src/mirror/git.rs Normal file
View File

@ -0,0 +1,93 @@
use base64::{engine::general_purpose, Engine};
use dircpy::*;
use helmzoo_lib::git::{CheckoutOptions, CommitOptions, Git, GitOptions, PushOptions};
use helmzoo_lib::template;
use serde::{Deserialize, Serialize};
use crate::config::ChartExtended;
use super::Target;
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub(crate) struct GitMirror {
pub(crate) git_dir: Option<String>,
pub(crate) url: String,
pub(crate) path: String,
#[serde(default = "default_branch")]
pub(crate) branch: String,
#[serde(default = "default_commit")]
pub(crate) commit: String,
pub(crate) rebase_to: Option<String>,
#[serde(default = "default_git_bin")]
pub(crate) git_bin: String,
}
fn default_commit() -> String {
"helmuled {{ name }}-{{ version }}".to_string()
}
fn default_branch() -> String {
"main".to_string()
}
fn default_git_bin() -> String {
"git".to_string()
}
impl Target for GitMirror {
fn push(
&self,
workdir_path: String,
chart_path: String,
chart_local: ChartExtended,
dry_run: bool,
) -> Result<(), Box<dyn std::error::Error>> {
let repo_path = match self.git_dir.clone() {
Some(dir) => template::render(dir.clone(), &chart_local)?,
None => general_purpose::STANDARD_NO_PAD.encode(self.url.clone()),
};
let git_instance = Git {
url: template::render(self.url.clone(), &chart_local)?,
repo_path: repo_path.clone(),
};
let git_opts = GitOptions::new(self.git_bin.clone(), Some(workdir_path.clone()));
git_instance.clone(git_opts.clone())?;
// Prepare branch
let checkout_opts = CheckoutOptions {
create: true,
git_ref: template::render(self.branch.clone(), &chart_local)?,
};
git_instance.checkout(git_opts.clone(), checkout_opts)?;
// Prepare path
let path = template::render(self.path.clone(), &chart_local)?;
let repo_local_full_path = format!("{}/{}/{}", workdir_path.clone(), repo_path, path);
CopyBuilder::new(chart_path.clone(), repo_local_full_path.clone())
.overwrite_if_size_differs(true)
.run()?;
// Prepare the commit message
let commit_opts = CommitOptions {
message: template::render(self.commit.clone(), &chart_local)?,
add: true,
};
git_instance.commit(git_opts.clone(), commit_opts)?;
if !dry_run {
let force_push = match self.rebase_to {
Some(_) => true,
None => false,
};
let push_opts = PushOptions {
rebase_to: self.rebase_to.clone(),
force: force_push,
brahcn: self.branch.clone(),
};
git_instance.push(git_opts.clone(), push_opts)?;
}
Ok(())
}
}

46
helmule/src/mirror/mod.rs Normal file
View File

@ -0,0 +1,46 @@
use serde::{Deserialize, Serialize};
use crate::config::ChartExtended;
pub(crate) mod custom_command;
pub(crate) mod git;
pub(crate) trait Target {
fn push(
&self,
workdir_path: String,
chart_path: String,
chart_local: ChartExtended,
dry_run: bool,
) -> Result<(), Box<dyn std::error::Error>>;
}
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub(crate) struct Mirror {
pub(crate) name: String,
pub(crate) git: Option<git::GitMirror>,
pub(crate) custom_command: Option<CustomCommandsMirror>,
}
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub(crate) struct CustomCommandsMirror {
pub(crate) package: Vec<String>,
pub(crate) upload: Vec<String>,
}
pub(crate) fn mirror_from_mirror_obj(
mirror: Mirror,
) -> Result<Box<dyn Target>, Box<dyn std::error::Error>> {
if let Some(git) = mirror.git {
return Ok(Box::from(git));
} else if let Some(command) = mirror.custom_command {
return Ok(Box::from(custom_command::CustomCommands {
package: command.package,
upload: command.upload,
}));
}
Err(Box::from(format!(
"a kind is unknown for the mirror {}",
mirror.name
)))
}

313
helmule/src/patches.rs Normal file
View File

@ -0,0 +1,313 @@
use std::{
fs::{self, read_dir, remove_dir_all, File, OpenOptions},
io::Write,
path::{Path, PathBuf},
};
use helmzoo_lib::{
cli::{cli_exec, cli_exec_from_dir},
output::message_info,
};
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub(crate) struct RegexpPatchObj {
pub(crate) name: String,
pub(crate) targets: Vec<String>,
pub(crate) before: Option<String>,
pub(crate) after: Option<String>,
}
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub(crate) struct RegexpPatch {
pub(crate) path: String,
}
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub(crate) struct GitPatch {
path: String,
}
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub(crate) enum YqOperations {
Add,
Delete,
Replace,
}
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub(crate) struct YqPatch {
file: String,
op: YqOperations,
key: String,
value: Option<String>,
}
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub(crate) struct CustomCommandPatch {
commands: Vec<String>,
}
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub(crate) struct Patch {
name: Option<String>,
regexp: Option<RegexpPatch>,
git: Option<GitPatch>,
custom_command: Option<CustomCommandPatch>,
yq: Option<YqPatch>,
}
impl Patch {
pub(crate) fn apply(
&self,
chart_local_path: String,
global_patches: Option<Vec<Patch>>,
) -> Result<(), Box<dyn std::error::Error>> {
let patch_action: Box<dyn PatchInterface>;
if self.is_ref() {
let patch_ref = self.get_ref(global_patches)?;
patch_action = Box::from(patch_action_from_definition(patch_ref)?);
} else {
patch_action = Box::from(patch_action_from_definition(self.clone())?);
}
patch_action.apply(chart_local_path)
}
pub(crate) fn get_path(&self) -> String {
match patch_action_from_definition(self.clone()) {
Ok(patch) => patch.get_path(),
Err(_) => "".to_string(),
}
}
pub(crate) fn set_path(&mut self, path: String) {
if let Some(ref mut regexp) = self.regexp {
regexp.path = path;
} else if let Some(ref mut git) = self.git {
git.path = path;
}
}
fn is_ref(&self) -> bool {
self.regexp.is_none()
&& self.git.is_none()
&& self.custom_command.is_none()
&& self.yq.is_none()
&& self.name.is_some()
}
pub(crate) fn get_ref(
&self,
global_patches: Option<Vec<Patch>>,
) -> Result<Patch, Box<dyn std::error::Error>> {
match global_patches {
Some(patches) => {
let patch = patches
.iter()
.find(|&patch| patch.clone().name.unwrap() == self.clone().name.unwrap());
match patch {
Some(patch) => {
return Ok(patch.clone());
}
None => {
return Err(Box::from(format!(
"global patch is not found: {}",
self.clone().name.unwrap()
)))
}
}
}
None => {
return Err(Box::from(format!(
"patch {} is recognized as a reference, but global patches are not defined",
self.clone().name.unwrap()
)))
}
}
}
}
trait PatchInterface {
fn apply(&self, chart_local_path: String) -> Result<(), Box<dyn std::error::Error>>;
fn get_path(&self) -> String;
fn set_path(&mut self, new_path: String);
}
impl PatchInterface for YqPatch {
fn apply(&self, chart_local_path: String) -> Result<(), Box<dyn std::error::Error>> {
let cmd = match self.op {
YqOperations::Add => {
let value = match self
.value
.clone()
.unwrap()
.starts_with(['{', '[', '\"', '\''])
{
true => self.value.clone().unwrap(),
false => format!("\"{}\"", self.value.clone().unwrap()),
};
format!("yq -i '{} += {}' {}", self.key, value, self.file)
}
YqOperations::Delete => format!("yq -i \'del({})\' {}", self.key, self.file),
YqOperations::Replace => {
let value = match self.value.clone().unwrap().starts_with(['{', '[']) {
true => self.value.clone().unwrap(),
false => format!("\"{}\"", self.value.clone().unwrap()),
};
format!("yq e -i '{} = {}' {}", self.key, value, self.file)
}
};
cli_exec_from_dir(cmd, chart_local_path)?;
Ok(())
}
fn get_path(&self) -> String {
"".to_string()
}
fn set_path(&mut self, _new_path: String) {}
}
impl PatchInterface for RegexpPatch {
fn apply(&self, chart_local_path: String) -> Result<(), Box<dyn std::error::Error>> {
for entry in read_dir(self.path.clone())? {
let entry = entry?;
let filetype = entry.file_type()?;
if filetype.is_dir() {
message_info(&format!(
"reading dirs is not supported yet, skipping {:?}",
entry.path()
));
} else {
message_info(&format!("reading a patch file: {:?}", entry.path()));
let config_content = File::open(entry.path())?;
for patch_des in serde_yaml::Deserializer::from_reader(config_content) {
let patch: RegexpPatchObj = match RegexpPatchObj::deserialize(patch_des) {
Ok(patch) => patch,
Err(err) => return Err(Box::from(err)),
};
message_info(&format!("applying patch: {}", patch.name));
let after = match patch.after {
Some(after) => after,
None => "".to_string(),
};
match patch.before {
Some(before) => {
let patch_regexp = regex::Regex::new(before.as_str())?;
for target in patch.targets {
let file_path = format!("{}/{}", chart_local_path, target);
let file_content = fs::read_to_string(file_path.clone())?;
let new_content =
patch_regexp.replace_all(file_content.as_str(), after.clone());
let mut file = OpenOptions::new()
.write(true)
.truncate(true)
.open(file_path.clone())?;
file.write(new_content.as_bytes())?;
}
}
None => {
for target in patch.targets {
let file_path = format!("{}/{}", chart_local_path, target);
let file_content = fs::read_to_string(file_path.clone())?;
let new_content = format!("{}\n{}", file_content, after);
let mut file = OpenOptions::new()
.write(true)
.append(false)
.open(file_path.clone())?;
file.write(new_content.as_bytes())?;
}
}
};
}
}
}
Ok(())
}
fn get_path(&self) -> String {
self.path.clone()
}
fn set_path(&mut self, new_path: String) {
self.path = new_path
}
}
impl PatchInterface for GitPatch {
fn apply(&self, chart_local_path: String) -> Result<(), Box<dyn std::error::Error>> {
if !is_git_repo(chart_local_path.clone()) {
init_git_repo(chart_local_path.clone())?;
};
let cmd = format!("git -C {} apply {}", chart_local_path, self.path);
cli_exec(cmd)?;
remove_dir_all(chart_local_path + "/.git")?;
Ok(())
}
fn get_path(&self) -> String {
self.path.clone()
}
fn set_path(&mut self, new_path: String) {
self.path = new_path
}
}
impl PatchInterface for CustomCommandPatch {
fn apply(&self, chart_local_path: String) -> Result<(), Box<dyn std::error::Error>> {
for cmd in self.commands.clone() {
cli_exec_from_dir(cmd, chart_local_path.clone())?;
}
Ok(())
}
fn get_path(&self) -> String {
// Empty stings, cause cc patch doesn't have a path
"".to_string()
}
fn set_path(&mut self, _new_path: String) {
()
}
}
fn patch_action_from_definition(
patch: Patch,
) -> Result<Box<dyn PatchInterface>, Box<dyn std::error::Error>> {
if let Some(regexp) = patch.regexp {
Ok(Box::new(RegexpPatch { path: regexp.path }))
} else if let Some(git) = patch.git {
return Ok(Box::new(GitPatch {
path: {
let path = PathBuf::from(git.path.clone());
match fs::canonicalize(path).ok() {
Some(can_path) => can_path.into_os_string().into_string().ok().unwrap(),
None => git.path.clone(),
}
},
}));
} else if let Some(custom_command) = patch.custom_command {
return Ok(Box::new(CustomCommandPatch {
commands: custom_command.commands,
}));
} else if let Some(yq) = patch.yq {
if yq.op != YqOperations::Delete && yq.value.is_none() {
return Err(Box::from("yq patch of non kind 'delete' requires a value"));
};
return Ok(Box::from(yq));
} else {
return Err(Box::from("unknown patch type"));
}
}
fn is_git_repo(path: String) -> bool {
let dot_git_path = path + ".git";
Path::new(dot_git_path.as_str()).exists()
}
pub(crate) fn init_git_repo(path: String) -> Result<(), Box<dyn std::error::Error>> {
cli_exec(format!("git -C {} init .", path))?;
cli_exec(format!("git -C {} add .", path))?;
cli_exec(format!("git -C {} commit -m 'Init commit'", path))?;
Ok(())
}

21
lib/Cargo.toml Normal file
View File

@ -0,0 +1,21 @@
[package]
name = "helmzoo_lib"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
serde = { workspace = true }
serde_json ={ workspace = true }
serde_yaml = { workspace = true }
tempfile = { workspace = true }
base64 = { workspace = true }
console = "0.15.8"
dialoguer = "0.11.0"
env_logger = "0.10.1"
indicatif = "0.17.7"
log = "0.4.20"
which = "6.0.0"
handlebars = "5.0.0"
chrono = "0.4.31"

144
lib/src/cli.rs Normal file
View File

@ -0,0 +1,144 @@
use std::{
error::Error,
fs::{self, read_dir},
path::{Path, PathBuf},
process::Command,
};
use which::which;
use crate::output::message_info;
pub fn cli_exec(command: String) -> Result<String, Box<dyn Error>> {
message_info(&format!("executing: {}", command));
let expect = format!("command has failed: {}", command);
let output = Command::new("sh")
.arg("-c")
.arg(command)
.output()
.expect(&expect);
let stderr = String::from_utf8_lossy(&output.stderr);
if !&output.status.success() {
return Err(Box::from(stderr));
};
Ok(String::from_utf8_lossy(&output.stdout).to_string())
}
pub fn cli_exec_from_dir(command: String, dir: String) -> Result<String, Box<dyn Error>> {
message_info(&format!("executing: {} from {}", command, dir));
let expect = format!("command has failed: {}", command);
let output = Command::new("sh")
.arg("-c")
.current_dir(dir)
.arg(command)
.output()
.expect(&expect);
let stderr = String::from_utf8_lossy(&output.stderr);
if !&output.status.success() {
return Err(Box::from(stderr));
};
let mut stdout = String::from_utf8_lossy(&output.stdout).to_string();
stdout.pop();
Ok(stdout)
}
// A helper that checks wheter all the required binaries are installed
pub fn check_prerequisites(bins: Vec<String>) -> Result<(), Box<dyn Error>> {
message_info(&"checking prerequisites".to_string());
for bin in bins {
message_info(&format!("checking {}", bin));
which(bin)?;
}
Ok(())
}
pub fn get_full_path(rel_path: String) -> Result<String, Box<dyn Error>> {
match PathBuf::from(&rel_path)
.canonicalize()?
.into_os_string()
.into_string()
{
Ok(path) => Ok(path),
Err(_) => Err(Box::from(format!(
"{} can't be converted into a full path",
rel_path
))),
}
}
pub fn get_full_path_dir(rel_path: String) -> Result<String, Box<dyn Error>> {
let res = match PathBuf::from(&rel_path).parent() {
Some(res) => res.canonicalize()?.into_os_string().into_string(),
None => PathBuf::from(&rel_path)
.canonicalize()?
.into_os_string()
.into_string(),
};
match res {
Ok(path) => Ok(path),
Err(_) => Err(Box::from(format!(
"{} can't be converted into a full path",
rel_path
))),
}
}
/// Copy files from source to destination recursively.
pub fn copy_recursively(
source: impl AsRef<Path>,
destination: impl AsRef<Path>,
) -> Result<(), Box<dyn Error>> {
for entry in read_dir(source)? {
let entry = entry?;
let filetype = entry.file_type()?;
if filetype.is_dir() {
copy_recursively(entry.path(), destination.as_ref().join(entry.file_name()))?;
} else {
message_info(&format!("trying to copy {:?}", entry.path()));
fs::copy(entry.path(), destination.as_ref().join(entry.file_name()))?;
}
}
Ok(())
}
pub fn is_path_relative(path: String) -> bool {
!path.starts_with('/')
}
#[cfg(test)]
mod tests {
use super::{cli_exec, cli_exec_from_dir};
use tempfile::TempDir;
#[test]
fn test_stderr() {
let command = ">&2 echo \"error\" && exit 1";
let test = cli_exec(command.to_string());
assert_eq!(test.err().unwrap().to_string(), "error\n".to_string());
}
#[test]
fn test_stdout() {
let command = "echo test";
let test = cli_exec(command.to_string());
assert_eq!(test.unwrap().to_string(), "test\n".to_string());
}
#[test]
fn test_stdout_current_dir() {
let dir = TempDir::new().unwrap();
let dir_str = dir.path().to_str().unwrap().to_string();
let command = "echo $PWD";
let test = cli_exec_from_dir(command.to_string(), dir_str.clone());
assert!(test.unwrap().to_string().contains(dir_str.as_str()));
}
#[test]
fn test_stderr_current_dir() {
let dir = TempDir::new().unwrap();
let dir_str = dir.path().to_str().unwrap().to_string();
let command = ">&2 echo \"error\" && exit 1";
let test = cli_exec_from_dir(command.to_string(), dir_str.clone());
assert_eq!(test.err().unwrap().to_string(), "error\n".to_string());
}
}

86
lib/src/config.rs Normal file
View File

@ -0,0 +1,86 @@
use std::{error::Error, ffi::OsStr, fs::File, path::Path};
use serde::de::DeserializeOwned;
pub trait ConfigImpl {
fn apply_includes(&mut self, config_path: String) -> Result<(), Box<dyn Error>>;
}
pub fn read_config<T: DeserializeOwned>(path: String) -> Result<T, Box<dyn Error>> {
let config_content = File::open(path.clone())?;
let config = match get_extension_from_filename(&path) {
Some(ext) => match ext {
"yaml" | "yml" => serde_yaml::from_reader(config_content)?,
_ => return Err(Box::from(format!("{} files are not supported", ext))),
},
None => return Err(Box::from("can't read file without extension")),
};
Ok(config)
}
fn get_extension_from_filename(filename: &str) -> Option<&str> {
Path::new(filename).extension().and_then(OsStr::to_str)
}
#[cfg(test)]
mod tests {
use super::{get_extension_from_filename, read_config};
use serde::{Deserialize, Serialize};
use std::{error::Error, fs::File, io::Write};
use tempfile::tempdir;
#[test]
fn test_extension_getter() {
let filepath = "/tmp/config.yaml";
let extension = get_extension_from_filename(filepath);
assert_eq!(extension, Some("yaml"));
}
#[test]
fn test_extension_getter_empty() {
let filepath = "/tmp/config";
let extension = get_extension_from_filename(filepath);
assert_eq!(extension, None);
}
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
struct DummyConfig {
string: String,
amounts: Vec<DummyProperty>,
}
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
struct DummyProperty {
amount: i32,
}
fn prepare_test_file(name: &str, data: &str) -> Result<String, Box<dyn Error>> {
let dir = tempdir()?;
let file_path = dir.into_path().join(&name);
let mut file = File::create(file_path.clone())?;
file.write_all(data.as_bytes())?;
let path = file_path.into_os_string().to_str().unwrap().to_string();
Ok(path)
}
#[test]
fn test_config_reader() -> Result<(), Box<dyn Error>> {
let content = "---
string: test
amounts:
- amount: 4
- amount: 5
";
let file_path = prepare_test_file("config.yaml", content)?;
let config_data: DummyConfig;
config_data = read_config(file_path)?;
let expected = DummyConfig {
string: "test".to_string(),
amounts: vec![DummyProperty { amount: 4 }, DummyProperty { amount: 5 }],
};
assert_eq!(expected, config_data);
Ok(())
}
}

327
lib/src/git.rs Normal file
View File

@ -0,0 +1,327 @@
use std::error::Error;
use serde::{Deserialize, Serialize};
use crate::cli::{cli_exec, cli_exec_from_dir};
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub struct GitOptions {
bin: String,
workdir: Option<String>,
}
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub struct CheckoutOptions {
// Checkout with -b if branch doesn't exist
pub create: bool,
#[serde(alias = "ref")]
pub git_ref: String,
}
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub struct PushOptions {
pub rebase_to: Option<String>,
// If rebase, should be always set to true
pub force: bool,
pub brahcn: String,
}
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub struct CommitOptions {
pub message: String,
pub add: bool,
}
impl GitOptions {
pub fn new(bin: String, workdir: Option<String>) -> Self {
Self { bin, workdir }
}
}
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub struct Git {
pub url: String,
pub repo_path: String,
}
impl Git {
pub fn new(url: String, repo_path: String) -> Self {
Self { url, repo_path }
}
pub fn clone(&self, git_opts: GitOptions) -> Result<(), Box<dyn Error>> {
let cmd = format!("{} clone {} {}", git_opts.bin, self.url, self.repo_path);
match self.exec(cmd, git_opts.workdir.clone()) {
Ok(_) => Ok(()),
Err(err) => Err(err),
}
}
pub fn checkout(
&self,
git_opts: GitOptions,
opts: CheckoutOptions,
) -> Result<(), Box<dyn Error>> {
let cmd = format!(
"{} -C {} checkout {}",
git_opts.bin, self.repo_path, opts.git_ref
);
match self.exec(cmd, git_opts.workdir.clone()) {
Ok(_) => Ok(()),
Err(err) => match opts.create {
true => {
let cmd = format!(
"{} -C {} checkout -b {}",
git_opts.bin, self.repo_path, opts.git_ref
);
match self.exec(cmd, git_opts.workdir.clone()) {
Ok(_) => Ok(()),
Err(err) => Err(err),
}
}
false => Err(err),
},
}
}
pub fn commit(&self, git_opts: GitOptions, opts: CommitOptions) -> Result<(), Box<dyn Error>> {
if opts.add {
let cmd = format!("{} -C {} add .", git_opts.bin, self.repo_path);
let _ = self.exec(cmd, git_opts.workdir.clone())?;
}
let cmd = format!(
"{} diff --staged --quiet || {} -C {} commit -m \"{}\"",
git_opts.bin, git_opts.bin, self.repo_path, opts.message
);
match self.exec(cmd, git_opts.workdir.clone()) {
Ok(_) => Ok(()),
Err(err) => Err(err),
}
}
// TODO: Add tests for rebase and force
pub fn push(&self, git_opts: GitOptions, opts: PushOptions) -> Result<(), Box<dyn Error>> {
if let Some(branch) = opts.rebase_to {
let cmd = format!("{} -C {} rebase {}", git_opts.bin, self.repo_path, branch);
let _ = self.exec(cmd, git_opts.workdir.clone())?;
}
let mut args = String::new();
if opts.force {
args = format!("{} --force", args);
}
let cmd = format!(
"{} -C {} push --set-upstream origin {} {}",
git_opts.bin, self.repo_path, opts.brahcn, args
);
let _ = self.exec(cmd, git_opts.workdir.clone());
Ok(())
}
// TODO: Add tests
pub fn init(&self, git_opts: GitOptions) -> Result<(), Box<dyn Error>> {
let cmd = format!("{} -C {} init", git_opts.bin, self.repo_path);
let _ = self.exec(cmd, git_opts.workdir.clone())?;
let cmd = format!(
"{} -C {} remote add origin {}",
git_opts.bin, self.repo_path, self.url
);
let _ = self.exec(cmd, git_opts.workdir.clone())?;
Ok(())
}
fn exec(&self, cmd: String, workdir: Option<String>) -> Result<String, Box<dyn Error>> {
match workdir {
Some(workdir) => cli_exec_from_dir(cmd, workdir),
None => cli_exec(cmd),
}
}
}
#[cfg(test)]
mod tests {
use crate::cli::cli_exec_from_dir;
use crate::git::{CheckoutOptions, CommitOptions, Git, PushOptions};
use std::error::Error;
use std::path::Path;
use tempfile::tempdir;
use super::GitOptions;
fn prepare_a_repo() -> Result<String, Box<dyn Error>> {
let tmp_dir = tempdir()?
.into_path()
.into_os_string()
.into_string()
.unwrap();
cli_exec_from_dir("git init".to_string(), tmp_dir.clone())?;
Ok(tmp_dir)
}
fn prepare_a_workdir() -> Result<String, Box<dyn Error>> {
let tmp_dir = tempdir()?
.into_path()
.into_os_string()
.into_string()
.unwrap();
Ok(tmp_dir)
}
#[test]
fn test_pull_no_wd() -> Result<(), Box<dyn Error>> {
let tmp_dir = prepare_a_workdir()?;
let git_dir = format!("{}/test", tmp_dir);
let git = Git::new(prepare_a_repo()?, git_dir.clone());
let git_opts = GitOptions::new("git".to_string(), Some(tmp_dir.clone()));
git.clone(git_opts)?;
let result = Path::new(&git_dir).exists();
assert!(result);
Ok(())
}
#[test]
fn test_pull_wd() -> Result<(), Box<dyn Error>> {
let tmp_dir = prepare_a_workdir()?;
let git_dir = "test".to_string();
println!("{}", tmp_dir.clone());
let git = Git::new(prepare_a_repo()?, git_dir.clone());
let git_opts = GitOptions::new("git".to_string(), Some(tmp_dir.clone()));
git.clone(git_opts)?;
let result = Path::new(&format!("{}/{}", tmp_dir, git_dir)).exists();
assert!(result);
Ok(())
}
#[test]
fn test_checkout_no_create() -> Result<(), Box<dyn Error>> {
let tmp_dir = prepare_a_workdir()?;
let git_dir = "test".to_string();
let git = Git::new(prepare_a_repo()?, git_dir.clone());
let git_opts = GitOptions::new("git".to_string(), Some(tmp_dir.clone()));
cli_exec_from_dir("git checkout -b test".to_string(), git.url.clone())?;
cli_exec_from_dir(
"touch test.txt && git add . && git commit -m test".to_string(),
git.url.clone(),
)?;
cli_exec_from_dir("git checkout -b main".to_string(), git.url.clone())?;
let checkout_options = CheckoutOptions {
create: false,
git_ref: "test".to_string(),
};
git.clone(git_opts.clone())?;
git.checkout(git_opts, checkout_options)?;
let result = cli_exec_from_dir(
"git rev-parse --abbrev-ref HEAD".to_string(),
format!("{}/{}", tmp_dir.clone(), git_dir.clone()),
)?;
assert_eq!(result, "test");
Ok(())
}
#[test]
fn test_checkout_no_create_err() -> Result<(), Box<dyn Error>> {
let tmp_dir = prepare_a_workdir()?;
let git_dir = "test".to_string();
let git = Git::new(prepare_a_repo()?, git_dir.clone());
let git_opts = GitOptions::new("git".to_string(), Some(tmp_dir.clone()));
cli_exec_from_dir("git checkout -b main".to_string(), git.url.clone())?;
cli_exec_from_dir(
"touch test.txt && git add . && git commit -m test".to_string(),
git.url.clone(),
)?;
git.clone(git_opts.clone())?;
let checkout_options = CheckoutOptions {
create: false,
git_ref: "test".to_string(),
};
let res = git.checkout(git_opts, checkout_options);
assert!(res.is_err());
Ok(())
}
#[test]
fn test_checkout_create() -> Result<(), Box<dyn Error>> {
let tmp_dir = prepare_a_workdir()?;
let git_dir = "test".to_string();
let git = Git::new(prepare_a_repo()?, git_dir.clone());
let git_opts = GitOptions::new("git".to_string(), Some(tmp_dir.clone()));
cli_exec_from_dir("git checkout -b main".to_string(), git.url.clone())?;
cli_exec_from_dir(
"touch test.txt && git add . && git commit -m test".to_string(),
git.url.clone(),
)?;
git.clone(git_opts.clone())?;
let checkout_options = CheckoutOptions {
create: true,
git_ref: "test".to_string(),
};
git.checkout(git_opts, checkout_options)?;
let result = cli_exec_from_dir(
"git rev-parse --abbrev-ref HEAD".to_string(),
format!("{}/{}", tmp_dir.clone(), git_dir.clone()),
)?;
assert_eq!(result, "test");
Ok(())
}
#[test]
fn test_commit() -> Result<(), Box<dyn Error>> {
let tmp_dir = prepare_a_workdir()?;
let git_dir = "test".to_string();
let full_path = format!("{}/{}", tmp_dir.clone(), git_dir.clone());
let git = Git::new(prepare_a_repo()?, git_dir.clone());
let git_opts = GitOptions::new("git".to_string(), Some(tmp_dir.clone()));
git.clone(git_opts.clone())?;
let commit_options = CommitOptions {
message: "test commit".to_string(),
add: false,
};
cli_exec_from_dir("touch test.txt && git add .".to_string(), full_path.clone())?;
git.commit(git_opts, commit_options)?;
let result = cli_exec_from_dir("git log --format=%B -n 1 HEAD".to_string(), full_path)?;
assert_eq!(result, "test commit\n");
Ok(())
}
#[test]
fn test_commit_add() -> Result<(), Box<dyn Error>> {
let tmp_dir = prepare_a_workdir()?;
let git_dir = "test".to_string();
let full_path = format!("{}/{}", tmp_dir.clone(), git_dir.clone());
let git = Git::new(prepare_a_repo()?, git_dir.clone());
let git_opts = GitOptions::new("git".to_string(), Some(tmp_dir.clone()));
git.clone(git_opts.clone())?;
let commit_options = CommitOptions {
message: "test commit".to_string(),
add: true,
};
cli_exec_from_dir("touch test.txt".to_string(), full_path.clone())?;
git.commit(git_opts, commit_options)?;
let result = cli_exec_from_dir("git log --format=%B -n 1 HEAD".to_string(), full_path)?;
assert_eq!(result, "test commit\n");
Ok(())
}
#[test]
fn test_push_no_rebase() -> Result<(), Box<dyn Error>> {
let tmp_dir = prepare_a_workdir()?;
let git_dir = "test".to_string();
let full_path = format!("{}/{}", tmp_dir.clone(), git_dir.clone());
let git = Git::new(prepare_a_repo()?, git_dir.clone());
let git_opts = GitOptions::new("git".to_string(), Some(tmp_dir.clone()));
git.clone(git_opts.clone())?;
let push_options = PushOptions {
rebase_to: None,
force: false,
brahcn: "main".to_string(),
};
cli_exec_from_dir("git checkout -b main".to_string(), full_path.clone())?;
cli_exec_from_dir(
"touch test.txt && git add . && git commit -m 'test commit'".to_string(),
full_path.clone(),
)?;
git.push(git_opts, push_options)?;
cli_exec_from_dir("git checkout main".to_string(), git.url.clone())?;
let result = cli_exec_from_dir("git log --format=%B -n 1 HEAD".to_string(), git.url)?;
assert_eq!(result, "test commit\n");
Ok(())
}
}

75
lib/src/helm/chart.rs Normal file
View File

@ -0,0 +1,75 @@
use std::error::Error;
use serde::{Deserialize, Serialize};
use super::repository::{Repository, RepositoryImpl};
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone, Default)]
pub struct Chart {
// A name of the helm chart
pub name: String,
// A reference to repository by name
pub repository: String,
pub mirrors: Vec<String>,
// Versions to be mirrored
#[serde(default = "latest")]
pub version: String,
}
pub(crate) fn latest() -> String {
"latest".to_string()
}
impl Chart {
pub fn find_repo(
&self,
repositories: Vec<Repository>,
) -> Result<Box<dyn RepositoryImpl>, Box<dyn Error>> {
for repository in repositories {
if repository.name == self.repository {
if let Some(helm) = repository.helm {
return Ok(Box::from(helm));
} else if let Some(git) = repository.git {
return Ok(Box::from(git));
} else {
return Err(Box::from("unsupported kind of repository"));
}
}
}
//let err = error!("repo {} is not found in the repo list", self.repository);
let error_msg = format!("repo {} is not found in the repo list", self.repository);
Err(Box::from(error_msg))
}
}
#[cfg(test)]
mod tests {
use std::error::Error;
use crate::helm::{
chart::latest,
helm_repository::HelmRepo,
repository::{Repository, RepositoryImpl},
};
use super::Chart;
#[test]
fn test_find_repo() -> Result<(), Box<dyn Error>> {
let chart = Chart {
name: "test".to_string(),
repository: "test".to_string(),
mirrors: vec!["test".to_string()],
version: latest(),
};
let repo = Repository {
name: "test".to_string(),
helm: Some(HelmRepo {
url: "test.rocks".to_string(),
}),
};
let res = chart.find_repo(vec![repo])?;
assert_eq!(res.get_url(), "test.rocks".to_string());
Ok(())
}
}

View File

@ -0,0 +1,76 @@
use crate::cli::cli_exec_from_dir;
use crate::git::CheckoutOptions;
use crate::git::GitOptions;
use crate::{cli::cli_exec, helm::repository::Version};
use std::error::Error;
use std::fs::{self, rename};
use base64::{engine::general_purpose, Engine};
use serde::Deserialize;
use serde::Serialize;
use crate::git::Git;
use super::{chart::Chart, repository::RepositoryImpl};
// A struct that represents a git repo with a chart
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub struct GitRepo {
pub url: String,
#[serde(alias = "ref")]
pub git_ref: String,
pub path: String,
#[serde(default = "default_git_bin")]
pub(crate) git_bin: String,
}
fn default_git_bin() -> String {
"git".to_string()
}
impl RepositoryImpl for GitRepo {
fn pull_chart(&self, chart: Chart, workdir_path: String) -> Result<String, Box<dyn Error>> {
let repo_name = general_purpose::STANDARD_NO_PAD.encode(self.get_url().clone());
let git_instance = Git::new(self.get_url(), repo_name.clone());
let git_opts = GitOptions::new(self.git_bin.clone(), Some(workdir_path.clone()));
git_instance.clone(git_opts.clone())?;
let checkout_opts = CheckoutOptions {
create: true,
git_ref: self.git_ref.clone(),
};
git_instance.checkout(git_opts, checkout_opts)?;
let old_dir_name = format!(
"{}/{}/{}/{}",
workdir_path,
repo_name,
self.path,
chart.name.clone()
);
let cmd = format!("helm show chart {}", old_dir_name);
let helm_stdout = cli_exec(cmd)?;
let new_dir_name: String;
match serde_yaml::from_str::<Version>(&helm_stdout) {
Ok(res) => {
new_dir_name = format!("{}/{}-{}", workdir_path, chart.name.clone(), res.version);
rename(old_dir_name, new_dir_name.clone())?;
}
Err(err) => return Err(Box::from(err)),
};
// Cleaning up
fs::remove_dir_all(format!("{}/{}", workdir_path, repo_name))?;
// Get the version
let cmd = "helm show chart . | yq '.version'".to_string();
let _version = cli_exec_from_dir(cmd, new_dir_name.clone())?;
Ok(new_dir_name)
}
fn get_url(&self) -> String {
self.url.clone()
}
}

View File

@ -0,0 +1,133 @@
use std::{error::Error, fs::rename};
use base64::{engine::general_purpose, Engine};
use serde::{Deserialize, Serialize};
use crate::cli::{cli_exec, cli_exec_from_dir};
use super::{
chart::Chart,
repository::{RepositoryImpl, Version},
};
// A struct that represents a regular helm repo
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub struct HelmRepo {
// A URL of the helm repository
pub url: String,
}
const LATEST_VERSION: &str = "latest";
impl RepositoryImpl for HelmRepo {
fn pull_chart(&self, chart: Chart, workdir_path: String) -> Result<String, Box<dyn Error>> {
match self.repo_kind_from_url(self.clone())? {
RepoKind::Default => self.pull_default(chart, workdir_path),
RepoKind::Oci => self.pull_oci(chart, workdir_path),
}
}
fn get_url(&self) -> String {
self.url.clone()
}
}
pub(crate) enum RepoKind {
Default,
Oci,
}
impl HelmRepo {
fn repo_kind_from_url(&self, repository: HelmRepo) -> Result<RepoKind, Box<dyn Error>> {
let prefix = repository
.url
.chars()
.take_while(|&ch| ch != ':')
.collect::<String>();
match prefix.as_str() {
"oci" => Ok(RepoKind::Oci),
"https" | "http" => Ok(RepoKind::Default),
_ => Err(Box::from(format!(
"repo kind is not defined by the prefix: {}",
prefix
))),
}
}
fn pull_oci(&self, chart: Chart, workdir_path: String) -> Result<String, Box<dyn Error>> {
let args = match chart.version.as_str() {
LATEST_VERSION => "".to_string(),
_ => format!("--version {}", chart.version.clone()),
};
let repo = match self.get_url().ends_with('/') {
true => {
let mut repo = self.get_url().clone();
repo.pop();
repo
}
false => self.get_url().clone(),
};
let cmd = format!(
"helm pull {}/{} {} --destination {} --untar",
repo, chart.name, args, workdir_path
);
cli_exec(cmd)?;
// Get the version
let cmd = format!("helm show chart {}/{}", workdir_path, chart.name);
let helm_stdout = cli_exec(cmd)?;
let old_dir_name = format!("{}/{}", workdir_path, chart.name);
let new_dir_name: String;
match serde_yaml::from_str::<Version>(&helm_stdout) {
Ok(res) => {
new_dir_name = format!("{}-{}", old_dir_name, res.version);
rename(old_dir_name, new_dir_name.clone())?;
}
Err(err) => return Err(Box::from(err)),
};
// TODO: Do we really need it?
let cmd = "helm show chart . | yq '.version'".to_string();
let _version = cli_exec_from_dir(cmd, new_dir_name.clone())?;
Ok(new_dir_name)
}
fn pull_default(&self, chart: Chart, workdir_path: String) -> Result<String, Box<dyn Error>> {
// Add repo and update
let repo_local_name = general_purpose::STANDARD_NO_PAD.encode(self.get_url());
let cmd = format!("helm repo add {} {}", repo_local_name, self.get_url());
cli_exec(cmd)?;
cli_exec("helm repo update".to_string())?;
let args = match chart.version.as_str() {
LATEST_VERSION => "".to_string(),
_ => format!("--version {}", chart.version.clone()),
};
let cmd = format!(
"helm pull {}/{} {} --destination {} --untar",
repo_local_name, chart.name, args, workdir_path
);
cli_exec(cmd)?;
// Get the version
let cmd = format!("helm show chart {}/{}", workdir_path, chart.name);
let helm_stdout = cli_exec(cmd)?;
let old_dir_name = format!("{}/{}", workdir_path, chart.name);
let new_dir_name: String;
match serde_yaml::from_str::<Version>(&helm_stdout) {
Ok(res) => {
new_dir_name = format!("{}-{}", old_dir_name, res.version);
rename(old_dir_name, new_dir_name.clone())?;
}
Err(err) => return Err(Box::from(err)),
};
//cleaning up
let cmd = format!("helm repo remove {}", repo_local_name);
cli_exec(cmd)?;
// TODO: Do we really need it?
let cmd = "helm show chart . | yq '.version'".to_string();
let _version = cli_exec_from_dir(cmd, new_dir_name.clone())?;
Ok(new_dir_name)
}
}

4
lib/src/helm/mod.rs Normal file
View File

@ -0,0 +1,4 @@
pub mod chart;
pub mod git_repository;
pub mod helm_repository;
pub mod repository;

View File

@ -0,0 +1,36 @@
use std::error::Error;
use serde::{Deserialize, Serialize};
use super::{chart::Chart, git_repository::GitRepo, helm_repository::HelmRepo};
// A struct that represents a helm repository
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub struct Repository {
// A name of the repo. It's going to be used by tools
// to get a URL, so it can be any string
pub name: String,
pub helm: Option<HelmRepo>,
pub git: Option<GitRepo>,
}
// Supported kinds of helm repos
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub(crate) enum RepositoryKind {
// Regular helm repos and OCI
Helm,
// Git, it's not supposed to use helm-git plugin
// but instead it's just using git to get a repo
// and then look for charts in the repo
Git,
}
pub trait RepositoryImpl {
fn pull_chart(&self, chart: Chart, workdir_path: String) -> Result<String, Box<dyn Error>>;
fn get_url(&self) -> String;
}
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub(crate) struct Version {
pub(crate) version: String,
}

7
lib/src/include.rs Normal file
View File

@ -0,0 +1,7 @@
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub struct Include<T> {
pub path: String,
pub kind: T,
}

23
lib/src/lib.rs Normal file
View File

@ -0,0 +1,23 @@
pub mod cli;
pub mod config;
pub mod git;
pub mod helm;
pub mod include;
pub mod output;
pub mod template;
pub mod workdir;
pub fn add(left: usize, right: usize) -> usize {
left + right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
}
}

18
lib/src/output.rs Normal file
View File

@ -0,0 +1,18 @@
use console::style;
use std::error::Error;
pub fn message_empty(msg: &str) {
println!(" {}", style(msg).blue());
}
pub fn message_info(msg: &str) {
let prefix = format!("{}", style("-->"));
let msg = format!("{} {}", prefix, msg,);
println!(" {}", style(msg).blue());
}
pub fn message_error(err: Box<dyn Error>) {
let prefix = format!("{}", style("!->").red());
let msg = format!("{} {}", prefix, err);
println!(" {}", style(msg).red());
}

55
lib/src/template.rs Normal file
View File

@ -0,0 +1,55 @@
use chrono::prelude::*;
use handlebars::{handlebars_helper, Handlebars};
use serde::Serialize;
handlebars_helper!(date_helper: | | Utc::now().format("%Y-%m-%d").to_string());
handlebars_helper!(time_helper: | | Utc::now().format("%H-%M-%S").to_string());
pub fn register_handlebars() -> Handlebars<'static> {
let mut handlebars = Handlebars::new();
handlebars.register_helper("date", Box::new(date_helper));
handlebars.register_helper("time", Box::new(time_helper));
handlebars
}
pub fn render<T>(string: String, data: &T) -> Result<String, Box<dyn std::error::Error>>
where
T: Serialize,
{
let mut reg = register_handlebars();
let tmpl_name = "template";
reg.register_template_string(tmpl_name, string)?;
let result = reg.render(tmpl_name, data)?;
Ok(result)
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use chrono::Utc;
use crate::template::register_handlebars;
#[test]
fn test_date_helper() {
let mut reg = register_handlebars();
reg.register_template_string("test", "{{ date }}").unwrap();
let result = reg
.render("test", &HashMap::<String, String>::new())
.unwrap();
let expected = Utc::now().format("%Y-%m-%d").to_string();
assert_eq!(result, expected);
}
#[test]
fn test_time_helper() {
let mut reg = register_handlebars();
reg.register_template_string("test", "{{ time }}").unwrap();
let result = reg
.render("test", &HashMap::<String, String>::new())
.unwrap();
let expected = Utc::now().format("%H-%M-%S").to_string();
assert_eq!(result, expected);
}
}

56
lib/src/workdir.rs Normal file
View File

@ -0,0 +1,56 @@
use tempfile::tempdir;
use crate::output::message_info;
use std::{error::Error, fs::create_dir, path::PathBuf};
pub fn setup_workdir(path: Option<String>) -> Result<String, Box<dyn Error>> {
let path = match path {
Some(res) => {
message_info(&format!("trying to create a dir: {}", res));
match create_dir(res.clone()) {
Ok(_) => PathBuf::from(res),
Err(err) => {
let _msg = format!("couldn't create dir {}", res);
return Err(Box::from(err));
}
}
}
None => {
message_info("trying to create a temporary dir");
// I'm using into_path to prevent the dir from being removed
tempdir()?.into_path()
}
};
Ok(path.into_os_string().into_string().unwrap())
}
#[cfg(test)]
mod tests {
use std::{error::Error, fs::remove_dir_all, path::Path};
use tempfile::tempdir;
use crate::workdir::setup_workdir;
#[test]
fn test_temporary_dir() -> Result<(), Box<dyn Error>> {
let wd = setup_workdir(None)?;
let result = Path::new(&wd).exists();
assert!(result);
remove_dir_all(wd)?;
Ok(())
}
#[test]
fn test_specified_dir() -> Result<(), Box<dyn Error>> {
let dir = tempdir()?;
let path = dir.path().join("test").to_str().unwrap().to_string();
let wd = setup_workdir(Some(path.clone()))?;
let result = Path::new(&wd).exists();
assert!(result);
assert!(setup_workdir(Some(path)).is_err());
remove_dir_all(dir)?;
Ok(())
}
}