A GitOps tool that aggregates all default TypeScript exports from a directory into a Kubernetes resource list (JSON format) for streamlined GitOps workflows.
WARNING
Due to the limitation of deno's dynamic import, this project uses child process to evaluate .ts files.
.ts and .mts files in your projectUnlike other manifest generation approaches like Jsonnet, Kustomize, or plain YAML templating, our TypeScript-based solution offers significant advantages:
@kubernetes/client-node) for complete type definitionsWith TypeScript and Kubernetes types:
import { V1Deployment } from "@kubernetes/client-node";
// TypeScript catches errors immediately!
const deployment: V1Deployment = {
apiVersion: "apps/v1",
kind: "Deployment",
metadata: {
name: "my-app",
// TypeScript error: 'lables' does not exist. Did you mean 'labels'?
lables: { app: "my-app" },
},
spec: {
// TypeScript error: Type 'string' is not assignable to type 'number'
replicas: "3",
selector: {
matchLabels: { app: "my-app" },
},
template: {
// Full auto-completion for all nested fields!
metadata: { labels: { app: "my-app" } },
spec: {
containers: [
{
name: "app",
image: "my-app:latest",
// TypeScript knows this should be an array of objects
ports: [{ containerPort: 8080 }],
},
],
},
},
},
};
Compare this to Jsonnet or plain YAML where:
# Install directly from source
deno install -A --name deno-manifest https://raw.githubusercontent.com/yankeguo/deno-manifest/main/main.ts
# Or run directly without installation
deno run -A https://raw.githubusercontent.com/yankeguo/deno-manifest/main/main.ts
# Or use my short link
deno run -A https://gyk.me/r/deno-manifest.ts
Navigate to your directory containing TypeScript files and run:
# Output JSON to stdout
deno-manifest
# Save JSON output to file
deno-manifest > manifests.json
# Convert to YAML if needed (requires yq)
deno-manifest | yq eval -P > manifests.yaml
my-app/ ├── deployment.ts ├── service.ts ├── configmap.ts ├── components/ │ ├── redis.ts │ └── postgres.ts └── _test.ts # Ignored (starts with _)
deployment.ts:
export default {
apiVersion: "apps/v1",
kind: "Deployment",
metadata: {
name: "my-app",
namespace: "default",
},
spec: {
replicas: 3,
selector: {
matchLabels: { app: "my-app" },
},
template: {
metadata: {
labels: { app: "my-app" },
},
spec: {
containers: [
{
name: "app",
image: "my-app:latest",
ports: [{ containerPort: 8080 }],
},
],
},
},
},
};
service.ts:
export default {
apiVersion: "v1",
kind: "Service",
metadata: {
name: "my-app-service",
namespace: "default",
},
spec: {
selector: { app: "my-app" },
ports: [
{
port: 80,
targetPort: 8080,
},
],
type: "ClusterIP",
},
};
configmap.ts (Dynamic generation):
export default function () {
return {
apiVersion: "v1",
kind: "ConfigMap",
metadata: {
name: "my-app-config",
namespace: "default",
},
data: {
"config.yaml": `
env: ${Deno.env.get("NODE_ENV") || "development"}
timestamp: ${new Date().toISOString()}
`.trim(),
},
};
}
components/redis.ts (Multiple resources):
const namespace = "default";
export default [
{
apiVersion: "apps/v1",
kind: "Deployment",
metadata: {
name: "redis",
namespace,
},
spec: {
replicas: 1,
selector: { matchLabels: { app: "redis" } },
template: {
metadata: { labels: { app: "redis" } },
spec: {
containers: [
{
name: "redis",
image: "redis:7-alpine",
ports: [{ containerPort: 6379 }],
},
],
},
},
},
},
{
apiVersion: "v1",
kind: "Service",
metadata: {
name: "redis",
namespace,
},
spec: {
selector: { app: "redis" },
ports: [{ port: 6379, targetPort: 6379 }],
},
},
];
Running deno-manifest on the above structure produces:
{
"apiVersion": "v1",
"kind": "List",
"items": [
{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": {
"name": "my-app",
"namespace": "default"
},
"spec": {
"replicas": 3,
"selector": {
"matchLabels": { "app": "my-app" }
},
"template": {
"metadata": {
"labels": { "app": "my-app" }
},
"spec": {
"containers": [
{
"name": "app",
"image": "my-app:latest",
"ports": [{ "containerPort": 8080 }]
}
]
}
}
}
},
{
"apiVersion": "v1",
"kind": "Service",
"metadata": {
"name": "my-app-service",
"namespace": "default"
},
"spec": {
"selector": { "app": "my-app" },
"ports": [
{
"port": 80,
"targetPort": 8080
}
],
"type": "ClusterIP"
}
},
{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": {
"name": "my-app-config",
"namespace": "default"
},
"data": {
"config.yaml": "env: development\ntimestamp: 2024-01-15T10:30:00.000Z"
}
},
{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": {
"name": "redis",
"namespace": "default"
},
"spec": {
"replicas": 1,
"selector": { "matchLabels": { "app": "redis" } },
"template": {
"metadata": { "labels": { "app": "redis" } },
"spec": {
"containers": [
{
"name": "redis",
"image": "redis:7-alpine",
"ports": [{ "containerPort": 6379 }]
}
]
}
}
}
},
{
"apiVersion": "v1",
"kind": "Service",
"metadata": {
"name": "redis",
"namespace": "default"
},
"spec": {
"selector": { "app": "redis" },
"ports": [{ "port": 6379, "targetPort": 6379 }]
}
}
]
}
# Generate manifests and apply to cluster (JSON format)
deno-manifest | kubectl apply -f -
# Save JSON to file for GitOps repository
deno-manifest > k8s-manifests.json
git add k8s-manifests.json
git commit -m "feat: update kubernetes manifests"
# Convert to YAML if your GitOps workflow prefers YAML
deno-manifest | yq eval -P > k8s-manifests.yaml
Create different directories for different environments:
environments/ ├── development/ │ ├── deployment.ts │ └── service.ts ├── staging/ │ ├── deployment.ts │ └── service.ts └── production/ ├── deployment.ts └── service.ts
# Generate manifests for specific environment
cd environments/production
deno-manifest > ../../manifests/production.json
Use TypeScript's power for dynamic configurations:
// scaling.ts
export default function () {
const replicas = Deno.env.get("REPLICAS") || "3";
const environment = Deno.env.get("ENVIRONMENT") || "development";
return {
apiVersion: "apps/v1",
kind: "Deployment",
metadata: {
name: `app-${environment}`,
labels: {
environment,
"managed-by": "deno-manifest",
},
},
spec: {
replicas: parseInt(replicas),
// ... rest of deployment spec
},
};
}
*.ts - TypeScript files*.mts - TypeScript module files. or _ (hidden/private files)*.d.ts, *.d.mts - TypeScript definition files*_test.ts, *_test.mts - Test files., _, or node_modulesexport default {
/* Kubernetes resource */
};
export default function () {
return {
/* Kubernetes resource */
};
}
export default [
{
/* Resource 1 */
},
{
/* Resource 2 */
},
];
deno-manifest integrates seamlessly with ArgoCD as a Config Management Plugin (CMP), enabling you to use TypeScript for generating Kubernetes manifests in your GitOps workflows.
ArgoCD automatically detects TypeScript files in your repository and uses deno-manifest to generate Kubernetes resources. No manual configuration or plugin name specification required - it just works!
# argocd-cm-plugin.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: deno-manifest-plugin
namespace: argocd
data:
plugin.yaml: |
apiVersion: argoproj.io/v1alpha1
kind: ConfigManagementPlugin
metadata:
name: deno-manifest
spec:
discover:
find:
glob: "**/*.ts"
generate:
command:
- deno
- run
- -A
- https://raw.githubusercontent.com/yankeguo/deno-manifest/main/main.ts
# argocd-repo-server-patch.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: argocd-repo-server
namespace: argocd
spec:
template:
spec:
containers:
- name: deno-manifest-plugin
image: denoland/deno:latest
command: [/var/run/argocd/argocd-cmp-server]
securityContext:
runAsNonRoot: true
runAsUser: 999
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
readOnlyRootFilesystem: true
seccompProfile:
type: RuntimeDefault
volumeMounts:
- mountPath: /var/run/argocd
name: var-files
- mountPath: /home/argocd/cmp-server/plugins
name: plugins
- mountPath: /home/argocd/cmp-server/config/plugin.yaml
subPath: plugin.yaml
name: deno-manifest-plugin
- mountPath: /tmp
name: tmp
volumes:
- configMap:
name: deno-manifest-plugin
name: deno-manifest-plugin
- emptyDir: {}
name: tmp
kubectl apply -f argocd-cm-plugin.yaml
kubectl apply -f argocd-repo-server-patch.yaml
# Restart repo server to load the plugin
kubectl rollout restart deployment/argocd-repo-server -n argocd
Once the plugin is installed, ArgoCD will automatically use deno-manifest for any repository containing TypeScript files:
# example-app.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: my-app
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/yourorg/your-repo
targetRevision: main
path: manifests/ # Directory containing your .ts files
# No plugin specification needed - auto-discovered!
destination:
server: https://kubernetes.default.svc
namespace: default
syncPolicy:
automated:
prune: true
selfHeal: true
You can pass environment variables to your TypeScript manifests through ArgoCD:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: my-app
spec:
source:
plugin:
env:
- name: ENVIRONMENT
value: production
- name: REPLICAS
value: "5"
For convenience, you can use the short URL in your plugin configuration:
generate:
command:
- deno
- run
- -A
- https://gyk.me/r/deno-manifest.ts
GUO YANKE, MIT License