kdep 0.4.2 → 0.4.3
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.
- checksums.yaml +4 -4
- data/lib/kdep/cli.rb +2 -0
- data/lib/kdep/commands/apply.rb +1 -1
- data/lib/kdep/commands/build.rb +8 -2
- data/lib/kdep/commands/bump.rb +4 -4
- data/lib/kdep/commands/config.rb +56 -0
- data/lib/kdep/commands/diff.rb +1 -1
- data/lib/kdep/commands/env.rb +46 -0
- data/lib/kdep/commands/env_check.rb +104 -0
- data/lib/kdep/commands/helm_install.rb +2 -1
- data/lib/kdep/commands/init.rb +14 -6
- data/lib/kdep/commands/migrate.rb +1 -1
- data/lib/kdep/commands/push.rb +8 -2
- data/lib/kdep/commands/render.rb +1 -1
- data/lib/kdep/renderer.rb +126 -18
- data/lib/kdep/user_config.rb +38 -0
- data/lib/kdep/version.rb +1 -1
- data/lib/kdep.rb +4 -0
- data/templates/init/env.spec +33 -0
- data/templates/resources/infisical_secret.yml.erb +38 -0
- metadata +22 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ff3707147a8d4a32a335ba5ea6637e05275705d0a91c0fec08bdf649b506ba34
|
|
4
|
+
data.tar.gz: f3b17aeb70128be4e3b4ae9e3b46d359a11698657159a04f386a849daef6c468
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5ca718ac0ff7e9f29fcafeecaae8a77e68abbaa90d6ac7c8dcea130523358e615775c8a6605becc1b7c46d9bee687f33ddf65d599eacf4d2ace92226ff7a7df2
|
|
7
|
+
data.tar.gz: a1750b835fee48cd3de6926e8178e904872e24a92b0a7a95433725212c4daa71b5d4cbff678db9c81ae49f21a1f0d52a45b5e05b93497c31f6852e3a48fe7b00
|
data/lib/kdep/cli.rb
CHANGED
data/lib/kdep/commands/apply.rb
CHANGED
data/lib/kdep/commands/build.rb
CHANGED
|
@@ -42,9 +42,15 @@ module Kdep
|
|
|
42
42
|
# Load config
|
|
43
43
|
config = Kdep::Config.new(deploy_dir, env).load
|
|
44
44
|
|
|
45
|
-
#
|
|
45
|
+
# Resolve tag from state.yml — app.yml has no tag field by design;
|
|
46
|
+
# state.yml is the source of truth for the last bumped version.
|
|
47
|
+
tag = Kdep::State.tag(deploy_dir)
|
|
48
|
+
unless tag
|
|
49
|
+
@ui.error("No tag in state.yml for #{deploy_dir}. Run `kdep bump` first.")
|
|
50
|
+
exit 1
|
|
51
|
+
end
|
|
52
|
+
|
|
46
53
|
image = config["image"] || config["name"]
|
|
47
|
-
tag = config["tag"] || "latest"
|
|
48
54
|
registry = config["registry"]
|
|
49
55
|
|
|
50
56
|
if registry && !registry.to_s.empty?
|
data/lib/kdep/commands/bump.rb
CHANGED
|
@@ -60,7 +60,7 @@ module Kdep
|
|
|
60
60
|
run_helm_pipeline(deploy_dir, env)
|
|
61
61
|
return
|
|
62
62
|
elsif preset == "custom"
|
|
63
|
-
run_custom_pipeline(deploy_dir, config, kdep_dir)
|
|
63
|
+
run_custom_pipeline(deploy_dir, config, kdep_dir, env)
|
|
64
64
|
return
|
|
65
65
|
end
|
|
66
66
|
|
|
@@ -138,7 +138,7 @@ module Kdep
|
|
|
138
138
|
|
|
139
139
|
writer = Kdep::Writer.new(output_dir)
|
|
140
140
|
writer.clean
|
|
141
|
-
renderer = Kdep::Renderer.new(config, deploy_dir)
|
|
141
|
+
renderer = Kdep::Renderer.new(config, deploy_dir, env: env)
|
|
142
142
|
validator = Kdep::Validator.new
|
|
143
143
|
validation_errors = []
|
|
144
144
|
|
|
@@ -230,7 +230,7 @@ module Kdep
|
|
|
230
230
|
end
|
|
231
231
|
|
|
232
232
|
# Feature E: custom-preset bump = render + apply, no docker, no state.
|
|
233
|
-
def run_custom_pipeline(deploy_dir, config, kdep_dir)
|
|
233
|
+
def run_custom_pipeline(deploy_dir, config, kdep_dir, env = nil)
|
|
234
234
|
# Context guard for safety.
|
|
235
235
|
begin
|
|
236
236
|
Kdep::ContextGuard.new(config["context"]).validate!
|
|
@@ -245,7 +245,7 @@ module Kdep
|
|
|
245
245
|
|
|
246
246
|
writer = Kdep::Writer.new(output_dir)
|
|
247
247
|
writer.clean
|
|
248
|
-
renderer = Kdep::Renderer.new(config, deploy_dir)
|
|
248
|
+
renderer = Kdep::Renderer.new(config, deploy_dir, env: env)
|
|
249
249
|
preset_resources = Kdep::Preset.new("custom", deploy_dir).resources
|
|
250
250
|
|
|
251
251
|
preset_resources.each_with_index do |resource, idx|
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
require "optparse"
|
|
2
|
+
|
|
3
|
+
module Kdep
|
|
4
|
+
module Commands
|
|
5
|
+
# `kdep config set <key> <value>` — writes a dotted key into ~/.kdep/config.yml.
|
|
6
|
+
# `kdep config get <key>` — reads a dotted key.
|
|
7
|
+
#
|
|
8
|
+
# Currently used to seed Infisical settings:
|
|
9
|
+
# kdep config set infisical.identity_id <uuid>
|
|
10
|
+
# kdep config set infisical.host_api https://dev-env.leadfy.xyz/api
|
|
11
|
+
class Config
|
|
12
|
+
SUBCOMMANDS = %w[set get show].freeze
|
|
13
|
+
|
|
14
|
+
def self.option_parser
|
|
15
|
+
OptionParser.new do |opts|
|
|
16
|
+
opts.banner = "Usage: kdep config <set|get|show> [key] [value]"
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def initialize(global_options:, command_options:, args:)
|
|
21
|
+
@global_options = global_options
|
|
22
|
+
@command_options = command_options
|
|
23
|
+
@args = args
|
|
24
|
+
@ui = Kdep::UI.new
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def execute
|
|
28
|
+
sub = @args.shift
|
|
29
|
+
unless SUBCOMMANDS.include?(sub)
|
|
30
|
+
@ui.error("Unknown subcommand: #{sub.inspect}. Available: #{SUBCOMMANDS.join(', ')}")
|
|
31
|
+
exit 2
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
case sub
|
|
35
|
+
when "set"
|
|
36
|
+
key, value = @args[0], @args[1]
|
|
37
|
+
unless key && value
|
|
38
|
+
@ui.error("Usage: kdep config set <key> <value>")
|
|
39
|
+
exit 2
|
|
40
|
+
end
|
|
41
|
+
path = Kdep::UserConfig.set(key, value)
|
|
42
|
+
@ui.success("Set #{key} -> #{value} (#{path})")
|
|
43
|
+
when "get"
|
|
44
|
+
key = @args[0]
|
|
45
|
+
unless key
|
|
46
|
+
@ui.error("Usage: kdep config get <key>")
|
|
47
|
+
exit 2
|
|
48
|
+
end
|
|
49
|
+
puts Kdep::UserConfig.get(*key.split("."))
|
|
50
|
+
when "show"
|
|
51
|
+
puts Kdep::UserConfig.load.to_yaml
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
data/lib/kdep/commands/diff.rb
CHANGED
|
@@ -60,7 +60,7 @@ module Kdep
|
|
|
60
60
|
Dir.mktmpdir("kdep-diff-") do |tmpdir|
|
|
61
61
|
preset = Kdep::Preset.new(config["preset"], deploy_dir)
|
|
62
62
|
writer = Kdep::Writer.new(tmpdir)
|
|
63
|
-
renderer = Kdep::Renderer.new(config, deploy_dir)
|
|
63
|
+
renderer = Kdep::Renderer.new(config, deploy_dir, env: env)
|
|
64
64
|
|
|
65
65
|
preset.resources.each_with_index do |resource_name, idx|
|
|
66
66
|
begin
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
require "optparse"
|
|
2
|
+
|
|
3
|
+
module Kdep
|
|
4
|
+
module Commands
|
|
5
|
+
# Dispatcher for `kdep env <subcommand>`. Currently only `check` is wired.
|
|
6
|
+
class Env
|
|
7
|
+
SUBCOMMANDS = %w[check].freeze
|
|
8
|
+
|
|
9
|
+
def self.option_parser
|
|
10
|
+
OptionParser.new do |opts|
|
|
11
|
+
opts.banner = "Usage: kdep env <subcommand> [options] [deploy] [env]"
|
|
12
|
+
opts.separator ""
|
|
13
|
+
opts.separator "Subcommands:"
|
|
14
|
+
opts.separator " check Validate ConfigMap+Secret in cluster against env.spec"
|
|
15
|
+
opts.separator ""
|
|
16
|
+
opts.on("--env=ENV", "Environment scope (production|staging|dev)")
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def initialize(global_options:, command_options:, args:)
|
|
21
|
+
@global_options = global_options
|
|
22
|
+
@command_options = command_options
|
|
23
|
+
@args = args
|
|
24
|
+
@ui = Kdep::UI.new
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def execute
|
|
28
|
+
sub = @args.shift
|
|
29
|
+
unless SUBCOMMANDS.include?(sub)
|
|
30
|
+
@ui.error("Unknown subcommand: #{sub.inspect}. Available: #{SUBCOMMANDS.join(', ')}")
|
|
31
|
+
exit 2
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
case sub
|
|
35
|
+
when "check"
|
|
36
|
+
require "kdep/commands/env_check"
|
|
37
|
+
Kdep::Commands::EnvCheck.new(
|
|
38
|
+
global_options: @global_options,
|
|
39
|
+
command_options: @command_options,
|
|
40
|
+
args: @args
|
|
41
|
+
).execute
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
require "yaml"
|
|
2
|
+
require "base64"
|
|
3
|
+
|
|
4
|
+
module Kdep
|
|
5
|
+
module Commands
|
|
6
|
+
# Validates the live ConfigMap+Secret in the cluster against the contract
|
|
7
|
+
# declared in `env.spec` at the repo root.
|
|
8
|
+
#
|
|
9
|
+
# Reports three buckets:
|
|
10
|
+
# - missing : keys declared (and required) in env.spec but absent in
|
|
11
|
+
# cluster ConfigMap/Secret
|
|
12
|
+
# - extra : keys present in cluster but not declared in env.spec
|
|
13
|
+
# - invalid : declared keys whose value fails type validation
|
|
14
|
+
#
|
|
15
|
+
# Independent of Infisical: just compares declared shape vs live K8s state.
|
|
16
|
+
class EnvCheck
|
|
17
|
+
def initialize(global_options:, command_options:, args:)
|
|
18
|
+
@global_options = global_options
|
|
19
|
+
@command_options = command_options
|
|
20
|
+
@args = args
|
|
21
|
+
@ui = Kdep::UI.new
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def execute
|
|
25
|
+
deploy_name = @args[0]
|
|
26
|
+
env_arg = @args[1] || @command_options[:env]
|
|
27
|
+
|
|
28
|
+
discovery = Kdep::Discovery.new
|
|
29
|
+
kdep_dir = discovery.find_kdep_dir
|
|
30
|
+
unless kdep_dir
|
|
31
|
+
@ui.error("No kdep/ directory found")
|
|
32
|
+
exit 1
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
deploy_dir = resolve_deploy_dir(kdep_dir, deploy_name, discovery)
|
|
36
|
+
exit 1 unless deploy_dir
|
|
37
|
+
|
|
38
|
+
repo_root = File.expand_path("..", kdep_dir)
|
|
39
|
+
spec_path = File.join(repo_root, "env.spec")
|
|
40
|
+
unless File.exist?(spec_path)
|
|
41
|
+
@ui.error("env.spec not found at #{spec_path}")
|
|
42
|
+
exit 1
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
spec = EnvSpec.parse_file(spec_path)
|
|
46
|
+
config = Kdep::Config.new(deploy_dir, env_arg).load
|
|
47
|
+
namespace = config["namespace"]
|
|
48
|
+
unless namespace
|
|
49
|
+
@ui.error("namespace missing in app.yml -- cannot query cluster")
|
|
50
|
+
exit 1
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
configmap_name = config["configmap_name"] || "config-#{config["name"]}"
|
|
54
|
+
secret_name = config["secret_name"] || "#{File.basename(deploy_dir)}-secrets"
|
|
55
|
+
|
|
56
|
+
live_cm = fetch_data("configmap", configmap_name, namespace)
|
|
57
|
+
live_sec = fetch_data("secret", secret_name, namespace, decode: true)
|
|
58
|
+
|
|
59
|
+
merged = live_cm.merge(live_sec)
|
|
60
|
+
problems = spec.validate(merged, env: env_arg || "*", strict: true)
|
|
61
|
+
|
|
62
|
+
scope_label = env_arg || "shared"
|
|
63
|
+
if problems.empty?
|
|
64
|
+
@ui.success("env satisfies env.spec (#{scope_label}, namespace=#{namespace})")
|
|
65
|
+
exit 0
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
@ui.error("env.spec mismatches in #{namespace} (scope=#{scope_label}):")
|
|
69
|
+
problems.each { |p| @ui.error(" - #{p}") }
|
|
70
|
+
exit 1
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
def resolve_deploy_dir(kdep_dir, deploy_name, discovery)
|
|
76
|
+
if deploy_name
|
|
77
|
+
dir = File.join(kdep_dir, deploy_name)
|
|
78
|
+
unless File.directory?(dir)
|
|
79
|
+
@ui.error("Deploy not found: #{deploy_name}")
|
|
80
|
+
return nil
|
|
81
|
+
end
|
|
82
|
+
dir
|
|
83
|
+
else
|
|
84
|
+
deploys = discovery.find_deploys
|
|
85
|
+
if deploys.length == 1
|
|
86
|
+
File.join(kdep_dir, deploys[0])
|
|
87
|
+
else
|
|
88
|
+
@ui.error("Specify a deploy: #{deploys.join(', ')}")
|
|
89
|
+
nil
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def fetch_data(kind, name, namespace, decode: false)
|
|
95
|
+
yaml = Kdep::Kubectl.get(kind, name, namespace: namespace)
|
|
96
|
+
return {} unless yaml
|
|
97
|
+
parsed = YAML.safe_load(yaml) || {}
|
|
98
|
+
data = parsed["data"] || {}
|
|
99
|
+
return data unless decode
|
|
100
|
+
data.each_with_object({}) { |(k, v), out| out[k] = Base64.decode64(v.to_s) }
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
@@ -26,6 +26,7 @@ module Kdep
|
|
|
26
26
|
def execute
|
|
27
27
|
deploy_name = @args[0]
|
|
28
28
|
env = @args[1]
|
|
29
|
+
@env = env
|
|
29
30
|
@dry_run = @command_options[:"dry-run"]
|
|
30
31
|
|
|
31
32
|
# Discover kdep/ directory
|
|
@@ -239,7 +240,7 @@ module Kdep
|
|
|
239
240
|
|
|
240
241
|
writer = Kdep::Writer.new(output_dir)
|
|
241
242
|
writer.clean
|
|
242
|
-
renderer = Kdep::Renderer.new(config, @deploy_dir)
|
|
243
|
+
renderer = Kdep::Renderer.new(config, @deploy_dir, env: @env)
|
|
243
244
|
validator = Kdep::Validator.new
|
|
244
245
|
|
|
245
246
|
files_written = 0
|
data/lib/kdep/commands/init.rb
CHANGED
|
@@ -72,12 +72,20 @@ module Kdep
|
|
|
72
72
|
File.write(File.join(target_dir, "app.yml"), app_yml_content)
|
|
73
73
|
@ui.file_written("kdep/#{deploy_name}/app.yml")
|
|
74
74
|
|
|
75
|
-
#
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
)
|
|
80
|
-
|
|
75
|
+
# Drop env.spec at the repo root (one per repo, not per deploy).
|
|
76
|
+
# Skip if it already exists -- another deploy in this repo may have
|
|
77
|
+
# created it, and the spec is shared.
|
|
78
|
+
repo_root = Dir.pwd
|
|
79
|
+
env_spec_path = File.join(repo_root, "env.spec")
|
|
80
|
+
if File.exist?(env_spec_path)
|
|
81
|
+
@ui.info("env.spec already exists at repo root -- leaving as-is")
|
|
82
|
+
else
|
|
83
|
+
FileUtils.cp(
|
|
84
|
+
File.join(Kdep.templates_dir, "init", "env.spec"),
|
|
85
|
+
env_spec_path
|
|
86
|
+
)
|
|
87
|
+
@ui.file_written("env.spec")
|
|
88
|
+
end
|
|
81
89
|
|
|
82
90
|
# Copy .gitignore
|
|
83
91
|
FileUtils.cp(
|
|
@@ -157,7 +157,7 @@ module Kdep
|
|
|
157
157
|
output_dir = File.join(tmpdir, ".rendered")
|
|
158
158
|
writer = Kdep::Writer.new(output_dir)
|
|
159
159
|
writer.clean
|
|
160
|
-
renderer = Kdep::Renderer.new(config, target_dir)
|
|
160
|
+
renderer = Kdep::Renderer.new(config, target_dir, env: env)
|
|
161
161
|
|
|
162
162
|
rendered_files = []
|
|
163
163
|
preset.resources.each_with_index do |res, idx|
|
data/lib/kdep/commands/push.rb
CHANGED
|
@@ -41,9 +41,15 @@ module Kdep
|
|
|
41
41
|
# Load config
|
|
42
42
|
config = Kdep::Config.new(deploy_dir, env).load
|
|
43
43
|
|
|
44
|
-
#
|
|
44
|
+
# Resolve tag from state.yml — app.yml has no tag field by design;
|
|
45
|
+
# state.yml is the source of truth for the last bumped version.
|
|
46
|
+
tag = Kdep::State.tag(deploy_dir)
|
|
47
|
+
unless tag
|
|
48
|
+
@ui.error("No tag in state.yml for #{deploy_dir}. Run `kdep bump` first.")
|
|
49
|
+
exit 1
|
|
50
|
+
end
|
|
51
|
+
|
|
45
52
|
image = config["image"] || config["name"]
|
|
46
|
-
tag = config["tag"] || "latest"
|
|
47
53
|
registry = config["registry"]
|
|
48
54
|
|
|
49
55
|
if registry && !registry.to_s.empty?
|
data/lib/kdep/commands/render.rb
CHANGED
|
@@ -57,7 +57,7 @@ module Kdep
|
|
|
57
57
|
# Set up writer and renderer
|
|
58
58
|
writer = Kdep::Writer.new(output_dir)
|
|
59
59
|
writer.clean
|
|
60
|
-
renderer = Kdep::Renderer.new(config, deploy_dir)
|
|
60
|
+
renderer = Kdep::Renderer.new(config, deploy_dir, env: env)
|
|
61
61
|
validator = Kdep::Validator.new
|
|
62
62
|
|
|
63
63
|
files_written = 0
|
data/lib/kdep/renderer.rb
CHANGED
|
@@ -3,28 +3,46 @@ require "yaml"
|
|
|
3
3
|
|
|
4
4
|
module Kdep
|
|
5
5
|
class Renderer
|
|
6
|
-
|
|
6
|
+
# Convention: Infisical project slug = K8s namespace stripped of -stage/-dev
|
|
7
|
+
# suffix; envSlug = stage/dev/prod derived from the same suffix.
|
|
8
|
+
NS_ENV_SUFFIXES = { "-stage" => "stage", "-staging" => "stage", "-dev" => "dev" }.freeze
|
|
9
|
+
|
|
10
|
+
def initialize(config, deploy_dir, env: nil)
|
|
7
11
|
@config = config
|
|
8
12
|
@deploy_dir = deploy_dir
|
|
13
|
+
@env = env
|
|
9
14
|
end
|
|
10
15
|
|
|
11
16
|
def render_resource(resource_name)
|
|
12
|
-
|
|
13
|
-
|
|
17
|
+
# When env.spec is present, the `secret` slot becomes an InfisicalSecret CR
|
|
18
|
+
# instead of a native K8s Secret -- the Infisical operator materializes
|
|
19
|
+
# the K8s Secret in-cluster. Same env_from: secretRef wiring, no template
|
|
20
|
+
# changes in user-overridden resources.
|
|
21
|
+
effective_resource = resource_name
|
|
22
|
+
if resource_name == "secret" && env_spec
|
|
23
|
+
effective_resource = "infisical_secret"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
template_path = resolve_template(effective_resource)
|
|
27
|
+
raise "Template not found: #{effective_resource}.yml.erb" unless template_path
|
|
14
28
|
|
|
15
29
|
template_content = File.read(template_path)
|
|
16
|
-
erb =
|
|
17
|
-
ERB.new(template_content, trim_mode: "-")
|
|
18
|
-
else
|
|
19
|
-
ERB.new(template_content, nil, "-")
|
|
20
|
-
end
|
|
30
|
+
erb = build_erb(template_content)
|
|
21
31
|
|
|
32
|
+
effective_config = @config
|
|
22
33
|
secrets = {}
|
|
23
|
-
|
|
24
|
-
|
|
34
|
+
|
|
35
|
+
case effective_resource
|
|
36
|
+
when "configmap"
|
|
37
|
+
effective_config = @config.merge("configmap" => resolved_configmap)
|
|
38
|
+
when "infisical_secret"
|
|
39
|
+
return nil if secret_keys_for_env.empty?
|
|
40
|
+
effective_config = @config.merge("infisical" => infisical_render_context)
|
|
41
|
+
when "secret"
|
|
42
|
+
secrets = legacy_load_secrets
|
|
25
43
|
end
|
|
26
44
|
|
|
27
|
-
context = TemplateContext.new(
|
|
45
|
+
context = TemplateContext.new(effective_config, secrets)
|
|
28
46
|
erb.result(context.get_binding)
|
|
29
47
|
end
|
|
30
48
|
|
|
@@ -36,12 +54,7 @@ module Kdep
|
|
|
36
54
|
|
|
37
55
|
template_path = File.join(Kdep.templates_dir, "resources", "helm_ingress.yml.erb")
|
|
38
56
|
raise "helm_ingress template not found at #{template_path}" unless File.exist?(template_path)
|
|
39
|
-
|
|
40
|
-
erb = if RUBY_VERSION >= "2.6"
|
|
41
|
-
ERB.new(template_content, trim_mode: "-")
|
|
42
|
-
else
|
|
43
|
-
ERB.new(template_content, nil, "-")
|
|
44
|
-
end
|
|
57
|
+
erb = build_erb(File.read(template_path))
|
|
45
58
|
|
|
46
59
|
entries.map do |ingress|
|
|
47
60
|
context = TemplateContext.new(@config)
|
|
@@ -50,8 +63,101 @@ module Kdep
|
|
|
50
63
|
end
|
|
51
64
|
end
|
|
52
65
|
|
|
66
|
+
# Public so commands (e.g. `kdep env check`) can introspect the contract.
|
|
67
|
+
def env_spec
|
|
68
|
+
return @env_spec if defined?(@env_spec)
|
|
69
|
+
path = env_spec_path
|
|
70
|
+
@env_spec = path && File.exist?(path) ? EnvSpec.parse_file(path) : nil
|
|
71
|
+
end
|
|
72
|
+
|
|
53
73
|
private
|
|
54
74
|
|
|
75
|
+
def build_erb(content)
|
|
76
|
+
if RUBY_VERSION >= "2.6"
|
|
77
|
+
ERB.new(content, trim_mode: "-")
|
|
78
|
+
else
|
|
79
|
+
ERB.new(content, nil, "-")
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def env_spec_path
|
|
84
|
+
File.join(repo_root, "env.spec")
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def repo_root
|
|
88
|
+
# deploy_dir is <repo>/kdep/<deploy>
|
|
89
|
+
File.expand_path("../..", @deploy_dir)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def env_scope
|
|
93
|
+
@env || "*"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def resolved_configmap
|
|
97
|
+
app_values = @config["configmap"] || {}
|
|
98
|
+
return app_values unless env_spec
|
|
99
|
+
|
|
100
|
+
declared = env_spec.configmap_for(env_scope)
|
|
101
|
+
out = {}
|
|
102
|
+
declared.each do |key, entry|
|
|
103
|
+
if app_values.key?(key)
|
|
104
|
+
out[key] = app_values[key]
|
|
105
|
+
elsif !entry.default.nil?
|
|
106
|
+
out[key] = entry.default
|
|
107
|
+
elsif !entry.optional
|
|
108
|
+
raise "env.spec: required ConfigMap key '#{key}' has no value " \
|
|
109
|
+
"in app.yml configmap and no default (declared at line #{entry.line})"
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
# Allow app.yml to carry keys not in spec (e.g. legacy or transitional)
|
|
113
|
+
app_values.each { |k, v| out[k] = v unless out.key?(k) }
|
|
114
|
+
out
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def secret_keys_for_env
|
|
118
|
+
return [] unless env_spec
|
|
119
|
+
env_spec.secrets_for(env_scope).keys
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def infisical_render_context
|
|
123
|
+
ns = @config["namespace"] || @config["secret_namespace"]
|
|
124
|
+
raise "infisical_secret: namespace missing in app.yml" unless ns
|
|
125
|
+
project, env_from_ns = split_namespace(ns)
|
|
126
|
+
env = env_from_ns || "prod"
|
|
127
|
+
|
|
128
|
+
# The materialized K8s Secret name defaults to <deploy>-secrets but
|
|
129
|
+
# respects an explicit app.yml secret_name override -- this preserves
|
|
130
|
+
# legacy env_from references (e.g. `secret/secret-foo`) so a repo
|
|
131
|
+
# can adopt env.spec without rewriting its deployment manifests.
|
|
132
|
+
managed_secret_name = @config["secret_name"] || "#{deploy_slug}-secrets"
|
|
133
|
+
|
|
134
|
+
{
|
|
135
|
+
"deploy" => deploy_slug,
|
|
136
|
+
"secret_name" => managed_secret_name,
|
|
137
|
+
"namespace" => ns,
|
|
138
|
+
"identity_id" => Kdep::UserConfig.get("infisical", "identity_id"),
|
|
139
|
+
"project_slug" => project,
|
|
140
|
+
"secrets_path" => "/#{deploy_slug}",
|
|
141
|
+
"env_slug" => env,
|
|
142
|
+
"host_api" => Kdep::UserConfig.get("infisical", "host_api") || "https://dev-env.leadfy.xyz/api",
|
|
143
|
+
"managed_keys" => secret_keys_for_env,
|
|
144
|
+
}
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def deploy_slug
|
|
148
|
+
File.basename(@deploy_dir)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# "leadfy-app" -> ["leadfy-app", nil] (-> envSlug=prod)
|
|
152
|
+
# "leadfy-app-stage" -> ["leadfy-app", "stage"]
|
|
153
|
+
# "selly-dev" -> ["selly", "dev"]
|
|
154
|
+
def split_namespace(ns)
|
|
155
|
+
NS_ENV_SUFFIXES.each do |suffix, env|
|
|
156
|
+
return [ns.sub(/#{Regexp.escape(suffix)}\z/, ""), env] if ns.end_with?(suffix)
|
|
157
|
+
end
|
|
158
|
+
[ns, nil]
|
|
159
|
+
end
|
|
160
|
+
|
|
55
161
|
def resolve_template(resource_name)
|
|
56
162
|
# Check user override first
|
|
57
163
|
user_path = File.join(@deploy_dir, "resources", "#{resource_name}.yml.erb")
|
|
@@ -64,7 +170,9 @@ module Kdep
|
|
|
64
170
|
nil
|
|
65
171
|
end
|
|
66
172
|
|
|
67
|
-
|
|
173
|
+
# Legacy: kdep/<deploy>/secrets.yml -- only used if env.spec is absent and
|
|
174
|
+
# the project still ships the native K8s Secret template.
|
|
175
|
+
def legacy_load_secrets
|
|
68
176
|
secrets_path = File.join(@deploy_dir, "secrets.yml")
|
|
69
177
|
return {} unless File.exist?(secrets_path)
|
|
70
178
|
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
require "yaml"
|
|
2
|
+
require "fileutils"
|
|
3
|
+
|
|
4
|
+
module Kdep
|
|
5
|
+
# Per-user kdep config persisted at ~/.kdep/config.yml. Currently holds
|
|
6
|
+
# only Infisical settings (identity_id, host_api), but kept generic so
|
|
7
|
+
# other CLI-wide knobs can land here.
|
|
8
|
+
module UserConfig
|
|
9
|
+
PATH = File.expand_path("~/.kdep/config.yml").freeze
|
|
10
|
+
|
|
11
|
+
def self.load
|
|
12
|
+
return {} unless File.exist?(PATH)
|
|
13
|
+
YAML.safe_load(File.read(PATH)) || {}
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.get(*keys)
|
|
17
|
+
load.dig(*keys.map(&:to_s))
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.set(dotted_key, value)
|
|
21
|
+
keys = dotted_key.to_s.split(".")
|
|
22
|
+
data = load
|
|
23
|
+
cursor = data
|
|
24
|
+
keys[0..-2].each do |k|
|
|
25
|
+
cursor[k] = {} unless cursor[k].is_a?(Hash)
|
|
26
|
+
cursor = cursor[k]
|
|
27
|
+
end
|
|
28
|
+
cursor[keys.last] = value
|
|
29
|
+
write(data)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.write(data)
|
|
33
|
+
FileUtils.mkdir_p(File.dirname(PATH))
|
|
34
|
+
File.write(PATH, data.to_yaml)
|
|
35
|
+
PATH
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
data/lib/kdep/version.rb
CHANGED
data/lib/kdep.rb
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
require "envspec"
|
|
1
2
|
require "kdep/version"
|
|
2
3
|
require "kdep/ui"
|
|
3
4
|
require "kdep/yaml_compat"
|
|
@@ -18,6 +19,7 @@ require "kdep/helm"
|
|
|
18
19
|
require "kdep/registry"
|
|
19
20
|
require "kdep/version_tagger"
|
|
20
21
|
require "kdep/state"
|
|
22
|
+
require "kdep/user_config"
|
|
21
23
|
require "kdep/path_resolver"
|
|
22
24
|
require "kdep/configmap_overlay"
|
|
23
25
|
require "kdep/rollout_tracker"
|
|
@@ -42,6 +44,8 @@ require "kdep/commands/helm_install"
|
|
|
42
44
|
require "kdep/commands/dashboard"
|
|
43
45
|
require "kdep/commands/doctor"
|
|
44
46
|
require "kdep/commands/check"
|
|
47
|
+
require "kdep/commands/env"
|
|
48
|
+
require "kdep/commands/config"
|
|
45
49
|
require "kdep/dashboard/screen"
|
|
46
50
|
require "kdep/dashboard/layout"
|
|
47
51
|
require "kdep/dashboard/panel"
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# env.spec -- declarative env var contract for this repo.
|
|
2
|
+
#
|
|
3
|
+
# This file lives at the repo root, NOT inside kdep/. One spec per repo.
|
|
4
|
+
# Values come from app.yml configmap: (for ConfigMap keys) and from
|
|
5
|
+
# Infisical (for [secrets] keys -- materialized into the K8s Secret by
|
|
6
|
+
# the Infisical operator at deploy time).
|
|
7
|
+
#
|
|
8
|
+
# Syntax:
|
|
9
|
+
# KEY # required str, ConfigMap, shared (all envs)
|
|
10
|
+
# KEY? # optional
|
|
11
|
+
# KEY: int # typed (str | int | bool | dsn | enum(a,b))
|
|
12
|
+
# KEY: str = default # ConfigMap default (secrets cannot have defaults)
|
|
13
|
+
#
|
|
14
|
+
# [secrets] # MODIFIER: subsequent keys become Secrets
|
|
15
|
+
# [env: production] # SELECTOR: scope -> production (resets type to ConfigMap)
|
|
16
|
+
# [env: production, staging]# multi-env scope
|
|
17
|
+
#
|
|
18
|
+
# Validate against live cluster: kdep env check <deploy> --env=production
|
|
19
|
+
# Lint syntax: envspec lint env.spec
|
|
20
|
+
#
|
|
21
|
+
# Example:
|
|
22
|
+
# APP_NAME
|
|
23
|
+
# LOG_LEVEL: enum(debug, info, warn, error) = info
|
|
24
|
+
# PORT: int = 3000
|
|
25
|
+
#
|
|
26
|
+
# [secrets]
|
|
27
|
+
# DATABASE_URL: dsn
|
|
28
|
+
# OPENAI_API_KEY
|
|
29
|
+
#
|
|
30
|
+
# [env: production]
|
|
31
|
+
# SENTRY_DSN: dsn
|
|
32
|
+
# [secrets]
|
|
33
|
+
# STRIPE_LIVE_KEY
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<%
|
|
2
|
+
inf = @config["infisical"] || {}
|
|
3
|
+
managed_keys = inf["managed_keys"] || []
|
|
4
|
+
raise "infisical_secret: identity_id missing -- run 'kdep config set infisical.identity_id <uuid>'" unless inf["identity_id"]
|
|
5
|
+
raise "infisical_secret: env_slug missing -- pass --env=<production|staging|dev>" unless inf["env_slug"]
|
|
6
|
+
-%>
|
|
7
|
+
apiVersion: secrets.infisical.com/v1alpha1
|
|
8
|
+
kind: InfisicalSecret
|
|
9
|
+
metadata:
|
|
10
|
+
name: <%= inf["deploy"] %>
|
|
11
|
+
namespace: <%= inf["namespace"] %>
|
|
12
|
+
spec:
|
|
13
|
+
hostAPI: <%= inf["host_api"] %>
|
|
14
|
+
resyncInterval: 60
|
|
15
|
+
authentication:
|
|
16
|
+
kubernetesAuth:
|
|
17
|
+
identityId: <%= inf["identity_id"] %>
|
|
18
|
+
# K8s >=1.24 no longer auto-creates legacy SA token Secrets. Tell
|
|
19
|
+
# the operator to mint short-lived projected tokens via TokenRequest.
|
|
20
|
+
autoCreateServiceAccountToken: true
|
|
21
|
+
serviceAccountRef:
|
|
22
|
+
name: default
|
|
23
|
+
namespace: <%= inf["namespace"] %>
|
|
24
|
+
secretsScope:
|
|
25
|
+
projectSlug: <%= inf["project_slug"] %>
|
|
26
|
+
envSlug: <%= inf["env_slug"] %>
|
|
27
|
+
secretsPath: "<%= inf["secrets_path"] %>"
|
|
28
|
+
recursive: false
|
|
29
|
+
managedSecretReference:
|
|
30
|
+
secretName: <%= inf["secret_name"] %>
|
|
31
|
+
secretNamespace: <%= inf["namespace"] %>
|
|
32
|
+
creationPolicy: Owner
|
|
33
|
+
<% if managed_keys.any? -%>
|
|
34
|
+
# Managed keys (for human reference; operator pulls live from Infisical):
|
|
35
|
+
<% managed_keys.sort.each do |k| -%>
|
|
36
|
+
# - <%= k %>
|
|
37
|
+
<% end -%>
|
|
38
|
+
<% end -%>
|
metadata
CHANGED
|
@@ -1,15 +1,29 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: kdep
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.4.
|
|
4
|
+
version: 0.4.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Leadfy
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-05-06 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: envspec
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '0.1'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '0.1'
|
|
13
27
|
- !ruby/object:Gem::Dependency
|
|
14
28
|
name: bundler
|
|
15
29
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -72,10 +86,13 @@ files:
|
|
|
72
86
|
- lib/kdep/commands/build.rb
|
|
73
87
|
- lib/kdep/commands/bump.rb
|
|
74
88
|
- lib/kdep/commands/check.rb
|
|
89
|
+
- lib/kdep/commands/config.rb
|
|
75
90
|
- lib/kdep/commands/dashboard.rb
|
|
76
91
|
- lib/kdep/commands/diff.rb
|
|
77
92
|
- lib/kdep/commands/doctor.rb
|
|
78
93
|
- lib/kdep/commands/eject.rb
|
|
94
|
+
- lib/kdep/commands/env.rb
|
|
95
|
+
- lib/kdep/commands/env_check.rb
|
|
79
96
|
- lib/kdep/commands/helm_install.rb
|
|
80
97
|
- lib/kdep/commands/init.rb
|
|
81
98
|
- lib/kdep/commands/log.rb
|
|
@@ -121,12 +138,14 @@ files:
|
|
|
121
138
|
- lib/kdep/template_context.rb
|
|
122
139
|
- lib/kdep/ui.rb
|
|
123
140
|
- lib/kdep/update_check.rb
|
|
141
|
+
- lib/kdep/user_config.rb
|
|
124
142
|
- lib/kdep/validator.rb
|
|
125
143
|
- lib/kdep/version.rb
|
|
126
144
|
- lib/kdep/version_tagger.rb
|
|
127
145
|
- lib/kdep/writer.rb
|
|
128
146
|
- lib/kdep/yaml_compat.rb
|
|
129
147
|
- templates/init/app.yml.erb
|
|
148
|
+
- templates/init/env.spec
|
|
130
149
|
- templates/init/gitignore
|
|
131
150
|
- templates/init/secrets.yml
|
|
132
151
|
- templates/presets/cronjob
|
|
@@ -141,6 +160,7 @@ files:
|
|
|
141
160
|
- templates/resources/cronjob.yml.erb
|
|
142
161
|
- templates/resources/deployment.yml.erb
|
|
143
162
|
- templates/resources/helm_ingress.yml.erb
|
|
163
|
+
- templates/resources/infisical_secret.yml.erb
|
|
144
164
|
- templates/resources/ingress.yml.erb
|
|
145
165
|
- templates/resources/job.yml.erb
|
|
146
166
|
- templates/resources/secret.yml.erb
|