Module: LanguageOperator::CLI::Commands::Model::Test

Included in:
Base
Defined in:
lib/language_operator/cli/commands/model/test.rb

Class Method Summary collapse

Class Method Details

.included(base) ⇒ Object



13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
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
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
# File 'lib/language_operator/cli/commands/model/test.rb', line 13

def self.included(base)
  base.class_eval do
    desc 'test NAME', 'Test model connectivity and functionality'
    long_desc <<-DESC
      Test that a model is operational by:
      1. Verifying the pod is running
      2. Testing the chat completion endpoint with a simple message

      This command helps diagnose model deployment issues.
    DESC
    option :cluster, type: :string, desc: 'Override current cluster context'
    option :timeout, type: :numeric, default: 30, desc: 'Timeout in seconds for endpoint test'
    def test(name)
      handle_command_error('test model') do
        # 1. Get model resource
        model = get_resource_or_exit(RESOURCE_MODEL, name)
        model_name = model.dig('spec', 'modelName')

        # 2. Check deployment status
        deployment = check_deployment_status(name)

        # 3. Check pod status
        pod = check_pod_status(name, deployment)

        # 4. Test chat completion endpoint
        test_chat_completion(name, model_name, pod, options[:timeout])
      end
    end

    private

    def check_deployment_status(name)
      Formatters::ProgressFormatter.with_spinner('Verifying deployment') do
        deployment = ctx.client.get_resource('Deployment', name, ctx.namespace)
        replicas = deployment.dig('spec', 'replicas') || 1
        ready_replicas = deployment.dig('status', 'readyReplicas') || 0

        unless ready_replicas >= replicas
          raise "Deployment not ready (#{ready_replicas}/#{replicas}). " \
                "Run 'kubectl get deployment #{name} -n #{ctx.namespace}' for details."
        end

        deployment
      end
    rescue K8s::Error::NotFound
      Formatters::ProgressFormatter.error("Deployment '#{name}' not found")
      exit 1
    end

    def check_pod_status(name, deployment)
      Formatters::ProgressFormatter.with_spinner('Verifying pod') do
        labels = deployment.dig('spec', 'selector', 'matchLabels')

        # Find matching pods using centralized utility
        pods = CLI::Helpers::LabelUtils.find_pods_by_deployment_labels(ctx, name, labels)
        raise "No pods found for model '#{name}'" if pods.empty?

        # Find a running pod
        running_pod = pods.find do |pod|
          pod.dig('status', 'phase') == 'Running' &&
            pod.dig('status', 'conditions')&.any? { |c| c['type'] == 'Ready' && c['status'] == 'True' }
        end

        if running_pod.nil?
          pod_phases = pods.map { |p| p.dig('status', 'phase') }.join(', ')
          # Get label selector for error message
          labels_hash = labels.respond_to?(:to_h) ? labels.to_h : labels
          label_selector = labels_hash.map { |k, v| "#{k}=#{v}" }.join(',')
          raise "No running pods found. Pod phases: #{pod_phases}. " \
                "Run 'kubectl get pods -l #{label_selector} -n #{ctx.namespace}' for details."
        end

        running_pod
      end
    end

    def test_chat_completion(_name, model_name, pod, timeout)
      Formatters::ProgressFormatter.with_spinner('Verifying chat completion requests') do
        pod_name = pod.dig('metadata', 'name')

        # Build the JSON payload
        payload = JSON.generate({
                                  model: model_name,
                                  messages: [{ role: 'user', content: 'hello' }],
                                  max_tokens: 10
                                })

        # Create temporary file to avoid command injection
        temp_file = nil
        begin
          temp_file = Tempfile.new(['model_test_payload', '.json'])
          temp_file.write(payload)
          temp_file.close

          # Build secure curl command using temp file
          # This eliminates shell injection vulnerability
          curl_command = 'curl -s -X POST http://localhost:4000/v1/chat/completions ' \
                         "-H 'Content-Type: application/json' -d @#{temp_file.path} --max-time #{timeout.to_i}"

          # Execute the curl command inside the pod
          result = execute_in_pod(pod_name, curl_command)
        ensure
          # Clean up temporary file
          temp_file&.unlink
        end

        # Parse the response
        response = JSON.parse(result)

        if response['error']
          error_msg = response['error']['message'] || response['error']
          raise error_msg
        elsif !response['choices']
          raise "Unexpected response format: #{result.lines.first.strip}"
        end

        response
      rescue JSON::ParserError => e
        raise "Failed to parse response: #{e.message}"
      end
    rescue StandardError => e
      # Display error in bold red
      puts
      puts Formatters::ProgressFormatter.pastel.bold.red(e.message)
      exit 1
    end

    def execute_in_pod(pod_name, command)
      # Use kubectl exec to run command in pod
      # command can be a string or array
      kubectl_command = if command.is_a?(String)
                          "kubectl exec -n #{ctx.namespace} #{pod_name} -- sh -c #{Shellwords.escape(command)}"
                        else
                          Shellwords.join(['kubectl', 'exec', '-n', ctx.namespace, pod_name, '--'] + command)
                        end

      output = `#{kubectl_command} 2>&1`
      exit_code = $CHILD_STATUS.exitstatus

      if exit_code != 0
        Formatters::ProgressFormatter.error("Failed to execute command in pod: #{output}")
        exit 1
      end

      output
    end
  end
end