Skip to content

Commit 0886a44

Browse files
committed
add s3 smoke test
1 parent 25f7047 commit 0886a44

3 files changed

Lines changed: 286 additions & 8 deletions

File tree

.github/workflows/infinia-deploy-aws.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,3 +120,17 @@ jobs:
120120
done
121121
echo "Verification failed after ${ATTEMPTS} attempts ❌"
122122
exit 1
123+
124+
125+
# ---------- S3 Smoke test --------------------------
126+
- name: S3 API smoke test (Python via SSM)
127+
working-directory: ${{ github.workspace }}
128+
env:
129+
AWS_REGION: us-east-1
130+
run: |
131+
python3 scripts/s3_smoke_test.py \
132+
--region "${{ inputs.aws_region }}" \
133+
--admin-password "${{ secrets.INFINIA_ADMIN_PASSWORD }}" \
134+
--realm-tag-key "Role" --realm-tag-value "realm" \
135+
--client-tag-key "Role" --client-tag-value "client" \
136+
--endpoint-port 8111

.github/workflows/main-aws.yml

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,11 @@ jobs:
3939
infinia_version: ${{ inputs.infinia_version }}
4040
secrets: inherit
4141

42-
infinia-destroy:
43-
name: Destroy Infinia Infrastructure
44-
needs: infinia-deploy
45-
if: always()
46-
uses: ./.github/workflows/infinia-destroy-aws.yml
47-
with:
48-
aws_region: us-east-1
49-
secrets: inherit
42+
# infinia-destroy:
43+
# name: Destroy Infinia Infrastructure
44+
# needs: infinia-deploy
45+
# if: always()
46+
# uses: ./.github/workflows/infinia-destroy-aws.yml
47+
# with:
48+
# aws_region: us-east-1
49+
# secrets: inherit

scripts/s3_smoke_test.py

Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
#!/usr/bin/env python3
2+
# -*- coding: utf-8 -*-
3+
4+
"""
5+
End-to-end S3 API smoke test for Infinia, driven via AWS SSM.
6+
7+
Flow:
8+
1) Find the realm EC2 by tag (e.g., Role=realm). Get its instance ID and public IP.
9+
2) On the realm instance (via SSM), run redcli to create s3_test_user and mint S3 access keys.
10+
3) Parse S3_KEY and S3_SECRET from the SSM output.
11+
4) Find all client EC2 instances by tag (e.g., Role=client).
12+
5) On each client (via SSM), write ~/.aws/credentials with the generated keys, then:
13+
- create a unique bucket (ignore “already exists”),
14+
- upload a timestamped file via aws s3 cp to the Infinia endpoint,
15+
- list bucket objects and verify the file appears.
16+
17+
Exit codes:
18+
0 on success; non-zero on failure.
19+
20+
Requirements on the runner:
21+
- AWS credentials set (GitHub action `configure-aws-credentials` is fine)
22+
- boto3, botocore installed (your pipeline already installs boto3)
23+
24+
Assumptions:
25+
- Clients already have AWS CLI v2 installed (as per your note).
26+
- redcli is installed on the realm instance.
27+
- The S3 endpoint is https://<realm-public-ip>:<endpoint_port>.
28+
"""
29+
30+
import argparse
31+
import os
32+
import re
33+
import sys
34+
import time
35+
import json
36+
import random
37+
import datetime
38+
39+
import boto3
40+
from botocore.exceptions import ClientError
41+
42+
43+
def eprint(*args, **kwargs):
44+
print(*args, file=sys.stderr, **kwargs)
45+
46+
47+
def find_one_instance_id_by_tag(ec2, key, value):
48+
resp = ec2.describe_instances(
49+
Filters=[
50+
{"Name": f"tag:{key}", "Values": [value]},
51+
{"Name": "instance-state-name", "Values": ["running"]},
52+
],
53+
MaxResults=50,
54+
)
55+
for r in resp.get("Reservations", []):
56+
for i in r.get("Instances", []):
57+
return i["InstanceId"]
58+
return None
59+
60+
61+
def get_public_ip_by_instance_id(ec2, instance_id):
62+
resp = ec2.describe_instances(InstanceIds=[instance_id])
63+
inst = resp["Reservations"][0]["Instances"][0]
64+
return inst.get("PublicIpAddress")
65+
66+
67+
def list_instance_ids_by_tag(ec2, key, value):
68+
ids = []
69+
paginator = ec2.get_paginator("describe_instances")
70+
for page in paginator.paginate(
71+
Filters=[
72+
{"Name": f"tag:{key}", "Values": [value]},
73+
{"Name": "instance-state-name", "Values": ["running"]},
74+
]
75+
):
76+
for r in page.get("Reservations", []):
77+
for i in r.get("Instances", []):
78+
ids.append(i["InstanceId"])
79+
return ids
80+
81+
82+
def ssm_send_and_wait(ssm, instance_ids, commands, comment, timeout_sec=600, poll_sec=5):
83+
"""Send AWS-RunShellScript to one or many instances and wait for completion.
84+
85+
Returns dict: {instance_id: {"Status": str, "StdOut": str, "StdErr": str}}
86+
"""
87+
if isinstance(instance_ids, str):
88+
instance_ids = [instance_ids]
89+
90+
resp = ssm.send_command(
91+
DocumentName="AWS-RunShellScript",
92+
InstanceIds=instance_ids,
93+
Comment=comment,
94+
Parameters={"commands": commands},
95+
)
96+
cmd_id = resp["Command"]["CommandId"]
97+
98+
deadline = time.time() + timeout_sec
99+
results = {iid: None for iid in instance_ids}
100+
101+
while time.time() < deadline:
102+
done = True
103+
for iid in instance_ids:
104+
if results[iid] is not None:
105+
continue
106+
try:
107+
inv = ssm.get_command_invocation(CommandId=cmd_id, InstanceId=iid)
108+
except ssm.exceptions.InvocationDoesNotExist:
109+
done = False
110+
continue
111+
112+
status = inv.get("Status")
113+
if status in ("Success", "Failed", "Cancelled", "TimedOut"):
114+
results[iid] = {
115+
"Status": status,
116+
"StdOut": inv.get("StandardOutputContent", ""),
117+
"StdErr": inv.get("StandardErrorContent", ""),
118+
}
119+
else:
120+
done = False
121+
122+
if done:
123+
break
124+
time.sleep(poll_sec)
125+
126+
# Fill any missing as timeout
127+
for iid in instance_ids:
128+
if results[iid] is None:
129+
results[iid] = {"Status": "TimedOut", "StdOut": "", "StdErr": ""}
130+
131+
return results
132+
133+
134+
def parse_redcli_creds_from_text(text):
135+
"""
136+
Extract S3_KEY and S3_SECRET from redcli pretty-table-like output.
137+
138+
Accepts lines like:
139+
│ S3_KEY │ AKIA... │
140+
│ S3_SECRET │ wJalrXUtnFEMI/K7MDENG/bPxRfiCY... │
141+
"""
142+
key_match = re.search(r"S3_KEY[^\w-]*([A-Za-z0-9_-]+)", text)
143+
sec_match = re.search(r"S3_SECRET[^\w+/=_-]*([A-Za-z0-9+/=_-]+)", text)
144+
s3_key = key_match.group(1) if key_match else ""
145+
s3_secret = sec_match.group(1) if sec_match else ""
146+
return s3_key.strip(), s3_secret.strip()
147+
148+
149+
def main():
150+
ap = argparse.ArgumentParser(description="Infinia S3 API smoke test via SSM")
151+
ap.add_argument("--region", required=True, help="AWS region (e.g. us-east-1)")
152+
ap.add_argument("--admin-password", required=True, help="Infinia realm_admin password")
153+
ap.add_argument("--realm-tag-key", default="Role")
154+
ap.add_argument("--realm-tag-value", default="realm")
155+
ap.add_argument("--client-tag-key", default="Role")
156+
ap.add_argument("--client-tag-value", default="client")
157+
ap.add_argument("--endpoint-port", type=int, default=8111)
158+
ap.add_argument("--no-verify-ssl", action="store_true", default=True,
159+
help="Use --no-verify-ssl against the endpoint")
160+
ap.add_argument("--timeout-sec", type=int, default=900)
161+
args = ap.parse_args()
162+
163+
session = boto3.Session(region_name=args.region)
164+
ec2 = session.client("ec2")
165+
ssm = session.client("ssm")
166+
167+
# 1) Realm
168+
realm_id = find_one_instance_id_by_tag(ec2, args.realm_tag_key, args.realm_tag_value)
169+
if not realm_id:
170+
eprint(f"❌ No running realm instance found with tag {args.realm_tag_key}={args.realm_tag_value}")
171+
return 2
172+
eprint(f"Realm instance: {realm_id}")
173+
174+
realm_ip = get_public_ip_by_instance_id(ec2, realm_id)
175+
if not realm_ip:
176+
eprint("❌ Realm public IP not found")
177+
return 2
178+
endpoint_url = f"https://{realm_ip}:{args.endpoint_port}"
179+
eprint(f"Realm endpoint: {endpoint_url}")
180+
181+
# 2) Create S3 creds on realm (SSM → redcli)
182+
redcli_cmds = [
183+
'set -euo pipefail',
184+
f'redcli user login realm_admin -p "{args.admin_password}"',
185+
'redcli user add s3_test_user -p Adminpassword -r red -t red || true',
186+
'redcli user grant s3_test_user red/red -t red || true',
187+
'OUT="$(redcli s3 access add s3_test_user -t red -r red/red/redobj -e 10y)"',
188+
'echo "===BEGIN-OUT==="',
189+
'echo "$OUT"',
190+
'echo "===END-OUT==="',
191+
# Friendly parse lines for easier regex later
192+
'echo "S3_KEY=$(echo "$OUT" | sed -n \'s/.*S3_KEY[^A-Za-z0-9_-]*\\([A-Za-z0-9_-]\\+\\).*/\\1/p\' | head -n1)"',
193+
'echo "S3_SECRET=$(echo "$OUT" | sed -n \'s/.*S3_SECRET[^A-Za-z0-9+\\/=_-]*\\([A-Za-z0-9+\\/=_-]\\+\\).*/\\1/p\' | head -n1)"',
194+
]
195+
realm_result = ssm_send_and_wait(
196+
ssm, realm_id, redcli_cmds, "Create s3_test_user and S3 access", timeout_sec=min(args.timeout_sec, 600)
197+
)[realm_id]
198+
199+
if realm_result["Status"] != "Success":
200+
eprint("❌ redcli SSM command failed on realm")
201+
eprint("STDERR:\n" + realm_result["StdErr"])
202+
eprint("STDOUT:\n" + realm_result["StdOut"])
203+
return 3
204+
205+
s3_key, s3_secret = parse_redcli_creds_from_text(realm_result["StdOut"])
206+
if not s3_key or not s3_secret:
207+
eprint("❌ Failed to parse S3 credentials from realm output")
208+
eprint("STDOUT:\n" + realm_result["StdOut"])
209+
return 4
210+
211+
# Do not print secrets in logs
212+
eprint(f"S3_KEY parsed: {s3_key[:4]}… (masked)")
213+
eprint("S3_SECRET parsed: **** (masked)")
214+
215+
# 3) Find clients
216+
client_ids = list_instance_ids_by_tag(ec2, args.client_tag_key, args.client_tag_value)
217+
if not client_ids:
218+
eprint(f"❌ No running client instances found with tag {args.client_tag_key}={args.client_tag_value}")
219+
return 5
220+
eprint(f"Client instances: {', '.join(client_ids)}")
221+
222+
# 4) S3 test on each client
223+
now = datetime.datetime.utcnow().strftime("%Y%m%d%H%M%S")
224+
bucket = f"infinia-smoke-{now}-{random.randint(1000,9999)}"
225+
test_file_cmd = r'TS=$(date +%Y%m%dT%H%M%S); echo "S3 test file created at $TS" > /home/ubuntu/test-$TS.txt'
226+
227+
create_list_upload_cmds = [
228+
"set -euo pipefail",
229+
"mkdir -p /home/ubuntu/.aws && chmod 700 /home/ubuntu/.aws",
230+
# write credentials (no echo in logs)
231+
f"cat > /home/ubuntu/.aws/credentials <<'EOF'\n[default]\naws_access_key_id={s3_key}\naws_secret_access_key={s3_secret}\nEOF",
232+
test_file_cmd,
233+
"export AWS_SHARED_CREDENTIALS_FILE=/home/ubuntu/.aws/credentials",
234+
"aws --version || true",
235+
f"aws {'--no-verify-ssl' if args.no_verify_ssl else ''} --endpoint-url={endpoint_url} s3api create-bucket --bucket {bucket} || true",
236+
r"FILE=$(ls -1t /home/ubuntu/test-*.txt | head -n1)",
237+
f"aws {'--no-verify-ssl' if args.no_verify_ssl else ''} --endpoint-url={endpoint_url} s3 cp \"$FILE\" s3://{bucket}/",
238+
f"aws {'--no-verify-ssl' if args.no_verify_ssl else ''} --endpoint-url={endpoint_url} s3api list-objects-v2 --bucket {bucket} --output json --query 'Contents[].Key'",
239+
"echo OK",
240+
]
241+
242+
client_results = ssm_send_and_wait(
243+
ssm, client_ids, create_list_upload_cmds, "Infinia S3 smoke test", timeout_sec=args.timeout_sec
244+
)
245+
246+
had_fail = False
247+
for iid, res in client_results.items():
248+
print(f"---- Client {iid} ({res['Status']}) ----")
249+
# Show stdout for visibility but keep credentials masked (we never echo them above)
250+
print(res["StdOut"])
251+
if res["Status"] != "Success" or "OK" not in res["StdOut"]:
252+
eprint(f"❌ S3 smoke test failed on client {iid}")
253+
eprint(res["StdErr"])
254+
had_fail = True
255+
256+
if had_fail:
257+
return 6
258+
259+
print("✅ S3 smoke test passed on all clients")
260+
return 0
261+
262+
263+
if __name__ == "__main__":
264+
sys.exit(main())

0 commit comments

Comments
 (0)