tsikol 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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +22 -0
- data/CONTRIBUTING.md +84 -0
- data/LICENSE +21 -0
- data/README.md +579 -0
- data/Rakefile +12 -0
- data/docs/README.md +69 -0
- data/docs/api/middleware.md +721 -0
- data/docs/api/prompt.md +858 -0
- data/docs/api/resource.md +651 -0
- data/docs/api/server.md +509 -0
- data/docs/api/test-helpers.md +591 -0
- data/docs/api/tool.md +527 -0
- data/docs/cookbook/authentication.md +651 -0
- data/docs/cookbook/caching.md +877 -0
- data/docs/cookbook/dynamic-tools.md +970 -0
- data/docs/cookbook/error-handling.md +887 -0
- data/docs/cookbook/logging.md +1044 -0
- data/docs/cookbook/rate-limiting.md +717 -0
- data/docs/examples/code-assistant.md +922 -0
- data/docs/examples/complete-server.md +726 -0
- data/docs/examples/database-manager.md +1198 -0
- data/docs/examples/devops-tools.md +1382 -0
- data/docs/examples/echo-server.md +501 -0
- data/docs/examples/weather-service.md +822 -0
- data/docs/guides/completion.md +472 -0
- data/docs/guides/getting-started.md +462 -0
- data/docs/guides/middleware.md +823 -0
- data/docs/guides/project-structure.md +434 -0
- data/docs/guides/prompts.md +920 -0
- data/docs/guides/resources.md +720 -0
- data/docs/guides/sampling.md +804 -0
- data/docs/guides/testing.md +863 -0
- data/docs/guides/tools.md +627 -0
- data/examples/README.md +92 -0
- data/examples/advanced_features.rb +129 -0
- data/examples/basic-migrated/app/prompts/weather_chat.rb +44 -0
- data/examples/basic-migrated/app/resources/weather_alerts.rb +18 -0
- data/examples/basic-migrated/app/tools/get_current_weather.rb +34 -0
- data/examples/basic-migrated/app/tools/get_forecast.rb +30 -0
- data/examples/basic-migrated/app/tools/get_weather_by_coords.rb +48 -0
- data/examples/basic-migrated/server.rb +25 -0
- data/examples/basic.rb +73 -0
- data/examples/full_featured.rb +175 -0
- data/examples/middleware_example.rb +112 -0
- data/examples/sampling_example.rb +104 -0
- data/examples/weather-service/app/prompts/weather/chat.rb +90 -0
- data/examples/weather-service/app/resources/weather/alerts.rb +59 -0
- data/examples/weather-service/app/tools/weather/get_current.rb +82 -0
- data/examples/weather-service/app/tools/weather/get_forecast.rb +90 -0
- data/examples/weather-service/server.rb +28 -0
- data/exe/tsikol +6 -0
- data/lib/tsikol/cli/templates/Gemfile.erb +10 -0
- data/lib/tsikol/cli/templates/README.md.erb +38 -0
- data/lib/tsikol/cli/templates/gitignore.erb +49 -0
- data/lib/tsikol/cli/templates/prompt.rb.erb +53 -0
- data/lib/tsikol/cli/templates/resource.rb.erb +29 -0
- data/lib/tsikol/cli/templates/server.rb.erb +24 -0
- data/lib/tsikol/cli/templates/tool.rb.erb +60 -0
- data/lib/tsikol/cli.rb +203 -0
- data/lib/tsikol/error_handler.rb +141 -0
- data/lib/tsikol/health.rb +198 -0
- data/lib/tsikol/http_transport.rb +72 -0
- data/lib/tsikol/lifecycle.rb +149 -0
- data/lib/tsikol/middleware.rb +168 -0
- data/lib/tsikol/prompt.rb +101 -0
- data/lib/tsikol/resource.rb +53 -0
- data/lib/tsikol/router.rb +190 -0
- data/lib/tsikol/server.rb +660 -0
- data/lib/tsikol/stdio_transport.rb +108 -0
- data/lib/tsikol/test_helpers.rb +261 -0
- data/lib/tsikol/tool.rb +111 -0
- data/lib/tsikol/version.rb +5 -0
- data/lib/tsikol.rb +72 -0
- metadata +219 -0
@@ -0,0 +1,1382 @@
|
|
1
|
+
# DevOps Tools Example
|
2
|
+
|
3
|
+
A comprehensive MCP server providing DevOps automation tools including Docker management, Kubernetes operations, CI/CD pipelines, and infrastructure monitoring.
|
4
|
+
|
5
|
+
## Overview
|
6
|
+
|
7
|
+
This example demonstrates:
|
8
|
+
- Docker container management
|
9
|
+
- Kubernetes deployment operations
|
10
|
+
- CI/CD pipeline execution
|
11
|
+
- Infrastructure monitoring
|
12
|
+
- Log aggregation and analysis
|
13
|
+
- Configuration management
|
14
|
+
- Deployment automation
|
15
|
+
|
16
|
+
## Implementation
|
17
|
+
|
18
|
+
### server.rb
|
19
|
+
|
20
|
+
```ruby
|
21
|
+
#!/usr/bin/env ruby
|
22
|
+
|
23
|
+
require 'tsikol'
|
24
|
+
require 'docker-api'
|
25
|
+
require 'kubeclient'
|
26
|
+
require 'net/ssh'
|
27
|
+
require 'yaml'
|
28
|
+
|
29
|
+
# Docker operations tool
|
30
|
+
class DockerTool < Tsikol::Tool
|
31
|
+
name "docker"
|
32
|
+
description "Manage Docker containers and images"
|
33
|
+
|
34
|
+
parameter :operation do
|
35
|
+
type :string
|
36
|
+
required
|
37
|
+
enum ["list", "run", "stop", "remove", "logs", "inspect", "build"]
|
38
|
+
description "Docker operation to perform"
|
39
|
+
end
|
40
|
+
|
41
|
+
parameter :options do
|
42
|
+
type :object
|
43
|
+
optional
|
44
|
+
default {}
|
45
|
+
description "Operation-specific options"
|
46
|
+
end
|
47
|
+
|
48
|
+
def execute(operation:, options: {})
|
49
|
+
case operation
|
50
|
+
when "list"
|
51
|
+
list_containers(options)
|
52
|
+
when "run"
|
53
|
+
run_container(options)
|
54
|
+
when "stop"
|
55
|
+
stop_container(options)
|
56
|
+
when "remove"
|
57
|
+
remove_container(options)
|
58
|
+
when "logs"
|
59
|
+
get_logs(options)
|
60
|
+
when "inspect"
|
61
|
+
inspect_container(options)
|
62
|
+
when "build"
|
63
|
+
build_image(options)
|
64
|
+
else
|
65
|
+
"Unknown operation: #{operation}"
|
66
|
+
end
|
67
|
+
rescue Docker::Error::NotFoundError => e
|
68
|
+
"Docker error: #{e.message}"
|
69
|
+
rescue Excon::Error::Socket => e
|
70
|
+
"Cannot connect to Docker daemon. Is it running?"
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
def list_containers(options)
|
76
|
+
all = options["all"] || false
|
77
|
+
containers = Docker::Container.all(all: all)
|
78
|
+
|
79
|
+
if containers.empty?
|
80
|
+
return "No containers found"
|
81
|
+
end
|
82
|
+
|
83
|
+
lines = ["CONTAINER ID IMAGE STATUS NAMES"]
|
84
|
+
lines << "-" * 70
|
85
|
+
|
86
|
+
containers.each do |container|
|
87
|
+
info = container.info
|
88
|
+
id = info["Id"][0..11]
|
89
|
+
image = info["Image"]
|
90
|
+
status = info["Status"]
|
91
|
+
names = info["Names"].join(", ").gsub(/^\//, "")
|
92
|
+
|
93
|
+
lines << "%-15s %-20s %-15s %s" % [id, image[0..19], status[0..14], names]
|
94
|
+
end
|
95
|
+
|
96
|
+
lines.join("\n")
|
97
|
+
end
|
98
|
+
|
99
|
+
def run_container(options)
|
100
|
+
image = options["image"] || raise("Image name required")
|
101
|
+
name = options["name"]
|
102
|
+
ports = options["ports"] || {}
|
103
|
+
env = options["env"] || []
|
104
|
+
volumes = options["volumes"] || {}
|
105
|
+
command = options["command"]
|
106
|
+
|
107
|
+
config = {
|
108
|
+
'Image' => image,
|
109
|
+
'Env' => env
|
110
|
+
}
|
111
|
+
|
112
|
+
config['Cmd'] = command.split if command
|
113
|
+
|
114
|
+
host_config = {
|
115
|
+
'PortBindings' => {},
|
116
|
+
'Binds' => []
|
117
|
+
}
|
118
|
+
|
119
|
+
# Configure port mappings
|
120
|
+
ports.each do |container_port, host_port|
|
121
|
+
host_config['PortBindings']["#{container_port}/tcp"] = [
|
122
|
+
{ 'HostPort' => host_port.to_s }
|
123
|
+
]
|
124
|
+
end
|
125
|
+
|
126
|
+
# Configure volume mounts
|
127
|
+
volumes.each do |host_path, container_path|
|
128
|
+
host_config['Binds'] << "#{host_path}:#{container_path}"
|
129
|
+
end
|
130
|
+
|
131
|
+
config['HostConfig'] = host_config
|
132
|
+
config['name'] = name if name
|
133
|
+
|
134
|
+
container = Docker::Container.create(config)
|
135
|
+
container.start
|
136
|
+
|
137
|
+
"Container started: #{container.id[0..11]} (#{image})"
|
138
|
+
end
|
139
|
+
|
140
|
+
def stop_container(options)
|
141
|
+
id = options["id"] || raise("Container ID required")
|
142
|
+
timeout = options["timeout"] || 10
|
143
|
+
|
144
|
+
container = Docker::Container.get(id)
|
145
|
+
container.stop(t: timeout)
|
146
|
+
|
147
|
+
"Container stopped: #{id}"
|
148
|
+
end
|
149
|
+
|
150
|
+
def get_logs(options)
|
151
|
+
id = options["id"] || raise("Container ID required")
|
152
|
+
lines = options["lines"] || 100
|
153
|
+
follow = options["follow"] || false
|
154
|
+
|
155
|
+
container = Docker::Container.get(id)
|
156
|
+
|
157
|
+
logs = container.logs(
|
158
|
+
stdout: true,
|
159
|
+
stderr: true,
|
160
|
+
tail: lines
|
161
|
+
)
|
162
|
+
|
163
|
+
logs
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
# Kubernetes operations tool
|
168
|
+
class KubernetesTool < Tsikol::Tool
|
169
|
+
name "kubernetes"
|
170
|
+
description "Manage Kubernetes resources"
|
171
|
+
|
172
|
+
parameter :operation do
|
173
|
+
type :string
|
174
|
+
required
|
175
|
+
enum ["get", "create", "apply", "delete", "scale", "rollout", "logs"]
|
176
|
+
description "Kubernetes operation"
|
177
|
+
end
|
178
|
+
|
179
|
+
parameter :resource_type do
|
180
|
+
type :string
|
181
|
+
required
|
182
|
+
enum ["pod", "deployment", "service", "configmap", "secret", "ingress"]
|
183
|
+
description "Type of Kubernetes resource"
|
184
|
+
end
|
185
|
+
|
186
|
+
parameter :options do
|
187
|
+
type :object
|
188
|
+
optional
|
189
|
+
default {}
|
190
|
+
description "Operation-specific options"
|
191
|
+
end
|
192
|
+
|
193
|
+
def execute(operation:, resource_type:, options: {})
|
194
|
+
client = create_k8s_client(resource_type)
|
195
|
+
|
196
|
+
case operation
|
197
|
+
when "get"
|
198
|
+
get_resources(client, resource_type, options)
|
199
|
+
when "create", "apply"
|
200
|
+
apply_resource(client, resource_type, options)
|
201
|
+
when "delete"
|
202
|
+
delete_resource(client, resource_type, options)
|
203
|
+
when "scale"
|
204
|
+
scale_resource(client, resource_type, options)
|
205
|
+
when "rollout"
|
206
|
+
manage_rollout(client, resource_type, options)
|
207
|
+
when "logs"
|
208
|
+
get_pod_logs(resource_type, options)
|
209
|
+
end
|
210
|
+
rescue Kubeclient::HttpError => e
|
211
|
+
"Kubernetes error: #{JSON.parse(e.response)['message']}"
|
212
|
+
rescue => e
|
213
|
+
"Error: #{e.message}"
|
214
|
+
end
|
215
|
+
|
216
|
+
private
|
217
|
+
|
218
|
+
def create_k8s_client(resource_type)
|
219
|
+
config = Kubeclient::Config.read(ENV['KUBECONFIG'] || "#{ENV['HOME']}/.kube/config")
|
220
|
+
context = config.context
|
221
|
+
|
222
|
+
endpoint = context.api_endpoint
|
223
|
+
|
224
|
+
case resource_type
|
225
|
+
when "deployment", "replicaset"
|
226
|
+
Kubeclient::Client.new(
|
227
|
+
endpoint + '/apis/apps',
|
228
|
+
'v1',
|
229
|
+
ssl_options: context.ssl_options,
|
230
|
+
auth_options: context.auth_options
|
231
|
+
)
|
232
|
+
else
|
233
|
+
Kubeclient::Client.new(
|
234
|
+
endpoint,
|
235
|
+
'v1',
|
236
|
+
ssl_options: context.ssl_options,
|
237
|
+
auth_options: context.auth_options
|
238
|
+
)
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
def get_resources(client, resource_type, options)
|
243
|
+
namespace = options["namespace"] || "default"
|
244
|
+
name = options["name"]
|
245
|
+
|
246
|
+
if name
|
247
|
+
resource = client.send("get_#{resource_type}", name, namespace)
|
248
|
+
format_resource(resource_type, [resource])
|
249
|
+
else
|
250
|
+
resources = client.send("get_#{resource_type}s", namespace: namespace)
|
251
|
+
format_resource(resource_type, resources)
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
def format_resource(type, resources)
|
256
|
+
if resources.empty?
|
257
|
+
return "No #{type}s found"
|
258
|
+
end
|
259
|
+
|
260
|
+
case type
|
261
|
+
when "pod"
|
262
|
+
format_pods(resources)
|
263
|
+
when "deployment"
|
264
|
+
format_deployments(resources)
|
265
|
+
when "service"
|
266
|
+
format_services(resources)
|
267
|
+
else
|
268
|
+
# Generic formatting
|
269
|
+
resources.map { |r| "#{r.metadata.name} (#{r.metadata.namespace})" }.join("\n")
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
273
|
+
def format_pods(pods)
|
274
|
+
lines = ["NAME READY STATUS RESTARTS AGE"]
|
275
|
+
lines << "-" * 70
|
276
|
+
|
277
|
+
pods.each do |pod|
|
278
|
+
name = pod.metadata.name
|
279
|
+
ready = "#{pod.status.containerStatuses&.count { |c| c.ready }}/#{pod.spec.containers.count}"
|
280
|
+
status = pod.status.phase
|
281
|
+
restarts = pod.status.containerStatuses&.sum { |c| c.restartCount } || 0
|
282
|
+
age = format_age(pod.metadata.creationTimestamp)
|
283
|
+
|
284
|
+
lines << "%-30s %-7s %-9s %-10s %s" % [name[0..29], ready, status, restarts, age]
|
285
|
+
end
|
286
|
+
|
287
|
+
lines.join("\n")
|
288
|
+
end
|
289
|
+
|
290
|
+
def format_age(timestamp)
|
291
|
+
seconds = Time.now - Time.parse(timestamp)
|
292
|
+
|
293
|
+
if seconds < 60
|
294
|
+
"#{seconds.to_i}s"
|
295
|
+
elsif seconds < 3600
|
296
|
+
"#{(seconds / 60).to_i}m"
|
297
|
+
elsif seconds < 86400
|
298
|
+
"#{(seconds / 3600).to_i}h"
|
299
|
+
else
|
300
|
+
"#{(seconds / 86400).to_i}d"
|
301
|
+
end
|
302
|
+
end
|
303
|
+
end
|
304
|
+
|
305
|
+
# CI/CD Pipeline tool
|
306
|
+
class PipelineTool < Tsikol::Tool
|
307
|
+
name "pipeline"
|
308
|
+
description "Execute CI/CD pipeline stages"
|
309
|
+
|
310
|
+
parameter :action do
|
311
|
+
type :string
|
312
|
+
required
|
313
|
+
enum ["run", "status", "logs", "retry", "cancel"]
|
314
|
+
description "Pipeline action"
|
315
|
+
end
|
316
|
+
|
317
|
+
parameter :pipeline_file do
|
318
|
+
type :string
|
319
|
+
optional
|
320
|
+
default ".pipeline.yml"
|
321
|
+
description "Path to pipeline configuration"
|
322
|
+
|
323
|
+
complete do |partial|
|
324
|
+
Dir.glob("#{partial}*.yml") + Dir.glob("#{partial}*.yaml")
|
325
|
+
end
|
326
|
+
end
|
327
|
+
|
328
|
+
parameter :stage do
|
329
|
+
type :string
|
330
|
+
optional
|
331
|
+
description "Specific stage to run"
|
332
|
+
end
|
333
|
+
|
334
|
+
parameter :env do
|
335
|
+
type :object
|
336
|
+
optional
|
337
|
+
default {}
|
338
|
+
description "Environment variables"
|
339
|
+
end
|
340
|
+
|
341
|
+
def execute(action:, pipeline_file: ".pipeline.yml", stage: nil, env: {})
|
342
|
+
unless File.exist?(pipeline_file)
|
343
|
+
return "Pipeline file not found: #{pipeline_file}"
|
344
|
+
end
|
345
|
+
|
346
|
+
pipeline = YAML.load_file(pipeline_file)
|
347
|
+
|
348
|
+
case action
|
349
|
+
when "run"
|
350
|
+
run_pipeline(pipeline, stage, env)
|
351
|
+
when "status"
|
352
|
+
pipeline_status(pipeline)
|
353
|
+
when "logs"
|
354
|
+
show_logs(pipeline, stage)
|
355
|
+
else
|
356
|
+
"Action not implemented: #{action}"
|
357
|
+
end
|
358
|
+
end
|
359
|
+
|
360
|
+
private
|
361
|
+
|
362
|
+
def run_pipeline(pipeline, specific_stage, env)
|
363
|
+
stages = specific_stage ? [pipeline["stages"].find { |s| s["name"] == specific_stage }] : pipeline["stages"]
|
364
|
+
|
365
|
+
results = []
|
366
|
+
|
367
|
+
stages.compact.each do |stage|
|
368
|
+
log :info, "Running stage", name: stage["name"]
|
369
|
+
|
370
|
+
result = run_stage(stage, env.merge(pipeline["env"] || {}))
|
371
|
+
results << "#{stage['name']}: #{result[:status]}"
|
372
|
+
|
373
|
+
if result[:status] == "failed" && !stage["allow_failure"]
|
374
|
+
results << "Pipeline failed at stage: #{stage['name']}"
|
375
|
+
break
|
376
|
+
end
|
377
|
+
end
|
378
|
+
|
379
|
+
results.join("\n")
|
380
|
+
end
|
381
|
+
|
382
|
+
def run_stage(stage, env)
|
383
|
+
start_time = Time.now
|
384
|
+
|
385
|
+
# Run pre-scripts
|
386
|
+
if stage["before_script"]
|
387
|
+
run_scripts(stage["before_script"], env)
|
388
|
+
end
|
389
|
+
|
390
|
+
# Run main scripts
|
391
|
+
success = run_scripts(stage["script"], env)
|
392
|
+
|
393
|
+
# Run post-scripts
|
394
|
+
if stage["after_script"]
|
395
|
+
run_scripts(stage["after_script"], env)
|
396
|
+
end
|
397
|
+
|
398
|
+
{
|
399
|
+
status: success ? "passed" : "failed",
|
400
|
+
duration: Time.now - start_time
|
401
|
+
}
|
402
|
+
end
|
403
|
+
|
404
|
+
def run_scripts(scripts, env)
|
405
|
+
scripts = [scripts] unless scripts.is_a?(Array)
|
406
|
+
|
407
|
+
scripts.all? do |script|
|
408
|
+
log :debug, "Executing", script: script
|
409
|
+
|
410
|
+
system(env, script)
|
411
|
+
end
|
412
|
+
end
|
413
|
+
end
|
414
|
+
|
415
|
+
# Server monitoring resource
|
416
|
+
class MonitoringResource < Tsikol::Resource
|
417
|
+
uri "monitoring/:target/:metric?"
|
418
|
+
description "System and application monitoring metrics"
|
419
|
+
|
420
|
+
def read
|
421
|
+
target = params[:target]
|
422
|
+
metric = params[:metric]
|
423
|
+
|
424
|
+
case target
|
425
|
+
when "system"
|
426
|
+
system_metrics(metric)
|
427
|
+
when "docker"
|
428
|
+
docker_metrics(metric)
|
429
|
+
when "kubernetes"
|
430
|
+
kubernetes_metrics(metric)
|
431
|
+
when "applications"
|
432
|
+
application_metrics(metric)
|
433
|
+
else
|
434
|
+
{ error: "Unknown monitoring target: #{target}" }.to_json
|
435
|
+
end
|
436
|
+
end
|
437
|
+
|
438
|
+
private
|
439
|
+
|
440
|
+
def system_metrics(metric)
|
441
|
+
metrics = {
|
442
|
+
timestamp: Time.now.iso8601,
|
443
|
+
hostname: `hostname`.strip
|
444
|
+
}
|
445
|
+
|
446
|
+
if metric.nil? || metric == "cpu"
|
447
|
+
metrics[:cpu] = {
|
448
|
+
load_average: `uptime`.match(/load average: (.+)$/)[1].split(", ").map(&:to_f),
|
449
|
+
usage_percent: get_cpu_usage
|
450
|
+
}
|
451
|
+
end
|
452
|
+
|
453
|
+
if metric.nil? || metric == "memory"
|
454
|
+
metrics[:memory] = get_memory_info
|
455
|
+
end
|
456
|
+
|
457
|
+
if metric.nil? || metric == "disk"
|
458
|
+
metrics[:disk] = get_disk_usage
|
459
|
+
end
|
460
|
+
|
461
|
+
if metric.nil? || metric == "network"
|
462
|
+
metrics[:network] = get_network_stats
|
463
|
+
end
|
464
|
+
|
465
|
+
metrics.to_json
|
466
|
+
end
|
467
|
+
|
468
|
+
def get_cpu_usage
|
469
|
+
# Read from /proc/stat on Linux
|
470
|
+
if File.exist?("/proc/stat")
|
471
|
+
stat1 = File.read("/proc/stat").lines.first.split
|
472
|
+
sleep(0.1)
|
473
|
+
stat2 = File.read("/proc/stat").lines.first.split
|
474
|
+
|
475
|
+
# Calculate CPU usage
|
476
|
+
prev_idle = stat1[4].to_i
|
477
|
+
prev_total = stat1[1..8].map(&:to_i).sum
|
478
|
+
|
479
|
+
idle = stat2[4].to_i
|
480
|
+
total = stat2[1..8].map(&:to_i).sum
|
481
|
+
|
482
|
+
diff_idle = idle - prev_idle
|
483
|
+
diff_total = total - prev_total
|
484
|
+
|
485
|
+
usage = 100.0 * (1.0 - diff_idle.to_f / diff_total)
|
486
|
+
usage.round(2)
|
487
|
+
else
|
488
|
+
# Fallback for macOS
|
489
|
+
`top -l 1 | grep "CPU usage" | awk '{print $3}'`.to_f
|
490
|
+
end
|
491
|
+
end
|
492
|
+
|
493
|
+
def get_memory_info
|
494
|
+
if File.exist?("/proc/meminfo")
|
495
|
+
# Linux
|
496
|
+
meminfo = File.read("/proc/meminfo")
|
497
|
+
total = meminfo.match(/MemTotal:\s+(\d+)/)[1].to_i
|
498
|
+
available = meminfo.match(/MemAvailable:\s+(\d+)/)[1].to_i
|
499
|
+
|
500
|
+
{
|
501
|
+
total_mb: total / 1024,
|
502
|
+
available_mb: available / 1024,
|
503
|
+
used_mb: (total - available) / 1024,
|
504
|
+
usage_percent: ((total - available).to_f / total * 100).round(2)
|
505
|
+
}
|
506
|
+
else
|
507
|
+
# macOS fallback
|
508
|
+
vm_stat = `vm_stat`.lines
|
509
|
+
page_size = vm_stat[0].match(/page size of (\d+) bytes/)[1].to_i
|
510
|
+
|
511
|
+
stats = {}
|
512
|
+
vm_stat[1..-1].each do |line|
|
513
|
+
if match = line.match(/(.+):\s+(\d+)/)
|
514
|
+
stats[match[1]] = match[2].to_i * page_size / 1024 / 1024
|
515
|
+
end
|
516
|
+
end
|
517
|
+
|
518
|
+
total = `sysctl -n hw.memsize`.to_i / 1024 / 1024
|
519
|
+
used = stats["Pages active"] + stats["Pages wired down"]
|
520
|
+
|
521
|
+
{
|
522
|
+
total_mb: total,
|
523
|
+
used_mb: used,
|
524
|
+
available_mb: total - used,
|
525
|
+
usage_percent: (used.to_f / total * 100).round(2)
|
526
|
+
}
|
527
|
+
end
|
528
|
+
end
|
529
|
+
end
|
530
|
+
|
531
|
+
# Deployment automation tool
|
532
|
+
class DeploymentTool < Tsikol::Tool
|
533
|
+
name "deploy"
|
534
|
+
description "Automated deployment operations"
|
535
|
+
|
536
|
+
parameter :environment do
|
537
|
+
type :string
|
538
|
+
required
|
539
|
+
enum ["development", "staging", "production"]
|
540
|
+
description "Target environment"
|
541
|
+
end
|
542
|
+
|
543
|
+
parameter :service do
|
544
|
+
type :string
|
545
|
+
required
|
546
|
+
description "Service to deploy"
|
547
|
+
end
|
548
|
+
|
549
|
+
parameter :version do
|
550
|
+
type :string
|
551
|
+
required
|
552
|
+
description "Version to deploy"
|
553
|
+
end
|
554
|
+
|
555
|
+
parameter :strategy do
|
556
|
+
type :string
|
557
|
+
optional
|
558
|
+
default "rolling"
|
559
|
+
enum ["rolling", "blue-green", "canary"]
|
560
|
+
description "Deployment strategy"
|
561
|
+
end
|
562
|
+
|
563
|
+
parameter :config do
|
564
|
+
type :object
|
565
|
+
optional
|
566
|
+
default {}
|
567
|
+
description "Deployment configuration"
|
568
|
+
end
|
569
|
+
|
570
|
+
def execute(environment:, service:, version:, strategy: "rolling", config: {})
|
571
|
+
log :info, "Starting deployment",
|
572
|
+
environment: environment,
|
573
|
+
service: service,
|
574
|
+
version: version,
|
575
|
+
strategy: strategy
|
576
|
+
|
577
|
+
# Load deployment configuration
|
578
|
+
deploy_config = load_deploy_config(environment, service)
|
579
|
+
deploy_config.merge!(config)
|
580
|
+
|
581
|
+
# Execute deployment strategy
|
582
|
+
case strategy
|
583
|
+
when "rolling"
|
584
|
+
rolling_deployment(deploy_config, version)
|
585
|
+
when "blue-green"
|
586
|
+
blue_green_deployment(deploy_config, version)
|
587
|
+
when "canary"
|
588
|
+
canary_deployment(deploy_config, version)
|
589
|
+
end
|
590
|
+
end
|
591
|
+
|
592
|
+
private
|
593
|
+
|
594
|
+
def load_deploy_config(environment, service)
|
595
|
+
config_file = "deploy/#{environment}/#{service}.yml"
|
596
|
+
|
597
|
+
if File.exist?(config_file)
|
598
|
+
YAML.load_file(config_file)
|
599
|
+
else
|
600
|
+
# Default configuration
|
601
|
+
{
|
602
|
+
"replicas" => 3,
|
603
|
+
"health_check" => "/health",
|
604
|
+
"timeout" => 300
|
605
|
+
}
|
606
|
+
end
|
607
|
+
end
|
608
|
+
|
609
|
+
def rolling_deployment(config, version)
|
610
|
+
steps = []
|
611
|
+
|
612
|
+
steps << "1. Building new version: #{version}"
|
613
|
+
# Simulate build
|
614
|
+
sleep(2)
|
615
|
+
|
616
|
+
steps << "2. Pushing to registry"
|
617
|
+
# Simulate push
|
618
|
+
sleep(1)
|
619
|
+
|
620
|
+
steps << "3. Updating Kubernetes deployment"
|
621
|
+
# Would actually update k8s deployment
|
622
|
+
|
623
|
+
steps << "4. Waiting for rollout to complete"
|
624
|
+
# Monitor rollout status
|
625
|
+
|
626
|
+
steps << "5. Running health checks"
|
627
|
+
# Verify deployment health
|
628
|
+
|
629
|
+
steps << "\nDeployment completed successfully!"
|
630
|
+
|
631
|
+
steps.join("\n")
|
632
|
+
end
|
633
|
+
end
|
634
|
+
|
635
|
+
# Log aggregation resource
|
636
|
+
class LogAggregatorResource < Tsikol::Resource
|
637
|
+
uri "logs/:source/:filter?"
|
638
|
+
description "Aggregate and search logs from multiple sources"
|
639
|
+
|
640
|
+
def read
|
641
|
+
source = params[:source]
|
642
|
+
filter = params[:filter]
|
643
|
+
options = parse_query_params
|
644
|
+
|
645
|
+
case source
|
646
|
+
when "application"
|
647
|
+
search_application_logs(filter, options)
|
648
|
+
when "system"
|
649
|
+
search_system_logs(filter, options)
|
650
|
+
when "container"
|
651
|
+
search_container_logs(filter, options)
|
652
|
+
else
|
653
|
+
{ error: "Unknown log source: #{source}" }.to_json
|
654
|
+
end
|
655
|
+
end
|
656
|
+
|
657
|
+
private
|
658
|
+
|
659
|
+
def search_application_logs(filter, options)
|
660
|
+
# In production, this would query a log aggregation service
|
661
|
+
logs = []
|
662
|
+
|
663
|
+
# Simulate log entries
|
664
|
+
10.times do |i|
|
665
|
+
logs << {
|
666
|
+
timestamp: (Time.now - i * 60).iso8601,
|
667
|
+
level: ["INFO", "WARN", "ERROR"].sample,
|
668
|
+
service: ["api", "web", "worker"].sample,
|
669
|
+
message: "Sample log message #{i}",
|
670
|
+
metadata: {
|
671
|
+
request_id: SecureRandom.uuid,
|
672
|
+
user_id: rand(1000)
|
673
|
+
}
|
674
|
+
}
|
675
|
+
end
|
676
|
+
|
677
|
+
# Apply filter if provided
|
678
|
+
if filter
|
679
|
+
logs = logs.select { |log| log[:message].include?(filter) || log[:level] == filter.upcase }
|
680
|
+
end
|
681
|
+
|
682
|
+
{
|
683
|
+
source: "application",
|
684
|
+
filter: filter,
|
685
|
+
count: logs.count,
|
686
|
+
logs: logs
|
687
|
+
}.to_json
|
688
|
+
end
|
689
|
+
end
|
690
|
+
|
691
|
+
# Configuration management tool
|
692
|
+
class ConfigTool < Tsikol::Tool
|
693
|
+
name "config"
|
694
|
+
description "Manage application configuration"
|
695
|
+
|
696
|
+
parameter :action do
|
697
|
+
type :string
|
698
|
+
required
|
699
|
+
enum ["get", "set", "list", "validate", "diff"]
|
700
|
+
description "Configuration action"
|
701
|
+
end
|
702
|
+
|
703
|
+
parameter :app do
|
704
|
+
type :string
|
705
|
+
required
|
706
|
+
description "Application name"
|
707
|
+
end
|
708
|
+
|
709
|
+
parameter :env do
|
710
|
+
type :string
|
711
|
+
optional
|
712
|
+
default "production"
|
713
|
+
description "Environment"
|
714
|
+
end
|
715
|
+
|
716
|
+
parameter :key do
|
717
|
+
type :string
|
718
|
+
optional
|
719
|
+
description "Configuration key"
|
720
|
+
end
|
721
|
+
|
722
|
+
parameter :value do
|
723
|
+
type :string
|
724
|
+
optional
|
725
|
+
description "Configuration value"
|
726
|
+
end
|
727
|
+
|
728
|
+
def execute(action:, app:, env: "production", key: nil, value: nil)
|
729
|
+
config_store = ConfigStore.new(app, env)
|
730
|
+
|
731
|
+
case action
|
732
|
+
when "get"
|
733
|
+
config_store.get(key)
|
734
|
+
when "set"
|
735
|
+
raise "Key and value required for set action" unless key && value
|
736
|
+
config_store.set(key, value)
|
737
|
+
when "list"
|
738
|
+
config_store.list
|
739
|
+
when "validate"
|
740
|
+
config_store.validate
|
741
|
+
when "diff"
|
742
|
+
config_store.diff(params[:compare_env] || "staging")
|
743
|
+
end
|
744
|
+
end
|
745
|
+
end
|
746
|
+
|
747
|
+
# Configuration store
|
748
|
+
class ConfigStore
|
749
|
+
def initialize(app, env)
|
750
|
+
@app = app
|
751
|
+
@env = env
|
752
|
+
@config_dir = "config/apps/#{app}"
|
753
|
+
@config_file = "#{@config_dir}/#{env}.yml"
|
754
|
+
end
|
755
|
+
|
756
|
+
def get(key)
|
757
|
+
config = load_config
|
758
|
+
|
759
|
+
if key
|
760
|
+
value = key.split('.').reduce(config) { |hash, k| hash[k] if hash }
|
761
|
+
value ? "#{key}: #{value}" : "Key not found: #{key}"
|
762
|
+
else
|
763
|
+
format_config(config)
|
764
|
+
end
|
765
|
+
end
|
766
|
+
|
767
|
+
def set(key, value)
|
768
|
+
config = load_config
|
769
|
+
|
770
|
+
# Navigate to the key and set value
|
771
|
+
keys = key.split('.')
|
772
|
+
last_key = keys.pop
|
773
|
+
|
774
|
+
hash = keys.reduce(config) { |h, k| h[k] ||= {} }
|
775
|
+
hash[last_key] = value
|
776
|
+
|
777
|
+
save_config(config)
|
778
|
+
|
779
|
+
"Configuration updated: #{key} = #{value}"
|
780
|
+
end
|
781
|
+
|
782
|
+
def list
|
783
|
+
config = load_config
|
784
|
+
format_config(config)
|
785
|
+
end
|
786
|
+
|
787
|
+
def validate
|
788
|
+
config = load_config
|
789
|
+
schema_file = "#{@config_dir}/schema.yml"
|
790
|
+
|
791
|
+
if File.exist?(schema_file)
|
792
|
+
schema = YAML.load_file(schema_file)
|
793
|
+
errors = validate_against_schema(config, schema)
|
794
|
+
|
795
|
+
if errors.empty?
|
796
|
+
"Configuration is valid"
|
797
|
+
else
|
798
|
+
"Validation errors:\n" + errors.join("\n")
|
799
|
+
end
|
800
|
+
else
|
801
|
+
"No schema found for validation"
|
802
|
+
end
|
803
|
+
end
|
804
|
+
|
805
|
+
private
|
806
|
+
|
807
|
+
def load_config
|
808
|
+
FileUtils.mkdir_p(@config_dir)
|
809
|
+
|
810
|
+
if File.exist?(@config_file)
|
811
|
+
YAML.load_file(@config_file) || {}
|
812
|
+
else
|
813
|
+
{}
|
814
|
+
end
|
815
|
+
end
|
816
|
+
|
817
|
+
def save_config(config)
|
818
|
+
File.write(@config_file, config.to_yaml)
|
819
|
+
end
|
820
|
+
|
821
|
+
def format_config(config, prefix = "")
|
822
|
+
lines = []
|
823
|
+
|
824
|
+
config.each do |key, value|
|
825
|
+
full_key = prefix.empty? ? key : "#{prefix}.#{key}"
|
826
|
+
|
827
|
+
if value.is_a?(Hash)
|
828
|
+
lines.concat(format_config(value, full_key))
|
829
|
+
else
|
830
|
+
lines << "#{full_key}: #{value}"
|
831
|
+
end
|
832
|
+
end
|
833
|
+
|
834
|
+
lines.join("\n")
|
835
|
+
end
|
836
|
+
end
|
837
|
+
|
838
|
+
# DevOps assistant prompt
|
839
|
+
class DevOpsAssistantPrompt < Tsikol::Prompt
|
840
|
+
name "devops_assistant"
|
841
|
+
description "Expert DevOps automation assistant"
|
842
|
+
|
843
|
+
argument :specialization do
|
844
|
+
type :string
|
845
|
+
required
|
846
|
+
enum ["containers", "kubernetes", "ci_cd", "monitoring", "security"]
|
847
|
+
description "Area of specialization"
|
848
|
+
end
|
849
|
+
|
850
|
+
argument :experience_level do
|
851
|
+
type :string
|
852
|
+
optional
|
853
|
+
default "intermediate"
|
854
|
+
enum ["beginner", "intermediate", "advanced"]
|
855
|
+
description "User's DevOps experience level"
|
856
|
+
end
|
857
|
+
|
858
|
+
def get_messages(specialization:, experience_level: "intermediate")
|
859
|
+
[
|
860
|
+
{
|
861
|
+
role: "system",
|
862
|
+
content: {
|
863
|
+
type: "text",
|
864
|
+
text: build_system_prompt(specialization, experience_level)
|
865
|
+
}
|
866
|
+
},
|
867
|
+
{
|
868
|
+
role: "user",
|
869
|
+
content: {
|
870
|
+
type: "text",
|
871
|
+
text: "Initialize DevOps assistant"
|
872
|
+
}
|
873
|
+
},
|
874
|
+
{
|
875
|
+
role: "assistant",
|
876
|
+
content: {
|
877
|
+
type: "text",
|
878
|
+
text: "I'm ready to help with your #{specialization.gsub('_', '/')} needs. I can assist with automation, troubleshooting, best practices, and optimization. What DevOps challenge are you working on?"
|
879
|
+
}
|
880
|
+
}
|
881
|
+
]
|
882
|
+
end
|
883
|
+
|
884
|
+
private
|
885
|
+
|
886
|
+
def build_system_prompt(specialization, level)
|
887
|
+
base = <<~PROMPT
|
888
|
+
You are an expert DevOps engineer specializing in #{specialization.gsub('_', ' ')}.
|
889
|
+
|
890
|
+
Available tools:
|
891
|
+
- docker: Manage containers and images
|
892
|
+
- kubernetes: Deploy and manage Kubernetes resources
|
893
|
+
- pipeline: Execute CI/CD pipelines
|
894
|
+
- deploy: Automated deployment operations
|
895
|
+
- config: Configuration management
|
896
|
+
|
897
|
+
User level: #{level}
|
898
|
+
|
899
|
+
Key responsibilities:
|
900
|
+
1. Automate infrastructure and deployment processes
|
901
|
+
2. Implement best practices for reliability and security
|
902
|
+
3. Optimize performance and resource usage
|
903
|
+
4. Troubleshoot issues systematically
|
904
|
+
5. Guide on tool selection and architecture
|
905
|
+
PROMPT
|
906
|
+
|
907
|
+
base + specialization_details(specialization)
|
908
|
+
end
|
909
|
+
|
910
|
+
def specialization_details(spec)
|
911
|
+
case spec
|
912
|
+
when "containers"
|
913
|
+
"\n\nContainer expertise:\n- Docker best practices\n- Image optimization\n- Security scanning\n- Multi-stage builds\n- Registry management"
|
914
|
+
when "kubernetes"
|
915
|
+
"\n\nKubernetes expertise:\n- Resource management\n- Scaling strategies\n- Service mesh\n- Observability\n- Security policies"
|
916
|
+
when "ci_cd"
|
917
|
+
"\n\nCI/CD expertise:\n- Pipeline optimization\n- Testing strategies\n- Artifact management\n- Deployment automation\n- GitOps practices"
|
918
|
+
when "monitoring"
|
919
|
+
"\n\nMonitoring expertise:\n- Metrics collection\n- Log aggregation\n- Alerting strategies\n- Performance analysis\n- Incident response"
|
920
|
+
else
|
921
|
+
""
|
922
|
+
end
|
923
|
+
end
|
924
|
+
end
|
925
|
+
|
926
|
+
# Start the server
|
927
|
+
Tsikol.start(
|
928
|
+
name: "devops-tools",
|
929
|
+
version: "1.0.0",
|
930
|
+
description: "Comprehensive DevOps automation MCP server"
|
931
|
+
) do
|
932
|
+
# Enable capabilities
|
933
|
+
logging true
|
934
|
+
prompts true
|
935
|
+
|
936
|
+
# Middleware
|
937
|
+
use Tsikol::LoggingMiddleware, level: :info
|
938
|
+
use Tsikol::AuthenticationMiddleware,
|
939
|
+
type: :api_key,
|
940
|
+
validator: ->(key) { validate_api_key(key) }
|
941
|
+
use Tsikol::RateLimitMiddleware,
|
942
|
+
max_requests: 100,
|
943
|
+
window_seconds: 60
|
944
|
+
|
945
|
+
# Tools
|
946
|
+
tool DockerTool
|
947
|
+
tool KubernetesTool
|
948
|
+
tool PipelineTool
|
949
|
+
tool DeploymentTool
|
950
|
+
tool ConfigTool
|
951
|
+
|
952
|
+
# Resources
|
953
|
+
resource MonitoringResource
|
954
|
+
resource LogAggregatorResource
|
955
|
+
|
956
|
+
# Prompts
|
957
|
+
prompt DevOpsAssistantPrompt
|
958
|
+
|
959
|
+
# Health check endpoint
|
960
|
+
resource "health" do
|
961
|
+
description "Service health status"
|
962
|
+
|
963
|
+
def read
|
964
|
+
{
|
965
|
+
status: "healthy",
|
966
|
+
timestamp: Time.now.iso8601,
|
967
|
+
version: "1.0.0",
|
968
|
+
checks: {
|
969
|
+
docker: docker_healthy?,
|
970
|
+
kubernetes: kubernetes_healthy?
|
971
|
+
}
|
972
|
+
}.to_json
|
973
|
+
end
|
974
|
+
|
975
|
+
def docker_healthy?
|
976
|
+
Docker.version
|
977
|
+
true
|
978
|
+
rescue
|
979
|
+
false
|
980
|
+
end
|
981
|
+
|
982
|
+
def kubernetes_healthy?
|
983
|
+
# Check if kubectl is available
|
984
|
+
system("kubectl version --client > /dev/null 2>&1")
|
985
|
+
end
|
986
|
+
end
|
987
|
+
|
988
|
+
def validate_api_key(key)
|
989
|
+
# In production, validate against secure key store
|
990
|
+
valid_keys = {
|
991
|
+
"admin-key" => { role: "admin", rate_limit: 1000 },
|
992
|
+
"user-key" => { role: "user", rate_limit: 100 }
|
993
|
+
}
|
994
|
+
|
995
|
+
valid_keys[key]
|
996
|
+
end
|
997
|
+
end
|
998
|
+
```
|
999
|
+
|
1000
|
+
### Pipeline Configuration Example
|
1001
|
+
|
1002
|
+
```yaml
|
1003
|
+
# .pipeline.yml
|
1004
|
+
name: microservice-pipeline
|
1005
|
+
description: CI/CD pipeline for microservice
|
1006
|
+
|
1007
|
+
env:
|
1008
|
+
DOCKER_REGISTRY: registry.example.com
|
1009
|
+
APP_NAME: my-service
|
1010
|
+
|
1011
|
+
stages:
|
1012
|
+
- name: test
|
1013
|
+
script:
|
1014
|
+
- echo "Running tests..."
|
1015
|
+
- bundle install
|
1016
|
+
- bundle exec rspec
|
1017
|
+
- bundle exec rubocop
|
1018
|
+
|
1019
|
+
- name: build
|
1020
|
+
script:
|
1021
|
+
- echo "Building Docker image..."
|
1022
|
+
- docker build -t $DOCKER_REGISTRY/$APP_NAME:$CI_COMMIT_SHA .
|
1023
|
+
- docker tag $DOCKER_REGISTRY/$APP_NAME:$CI_COMMIT_SHA $DOCKER_REGISTRY/$APP_NAME:latest
|
1024
|
+
only:
|
1025
|
+
- main
|
1026
|
+
- develop
|
1027
|
+
|
1028
|
+
- name: push
|
1029
|
+
script:
|
1030
|
+
- echo "Pushing to registry..."
|
1031
|
+
- docker push $DOCKER_REGISTRY/$APP_NAME:$CI_COMMIT_SHA
|
1032
|
+
- docker push $DOCKER_REGISTRY/$APP_NAME:latest
|
1033
|
+
only:
|
1034
|
+
- main
|
1035
|
+
|
1036
|
+
- name: deploy-staging
|
1037
|
+
script:
|
1038
|
+
- echo "Deploying to staging..."
|
1039
|
+
- kubectl set image deployment/$APP_NAME $APP_NAME=$DOCKER_REGISTRY/$APP_NAME:$CI_COMMIT_SHA -n staging
|
1040
|
+
- kubectl rollout status deployment/$APP_NAME -n staging
|
1041
|
+
only:
|
1042
|
+
- develop
|
1043
|
+
environment: staging
|
1044
|
+
|
1045
|
+
- name: deploy-production
|
1046
|
+
script:
|
1047
|
+
- echo "Deploying to production..."
|
1048
|
+
- kubectl set image deployment/$APP_NAME $APP_NAME=$DOCKER_REGISTRY/$APP_NAME:$CI_COMMIT_SHA -n production
|
1049
|
+
- kubectl rollout status deployment/$APP_NAME -n production
|
1050
|
+
only:
|
1051
|
+
- main
|
1052
|
+
environment: production
|
1053
|
+
when: manual
|
1054
|
+
```
|
1055
|
+
|
1056
|
+
### Kubernetes Deployment Example
|
1057
|
+
|
1058
|
+
```yaml
|
1059
|
+
# deploy/production/my-service.yml
|
1060
|
+
apiVersion: apps/v1
|
1061
|
+
kind: Deployment
|
1062
|
+
metadata:
|
1063
|
+
name: my-service
|
1064
|
+
namespace: production
|
1065
|
+
spec:
|
1066
|
+
replicas: 3
|
1067
|
+
selector:
|
1068
|
+
matchLabels:
|
1069
|
+
app: my-service
|
1070
|
+
template:
|
1071
|
+
metadata:
|
1072
|
+
labels:
|
1073
|
+
app: my-service
|
1074
|
+
spec:
|
1075
|
+
containers:
|
1076
|
+
- name: my-service
|
1077
|
+
image: registry.example.com/my-service:latest
|
1078
|
+
ports:
|
1079
|
+
- containerPort: 3000
|
1080
|
+
env:
|
1081
|
+
- name: RAILS_ENV
|
1082
|
+
value: production
|
1083
|
+
- name: DATABASE_URL
|
1084
|
+
valueFrom:
|
1085
|
+
secretKeyRef:
|
1086
|
+
name: my-service-secrets
|
1087
|
+
key: database-url
|
1088
|
+
resources:
|
1089
|
+
requests:
|
1090
|
+
memory: "256Mi"
|
1091
|
+
cpu: "100m"
|
1092
|
+
limits:
|
1093
|
+
memory: "512Mi"
|
1094
|
+
cpu: "500m"
|
1095
|
+
livenessProbe:
|
1096
|
+
httpGet:
|
1097
|
+
path: /health
|
1098
|
+
port: 3000
|
1099
|
+
initialDelaySeconds: 30
|
1100
|
+
periodSeconds: 10
|
1101
|
+
readinessProbe:
|
1102
|
+
httpGet:
|
1103
|
+
path: /ready
|
1104
|
+
port: 3000
|
1105
|
+
initialDelaySeconds: 5
|
1106
|
+
periodSeconds: 5
|
1107
|
+
---
|
1108
|
+
apiVersion: v1
|
1109
|
+
kind: Service
|
1110
|
+
metadata:
|
1111
|
+
name: my-service
|
1112
|
+
namespace: production
|
1113
|
+
spec:
|
1114
|
+
selector:
|
1115
|
+
app: my-service
|
1116
|
+
ports:
|
1117
|
+
- protocol: TCP
|
1118
|
+
port: 80
|
1119
|
+
targetPort: 3000
|
1120
|
+
---
|
1121
|
+
apiVersion: networking.k8s.io/v1
|
1122
|
+
kind: Ingress
|
1123
|
+
metadata:
|
1124
|
+
name: my-service
|
1125
|
+
namespace: production
|
1126
|
+
annotations:
|
1127
|
+
kubernetes.io/ingress.class: nginx
|
1128
|
+
cert-manager.io/cluster-issuer: letsencrypt-prod
|
1129
|
+
spec:
|
1130
|
+
tls:
|
1131
|
+
- hosts:
|
1132
|
+
- api.example.com
|
1133
|
+
secretName: my-service-tls
|
1134
|
+
rules:
|
1135
|
+
- host: api.example.com
|
1136
|
+
http:
|
1137
|
+
paths:
|
1138
|
+
- path: /
|
1139
|
+
pathType: Prefix
|
1140
|
+
backend:
|
1141
|
+
service:
|
1142
|
+
name: my-service
|
1143
|
+
port:
|
1144
|
+
number: 80
|
1145
|
+
```
|
1146
|
+
|
1147
|
+
### Testing
|
1148
|
+
|
1149
|
+
```ruby
|
1150
|
+
require 'minitest/autorun'
|
1151
|
+
require 'tsikol/test_helpers'
|
1152
|
+
|
1153
|
+
class DevOpsToolsTest < Minitest::Test
|
1154
|
+
include Tsikol::TestHelpers
|
1155
|
+
|
1156
|
+
def setup
|
1157
|
+
@server = create_test_server
|
1158
|
+
@client = TestClient.new(@server)
|
1159
|
+
|
1160
|
+
# Mock Docker and Kubernetes
|
1161
|
+
setup_mocks
|
1162
|
+
end
|
1163
|
+
|
1164
|
+
def test_docker_list_containers
|
1165
|
+
response = @client.call_tool("docker", {
|
1166
|
+
"operation" => "list",
|
1167
|
+
"options" => { "all" => true }
|
1168
|
+
})
|
1169
|
+
|
1170
|
+
assert_successful_response(response)
|
1171
|
+
result = response.dig(:result, :content, 0, :text)
|
1172
|
+
assert_match /CONTAINER ID/, result
|
1173
|
+
end
|
1174
|
+
|
1175
|
+
def test_kubernetes_get_pods
|
1176
|
+
response = @client.call_tool("kubernetes", {
|
1177
|
+
"operation" => "get",
|
1178
|
+
"resource_type" => "pod",
|
1179
|
+
"options" => { "namespace" => "default" }
|
1180
|
+
})
|
1181
|
+
|
1182
|
+
assert_successful_response(response)
|
1183
|
+
result = response.dig(:result, :content, 0, :text)
|
1184
|
+
assert_match /NAME.*READY.*STATUS/, result
|
1185
|
+
end
|
1186
|
+
|
1187
|
+
def test_pipeline_execution
|
1188
|
+
# Create test pipeline file
|
1189
|
+
File.write("test.pipeline.yml", {
|
1190
|
+
"name" => "test-pipeline",
|
1191
|
+
"stages" => [
|
1192
|
+
{
|
1193
|
+
"name" => "test",
|
1194
|
+
"script" => ["echo 'Running tests'", "exit 0"]
|
1195
|
+
}
|
1196
|
+
]
|
1197
|
+
}.to_yaml)
|
1198
|
+
|
1199
|
+
response = @client.call_tool("pipeline", {
|
1200
|
+
"action" => "run",
|
1201
|
+
"pipeline_file" => "test.pipeline.yml"
|
1202
|
+
})
|
1203
|
+
|
1204
|
+
assert_successful_response(response)
|
1205
|
+
result = response.dig(:result, :content, 0, :text)
|
1206
|
+
assert_match /test: passed/, result
|
1207
|
+
ensure
|
1208
|
+
File.delete("test.pipeline.yml") if File.exist?("test.pipeline.yml")
|
1209
|
+
end
|
1210
|
+
|
1211
|
+
def test_deployment_simulation
|
1212
|
+
response = @client.call_tool("deploy", {
|
1213
|
+
"environment" => "staging",
|
1214
|
+
"service" => "test-service",
|
1215
|
+
"version" => "v1.2.3",
|
1216
|
+
"strategy" => "rolling"
|
1217
|
+
})
|
1218
|
+
|
1219
|
+
assert_successful_response(response)
|
1220
|
+
result = response.dig(:result, :content, 0, :text)
|
1221
|
+
assert_match /Deployment completed successfully/, result
|
1222
|
+
end
|
1223
|
+
|
1224
|
+
def test_monitoring_metrics
|
1225
|
+
response = @client.read_resource("monitoring/system/cpu")
|
1226
|
+
|
1227
|
+
assert_successful_response(response)
|
1228
|
+
metrics = JSON.parse(response.dig(:result, :contents, 0, :text))
|
1229
|
+
|
1230
|
+
assert metrics["cpu"]
|
1231
|
+
assert metrics["cpu"]["usage_percent"]
|
1232
|
+
end
|
1233
|
+
|
1234
|
+
private
|
1235
|
+
|
1236
|
+
def create_test_server
|
1237
|
+
Tsikol::Server.new(name: "test-devops") do
|
1238
|
+
tool DockerTool
|
1239
|
+
tool KubernetesTool
|
1240
|
+
tool PipelineTool
|
1241
|
+
tool DeploymentTool
|
1242
|
+
resource MonitoringResource
|
1243
|
+
end
|
1244
|
+
end
|
1245
|
+
|
1246
|
+
def setup_mocks
|
1247
|
+
# Mock Docker responses
|
1248
|
+
Docker::Container.define_singleton_method(:all) do |options|
|
1249
|
+
[
|
1250
|
+
MockContainer.new("abc123", "nginx:latest", "Up 2 hours", ["web"])
|
1251
|
+
]
|
1252
|
+
end
|
1253
|
+
|
1254
|
+
# Mock Kubernetes client
|
1255
|
+
# Would need actual mock implementation
|
1256
|
+
end
|
1257
|
+
end
|
1258
|
+
|
1259
|
+
class MockContainer
|
1260
|
+
attr_reader :info
|
1261
|
+
|
1262
|
+
def initialize(id, image, status, names)
|
1263
|
+
@info = {
|
1264
|
+
"Id" => id,
|
1265
|
+
"Image" => image,
|
1266
|
+
"Status" => status,
|
1267
|
+
"Names" => names.map { |n| "/#{n}" }
|
1268
|
+
}
|
1269
|
+
end
|
1270
|
+
end
|
1271
|
+
```
|
1272
|
+
|
1273
|
+
## Best Practices Demonstrated
|
1274
|
+
|
1275
|
+
1. **Infrastructure as Code** - Configuration-driven deployments
|
1276
|
+
2. **Container Management** - Docker best practices
|
1277
|
+
3. **Kubernetes Operations** - Production-ready deployments
|
1278
|
+
4. **CI/CD Automation** - Pipeline-driven development
|
1279
|
+
5. **Monitoring Integration** - Comprehensive metrics
|
1280
|
+
6. **Security First** - API key validation, secure deployments
|
1281
|
+
7. **Error Handling** - Graceful degradation
|
1282
|
+
8. **Logging** - Structured logging for debugging
|
1283
|
+
9. **Testing** - Automated testing of DevOps operations
|
1284
|
+
10. **Documentation** - Self-documenting configurations
|
1285
|
+
|
1286
|
+
## Advanced Features
|
1287
|
+
|
1288
|
+
### GitOps Integration
|
1289
|
+
|
1290
|
+
```ruby
|
1291
|
+
class GitOpsTool < Tsikol::Tool
|
1292
|
+
name "gitops"
|
1293
|
+
description "GitOps workflow management"
|
1294
|
+
|
1295
|
+
parameter :action do
|
1296
|
+
type :string
|
1297
|
+
required
|
1298
|
+
enum ["sync", "diff", "rollback", "promote"]
|
1299
|
+
end
|
1300
|
+
|
1301
|
+
def execute(action:, **options)
|
1302
|
+
case action
|
1303
|
+
when "sync"
|
1304
|
+
sync_with_git(options)
|
1305
|
+
when "diff"
|
1306
|
+
show_git_diff(options)
|
1307
|
+
when "rollback"
|
1308
|
+
rollback_deployment(options)
|
1309
|
+
when "promote"
|
1310
|
+
promote_environment(options)
|
1311
|
+
end
|
1312
|
+
end
|
1313
|
+
|
1314
|
+
private
|
1315
|
+
|
1316
|
+
def sync_with_git(options)
|
1317
|
+
# Pull latest from git
|
1318
|
+
# Apply to cluster
|
1319
|
+
# Update status
|
1320
|
+
end
|
1321
|
+
end
|
1322
|
+
```
|
1323
|
+
|
1324
|
+
### Secret Management
|
1325
|
+
|
1326
|
+
```ruby
|
1327
|
+
class SecretsTool < Tsikol::Tool
|
1328
|
+
name "secrets"
|
1329
|
+
description "Secure secret management"
|
1330
|
+
|
1331
|
+
parameter :action do
|
1332
|
+
type :string
|
1333
|
+
required
|
1334
|
+
enum ["create", "read", "update", "rotate", "list"]
|
1335
|
+
end
|
1336
|
+
|
1337
|
+
parameter :backend do
|
1338
|
+
type :string
|
1339
|
+
optional
|
1340
|
+
default "vault"
|
1341
|
+
enum ["vault", "kubernetes", "aws-secrets-manager"]
|
1342
|
+
end
|
1343
|
+
|
1344
|
+
def execute(action:, backend: "vault", **options)
|
1345
|
+
secret_manager = create_backend(backend)
|
1346
|
+
|
1347
|
+
case action
|
1348
|
+
when "create"
|
1349
|
+
secret_manager.create(options)
|
1350
|
+
when "read"
|
1351
|
+
secret_manager.read(options)
|
1352
|
+
when "rotate"
|
1353
|
+
secret_manager.rotate(options)
|
1354
|
+
end
|
1355
|
+
end
|
1356
|
+
end
|
1357
|
+
```
|
1358
|
+
|
1359
|
+
## Security Considerations
|
1360
|
+
|
1361
|
+
1. **RBAC** - Role-based access control for all operations
|
1362
|
+
2. **Audit Logging** - Track all DevOps operations
|
1363
|
+
3. **Secret Management** - Never expose secrets in logs
|
1364
|
+
4. **Network Policies** - Restrict cluster communications
|
1365
|
+
5. **Image Scanning** - Scan containers for vulnerabilities
|
1366
|
+
|
1367
|
+
## Performance Optimization
|
1368
|
+
|
1369
|
+
1. **Parallel Execution** - Run independent tasks concurrently
|
1370
|
+
2. **Caching** - Cache build artifacts and test results
|
1371
|
+
3. **Resource Limits** - Set appropriate limits for all containers
|
1372
|
+
4. **Auto-scaling** - Scale based on metrics
|
1373
|
+
5. **Optimized Images** - Multi-stage builds, minimal base images
|
1374
|
+
|
1375
|
+
## Next Steps
|
1376
|
+
|
1377
|
+
- Add Terraform integration for infrastructure
|
1378
|
+
- Implement chaos engineering tools
|
1379
|
+
- Add cost optimization features
|
1380
|
+
- Create compliance checking tools
|
1381
|
+
- Implement disaster recovery automation
|
1382
|
+
- Add multi-cloud support
|