Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
134 changes: 133 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ jobs:

# Set environment variables for this session
export SSL_CERT_FILE="/etc/ssl/certs/ca-bundle.crt"
export NIX_SSL_CERT_FILE="/etc/ssl/certs/ca-bundle.crt"
export NIX_SSL_CERT_FILE="/etc/ssl/certs/ca-bundle.crt"
export CURL_CA_BUNDLE="/etc/ssl/certs/ca-bundle.crt"

# Add to nix.conf for daemon
Expand All @@ -58,3 +58,135 @@ jobs:
ls -la $(nix path-info .#default)/bin/ || echo "$out/bin not found"
echo "Testing compiled binary with direct path..."
$(nix path-info .#default)/bin/godon_cli --help

- name: Start Prism mock container
run: |
# Download the OpenAPI spec with retry
echo "Downloading OpenAPI spec..."
curl -L --retry 3 --retry-delay 5 -o openapi.yml \
https://raw.githubusercontent.com/godon-dev/godon-images/main/images/godon-api/openapi.yml

# Verify the spec was downloaded
if [ ! -f openapi.yml ]; then
echo "Failed to download OpenAPI spec"
exit 1
fi
echo "OpenAPI spec downloaded successfully"
echo "=== Spec file size ==="
wc -l openapi.yml
echo "=== First 20 lines of spec ==="
head -20 openapi.yml
echo "=== Paths section ==="
grep -A 20 "^paths:" openapi.yml || echo "No paths section found"
echo "=== All path definitions ==="
grep -E "^\s*/" openapi.yml || echo "No path definitions found"

# Start Prism container in background
docker run -d --name prism -p 4010:4010 \
-v $(pwd)/openapi.yml:/tmp/openapi.yml \
stoplight/prism:4 \
mock --host 0.0.0.0 /tmp/openapi.yml

# Check if container started
echo "Checking container status..."
docker ps | grep prism || echo "Container not found in docker ps"
echo "=== Container logs ==="
docker logs prism || echo "No logs available"
echo "=== Container inspection ==="
docker inspect prism | grep -A 10 -B 10 "State" || echo "Could not inspect container"
echo "=== Network ports ==="
docker port prism || echo "Could not get port mappings"

# Wait for Prism to start
echo "Waiting for Prism to start..."
sleep 15

# Test basic connectivity
echo "Testing basic connectivity to localhost:4010..."
curl -v http://localhost:4010 || echo "Basic connection failed"

# Verify Prism is running with retry
for i in {1..5}; do
echo "=== Attempt $i: Testing Prism health ==="
if curl -v http://localhost:4010/health 2>&1 | head -10; then
echo "✅ Prism container started successfully on port 4010"
break
else
echo "❌ Attempt $i: Prism not ready yet, waiting..."
echo "Container still running?"
docker ps | grep prism || echo "Container stopped"
echo "Recent logs:"
docker logs --tail 5 prism
sleep 5
fi
done

# Test available endpoints
echo "=== Testing available Prism endpoints ==="
echo "Testing root path:"
curl -v http://localhost:4010/ 2>&1 | head -10 || echo "Root path failed"

echo "Testing /v0 path:"
curl -v http://localhost:4010/v0 2>&1 | head -10 || echo "/v0 path failed"

echo "Testing /v0/breeders:"
curl -v http://localhost:4010/v0/breeders 2>&1 | head -10 || echo "/v0/breeders failed"

echo "Testing OpenAPI spec:"
curl -v http://localhost:4010/openapi.json 2>&1 | head -5 || echo "OpenAPI spec failed"

echo "=== Testing exact URLs that CLI will use ==="
echo "Testing /breeders (what CLI should call):"
curl -v http://localhost:4010/breeders 2>&1 | head -10 || echo "/breeders failed"

echo "Testing POST /breeders:"
curl -X POST -H "Content-Type: application/json" -d '{"name":"test"}' \
http://localhost:4010/breeders 2>&1 | head -10 || echo "POST /breeders failed"

- name: Integration tests against Prism mock
run: |
# Get the binary path
BINARY_PATH=$(nix path-info .#default)/bin/godon_cli

echo "Running integration tests against Prism mock..."

# Test breeder list
echo "Testing: breeder list"
$BINARY_PATH --hostname=localhost --port=4010 breeder list

# Create test YAML files using echo
echo 'name: "Test Breeder"' > test_breeder.yml
echo 'config: >' >> test_breeder.yml
echo ' {"setting1": "value1", "setting2": 42}' >> test_breeder.yml

echo "Testing: breeder create"
$BINARY_PATH --hostname=localhost --port=4010 breeder create --file=test_breeder.yml

# Test breeder show with a mock UUID
echo "Testing: breeder show"
$BINARY_PATH --hostname=localhost --port=4010 breeder show --id=550e8400-e29b-41d4-a716-446655440000

# Create update test file
echo 'uuid: "550e8400-e29b-41d4-a716-446655440000"' > test_breeder_update.yml
echo 'name: "Updated Test Breeder"' >> test_breeder_update.yml
echo 'description: "Updated integration test breeder"' >> test_breeder_update.yml
echo 'config: >' >> test_breeder_update.yml
echo ' {"setting1": "updated_value1", "setting2": 100, "new_setting": "new_value"}' >> test_breeder_update.yml

echo "Testing: breeder update"
$BINARY_PATH --hostname=localhost --port=4010 breeder update --file=test_breeder_update.yml

# Test breeder purge
echo "Testing: breeder purge"
$BINARY_PATH --hostname=localhost --port=4010 breeder purge --id=550e8400-e29b-41d4-a716-446655440000

# Test help commands
echo "Testing: help commands"
$BINARY_PATH --help
$BINARY_PATH breeder --help || true # May fail but tests argument parsing

- name: Cleanup Prism container
if: always()
run: |
docker stop prism || true
docker rm prism || true
6 changes: 4 additions & 2 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,10 @@
buildPhase = ''
echo "Building godon-cli version: ${version}"

# Refresh package list and build
nimble refresh
# Refresh package list and install dependencies only
nimble refresh --verbose
# Install yaml dependency without building our package
nimble install -y --depsOnly --verbose

# Build the CLI
mkdir -p bin
Expand Down
2 changes: 1 addition & 1 deletion godon_cli.nimble
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ license = "AGPL-3.0"

# Dependencies

requires "nim >= 2.0.0"
requires "nim >= 2.0.0", "yaml"

# Task definitions

Expand Down
54 changes: 31 additions & 23 deletions src/godon/breeder.nim
Original file line number Diff line number Diff line change
Expand Up @@ -2,50 +2,53 @@
## Implementation of breeder-related API endpoints

import std/[httpclient, json, strutils, uri]
import yaml
import client, types

proc listBreeders*(client: GodonClient): ApiResponse[seq[Breeder]] =
proc listBreeders*(client: GodonClient): ApiResponse[seq[BreederSummary]] =
## List all configured breeders
try:
let url = client.baseUrl() & "/breeders"
let response = client.httpClient.get(url)
result = handleResponse[seq[Breeder]](client, response)
result = handleResponse[seq[BreederSummary]](client, response)
except CatchableError as e:
result = ApiResponse[seq[Breeder]](success: false, data: @[], error: e.msg)
result = ApiResponse[seq[BreederSummary]](success: false, data: @[], error: e.msg)

proc createBreeder*(client: GodonClient, request: BreederCreateRequest): ApiResponse[Breeder] =
proc createBreeder*(client: GodonClient, request: BreederCreateRequest): ApiResponse[BreederSummary] =
## Create a new breeder
try:
let url = client.baseUrl() & "/breeders"
let jsonData = %*request
# Convert config string to JsonNode
var jsonData = %*{
"name": request.name,
"config": parseJson(request.config)
}
echo "Sending JSON: ", $jsonData
client.httpClient.headers = newHttpHeaders({"Content-Type": "application/json"})
let response = client.httpClient.post(url, $jsonData)
result = handleResponse[Breeder](client, response)
result = handleResponse[BreederSummary](client, response)
except CatchableError as e:
result = ApiResponse[Breeder](success: false, data: default(Breeder), error: e.msg)
result = ApiResponse[BreederSummary](success: false, data: default(BreederSummary), error: e.msg)

proc parseBreederFromYaml*(yamlContent: string): BreederCreateRequest =
## Parse breeder configuration from YAML content
## Note: This would require a YAML library like yaml.nim
## For now, this is a placeholder for the YAML parsing logic
## Users can pass JSON directly or we can add proper YAML parsing later
## Parse breeder configuration from YAML content using yaml library
try:
let jsonNode = parseJson(yamlContent)
result = jsonNode.to(BreederCreateRequest)
result = yaml.loadAs[BreederCreateRequest](yamlContent)
except CatchableError as e:
raise newException(ValueError, "Failed to parse YAML/JSON: " & e.msg)
raise newException(ValueError, "Failed to parse YAML: " & e.msg)

proc createBreederFromYaml*(client: GodonClient, yamlContent: string): ApiResponse[Breeder] =
proc createBreederFromYaml*(client: GodonClient, yamlContent: string): ApiResponse[BreederSummary] =
## Create a breeder from YAML content
try:
let request = parseBreederFromYaml(yamlContent)
result = client.createBreeder(request)
except CatchableError as e:
result = ApiResponse[Breeder](success: false, data: default(Breeder), error: e.msg)
result = ApiResponse[BreederSummary](success: false, data: default(BreederSummary), error: e.msg)

proc getBreeder*(client: GodonClient, uuid: string): ApiResponse[Breeder] =
## Get breeder details by UUID
try:
let url = client.baseUrl() & "/breeder?uuid=" & encodeUrl(uuid)
let url = client.baseUrl() & "/breeders/" & encodeUrl(uuid)
let response = client.httpClient.get(url)
result = handleResponse[Breeder](client, response)
except CatchableError as e:
Expand All @@ -54,8 +57,14 @@ proc getBreeder*(client: GodonClient, uuid: string): ApiResponse[Breeder] =
proc updateBreeder*(client: GodonClient, request: BreederUpdateRequest): ApiResponse[Breeder] =
## Update an existing breeder
try:
let url = client.baseUrl() & "/breeders"
let jsonData = %*request
let url = client.baseUrl() & "/breeders/" & encodeUrl(request.uuid)
# Convert config string to JsonNode
var jsonData = %*{
"name": request.name,
"description": request.description,
"config": parseJson(request.config)
}
client.httpClient.headers = newHttpHeaders({"Content-Type": "application/json"})
let response = client.httpClient.put(url, $jsonData)
result = handleResponse[Breeder](client, response)
except CatchableError as e:
Expand All @@ -64,10 +73,9 @@ proc updateBreeder*(client: GodonClient, request: BreederUpdateRequest): ApiResp
proc parseBreederUpdateFromYaml*(yamlContent: string): BreederUpdateRequest =
## Parse breeder update configuration from YAML content
try:
let jsonNode = parseJson(yamlContent)
result = jsonNode.to(BreederUpdateRequest)
result = yaml.loadAs[BreederUpdateRequest](yamlContent)
except CatchableError as e:
raise newException(ValueError, "Failed to parse YAML/JSON: " & e.msg)
raise newException(ValueError, "Failed to parse YAML: " & e.msg)

proc updateBreederFromYaml*(client: GodonClient, yamlContent: string): ApiResponse[Breeder] =
## Update a breeder from YAML content
Expand All @@ -80,7 +88,7 @@ proc updateBreederFromYaml*(client: GodonClient, yamlContent: string): ApiRespon
proc deleteBreeder*(client: GodonClient, uuid: string): ApiResponse[JsonNode] =
## Delete/purge a breeder by UUID
try:
let url = client.baseUrl() & "/breeder?uuid=" & encodeUrl(uuid)
let url = client.baseUrl() & "/breeders/" & encodeUrl(uuid)
let response = client.httpClient.delete(url)
result = handleResponse[JsonNode](client, response)
except CatchableError as e:
Expand Down
12 changes: 7 additions & 5 deletions src/godon/client.nim
Original file line number Diff line number Diff line change
Expand Up @@ -29,32 +29,34 @@ proc newGodonClient*(hostname: string = DefaultHostname,

proc baseUrl*(client: GodonClient): string =
## Get the base URL for API requests
result = "http://" & client.config.hostname & ":" & $client.config.port & "/" & client.config.apiVersion
result = "http://" & client.config.hostname & ":" & $client.config.port

proc handleResponse*[T](client: GodonClient; response: Response): ApiResponse[T] =
## Handle HTTP response and convert to ApiResponse
let statusCode = parseInt(response.status)
let statusCode = parseInt(split(response.status, " ")[0])
if statusCode >= 200 and statusCode < 300:
try:
echo "Raw response body: ", response.body
let jsonData = parseJson(response.body)
result = ApiResponse[T](success: true, data: jsonData.to(T), error: "")
except CatchableError as e:
result = ApiResponse[T](success: false, data: default(T), error: "JSON parse error: " & e.msg)
else:
try:
echo "HTTP Error Response Body: ", response.body
let errorJson = parseJson(response.body)
let errorMsg = errorJson.getOrDefault("message").getStr("HTTP Error: " & $statusCode)
let errorMsg = errorJson{"message"}.getStr("HTTP Error: " & $statusCode)
result = ApiResponse[T](success: false, data: default(T), error: errorMsg)
except CatchableError:
result = ApiResponse[T](success: false, data: default(T), error: "HTTP Error: " & $statusCode)

proc handleError*(client: GodonClient, response: Response): ref CatchableError =
## Convert HTTP error response to exception
let statusCode = parseInt(response.status)
let statusCode = parseInt(split(response.status, " ")[0])
var errorMsg = "HTTP Error: " & $statusCode
try:
let errorJson = parseJson(response.body)
errorMsg = errorJson.getOrDefault("message").getStr(errorMsg)
errorMsg = errorJson{"message"}.getStr(errorMsg)
except CatchableError:
discard

Expand Down
16 changes: 10 additions & 6 deletions src/godon/types.nim
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,28 @@
import std/json

type
BreederSummary* = object
id*: string
name*: string
status*: string
createdAt*: string

Breeder* = object
uuid*: string
id*: string
name*: string
description*: string
status*: string
config*: JsonNode
createdAt*: string
updatedAt*: string

BreederCreateRequest* = object
name*: string
description*: string
config*: JsonNode
config*: string

BreederUpdateRequest* = object
uuid*: string
name*: string
description*: string
config*: JsonNode
config*: string

ApiConfig* = object
hostname*: string
Expand Down
Loading