Skip to content

Commit ea34c22

Browse files
committed
test: add k8s plugin unit and integration tests
1 parent 123afcd commit ea34c22

6 files changed

Lines changed: 1403 additions & 0 deletions

File tree

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
//===----------------------------------------------------------------------===//
2+
// Copyright © 2026 Apple Inc. and the container project authors.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// https://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
//===----------------------------------------------------------------------===//
16+
17+
import ContainerTestSupport
18+
import Foundation
19+
import Testing
20+
21+
@Suite(.serialized)
22+
struct TestK8sLoadImageSerial {
23+
24+
private static let testImage = WarmupImage.alpine320.rawValue
25+
26+
private func dumpNodeDiagnostics(_ f: ContainerFixture, node: String) {
27+
print("=== NODE DIAGNOSTICS [\(node)] ===")
28+
let cmds: [(label: String, args: [String])] = [
29+
("ip-link", ["ip", "link", "show"]),
30+
("iptables-mss", ["iptables", "-t", "mangle", "-L", "-n", "-v"]),
31+
("containerd", ["systemctl", "status", "containerd", "--no-pager", "-l"]),
32+
("kubelet-log", ["journalctl", "-u", "kubelet", "--no-pager", "-n", "60"]),
33+
("crictl-images", ["crictl", "images"]),
34+
]
35+
for (label, args) in cmds {
36+
if let r = try? f.run(["exec", node] + args) {
37+
let out = r.output.trimmingCharacters(in: .whitespacesAndNewlines)
38+
let err = r.error.trimmingCharacters(in: .whitespacesAndNewlines)
39+
print("[\(label)] exit=\(r.status)")
40+
if !out.isEmpty { print(out) }
41+
if !err.isEmpty { print("stderr: \(err)") }
42+
}
43+
}
44+
print("=== END NODE DIAGNOSTICS ===")
45+
}
46+
47+
private func imageExistsInNode(_ f: ContainerFixture, node: String, image: String) throws -> Bool {
48+
print("[k8s-load] ctr images list (node: \(node), checking: \(image))")
49+
let result = try f.run(["exec", node, "ctr", "--namespace", "k8s.io", "images", "list"])
50+
print("[k8s-load] ctr images list exit=\(result.status) found=\(result.output.contains(image))")
51+
guard result.status == 0 else { return false }
52+
return result.output.contains(image)
53+
}
54+
55+
private func dumpEnv(_ f: ContainerFixture, clusterName: String) {
56+
print("=== ENV DUMP [\(clusterName)] ===")
57+
if let result = try? f.run(["system", "status"]) {
58+
print("[system status]\n\(result.output)")
59+
}
60+
if let result = try? f.run(["list"]) {
61+
print("[container list]\n\(result.output)")
62+
}
63+
if let result = try? f.run(["image", "list"]) {
64+
print("[image list]\n\(result.output)")
65+
}
66+
if let result = try? f.run(["inspect", clusterName]) {
67+
print("[inspect \(clusterName)]\nstdout: \(result.output)\nstderr: \(result.error)")
68+
}
69+
print("=== END ENV DUMP ===")
70+
}
71+
72+
@Test func testLoadImageIntoSingleNode() async throws {
73+
try await ContainerFixture.with { f in
74+
let name = "k8s-\(f.testID)"
75+
f.addCleanup { _ = try? f.run(["k8s", "delete", "--name", name]) }
76+
77+
print("[k8s-load] k8s create --name \(name)")
78+
let result = try f.run(["k8s", "create", "--name", name])
79+
print("[k8s-load] k8s create exit=\(result.status)")
80+
if result.status != 0 {
81+
print("[k8s-load] k8s create stderr: \(result.error)")
82+
dumpEnv(f, clusterName: name)
83+
dumpNodeDiagnostics(f, node: name)
84+
}
85+
try result.check()
86+
87+
print("[k8s-load] pulling \(Self.testImage)")
88+
try f.doPull(Self.testImage)
89+
90+
print("[k8s-load] k8s load-image --name \(name) \(Self.testImage)")
91+
let loadResult = try f.run(["k8s", "load-image", "--name", name, Self.testImage])
92+
print("[k8s-load] k8s load-image exit=\(loadResult.status)")
93+
if loadResult.status != 0 {
94+
print("[k8s-load] load-image stderr: \(loadResult.error)")
95+
}
96+
#expect(loadResult.status == 0)
97+
98+
#expect(try imageExistsInNode(f, node: name, image: "alpine"))
99+
}
100+
}
101+
}
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
//===----------------------------------------------------------------------===//
2+
// Copyright © 2026 Apple Inc. and the container project authors.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// https://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
//===----------------------------------------------------------------------===//
16+
17+
import ContainerTestSupport
18+
import Foundation
19+
import Testing
20+
21+
@Suite(.serialized)
22+
struct TestK8sNetworkingSerial {
23+
24+
private static let testImage = WarmupImage.alpine320.rawValue
25+
26+
private func dumpNodeDiagnostics(_ f: ContainerFixture, node: String) {
27+
print("=== NODE DIAGNOSTICS [\(node)] ===")
28+
let cmds: [(label: String, args: [String])] = [
29+
("ip-link", ["ip", "link", "show"]),
30+
("iptables-mss", ["iptables", "-t", "mangle", "-L", "-n", "-v"]),
31+
("containerd", ["systemctl", "status", "containerd", "--no-pager", "-l"]),
32+
("kubelet-log", ["journalctl", "-u", "kubelet", "--no-pager", "-n", "60"]),
33+
("crictl-images", ["crictl", "images"]),
34+
]
35+
for (label, args) in cmds {
36+
if let r = try? f.run(["exec", node] + args) {
37+
let out = r.output.trimmingCharacters(in: .whitespacesAndNewlines)
38+
let err = r.error.trimmingCharacters(in: .whitespacesAndNewlines)
39+
print("[\(label)] exit=\(r.status)")
40+
if !out.isEmpty { print(out) }
41+
if !err.isEmpty { print("stderr: \(err)") }
42+
}
43+
}
44+
print("=== END NODE DIAGNOSTICS ===")
45+
}
46+
47+
private func dumpEnv(_ f: ContainerFixture, clusterName: String) {
48+
print("=== ENV DUMP [\(clusterName)] ===")
49+
if let result = try? f.run(["system", "status"]) {
50+
print("[system status]\n\(result.output)")
51+
}
52+
if let result = try? f.run(["list"]) {
53+
print("[container list]\n\(result.output)")
54+
}
55+
if let result = try? f.run(["inspect", clusterName]) {
56+
print("[inspect \(clusterName)] stdout: \(result.output) stderr: \(result.error)")
57+
}
58+
print("=== END ENV DUMP ===")
59+
}
60+
61+
@discardableResult
62+
private func kubectl(_ f: ContainerFixture, node: String, args: [String]) throws -> (output: String, status: Int32) {
63+
print("[k8s-net] kubectl \(args.joined(separator: " ")) (node: \(node))")
64+
let result = try f.run(["exec", node, "kubectl"] + args)
65+
print("[k8s-net] kubectl exit=\(result.status) output=\(result.output.prefix(120).trimmingCharacters(in: .whitespacesAndNewlines))")
66+
let filteredStderr = result.error.components(separatedBy: "\n")
67+
.filter { !$0.contains("Warning! Running debug build") && !$0.isEmpty }
68+
.joined(separator: "\n")
69+
if !filteredStderr.isEmpty {
70+
print("[k8s-net] kubectl stderr: \(filteredStderr.prefix(300))")
71+
}
72+
return (result.output, result.status)
73+
}
74+
75+
private func waitForPod(_ f: ContainerFixture, node: String, podName: String, timeout: Int = 300) throws {
76+
print("[k8s-net] waitForPod \(podName) on \(node) (timeout=\(timeout)s)")
77+
let (_, status) = try kubectl(
78+
f, node: node,
79+
args: [
80+
"wait", "--for=condition=Ready", "pod/\(podName)", "--timeout=\(timeout)s",
81+
])
82+
guard status == 0 else {
83+
let (podStatus, _) = try kubectl(f, node: node, args: ["get", "pod", podName, "--no-headers"])
84+
print("[k8s-net] pod \(podName) status: \(podStatus.trimmingCharacters(in: .whitespacesAndNewlines))")
85+
let (podDesc, _) = try kubectl(f, node: node, args: ["describe", "pod", podName])
86+
print("[k8s-net] pod \(podName) describe:\n\(podDesc.prefix(1000))")
87+
throw CommandError.executionFailed("pod \(podName) did not become ready within \(timeout)s")
88+
}
89+
print("[k8s-net] pod \(podName) is Ready")
90+
}
91+
92+
// Verify that a pod schedules, reaches Running, and can be exec'd into.
93+
@Test func testPodsScheduleAndRun() async throws {
94+
try await ContainerFixture.with { f in
95+
let name = "k8s-\(f.testID)"
96+
f.addCleanup { _ = try? f.run(["k8s", "delete", "--name", name]) }
97+
98+
print("[k8s-net] k8s create --name \(name)")
99+
let result = try f.run(["k8s", "create", "--name", name])
100+
print("[k8s-net] k8s create exit=\(result.status)")
101+
if result.status != 0 {
102+
print("[k8s-net] k8s create stderr: \(result.error)")
103+
dumpEnv(f, clusterName: name)
104+
dumpNodeDiagnostics(f, node: name)
105+
}
106+
try result.check()
107+
108+
print("[k8s-net] pulling \(Self.testImage)")
109+
try f.doPull(Self.testImage)
110+
111+
print("[k8s-net] k8s load-image --name \(name) \(Self.testImage)")
112+
let loadResult = try f.run(["k8s", "load-image", "--name", name, Self.testImage])
113+
print("[k8s-net] k8s load-image exit=\(loadResult.status)")
114+
if loadResult.status != 0 { print("[k8s-net] k8s load-image stderr: \(loadResult.error)") }
115+
#expect(loadResult.status == 0)
116+
117+
let (_, createStatus) = try kubectl(
118+
f, node: name,
119+
args: [
120+
"run", "test-pod",
121+
"--image=\(Self.testImage)",
122+
"--image-pull-policy=Never",
123+
"--restart=Never",
124+
"--", "sleep", "300",
125+
])
126+
#expect(createStatus == 0)
127+
128+
try waitForPod(f, node: name, podName: "test-pod")
129+
130+
let (output, execStatus) = try kubectl(
131+
f, node: name,
132+
args: [
133+
"exec", "test-pod", "--", "echo", "hello",
134+
])
135+
#expect(execStatus == 0)
136+
#expect(output.contains("hello"))
137+
}
138+
}
139+
140+
// Verify pod-to-service communication and CoreDNS resolution on a single node.
141+
@Test func testPodToServiceCommunication() async throws {
142+
try await ContainerFixture.with { f in
143+
let name = "k8s-\(f.testID)"
144+
f.addCleanup { _ = try? f.run(["k8s", "delete", "--name", name]) }
145+
146+
print("[k8s-net] k8s create --name \(name)")
147+
let result = try f.run(["k8s", "create", "--name", name])
148+
print("[k8s-net] k8s create exit=\(result.status)")
149+
if result.status != 0 {
150+
print("[k8s-net] k8s create stderr: \(result.error)")
151+
dumpEnv(f, clusterName: name)
152+
}
153+
try result.check()
154+
155+
print("[k8s-net] pulling \(Self.testImage)")
156+
try f.doPull(Self.testImage)
157+
158+
print("[k8s-net] k8s load-image --name \(name) \(Self.testImage)")
159+
let loadResult = try f.run(["k8s", "load-image", "--name", name, Self.testImage])
160+
print("[k8s-net] k8s load-image exit=\(loadResult.status)")
161+
if loadResult.status != 0 { print("[k8s-net] k8s load-image stderr: \(loadResult.error)") }
162+
#expect(loadResult.status == 0)
163+
164+
// Server: alpine busybox httpd serving a static response.
165+
let (_, serverStatus) = try kubectl(
166+
f, node: name,
167+
args: [
168+
"run", "server",
169+
"--image=\(Self.testImage)",
170+
"--image-pull-policy=Never",
171+
"--restart=Never",
172+
"--port=8080",
173+
"--", "sh", "-c",
174+
"while true; do printf 'HTTP/1.1 200 OK\\r\\nContent-Length: 2\\r\\nConnection: close\\r\\n\\r\\nok' | nc -l -p 8080; done",
175+
])
176+
#expect(serverStatus == 0)
177+
try waitForPod(f, node: name, podName: "server")
178+
179+
// Expose server as a ClusterIP service.
180+
let (_, exposeStatus) = try kubectl(
181+
f, node: name,
182+
args: [
183+
"expose", "pod", "server", "--port=8080", "--name=server-svc",
184+
])
185+
#expect(exposeStatus == 0)
186+
187+
// Client pod that stays alive so we can exec into it.
188+
let (_, clientStatus) = try kubectl(
189+
f, node: name,
190+
args: [
191+
"run", "client",
192+
"--image=\(Self.testImage)",
193+
"--image-pull-policy=Never",
194+
"--restart=Never",
195+
"--", "sleep", "300",
196+
])
197+
#expect(clientStatus == 0)
198+
try waitForPod(f, node: name, podName: "client")
199+
200+
// Reach server via the service DNS name — exercises CoreDNS + kube-proxy.
201+
print("[k8s-net] wget from client to server-svc:8080")
202+
let (response, wgetStatus) = try kubectl(
203+
f, node: name,
204+
args: [
205+
"exec", "client", "--", "sh", "-c",
206+
"sleep 2 && wget -qO- http://server-svc:8080",
207+
])
208+
print("[k8s-net] wget exit=\(wgetStatus) response=\(response.trimmingCharacters(in: .whitespacesAndNewlines))")
209+
#expect(wgetStatus == 0)
210+
#expect(response.contains("ok"))
211+
}
212+
}
213+
}

0 commit comments

Comments
 (0)