Skip to content

Commit bbce986

Browse files
authored
Merge pull request #12 from godon-dev/credentials_commands
feat(cli): add credential management commands
2 parents 506d6e4 + 2c2c4c3 commit bbce986

5 files changed

Lines changed: 435 additions & 25 deletions

File tree

.github/workflows/ci.yml

Lines changed: 102 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,14 @@ jobs:
1111
steps:
1212
- name: Checkout code
1313
uses: actions/checkout@v4
14+
with:
15+
fetch-depth: 0
16+
clean: true
17+
18+
- name: Clean working directory
19+
run: |
20+
git clean -ffdx
21+
git status
1422
1523
- name: Install Nix
1624
uses: cachix/install-nix-action@v25
@@ -69,6 +77,13 @@ jobs:
6977
7078
- name: Start Prism mock container
7179
run: |
80+
# Docker debug info
81+
echo "=== Docker Debug Info ==="
82+
docker --version
83+
docker info
84+
echo "=== Checking Docker daemon ==="
85+
docker ps
86+
7287
# Download the OpenAPI spec with retry
7388
echo "Downloading OpenAPI spec..."
7489
curl -L --retry 3 --retry-delay 5 -o openapi.yml \
@@ -90,45 +105,69 @@ jobs:
90105
grep -E "^\s*/" openapi.yml || echo "No path definitions found"
91106
92107
# Start Prism container in background
108+
echo "=== Starting Prism container ==="
93109
docker run -d --name prism -p 4010:4010 \
110+
--log-driver json-file \
111+
--log-opt max-size=10m \
94112
-v $(pwd)/openapi.yml:/tmp/openapi.yml \
95113
stoplight/prism:4 \
96114
mock --host 0.0.0.0 /tmp/openapi.yml
97115
98116
# Check if container started
99-
echo "Checking container status..."
100-
docker ps | grep prism || echo "Container not found in docker ps"
101-
echo "=== Container logs ==="
117+
echo "=== Checking container status ==="
118+
sleep 5
119+
docker ps -a | grep prism || echo "Container not found in docker ps"
120+
121+
echo "=== Container detailed info ==="
122+
docker ps -a --filter name=prism
102123
docker logs prism || echo "No logs available"
124+
103125
echo "=== Container inspection ==="
104-
docker inspect prism | grep -A 10 -B 10 "State" || echo "Could not inspect container"
126+
docker inspect prism || echo "Could not inspect container"
105127
echo "=== Network ports ==="
106128
docker port prism || echo "Could not get port mappings"
107129
108-
# Wait for Prism to start
109-
echo "Waiting for Prism to start..."
110-
sleep 15
111-
112-
# Test basic connectivity
113-
echo "Testing basic connectivity to localhost:4010..."
114-
curl -v http://localhost:4010 || echo "Basic connection failed"
115-
116-
# Verify Prism is running with retry
117-
for i in {1..5}; do
118-
echo "=== Attempt $i: Testing Prism health ==="
119-
if curl -v http://localhost:4010/health 2>&1 | head -10; then
120-
echo "✅ Prism container started successfully on port 4010"
130+
# Wait for Prism to start with better error handling
131+
echo "=== Waiting for Prism to start ==="
132+
for i in {1..10}; do
133+
echo "Attempt $i: Checking if Prism is ready..."
134+
135+
# Check if container is still running
136+
if ! docker ps | grep -q prism; then
137+
echo "❌ Container stopped unexpectedly"
138+
echo "=== Container exit logs ==="
139+
docker logs prism
140+
docker ps -a --filter name=prism
141+
exit 1
142+
fi
143+
144+
# Try to connect to an actual endpoint from our spec
145+
if curl -sf http://localhost:4010/breeders > /dev/null 2>&1; then
146+
echo "✅ Prism is ready!"
121147
break
122148
else
123-
echo "❌ Attempt $i: Prism not ready yet, waiting..."
124-
echo "Container still running?"
125-
docker ps | grep prism || echo "Container stopped"
126-
echo "Recent logs:"
127-
docker logs --tail 5 prism
128-
sleep 5
149+
echo "Prism not ready yet, waiting..."
150+
sleep 3
151+
fi
152+
153+
if [ $i -eq 10 ]; then
154+
echo "❌ Prism failed to start after 10 attempts"
155+
echo "=== Final container state ==="
156+
docker ps -a --filter name=prism
157+
echo "=== Final container logs ==="
158+
docker logs prism
159+
exit 1
129160
fi
130161
done
131162
163+
# Test basic connectivity
164+
echo "=== Testing basic connectivity ==="
165+
curl -v http://localhost:4010 || echo "Basic connection failed"
166+
167+
# Verify Prism is responding
168+
echo "=== Testing Prism health ==="
169+
curl -v http://localhost:4010/health || echo "Health check failed"
170+
132171
# Test available endpoints
133172
echo "=== Testing available Prism endpoints ==="
134173
echo "Testing root path:"
@@ -151,6 +190,16 @@ jobs:
151190
curl -X POST -H "Content-Type: application/json" -d '{"name":"test"}' \
152191
http://localhost:4010/breeders 2>&1 | head -10 || echo "POST /breeders failed"
153192
193+
echo "Testing /credentials (new endpoints):"
194+
curl -v http://localhost:4010/credentials 2>&1 | head -10 || echo "/credentials failed"
195+
196+
echo "Testing POST /credentials:"
197+
curl -X POST -H "Content-Type: application/json" \
198+
-d '{"name":"test","credential_type":"ssh_private_key","content":"test_key_content"}' \
199+
http://localhost:4010/credentials 2>&1 | head -10 || echo "POST /credentials failed"
200+
201+
echo "=== Prism setup complete ==="
202+
154203
- name: Integration tests against Prism mock
155204
run: |
156205
# Get the binary path
@@ -209,6 +258,36 @@ jobs:
209258
$BINARY_PATH --help
210259
$BINARY_PATH breeder --help || true # May fail but tests argument parsing
211260
261+
# === Credential Command Tests ===
262+
echo ""
263+
echo "=== Testing Credential Commands ==="
264+
265+
# Test credential list
266+
echo "Testing: credential list"
267+
$BINARY_PATH --hostname=localhost --port=4010 credential list
268+
269+
# Create test credential YAML file
270+
echo 'name: "test_ssh_key"' > test_credential.yml
271+
echo 'credential_type: "ssh_private_key"' >> test_credential.yml
272+
echo 'description: "Test SSH key for CI"' >> test_credential.yml
273+
echo 'content: "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA2Z2H7V..."' >> test_credential.yml
274+
275+
# Test credential create
276+
echo "Testing: credential create"
277+
$BINARY_PATH --hostname=localhost --port=4010 credential create --file=test_credential.yml
278+
279+
# Test credential show with mock UUID
280+
echo "Testing: credential show (including content)"
281+
$BINARY_PATH --hostname=localhost --port=4010 credential show --id=550e8400-e29b-41d4-a716-446655440001
282+
283+
# Test credential delete
284+
echo "Testing: credential delete"
285+
$BINARY_PATH --hostname=localhost --port=4010 credential delete --id=550e8400-e29b-41d4-a716-446655440001
286+
287+
# Test help shows credential commands
288+
echo "Testing: help shows credential commands"
289+
$BINARY_PATH --help | grep -q "credential" && echo "✅ Credential commands documented in help" || echo "❌ Credential commands missing from help"
290+
212291
- name: Cleanup Prism container
213292
if: always()
214293
run: |

src/godon/client.nim

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,23 @@ proc handleResponse*[T](client: GodonClient; response: Response): ApiResponse[T]
6363
try:
6464
echo "Raw response body: ", response.body
6565
let jsonData = parseJson(response.body)
66+
67+
# Handle nested response structures like {"breeders": [...]} or {"credentials": [...]}
68+
if jsonData.kind == JObject:
69+
# Check for wrapped list responses
70+
when T is seq:
71+
for key, value in jsonData.pairs:
72+
if value.kind == JArray:
73+
result = ApiResponse[T](success: true, data: value.to(T), error: "")
74+
return
75+
# Check for wrapped single object responses
76+
else:
77+
for key, value in jsonData.pairs:
78+
if value.kind == JObject:
79+
result = ApiResponse[T](success: true, data: value.to(T), error: "")
80+
return
81+
82+
# Fallback to direct conversion
6683
result = ApiResponse[T](success: true, data: jsonData.to(T), error: "")
6784
except CatchableError as e:
6885
result = ApiResponse[T](success: false, data: default(T), error: "JSON parse error: " & e.msg)

src/godon/credential.nim

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
import std/[httpclient, json, strutils, uri, tables]
2+
import yaml
3+
import client, types
4+
5+
# Credential API client methods
6+
7+
proc listCredentials*(client: GodonClient): ApiResponseList[Credential] =
8+
## List all credentials
9+
let url = client.baseUrl() & "/credentials"
10+
11+
try:
12+
let response = client.httpClient.get(url)
13+
if response.code == Http200:
14+
let body = parseJson(response.body)
15+
let credentials = body["credentials"]
16+
17+
var result: seq[Credential] = @[]
18+
for cred in credentials.items:
19+
result.add(parseCredentialFromJson(cred))
20+
21+
return ApiResponseList[Credential](
22+
success: true,
23+
data: result,
24+
error: ""
25+
)
26+
else:
27+
return ApiResponseList[Credential](
28+
success: false,
29+
data: @[],
30+
error: "Failed to list credentials: " & response.status
31+
)
32+
except Exception as e:
33+
return ApiResponseList[Credential](
34+
success: false,
35+
data: @[],
36+
error: "Exception: " & e.msg
37+
)
38+
39+
proc createCredential*(client: GodonClient, credentialData: JsonNode): ApiResponseSingle[Credential] =
40+
## Create a new credential
41+
let url = client.baseUrl() & "/credentials"
42+
43+
try:
44+
client.httpClient.headers = newHttpHeaders({"Content-Type": "application/json"})
45+
let response = client.httpClient.post(url, $credentialData)
46+
if response.code == Http201:
47+
let body = parseJson(response.body)
48+
let credential = parseCredentialFromJson(body["credential"])
49+
50+
return ApiResponseSingle[Credential](
51+
success: true,
52+
data: credential,
53+
error: ""
54+
)
55+
else:
56+
return ApiResponseSingle[Credential](
57+
success: false,
58+
data: Credential(),
59+
error: "Failed to create credential: " & response.status
60+
)
61+
except Exception as e:
62+
return ApiResponseSingle[Credential](
63+
success: false,
64+
data: Credential(),
65+
error: "Exception: " & e.msg
66+
)
67+
68+
proc getCredential*(client: GodonClient, credentialId: string): ApiResponseSingle[Credential] =
69+
## Get a specific credential by ID (including content)
70+
let url = client.baseUrl() & "/credentials/" & encodeUrl(credentialId)
71+
72+
try:
73+
let response = client.httpClient.get(url)
74+
if response.code == Http200:
75+
let body = parseJson(response.body)
76+
let credential = parseCredentialFromJson(body["credential"])
77+
78+
return ApiResponseSingle[Credential](
79+
success: true,
80+
data: credential,
81+
error: ""
82+
)
83+
else:
84+
return ApiResponseSingle[Credential](
85+
success: false,
86+
data: Credential(),
87+
error: "Failed to get credential: " & response.status
88+
)
89+
except Exception as e:
90+
return ApiResponseSingle[Credential](
91+
success: false,
92+
data: Credential(),
93+
error: "Exception: " & e.msg
94+
)
95+
96+
proc deleteCredential*(client: GodonClient, credentialId: string): ApiResponse[JsonNode] =
97+
## Delete a credential by ID
98+
let url = client.baseUrl() & "/credentials/" & encodeUrl(credentialId)
99+
100+
try:
101+
let response = client.httpClient.delete(url)
102+
if response.code == Http200:
103+
let body = parseJson(response.body)
104+
105+
return ApiResponse[JsonNode](
106+
success: true,
107+
data: body,
108+
error: ""
109+
)
110+
else:
111+
return ApiResponse[JsonNode](
112+
success: false,
113+
data: nil,
114+
error: "Failed to delete credential: " & response.status
115+
)
116+
except Exception as e:
117+
return ApiResponse[JsonNode](
118+
success: false,
119+
data: nil,
120+
error: "Exception: " & e.msg
121+
)
122+
123+
proc createCredentialFromYaml*(client: GodonClient, yamlContent: string): ApiResponseSingle[Credential] =
124+
## Create credential from YAML content
125+
## This is a convenience method that parses YAML and converts to JSON
126+
127+
try:
128+
# Parse YAML to a generic table, then construct JsonNode manually
129+
let yamlData = yaml.loadAs[Table[string, string]](yamlContent)
130+
131+
# Build JsonNode from the parsed YAML
132+
var credentialData = newJObject()
133+
134+
# Add required fields
135+
credentialData.add("name", newJString(yamlData["name"]))
136+
credentialData.add("credential_type", newJString(yamlData["credential_type"]))
137+
138+
# Add optional fields
139+
if "description" in yamlData:
140+
credentialData.add("description", newJString(yamlData["description"]))
141+
else:
142+
credentialData.add("description", newJString(""))
143+
144+
# Ensure content field exists for credentials
145+
if "content" in yamlData:
146+
credentialData.add("content", newJString(yamlData["content"]))
147+
else:
148+
return ApiResponseSingle[Credential](
149+
success: false,
150+
data: Credential(),
151+
error: "Missing required field: content"
152+
)
153+
154+
return client.createCredential(credentialData)
155+
except Exception as e:
156+
return ApiResponseSingle[Credential](
157+
success: false,
158+
data: Credential(),
159+
error: "Failed to parse YAML: " & e.msg
160+
)

0 commit comments

Comments
 (0)