Skip to content

Commit 29ecfd4

Browse files
committed
Verify service discovery in integration test, add tool README
1 parent 30ea69e commit 29ecfd4

File tree

3 files changed

+153
-13
lines changed

3 files changed

+153
-13
lines changed

.github/workflows/CI.yml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -154,9 +154,13 @@ jobs:
154154
sleep 1
155155
done
156156
157-
# Login and create an OTT identity
157+
# Login, create an OTT identity, and preseed a service + dial policy so the
158+
# test verifies service discovery after auth (not just auth).
158159
ziti edge login localhost:1280 -u admin -p admin -y
159-
ziti edge create identity ztr-integ -o /tmp/ztr.jwt
160+
ziti edge create identity ztr-integ -a ztr-integ -o /tmp/ztr.jwt
161+
ziti edge create service ztr-svc -a ztr-svc
162+
ziti edge create service-policy ztr-svc-dial Dial \
163+
--identity-roles '#ztr-integ' --service-roles '#ztr-svc'
160164
161165
TOOL=./DerivedData/CZiti/Build/Products/Debug/ziti-test-runner
162166

ziti-test-runner/README.md

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
# ziti-test-runner
2+
3+
End-to-end integration test tool for the CZiti Swift SDK. Drives real enrollment
4+
and context bring-up against a live Ziti controller, returning a pass/fail exit
5+
code suitable for CI.
6+
7+
Used by the CI workflow (`.github/workflows/CI.yml`) against a
8+
`ziti edge quickstart` controller.
9+
10+
## What it does
11+
12+
**Default mode** - enroll, save, load, run, verify:
13+
14+
1. Enrolls a Ziti identity from a one-time JWT file (OTT by default)
15+
2. Saves the resulting identity to a `.zid` file
16+
3. Loads the identity back from the `.zid` file via `Ziti(fromFile:)`
17+
4. Runs the Ziti context
18+
5. Waits for a `ContextEvent` with status OK (auth success)
19+
6. Waits for a `ServiceEvent` with at least one service (service channel works)
20+
7. Exits 0
21+
22+
**`--only-run` mode** - load an existing `.zid` file and verify auth+services,
23+
no enrollment. Useful for verifying persistence across process boundaries.
24+
25+
## Usage
26+
27+
```
28+
ziti-test-runner [options] <jwt-file> # enroll, save, load, run, verify
29+
ziti-test-runner --only-run [options] <zid-file> # load an existing zid and run
30+
```
31+
32+
Options:
33+
- `--mode <ott|cert-jwt|token-jwt>` - enrollment mode (default: `ott`)
34+
- `--timeout <seconds>` - total test timeout (default: 60)
35+
- `--keep-zid <path>` - keep the enrolled `.zid` at this path
36+
- `--log-level <level>` - `WTF|ERROR|WARN|INFO|DEBUG|VERBOSE|TRACE` (default: `INFO`)
37+
- `--only-run` - input is a `.zid` file; skip enrollment
38+
39+
Exit codes:
40+
- `0` - success
41+
- `1` - enrollment failed
42+
- `2` - identity load / run failed
43+
- `3` - context status != OK, service timeout, or overall timeout
44+
- `64` - usage error
45+
46+
## Building with the insecure-keys test flag
47+
48+
macOS enrollment in this SDK uses the data protection keychain
49+
(`kSecUseDataProtectionKeychain = true` in `ZitiKeychain.createPrivateKey()`),
50+
which requires a provisioning-profile-backed `application-identifier`
51+
entitlement. Ad-hoc signed CLI tools don't have that, so enrollment fails with
52+
`errSecMissingEntitlement` (-34018) - both in CI and on dev machines using
53+
"Sign to Run Locally".
54+
55+
To work around this **in test builds only**, the SDK supports the compile-time
56+
condition `CZITI_TEST_INSECURE_KEYS`. When set:
57+
58+
- `ZitiKeychain.createPrivateKey()` generates an ephemeral RSA key with no
59+
keychain interaction.
60+
- `Ziti.enroll()` skips `storeCertificate()` in the keychain and writes the
61+
ephemeral private key PEM into `ZitiIdentity.key` instead.
62+
- `Ziti.run()` prefers `id.key` if present, over calling
63+
`ZitiKeychain.getPrivateKey()`.
64+
- A one-shot `⚠️ CZITI_TEST_INSECURE_KEYS build` warning prints to stderr the
65+
first time a key is minted in the process.
66+
67+
**This flag must never be used in a release build.** Keys end up in plaintext
68+
in the `.zid` file on disk, with none of the OS-level isolation the keychain
69+
provides.
70+
71+
### Command-line usage
72+
73+
```bash
74+
xcodebuild build -scheme ziti-test-runner \
75+
SWIFT_ACTIVE_COMPILATION_CONDITIONS='$(inherited) CZITI_TEST_INSECURE_KEYS'
76+
```
77+
78+
The flag propagates to the `CZiti-macOS` dependency automatically (xcodebuild
79+
applies command-line build settings across the whole build graph).
80+
81+
### Running locally
82+
83+
```bash
84+
# 1. Start a quickstart
85+
ziti edge quickstart --home /tmp/qs &
86+
87+
# 2. Log in and create an identity + service + dial policy
88+
ziti edge login localhost:1280 -u admin -p admin -y
89+
ziti edge create identity ztr -a ztr -o /tmp/ztr.jwt
90+
ziti edge create service ztr-svc -a ztr-svc
91+
ziti edge create service-policy ztr-dial Dial \
92+
--identity-roles '#ztr' --service-roles '#ztr-svc'
93+
94+
# 3. Run the tool (built with the flag)
95+
./DerivedData/CZiti/Build/Products/Debug/ziti-test-runner /tmp/ztr.jwt
96+
```
97+
98+
## Scope
99+
100+
Only OTT enrollment is exercised end-to-end. The `cert-jwt` and `token-jwt`
101+
modes compile under the flag but aren't wired through CI because:
102+
103+
- `Ziti.enrollToCert(jwtFile:)` / `enrollToToken(jwtFile:)` use the OIDC flow,
104+
which can't be driven non-interactively in CI without a mock JWT signer.
105+
- The `runEnrollTo(mode:)` path in `lib/Ziti.swift` has a keychain retag step
106+
(`retagPrivateKey(to:)`) that isn't bracketed by `CZITI_TEST_INSECURE_KEYS`
107+
and would likely fail under the flag.
108+
109+
Fixing either is a legitimate follow-up.

ziti-test-runner/main.swift

Lines changed: 38 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -158,25 +158,48 @@ func finish(_ code: Int32, _ msg: String) {
158158
exit(code)
159159
}
160160

161-
/// Load a saved identity, run Ziti, and succeed on the first OK context event.
161+
/// Load a saved identity, run Ziti, and succeed once we've seen:
162+
/// 1. A ContextEvent with status OK (authenticated with the controller)
163+
/// 2. A ServiceEvent with at least one service in `added`
164+
/// The second check verifies the service channel works end-to-end, not just auth.
165+
/// CI must configure at least one service + service-policy for the test identity,
166+
/// otherwise this will time out.
162167
func runFromZidFile(_ zidPath: String) {
163168
guard let ziti = Ziti(fromFile: zidPath) else {
164169
finish(2, "failed to load Ziti identity from \(zidPath)")
165170
return
166171
}
167172

173+
var contextOK = false
174+
var servicesSeen = false
175+
168176
ziti.registerEventCallback({ event in
169-
guard !done else { return }
170-
guard let event = event, event.type == .Context, let ctx = event.contextEvent else { return }
171-
if ctx.status == 0 {
172-
finish(0, "context authenticated (id=\(ziti.id.id) ztAPI=\(ziti.id.ztAPI))")
173-
ziti.shutdown()
174-
} else {
175-
let msg = ctx.err ?? "context error status=\(ctx.status)"
176-
finish(3, msg)
177+
guard !done, let event = event else { return }
178+
switch event.type {
179+
case .Context:
180+
guard let ctx = event.contextEvent else { return }
181+
if ctx.status == 0 {
182+
print("context authenticated")
183+
contextOK = true
184+
} else {
185+
let msg = ctx.err ?? "context error status=\(ctx.status)"
186+
finish(3, msg)
187+
ziti.shutdown()
188+
return
189+
}
190+
case .Service:
191+
guard let svc = event.serviceEvent, !svc.added.isEmpty else { return }
192+
let names = svc.added.compactMap { $0.name }.joined(separator: ", ")
193+
print("services received: [\(names)]")
194+
servicesSeen = true
195+
default:
196+
return
197+
}
198+
if contextOK && servicesSeen {
199+
finish(0, "context+service OK (id=\(ziti.id.id) ztAPI=\(ziti.id.ztAPI))")
177200
ziti.shutdown()
178201
}
179-
}, ZitiEvent.EventType.Context.rawValue)
202+
}, ZitiEvent.EventType.Context.rawValue | ZitiEvent.EventType.Service.rawValue)
180203

181204
ziti.run { zErr in
182205
if let zErr = zErr {
@@ -186,7 +209,11 @@ func runFromZidFile(_ zidPath: String) {
186209
ziti.startTimer(UInt64(timeoutSeconds) * 1000, 0) { timer in
187210
ziti.endTimer(timer)
188211
if !done {
189-
finish(3, "timeout after \(timeoutSeconds)s waiting for context event")
212+
let missing = [
213+
contextOK ? nil : "context",
214+
servicesSeen ? nil : "service"
215+
].compactMap { $0 }.joined(separator: "+")
216+
finish(3, "timeout after \(timeoutSeconds)s waiting for: \(missing)")
190217
ziti.shutdown()
191218
}
192219
}

0 commit comments

Comments
 (0)