kdep 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/exe/kdep +3 -0
  4. data/lib/kdep/cli.rb +72 -0
  5. data/lib/kdep/cluster_health.rb +44 -0
  6. data/lib/kdep/commands/apply.rb +147 -0
  7. data/lib/kdep/commands/build.rb +103 -0
  8. data/lib/kdep/commands/bump.rb +190 -0
  9. data/lib/kdep/commands/diff.rb +133 -0
  10. data/lib/kdep/commands/eject.rb +97 -0
  11. data/lib/kdep/commands/init.rb +99 -0
  12. data/lib/kdep/commands/log.rb +145 -0
  13. data/lib/kdep/commands/push.rb +94 -0
  14. data/lib/kdep/commands/render.rb +130 -0
  15. data/lib/kdep/commands/restart.rb +109 -0
  16. data/lib/kdep/commands/scale.rb +136 -0
  17. data/lib/kdep/commands/secrets.rb +223 -0
  18. data/lib/kdep/commands/sh.rb +117 -0
  19. data/lib/kdep/commands/status.rb +187 -0
  20. data/lib/kdep/config.rb +69 -0
  21. data/lib/kdep/context_guard.rb +23 -0
  22. data/lib/kdep/dashboard/health_panel.rb +53 -0
  23. data/lib/kdep/dashboard/layout.rb +40 -0
  24. data/lib/kdep/dashboard/log_panel.rb +54 -0
  25. data/lib/kdep/dashboard/panel.rb +135 -0
  26. data/lib/kdep/dashboard/resources_panel.rb +91 -0
  27. data/lib/kdep/dashboard/rollout_panel.rb +75 -0
  28. data/lib/kdep/dashboard/screen.rb +29 -0
  29. data/lib/kdep/dashboard.rb +258 -0
  30. data/lib/kdep/defaults.rb +21 -0
  31. data/lib/kdep/discovery.rb +29 -0
  32. data/lib/kdep/docker.rb +29 -0
  33. data/lib/kdep/kubectl.rb +41 -0
  34. data/lib/kdep/preset.rb +33 -0
  35. data/lib/kdep/registry.rb +94 -0
  36. data/lib/kdep/renderer.rb +53 -0
  37. data/lib/kdep/template_context.rb +22 -0
  38. data/lib/kdep/ui.rb +48 -0
  39. data/lib/kdep/validator.rb +73 -0
  40. data/lib/kdep/version.rb +3 -0
  41. data/lib/kdep/version_tagger.rb +27 -0
  42. data/lib/kdep/writer.rb +26 -0
  43. data/lib/kdep.rb +49 -0
  44. data/templates/init/app.yml.erb +49 -0
  45. data/templates/init/gitignore +3 -0
  46. data/templates/init/secrets.yml +7 -0
  47. data/templates/presets/cronjob +4 -0
  48. data/templates/presets/job +4 -0
  49. data/templates/presets/web +7 -0
  50. data/templates/presets/worker +4 -0
  51. data/templates/resources/configmap.yml.erb +7 -0
  52. data/templates/resources/cronjob.yml.erb +42 -0
  53. data/templates/resources/deployment.yml.erb +71 -0
  54. data/templates/resources/ingress.yml.erb +47 -0
  55. data/templates/resources/job.yml.erb +35 -0
  56. data/templates/resources/secret.yml.erb +15 -0
  57. data/templates/resources/service.yml.erb +13 -0
  58. metadata +142 -0
data/lib/kdep/ui.rb ADDED
@@ -0,0 +1,48 @@
1
+ module Kdep
2
+ class UI
3
+ COLORS = {
4
+ :red => "\e[31m",
5
+ :green => "\e[32m",
6
+ :yellow => "\e[33m",
7
+ :blue => "\e[34m",
8
+ :bold => "\e[1m",
9
+ :reset => "\e[0m",
10
+ }.freeze
11
+
12
+ def initialize(color: $stdout.tty?)
13
+ @color = color
14
+ end
15
+
16
+ def success(msg)
17
+ puts colorize(:green, msg)
18
+ end
19
+
20
+ def error(msg)
21
+ $stderr.puts colorize(:red, "error: #{msg}")
22
+ end
23
+
24
+ def warn(msg)
25
+ $stderr.puts colorize(:yellow, "warning: #{msg}")
26
+ end
27
+
28
+ def info(msg)
29
+ puts msg
30
+ end
31
+
32
+ def file_written(path)
33
+ puts " #{colorize(:green, 'create')} #{path}"
34
+ end
35
+
36
+ def summary(files_count, errors_count)
37
+ status = errors_count > 0 ? :red : :green
38
+ puts colorize(status, "#{files_count} files rendered, #{errors_count} errors")
39
+ end
40
+
41
+ private
42
+
43
+ def colorize(color, text)
44
+ return text unless @color
45
+ "#{COLORS[color]}#{text}#{COLORS[:reset]}"
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,73 @@
1
+ require "yaml"
2
+
3
+ module Kdep
4
+ class Validator
5
+ def validate(yaml_string, resource_name)
6
+ # Skip validation for empty/nil content (e.g., ingress with no domains)
7
+ if yaml_string.nil? || yaml_string.strip.empty?
8
+ return {"valid" => true, "errors" => []}
9
+ end
10
+
11
+ errors = []
12
+
13
+ # Parse YAML
14
+ begin
15
+ doc = YAML.safe_load(yaml_string)
16
+ rescue => e
17
+ return {"valid" => false, "errors" => ["YAML parse error: #{e.message}"]}
18
+ end
19
+
20
+ unless doc.is_a?(Hash)
21
+ return {"valid" => false, "errors" => ["YAML document is not a mapping"]}
22
+ end
23
+
24
+ # Common checks
25
+ errors << "Missing required field: apiVersion" unless doc["apiVersion"]
26
+ errors << "Missing required field: kind" unless doc["kind"]
27
+
28
+ if doc["metadata"].is_a?(Hash)
29
+ errors << "Missing required field: metadata.name" unless doc["metadata"]["name"]
30
+ errors << "Missing required field: metadata.namespace" unless doc["metadata"]["namespace"]
31
+ else
32
+ errors << "Missing required field: metadata"
33
+ end
34
+
35
+ # Per-kind checks
36
+ case resource_name
37
+ when "deployment"
38
+ if doc["spec"].is_a?(Hash)
39
+ errors << "Missing required field: spec.replicas" unless doc["spec"]["replicas"]
40
+ errors << "Missing required field: spec.template" unless doc["spec"]["template"]
41
+ else
42
+ errors << "Missing required field: spec"
43
+ end
44
+ when "service"
45
+ if doc["spec"].is_a?(Hash)
46
+ errors << "Missing required field: spec.ports" unless doc["spec"]["ports"]
47
+ else
48
+ errors << "Missing required field: spec"
49
+ end
50
+ when "ingress"
51
+ if doc["spec"].is_a?(Hash)
52
+ errors << "Missing required field: spec.rules" unless doc["spec"]["rules"]
53
+ else
54
+ errors << "Missing required field: spec"
55
+ end
56
+ when "job"
57
+ if doc["spec"].is_a?(Hash)
58
+ errors << "Missing required field: spec.template" unless doc["spec"]["template"]
59
+ else
60
+ errors << "Missing required field: spec"
61
+ end
62
+ when "cronjob"
63
+ if doc["spec"].is_a?(Hash)
64
+ errors << "Missing required field: spec.schedule" unless doc["spec"]["schedule"]
65
+ else
66
+ errors << "Missing required field: spec"
67
+ end
68
+ end
69
+
70
+ {"valid" => errors.empty?, "errors" => errors}
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,3 @@
1
+ module Kdep
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,27 @@
1
+ module Kdep
2
+ module VersionTagger
3
+ # Parses a "major.minor" version string into [Integer, Integer].
4
+ # Returns nil for invalid or non-version strings.
5
+ def self.parse(tag)
6
+ parts = tag.split(".", 2)
7
+ return nil unless parts.length == 2
8
+
9
+ major = Integer(parts[0]) rescue nil
10
+ minor = Integer(parts[1]) rescue nil
11
+ return nil unless major && minor
12
+
13
+ [major, minor]
14
+ end
15
+
16
+ # Given a list of tag strings, finds the highest version and returns
17
+ # the next version string with minor incremented by 1.
18
+ # Returns "0.1" when no valid version tags exist.
19
+ def self.next_tag(tags)
20
+ versions = tags.map { |t| parse(t) }.compact
21
+ return "0.1" if versions.empty?
22
+
23
+ highest = versions.sort_by { |major, minor| [major, minor] }.last
24
+ "#{highest[0]}.#{highest[1] + 1}"
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,26 @@
1
+ require "fileutils"
2
+
3
+ module Kdep
4
+ class Writer
5
+ def initialize(output_dir)
6
+ @output_dir = output_dir
7
+ end
8
+
9
+ def clean
10
+ FileUtils.rm_rf(@output_dir)
11
+ FileUtils.mkdir_p(@output_dir)
12
+ end
13
+
14
+ def write(resource_name, content, index)
15
+ # Skip empty/nil/whitespace-only content
16
+ return nil if content.nil? || content.strip.empty?
17
+
18
+ FileUtils.mkdir_p(@output_dir)
19
+
20
+ filename = format("%02d-%s.yml", index, resource_name)
21
+ path = File.join(@output_dir, filename)
22
+ File.write(path, content)
23
+ path
24
+ end
25
+ end
26
+ end
data/lib/kdep.rb ADDED
@@ -0,0 +1,49 @@
1
+ require "kdep/version"
2
+ require "kdep/ui"
3
+ require "kdep/defaults"
4
+ require "kdep/config"
5
+ require "kdep/preset"
6
+ require "kdep/discovery"
7
+ require "kdep/template_context"
8
+ require "kdep/renderer"
9
+ require "kdep/validator"
10
+ require "kdep/writer"
11
+ require "kdep/kubectl"
12
+ require "kdep/context_guard"
13
+ require "kdep/cluster_health"
14
+ require "kdep/docker"
15
+ require "kdep/registry"
16
+ require "kdep/version_tagger"
17
+ require "kdep/commands/render"
18
+ require "kdep/commands/init"
19
+ require "kdep/commands/eject"
20
+ require "kdep/commands/apply"
21
+ require "kdep/commands/diff"
22
+ require "kdep/commands/status"
23
+ require "kdep/commands/build"
24
+ require "kdep/commands/push"
25
+ require "kdep/commands/bump"
26
+ require "kdep/commands/log"
27
+ require "kdep/commands/secrets"
28
+ require "kdep/commands/sh"
29
+ require "kdep/commands/restart"
30
+ require "kdep/commands/scale"
31
+ require "kdep/dashboard/screen"
32
+ require "kdep/dashboard/layout"
33
+ require "kdep/dashboard/panel"
34
+ require "kdep/dashboard/rollout_panel"
35
+ require "kdep/dashboard/log_panel"
36
+ require "kdep/dashboard/resources_panel"
37
+ require "kdep/dashboard/health_panel"
38
+ require "kdep/dashboard"
39
+ require "kdep/cli"
40
+
41
+ module Kdep
42
+ def self.templates_dir
43
+ File.expand_path("../templates", __dir__)
44
+ end
45
+
46
+ def self.root
47
+ File.expand_path("..", __dir__)
48
+ end
49
+ end
@@ -0,0 +1,49 @@
1
+ # kdep configuration for <%= deploy_name %>
2
+ # Required fields:
3
+ preset: <%= preset %>
4
+ <% if namespace -%>
5
+ namespace: <%= namespace %>
6
+ <% else -%>
7
+ namespace: # REQUIRED -- your k8s namespace
8
+ <% end -%>
9
+ <% if registry -%>
10
+ registry: <%= registry %>
11
+ <% else -%>
12
+ # registry: ghcr.io/your-org # container registry
13
+ <% end -%>
14
+
15
+ # Optional fields (uncomment and customize as needed):
16
+ # name: <%= deploy_name %> # defaults to folder name
17
+ # image: <%= deploy_name %> # defaults to name
18
+ # tag: latest # image tag (auto-set by kdep bump)
19
+ # target: web # Docker multi-stage build target
20
+ # port: 8080 # container port
21
+ # replicas: 3 # pod replicas
22
+ # command: bundle exec rails server # override container command
23
+ #
24
+ # resources:
25
+ # requests:
26
+ # memory: 100Mi
27
+ # cpu: 10m
28
+ # limits:
29
+ # memory: 200Mi
30
+ #
31
+ # probe:
32
+ # path: /healthz # health check path
33
+ # port: 8080 # health check port (defaults to port)
34
+ # initial_delay: 30 # seconds before first check
35
+ # period: 10 # seconds between checks
36
+ #
37
+ # image_pull_secrets: my-registry-secret # K8s image pull secret name
38
+ #
39
+ # domains: # Ingress domains (web preset only)
40
+ # - example.com # simple: TLS auto, path / by default
41
+ # - host: api.example.com # complex: custom path routing
42
+ # path: /api
43
+ # path_type: Prefix
44
+ #
45
+ # env_from: # environment variable sources
46
+ # - configmap/my-config # from ConfigMap
47
+ # - secret/my-secret # from Secret
48
+ #
49
+ # schedule: "0 */6 * * *" # cron schedule (cronjob preset only)
@@ -0,0 +1,3 @@
1
+ # kdep generated files
2
+ secrets.yml
3
+ .rendered/
@@ -0,0 +1,7 @@
1
+ # Secrets for this deployment
2
+ # Plain key:value pairs -- kdep will base64-encode into K8s Secret manifest
3
+ # WARNING: Do not commit this file to git!
4
+ #
5
+ # Example:
6
+ # DATABASE_URL: postgres://user:pass@host/db
7
+ # API_KEY: your-secret-key
@@ -0,0 +1,4 @@
1
+ # cronjob preset: scheduled job
2
+ configmap
3
+ secret
4
+ cronjob
@@ -0,0 +1,4 @@
1
+ # job preset: one-off batch job
2
+ configmap
3
+ secret
4
+ job
@@ -0,0 +1,7 @@
1
+ # web preset: standard web application resources
2
+ # Order determines kubectl apply sequence
3
+ configmap
4
+ secret
5
+ service
6
+ deployment
7
+ ingress
@@ -0,0 +1,4 @@
1
+ # worker preset: background job processor
2
+ configmap
3
+ secret
4
+ deployment
@@ -0,0 +1,7 @@
1
+ apiVersion: v1
2
+ kind: ConfigMap
3
+ metadata:
4
+ name: <%= name %>
5
+ namespace: <%= namespace %>
6
+ labels:
7
+ app: <%= name %>
@@ -0,0 +1,42 @@
1
+ apiVersion: batch/v1
2
+ kind: CronJob
3
+ metadata:
4
+ name: <%= name %>
5
+ namespace: <%= namespace %>
6
+ labels:
7
+ app: <%= name %>
8
+ spec:
9
+ schedule: "<%= @config["schedule"] %>"
10
+ jobTemplate:
11
+ metadata:
12
+ name: <%= name %>
13
+ labels:
14
+ app: <%= name %>
15
+ spec:
16
+ template:
17
+ metadata:
18
+ labels:
19
+ app: <%= name %>
20
+ spec:
21
+ restartPolicy: Never
22
+ containers:
23
+ - name: <%= name %>
24
+ image: <%= @config["registry"] ? "#{@config["registry"]}/#{image}" : image %>:<%= @config["tag"] || "latest" %>
25
+ <% if @config["command"] -%>
26
+ command: <%= @config["command"].is_a?(Array) ? @config["command"].inspect : "[\"#{@config["command"]}\"]" %>
27
+ <% end -%>
28
+ <% if @config["resources"] -%>
29
+ resources:
30
+ <% if @config["resources"]["requests"] -%>
31
+ requests:
32
+ <% @config["resources"]["requests"].each do |k, v| -%>
33
+ <%= k %>: "<%= v %>"
34
+ <% end -%>
35
+ <% end -%>
36
+ <% if @config["resources"]["limits"] -%>
37
+ limits:
38
+ <% @config["resources"]["limits"].each do |k, v| -%>
39
+ <%= k %>: "<%= v %>"
40
+ <% end -%>
41
+ <% end -%>
42
+ <% end -%>
@@ -0,0 +1,71 @@
1
+ apiVersion: apps/v1
2
+ kind: Deployment
3
+ metadata:
4
+ name: <%= name %>
5
+ namespace: <%= namespace %>
6
+ labels:
7
+ app: <%= name %>
8
+ spec:
9
+ replicas: <%= replicas %>
10
+ selector:
11
+ matchLabels:
12
+ app: <%= name %>
13
+ template:
14
+ metadata:
15
+ labels:
16
+ app: <%= name %>
17
+ spec:
18
+ <% if @config["image_pull_secrets"] -%>
19
+ imagePullSecrets:
20
+ - name: <%= @config["image_pull_secrets"] %>
21
+ <% end -%>
22
+ containers:
23
+ - name: <%= name %>
24
+ image: <%= @config["registry"] ? "#{@config["registry"]}/#{image}" : image %>:<%= @config["tag"] || "latest" %>
25
+ <% if @config["command"] -%>
26
+ command: <%= @config["command"].is_a?(Array) ? @config["command"].inspect : "[\"#{@config["command"]}\"]" %>
27
+ <% end -%>
28
+ ports:
29
+ - containerPort: <%= port %>
30
+ <% if @config["resources"] -%>
31
+ resources:
32
+ <% if @config["resources"]["requests"] -%>
33
+ requests:
34
+ <% @config["resources"]["requests"].each do |k, v| -%>
35
+ <%= k %>: "<%= v %>"
36
+ <% end -%>
37
+ <% end -%>
38
+ <% if @config["resources"]["limits"] -%>
39
+ limits:
40
+ <% @config["resources"]["limits"].each do |k, v| -%>
41
+ <%= k %>: "<%= v %>"
42
+ <% end -%>
43
+ <% end -%>
44
+ <% end -%>
45
+ <% if @config["probe"] -%>
46
+ livenessProbe:
47
+ httpGet:
48
+ path: <%= @config["probe"]["path"] || "/healthz" %>
49
+ port: <%= @config["probe"]["port"] || port %>
50
+ initialDelaySeconds: <%= @config["probe"]["initial_delay"] || 30 %>
51
+ periodSeconds: <%= @config["probe"]["period"] || 10 %>
52
+ readinessProbe:
53
+ httpGet:
54
+ path: <%= @config["probe"]["path"] || "/healthz" %>
55
+ port: <%= @config["probe"]["port"] || port %>
56
+ initialDelaySeconds: <%= @config["probe"]["initial_delay"] || 5 %>
57
+ periodSeconds: <%= @config["probe"]["period"] || 10 %>
58
+ <% end -%>
59
+ <% if @config["env_from"] -%>
60
+ envFrom:
61
+ <% @config["env_from"].each do |ref| -%>
62
+ <% type, ref_name = ref.split("/", 2) -%>
63
+ <% if type == "configmap" -%>
64
+ - configMapRef:
65
+ name: <%= ref_name %>
66
+ <% elsif type == "secret" -%>
67
+ - secretRef:
68
+ name: <%= ref_name %>
69
+ <% end -%>
70
+ <% end -%>
71
+ <% end -%>
@@ -0,0 +1,47 @@
1
+ <% if @config["domains"] && !@config["domains"].empty? -%>
2
+ apiVersion: networking.k8s.io/v1
3
+ kind: Ingress
4
+ metadata:
5
+ name: <%= name %>
6
+ namespace: <%= namespace %>
7
+ labels:
8
+ app: <%= name %>
9
+ annotations:
10
+ cert-manager.io/cluster-issuer: "letsencrypt"
11
+ nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
12
+ spec:
13
+ ingressClassName: nginx
14
+ tls:
15
+ <% @config["domains"].each do |domain| -%>
16
+ <% host = domain.is_a?(Hash) ? domain["host"] : domain -%>
17
+ - hosts:
18
+ - <%= host %>
19
+ secretName: <%= host.gsub(".", "-") %>-tls
20
+ <% end -%>
21
+ rules:
22
+ <% @config["domains"].each do |domain| -%>
23
+ <% if domain.is_a?(Hash) -%>
24
+ - host: <%= domain["host"] %>
25
+ http:
26
+ paths:
27
+ - path: <%= domain["path"] || "/" %>
28
+ pathType: <%= domain["path_type"] || "Prefix" %>
29
+ backend:
30
+ service:
31
+ name: <%= name %>
32
+ port:
33
+ number: <%= port %>
34
+ <% else -%>
35
+ - host: <%= domain %>
36
+ http:
37
+ paths:
38
+ - path: /
39
+ pathType: Prefix
40
+ backend:
41
+ service:
42
+ name: <%= name %>
43
+ port:
44
+ number: <%= port %>
45
+ <% end -%>
46
+ <% end -%>
47
+ <% end -%>
@@ -0,0 +1,35 @@
1
+ apiVersion: batch/v1
2
+ kind: Job
3
+ metadata:
4
+ name: <%= name %>
5
+ namespace: <%= namespace %>
6
+ labels:
7
+ app: <%= name %>
8
+ spec:
9
+ template:
10
+ metadata:
11
+ labels:
12
+ app: <%= name %>
13
+ spec:
14
+ restartPolicy: Never
15
+ containers:
16
+ - name: <%= name %>
17
+ image: <%= @config["registry"] ? "#{@config["registry"]}/#{image}" : image %>:<%= @config["tag"] || "latest" %>
18
+ <% if @config["command"] -%>
19
+ command: <%= @config["command"].is_a?(Array) ? @config["command"].inspect : "[\"#{@config["command"]}\"]" %>
20
+ <% end -%>
21
+ <% if @config["resources"] -%>
22
+ resources:
23
+ <% if @config["resources"]["requests"] -%>
24
+ requests:
25
+ <% @config["resources"]["requests"].each do |k, v| -%>
26
+ <%= k %>: "<%= v %>"
27
+ <% end -%>
28
+ <% end -%>
29
+ <% if @config["resources"]["limits"] -%>
30
+ limits:
31
+ <% @config["resources"]["limits"].each do |k, v| -%>
32
+ <%= k %>: "<%= v %>"
33
+ <% end -%>
34
+ <% end -%>
35
+ <% end -%>
@@ -0,0 +1,15 @@
1
+ <% require "base64" -%>
2
+ apiVersion: v1
3
+ kind: Secret
4
+ metadata:
5
+ name: <%= name %>
6
+ namespace: <%= namespace %>
7
+ labels:
8
+ app: <%= name %>
9
+ type: Opaque
10
+ data:
11
+ <% if @secrets -%>
12
+ <% @secrets.each do |key, value| -%>
13
+ <%= key %>: <%= Base64.strict_encode64(value.to_s) %>
14
+ <% end -%>
15
+ <% end -%>
@@ -0,0 +1,13 @@
1
+ apiVersion: v1
2
+ kind: Service
3
+ metadata:
4
+ name: <%= name %>
5
+ namespace: <%= namespace %>
6
+ labels:
7
+ app: <%= name %>
8
+ spec:
9
+ selector:
10
+ app: <%= name %>
11
+ ports:
12
+ - port: <%= port %>
13
+ targetPort: <%= port %>