kube_cluster 1.5.0 → 1.7.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1e6e0a31873f5a706d272e0ff37b06a8f3565b0e8d8629b92915f24c572825d1
4
- data.tar.gz: '08b20a4bb972d5b52eddd66577c79462a59611c8f84e692d092720c09bced0da'
3
+ metadata.gz: c57aae5fbe1903b9bc6bd63073ed4dd9d5731102b524077c4969051593b84b86
4
+ data.tar.gz: 19a1be373f25d1faee87bf83072c45987ecac39cc384c932168e4afefc42a7ac
5
5
  SHA512:
6
- metadata.gz: 676e83947ff878d10e2ff3e46e9dd880120e856f480e5ed9b26d27046f7d901d07d5d044d0a3358fa99c927768f4c0c54e8af1e13fa5ef96b5806caeefa476d2
7
- data.tar.gz: e8ade4344a74dd80bfaf8abec1bb43c5b0531b1130ab0c8584c4a0edb08e0a8d339dead8c35012cf517847d19cf066516717af9937890b70df744611f0809d5a
6
+ metadata.gz: db92c45266952b68584a01029cd2502caa5ab1ea2e9d010e91bf70b9385227578378d36be82c3c83eb3cf3966b04b2547732d63bd706555cd3fef6e14c28c869
7
+ data.tar.gz: a53b5a9d8165400643e0693259940f5cf2695d97afd12a258e20dc7ecbd9bfd04aed135803e1d402ec2c3215bb5e17e64d98da8725cb45175a3d208b08c419b1
data/CHANGELOG.md CHANGED
@@ -5,6 +5,38 @@ All notable changes to this project are documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.7.0] - 2026-09-17
9
+
10
+ ### Added
11
+ - `Standard::EnvProcessing` now maps `ESO::ExternalSecret::KeyRef` (the struct
12
+ returned by `ExternalSecret#key`) to a `secretKeyRef` env var, mirroring the
13
+ support `VolumeProcessing` has always had. Unlike `.template`, `.key`
14
+ registers nothing on the ExternalSecret — it references a key the secret
15
+ already materialises (declared via `keys:` at construction, or by a
16
+ `.template` call elsewhere). This makes `Custom::PassboltSecret`-style
17
+ secrets — whose template is fully built at construction — usable from env
18
+ hashes, where previously the only option was a raw `valueFrom` hash or a
19
+ whole-secret `envFrom`.
20
+
21
+ ## [1.6.0] - 2026-08-03
22
+
23
+ ### Added
24
+ - `Standard::JuiceFS::Sync` — a wrapper for the juicefs-operator's `Sync` CRD
25
+ (pinned to `juicefs.io/v1`). `from`/`to` are the CRD's sink hashes, passed
26
+ through as-is. Documents two operator behaviours worth knowing before use: the
27
+ unquoted metadata-URL export that breaks `juicefsCE` sinks whose metaurl
28
+ contains `&`, and the `ttlSecondsAfterFinished` deletion that makes a
29
+ reconciler-managed Sync re-run forever.
30
+ - `Standard::JuiceFS::PersistentVolume` — a statically-provisioned JuiceFS PV,
31
+ the only way to give a volume an exact, readable directory name (`subPath`)
32
+ rather than the `pvc-<uuid>` a StorageClass would assign.
33
+ - `PersistentVolume` pinned to `v1/PersistentVolume` in the resolve table.
34
+
35
+ ### Changed
36
+ - Requires kube_schema `~> 1.10.0`, which ships the `juicefs.io/v1` schemas.
37
+ These wrappers previously needed a local CRD-registration shim that downloaded
38
+ `dist/crd.yaml` at load time; that is no longer necessary.
39
+
8
40
  ## [1.4.0] - 2026-07-13
9
41
 
10
42
  ### Added
data/Gemfile.lock CHANGED
@@ -1,10 +1,10 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- kube_cluster (1.5.0)
4
+ kube_cluster (1.7.0)
5
5
  activesupport (~> 8.0)
6
6
  kube_kubectl (~> 2.0)
7
- kube_schema (~> 1.9.2)
7
+ kube_schema (~> 1.10.0)
8
8
 
9
9
  GEM
10
10
  remote: https://rubygems.org/
@@ -132,7 +132,7 @@ GEM
132
132
  rubyshell (~> 1.5)
133
133
  shellwords (~> 0.2.2)
134
134
  string_builder (~> 1.2.2)
135
- kube_schema (1.9.2)
135
+ kube_schema (1.10.0)
136
136
  json_schemer (~> 2.5.0)
137
137
  rubyshell (~> 1.5.0)
138
138
  lefthook (2.1.10)
data/kube_cluster.gemspec CHANGED
@@ -32,7 +32,7 @@ Gem::Specification.new do |spec|
32
32
  spec.add_development_dependency "scampi", "~> 1.0"
33
33
  spec.add_development_dependency "lefthook", "~> 2.1"
34
34
 
35
- spec.add_dependency "kube_schema", "~> 1.9.2"
35
+ spec.add_dependency "kube_schema", "~> 1.10.0"
36
36
  spec.add_dependency "kube_kubectl", "~> 2.0"
37
37
  spec.add_dependency "activesupport", "~> 8.0"
38
38
  end
@@ -22,6 +22,14 @@ module Kube
22
22
  # "FOO" => Secret.new(name: "creds").key("bar")
23
23
  # => { name: "FOO", valueFrom: { secretKeyRef: { name: "creds", key: "bar" } } }
24
24
  #
25
+ # ExternalSecret::KeyRef values reference a key the ExternalSecret
26
+ # already materialises (declared via keys: at construction, or by a
27
+ # .template call elsewhere) -- unlike .template, .key registers
28
+ # NOTHING on the ExternalSecret. Reference a key that never lands in
29
+ # the target Secret and the pod sits in CreateContainerConfigError.
30
+ # "FOO" => ExternalSecret::Creds.key("bar")
31
+ # => { name: "FOO", valueFrom: { secretKeyRef: { name: "creds", key: "bar" } } }
32
+ #
25
33
  def self.process(env)
26
34
  return env if env.is_a?(Array)
27
35
  return [] if env.nil?
@@ -33,7 +41,7 @@ module Kube
33
41
  when ESO::ExternalSecret::TemplateRef
34
42
  value.secret.register_template!(key, value.template_value)
35
43
  { name: key, valueFrom: { secretKeyRef: { name: value.secret.secret_name, key: key } } }
36
- when Kube::Cluster::Standard::Secret::KeyRef
44
+ when ESO::ExternalSecret::KeyRef, Kube::Cluster::Standard::Secret::KeyRef
37
45
  { name: key, valueFrom: { secretKeyRef: { name: value.secret.secret_name, key: value.key_name } } }
38
46
  else
39
47
  { name: key, value: value.to_s }
@@ -58,6 +66,23 @@ describe "EnvProcessing" do
58
66
  ]
59
67
  end
60
68
 
69
+ it "maps an ExternalSecret::KeyRef to a secretKeyRef env var without registering anything" do
70
+ secret = Kube::Cluster::Standard::ESO::ExternalSecret.new(
71
+ name: "svix-jwt", store: "passbolt", remote_key: "svix-jwt",
72
+ keys: { "SVIX_JWT_SECRET" => "SVIX_JWT_SECRET" }
73
+ )
74
+
75
+ Kube::Cluster::Standard::EnvProcessing
76
+ .process("SVIX_JWT_SECRET" => secret.key("SVIX_JWT_SECRET"))
77
+ .should == [
78
+ { name: "SVIX_JWT_SECRET", valueFrom: { secretKeyRef: { name: "svix-jwt", key: "SVIX_JWT_SECRET" } } }
79
+ ]
80
+
81
+ # Unlike .template, .key must not touch the ExternalSecret's spec.
82
+ secret.to_h.dig(:spec, :target, :template).should.be.nil
83
+ secret.to_h.dig(:spec, :data).length.should == 1
84
+ end
85
+
61
86
  it "still maps plain string values to value env vars" do
62
87
  Kube::Cluster::Standard::EnvProcessing
63
88
  .process("FOO" => "bar")
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ # A statically-provisioned JuiceFS PersistentVolume.
4
+ #
5
+ # Dynamic provisioning (a `juicefs-sc` StorageClass) names each volume's
6
+ # directory `pvc-<uuid>` at the root of the filesystem. Static provisioning lets
7
+ # the directory be named explicitly via the `subPath` volume attribute — the
8
+ # only way to get an exact, readable folder name.
9
+ #
10
+ # `secret`/`secret_namespace` point at the JuiceFS "volume credentials" Secret
11
+ # (name / metaurl / storage / bucket / keys / encrypt_rsa_key). PersistentVolume
12
+ # is cluster-scoped and nodePublishSecretRef carries its own namespace, so the
13
+ # PV, its PVC, and that Secret may all live in different namespaces. The mount
14
+ # pod always runs in the CSI driver's namespace regardless.
15
+ #
16
+ # `storage` is a label only — JuiceFS enforces no quota through this field.
17
+
18
+ require "bundler/setup"
19
+ require "kube/cluster"
20
+
21
+ module Kube
22
+ module Cluster
23
+ module Standard
24
+ module JuiceFS
25
+ class PersistentVolume < Kube::Cluster["PersistentVolume"]
26
+ def initialize(name:, storage:, sub_path:, secret:, secret_namespace:,
27
+ access_modes: ["ReadWriteMany"], reclaim_policy: "Retain", &block)
28
+ super() {
29
+ metadata.name = name
30
+
31
+ spec.capacity = { storage: storage }
32
+ spec.accessModes = access_modes
33
+ spec.persistentVolumeReclaimPolicy = reclaim_policy
34
+ # Empty (not absent) so no StorageClass can claim this PV and the
35
+ # matching PVC binds by volumeName alone.
36
+ spec.storageClassName = ""
37
+ spec.csi = {
38
+ driver: "csi.juicefs.com",
39
+ volumeHandle: name,
40
+ fsType: "juicefs",
41
+ nodePublishSecretRef: { name: secret, namespace: secret_namespace },
42
+ volumeAttributes: { subPath: sub_path },
43
+ }
44
+
45
+ instance_exec(&block) if block
46
+ }
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+
54
+ __END__
55
+
56
+ describe "JuiceFS::PersistentVolume" do
57
+ pv = Kube::Cluster::Standard::JuiceFS::PersistentVolume
58
+
59
+ volume = pv.new(
60
+ name: "media",
61
+ storage: "1Ti",
62
+ sub_path: "media",
63
+ secret: "juicefs-secret",
64
+ secret_namespace: "kube-system",
65
+ )
66
+
67
+ it "mounts the named subPath through the JuiceFS CSI driver" do
68
+ yaml = volume.to_yaml
69
+
70
+ yaml.include?("driver: csi.juicefs.com").should == true
71
+ yaml.include?("subPath: media").should == true
72
+ end
73
+
74
+ it "defaults to a retained, ReadWriteMany volume with no StorageClass" do
75
+ yaml = volume.to_yaml
76
+
77
+ yaml.include?("persistentVolumeReclaimPolicy: Retain").should == true
78
+ yaml.include?("ReadWriteMany").should == true
79
+ yaml.include?("storageClassName: ''").should == true
80
+ end
81
+ end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ # juicefs.io/v1 Sync — a one-shot `juicefs sync` job run by the juicefs-operator
4
+ # (https://juicefs.com/docs/csi/guide/juicefs-operator/).
5
+ #
6
+ # The operator builds a manager pod (plus `replicas - 1` worker pods) from
7
+ # `image`, parses each sink into a URI, and runs
8
+ # `juicefs sync [--<options>] <from> <to>`.
9
+ #
10
+ # `from`/`to` are the CRD's sink hashes and are passed through as-is — they are
11
+ # the meaningful configuration, so wrapping them in a mini-DSL would only hide
12
+ # the CRD. Exactly one of these keys per sink:
13
+ #
14
+ # external: { uri:, accessKey:, secretKey:, extraVolumes: } any object store,
15
+ # a `file://` path, or a mounted PVC/hostPath
16
+ # juicefs: { volumeName:, token:, ... } enterprise edition
17
+ # juicefsCE: { metaURL:, metaPassWord:, path: } community edition
18
+ #
19
+ # NOTE ON juicefsCE: the operator emits its metadata-URL export UNQUOTED
20
+ # (pkg/utils/sync.go, `export %s=%s`). A metaurl containing a literal `&` (e.g.
21
+ # `?sslmode=require&search_path=juicefs`) is split by the pod's `sh -c`
22
+ # entrypoint — the export runs in a background subshell and the variable is
23
+ # empty in the shell that runs the sync. Use an `external` sink with a `pvc`
24
+ # extraVolume instead, and let the CSI mount pod do the JuiceFS work.
25
+ #
26
+ # NOTE ON ttlSecondsAfterFinished: DO NOT SET IT when the Sync is managed by a
27
+ # reconciler such as Flux or Argo CD. The controller DELETES the Sync object
28
+ # when the TTL expires (internal/controller/sync_controller.go); the reconciler
29
+ # then sees drift, recreates it, and the sync runs again — forever. Without a
30
+ # TTL the object simply stays `Completed`. To re-run, delete the object by hand
31
+ # or switch to a CronSync.
32
+
33
+ require "bundler/setup"
34
+ require "kube/cluster"
35
+
36
+ module Kube
37
+ module Cluster
38
+ module Standard
39
+ module JuiceFS
40
+ class Sync < Kube::Cluster["juicefs.io/v1/Sync"]
41
+ def initialize(name:, image:, from:, to:, replicas: 1, options: [], &block)
42
+ super() {
43
+ metadata.name = name
44
+ spec.image = image
45
+ spec.replicas = replicas
46
+ spec.options = options if options.any?
47
+ spec.from = from
48
+ spec.to = to
49
+
50
+ instance_exec(&block) if block
51
+ }
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+
59
+ __END__
60
+
61
+ describe "JuiceFS::Sync" do
62
+ sync = Kube::Cluster::Standard::JuiceFS::Sync
63
+
64
+ it "emits the juicefs.io/v1 apiVersion" do
65
+ sync
66
+ .new(
67
+ name: "media-backup",
68
+ image: "juicedata/mount:latest",
69
+ from: { external: { uri: "file:///mnt/media/" } },
70
+ to: { external: { uri: "s3://backup.example.com/media/" } },
71
+ )
72
+ .to_yaml
73
+ .include?("apiVersion: juicefs.io/v1")
74
+ .should == true
75
+ end
76
+
77
+ it "omits options when none are given" do
78
+ sync
79
+ .new(
80
+ name: "plain",
81
+ image: "juicedata/mount:latest",
82
+ from: { external: { uri: "file:///a/" } },
83
+ to: { external: { uri: "file:///b/" } },
84
+ )
85
+ .to_yaml
86
+ .include?("options:")
87
+ .should == false
88
+ end
89
+
90
+ it "carries options and replicas through" do
91
+ yaml = sync.new(
92
+ name: "parallel",
93
+ image: "juicedata/mount:latest",
94
+ from: { external: { uri: "file:///a/" } },
95
+ to: { external: { uri: "file:///b/" } },
96
+ replicas: 3,
97
+ options: ["--update"],
98
+ ).to_yaml
99
+
100
+ yaml.include?("replicas: 3").should == true
101
+ yaml.include?("--update").should == true
102
+ end
103
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Kube
4
4
  module Cluster
5
- VERSION = "1.5.0"
5
+ VERSION = "1.7.0"
6
6
  end
7
7
  end
data/lib/kube/cluster.rb CHANGED
@@ -92,6 +92,7 @@ require "kube/cluster/resource/persistence"
92
92
  Kube::Cluster.config do
93
93
  # Built-in Kubernetes kinds.
94
94
  resolve "ConfigMap", to: "v1/ConfigMap"
95
+ resolve "PersistentVolume", to: "v1/PersistentVolume"
95
96
  resolve "PersistentVolumeClaim", to: "v1/PersistentVolumeClaim"
96
97
  resolve "Secret", to: "v1/Secret"
97
98
  resolve "Service", to: "v1/Service"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kube_cluster
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.5.0
4
+ version: 1.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nathan K
@@ -57,14 +57,14 @@ dependencies:
57
57
  requirements:
58
58
  - - "~>"
59
59
  - !ruby/object:Gem::Version
60
- version: 1.9.2
60
+ version: 1.10.0
61
61
  type: :runtime
62
62
  prerelease: false
63
63
  version_requirements: !ruby/object:Gem::Requirement
64
64
  requirements:
65
65
  - - "~>"
66
66
  - !ruby/object:Gem::Version
67
- version: 1.9.2
67
+ version: 1.10.0
68
68
  - !ruby/object:Gem::Dependency
69
69
  name: kube_kubectl
70
70
  requirement: !ruby/object:Gem::Requirement
@@ -231,6 +231,8 @@ files:
231
231
  - lib/kube/cluster/standard/gateway_api/http_route.rb
232
232
  - lib/kube/cluster/standard/job.rb
233
233
  - lib/kube/cluster/standard/job_with_service_account.rb
234
+ - lib/kube/cluster/standard/juice_fs/persistent_volume.rb
235
+ - lib/kube/cluster/standard/juice_fs/sync.rb
234
236
  - lib/kube/cluster/standard/kube_virt/virtual_machine.rb
235
237
  - lib/kube/cluster/standard/meta_controller/composite_controller.rb
236
238
  - lib/kube/cluster/standard/meta_controller/decorator_controller.rb