Class: LanguageOperator::Kubernetes::Client

Inherits:
Object
  • Object
show all
Defined in:
lib/language_operator/kubernetes/client.rb

Overview

Kubernetes client wrapper for interacting with language-operator resources

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(kubeconfig: nil, context: nil, in_cluster: false) ⇒ Client

Returns a new instance of Client.



33
34
35
36
37
38
# File 'lib/language_operator/kubernetes/client.rb', line 33

def initialize(kubeconfig: nil, context: nil, in_cluster: false)
  @in_cluster = in_cluster
  @kubeconfig = kubeconfig || ENV.fetch('KUBECONFIG', LanguageOperator::Utils::SecurePath.expand_home_path('.kube/config'))
  @context = context
  @client = build_client
end

Instance Attribute Details

#clientObject (readonly)

Returns the value of attribute client.



12
13
14
# File 'lib/language_operator/kubernetes/client.rb', line 12

def client
  @client
end

#contextObject (readonly)

Returns the value of attribute context.



12
13
14
# File 'lib/language_operator/kubernetes/client.rb', line 12

def context
  @context
end

Class Method Details

.in_cluster?Boolean

Check if running inside a Kubernetes cluster

Returns:

  • (Boolean)

    True if in-cluster, false otherwise



29
30
31
# File 'lib/language_operator/kubernetes/client.rb', line 29

def self.in_cluster?
  File.exist?('/var/run/secrets/kubernetes.io/serviceaccount/token')
end

.instanceLanguageOperator::Kubernetes::Client

Get singleton K8s client instance with automatic config detection

Returns:

Raises:

  • (RuntimeError)

    if client initialization fails



17
18
19
# File 'lib/language_operator/kubernetes/client.rb', line 17

def self.instance
  @instance ||= build_singleton
end

.reset!nil

Reset the singleton (useful for testing)

Returns:

  • (nil)


23
24
25
# File 'lib/language_operator/kubernetes/client.rb', line 23

def self.reset!
  @instance = nil
end

Instance Method Details

#apply_resource(resource) ⇒ Object

Create or update a Kubernetes resource



70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
# File 'lib/language_operator/kubernetes/client.rb', line 70

def apply_resource(resource)
  namespace = resource.dig('metadata', 'namespace')
  name = resource.dig('metadata', 'name')
  kind = resource['kind']
  api_version = resource['apiVersion']

  begin
    # Try to get existing resource
    existing = get_resource(kind, name, namespace, api_version)
    if existing
      # Merge existing metadata (especially resourceVersion) with new resource
      merged_resource = if resource.is_a?(Hash)
                          resource.dup
                        else
                          resource.to_h
                        end
      merged_resource['metadata'] ||= {}
      merged_resource['metadata']['resourceVersion'] = existing..resourceVersion
      merged_resource['metadata']['uid'] = existing..uid if existing..uid

      # Update existing resource
      update_resource(kind, name, namespace, merged_resource, api_version)
    else
      # Create new resource
      create_resource(resource)
    end
  rescue K8s::Error::NotFound
    # Resource doesn't exist, create it
    create_resource(resource)
  end
end

#create_namespace(name, labels: {}) ⇒ Object

Create namespace



155
156
157
158
159
160
161
162
163
164
165
# File 'lib/language_operator/kubernetes/client.rb', line 155

def create_namespace(name, labels: {})
  resource = {
    'apiVersion' => 'v1',
    'kind' => 'Namespace',
    'metadata' => {
      'name' => name,
      'labels' => labels
    }
  }
  create_resource(resource)
end

#create_resource(resource) ⇒ Object

Create a resource



103
104
105
106
107
108
109
110
111
112
113
114
115
# File 'lib/language_operator/kubernetes/client.rb', line 103

def create_resource(resource)
  resource_client = resource_client_for_resource(resource)
  # Convert hash to K8s::Resource if needed
  k8s_resource = if resource.is_a?(K8s::Resource)
                   resource
                 else
                   # Remove resourceVersion if present - it should not be set on new resources
                   resource_hash = resource.dup
                   resource_hash['metadata']&.delete('resourceVersion')
                   K8s::Resource.new(resource_hash)
                 end
  resource_client.create_resource(k8s_resource)
end

#current_contextObject

Get the current Kubernetes context name



41
42
43
44
45
46
47
48
# File 'lib/language_operator/kubernetes/client.rb', line 41

def current_context
  return nil if @in_cluster

  config = K8s::Config.load_file(@kubeconfig)
  @context || config.current_context
rescue Errno::ENOENT
  nil
end

#current_namespaceString?

Get the current namespace from the context. Returns the namespace from service account (in-cluster) or kubeconfig context. Gracefully handles all filesystem errors and returns nil on failure.

Returns:

  • (String, nil)

    the current namespace, or nil if unable to determine



55
56
57
58
59
60
61
62
63
64
65
66
67
# File 'lib/language_operator/kubernetes/client.rb', line 55

def current_namespace
  if @in_cluster
    # In-cluster: read from service account namespace
    File.read('/var/run/secrets/kubernetes.io/serviceaccount/namespace').strip
  else
    config = K8s::Config.load_file(@kubeconfig)
    context_name = current_context
    context_obj = config.context(context_name)
    context_obj&.namespace
  end
rescue SystemCallError, IOError
  nil
end

#current_org_idString?

Get the current organization ID from cluster resources

Returns:

  • (String, nil)

    Organization ID or nil if not found/legacy mode



196
197
198
# File 'lib/language_operator/kubernetes/client.rb', line 196

def current_org_id
  Utils::OrgContext.current_org_id(self)
end

#delete_resource(kind, name, namespace = nil, api_version = nil) ⇒ Object

Delete a resource



141
142
143
144
# File 'lib/language_operator/kubernetes/client.rb', line 141

def delete_resource(kind, name, namespace = nil, api_version = nil)
  resource_client = resource_client_for(kind, namespace, api_version || default_api_version(kind))
  resource_client.delete(name)
end

#get_resource(kind, name, namespace = nil, api_version = nil) ⇒ Object

Get a resource



126
127
128
129
# File 'lib/language_operator/kubernetes/client.rb', line 126

def get_resource(kind, name, namespace = nil, api_version = nil)
  resource_client = resource_client_for(kind, namespace, api_version || default_api_version(kind))
  resource_client.get(name)
end

#list_namespaces(label_selector: nil) ⇒ Array<Hash>

List namespaces with optional label selector

Parameters:

  • label_selector (String, nil) (defaults to: nil)

    Label selector for filtering

Returns:

  • (Array<Hash>)

    Array of namespace resources



224
225
226
227
228
229
230
231
232
233
234
235
236
# File 'lib/language_operator/kubernetes/client.rb', line 224

def list_namespaces(label_selector: nil)
  namespaces_api = @client.api('v1').resource('namespaces')
  result = if label_selector
             namespaces_api.list(labelSelector: label_selector)
           else
             namespaces_api.list
           end
  # k8s-ruby returns an Array directly, not an object with .items
  Array(result)
rescue StandardError => e
  warn "Warning: Could not list namespaces: #{e.message}" if ENV['DEBUG']
  []
end

#list_resources(kind, namespace: nil, api_version: nil, label_selector: nil) ⇒ Object

List resources



132
133
134
135
136
137
138
# File 'lib/language_operator/kubernetes/client.rb', line 132

def list_resources(kind, namespace: nil, api_version: nil, label_selector: nil)
  resource_client = resource_client_for(kind, namespace, api_version || default_api_version(kind))
  opts = {}
  opts[:labelSelector] = label_selector if label_selector

  resource_client.list(**opts)
end

#namespace_exists?(name) ⇒ Boolean

Check if namespace exists

Returns:

  • (Boolean)


147
148
149
150
151
152
# File 'lib/language_operator/kubernetes/client.rb', line 147

def namespace_exists?(name)
  @client.api('v1').resource('namespaces').get(name)
  true
rescue K8s::Error::NotFound
  false
end

#operator_installed?Boolean

Check if operator is installed

Returns:

  • (Boolean)


168
169
170
171
172
173
174
# File 'lib/language_operator/kubernetes/client.rb', line 168

def operator_installed?
  # Check if LanguageCluster CRD exists
  @client.apis(prefetch_resources: true)
         .find { |api| api.api_version == 'langop.io/v1alpha1' }
rescue StandardError
  false
end

#operator_versionObject

Get operator version



177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
# File 'lib/language_operator/kubernetes/client.rb', line 177

def operator_version
  deployment = @client.api('apps/v1')
                      .resource('deployments', namespace: 'language-operator')
                      .get(Constants::KubernetesLabels::PROJECT_NAME)
  
  # Try to get version from Helm chart label first, then fallback to app version
  version = deployment.dig('metadata', 'labels', 'helm.sh/chart')&.split('-')&.last ||
            deployment.dig('metadata', 'labels', Constants::KubernetesLabels::VERSION)
  
  return version if version && version != 'latest'
  
  'unknown'
rescue K8s::Error::NotFound
  nil
end

#org_context?Boolean

Check if the current cluster has organization context

Returns:

  • (Boolean)

    True if organization context is available



203
204
205
# File 'lib/language_operator/kubernetes/client.rb', line 203

def org_context?
  !current_org_id.nil?
end

#org_infoHash

Get organization information for cluster listing

Returns:

  • (Hash)

    Organization info with id, namespace, and display name



210
211
212
213
214
215
216
217
218
# File 'lib/language_operator/kubernetes/client.rb', line 210

def org_info
  org_id = current_org_id
  return { id: nil, display: 'legacy' } unless org_id

  {
    id: org_id,
    display: org_id[0..7] # Show first 8 chars for readability
  }
end

#update_resource(kind, _name, namespace, resource, api_version) ⇒ Object

Update a resource



118
119
120
121
122
123
# File 'lib/language_operator/kubernetes/client.rb', line 118

def update_resource(kind, _name, namespace, resource, api_version)
  resource_client = resource_client_for(kind, namespace, api_version)
  # Convert hash to K8s::Resource if needed
  k8s_resource = resource.is_a?(K8s::Resource) ? resource : K8s::Resource.new(resource)
  resource_client.update_resource(k8s_resource)
end