Skip to content

Commit 7a9b1cb

Browse files
committed
add s3 smoke test
1 parent 0886a44 commit 7a9b1cb

4 files changed

Lines changed: 117 additions & 79 deletions

File tree

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,8 +129,9 @@ jobs:
129129
AWS_REGION: us-east-1
130130
run: |
131131
python3 scripts/s3_smoke_test.py \
132-
--region "${{ inputs.aws_region }}" \
132+
--region "us-east-1" \
133133
--admin-password "${{ secrets.INFINIA_ADMIN_PASSWORD }}" \
134134
--realm-tag-key "Role" --realm-tag-value "realm" \
135135
--client-tag-key "Role" --client-tag-value "client" \
136-
--endpoint-port 8111
136+
--endpoint-port 8111 \
137+
--no-verify-ssl

.github/workflows/prechecks-aws.yml

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,11 @@ jobs:
5555
infinia_version: ${{ needs.prepare.outputs.infinia_version }}
5656
secrets: inherit
5757

58-
infinia-destroy:
59-
name: Destroy Infinia Infrastructure
60-
needs: infinia-deploy
61-
if: always()
62-
uses: ./.github/workflows/infinia-destroy-aws.yml
63-
with:
64-
aws_region: us-east-1
65-
secrets: inherit
58+
# infinia-destroy:
59+
# name: Destroy Infinia Infrastructure
60+
# needs: infinia-deploy
61+
# if: always()
62+
# uses: ./.github/workflows/infinia-destroy-aws.yml
63+
# with:
64+
# aws_region: us-east-1
65+
# secrets: inherit
Lines changed: 106 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -5,67 +5,67 @@
55
End-to-end S3 API smoke test for Infinia, driven via AWS SSM.
66
77
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.
8+
1) Find the realm EC2 by tag (e.g., Role=realm). Get instance ID + IP.
9+
2) On the realm (via SSM), run redcli to mint S3 access keys for s3_test_user.
10+
3) Parse S3_KEY / S3_SECRET from the output (no secrets are printed to runner logs).
1111
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>.
12+
5) On each client (via SSM), write ~/.aws/credentials with the generated keys,
13+
create a unique bucket, upload a test file, list objects, and confirm success.
14+
15+
Robustness:
16+
- Uses public IP if available, else falls back to private IP automatically.
17+
- You can force private IP with --use-private-endpoint.
18+
- You can override the endpoint host with --endpoint-host (e.g. internal NLB DNS).
19+
- Optional --no-verify-ssl to skip TLS verification against the endpoint.
20+
21+
Exit codes: 0 = success; non-zero = failure.
2822
"""
2923

3024
import argparse
31-
import os
25+
import datetime
26+
import random
3227
import re
3328
import sys
3429
import time
35-
import json
36-
import random
37-
import datetime
30+
from typing import Optional, Dict, List
3831

3932
import boto3
40-
from botocore.exceptions import ClientError
4133

4234

4335
def eprint(*args, **kwargs):
4436
print(*args, file=sys.stderr, **kwargs)
4537

4638

47-
def find_one_instance_id_by_tag(ec2, key, value):
39+
# ----------------------------- EC2 helpers ----------------------------- #
40+
41+
def find_one_instance_id_by_tag(ec2, key: str, value: str) -> Optional[str]:
4842
resp = ec2.describe_instances(
4943
Filters=[
5044
{"Name": f"tag:{key}", "Values": [value]},
5145
{"Name": "instance-state-name", "Values": ["running"]},
5246
],
53-
MaxResults=50,
47+
MaxResults=100,
5448
)
5549
for r in resp.get("Reservations", []):
5650
for i in r.get("Instances", []):
5751
return i["InstanceId"]
5852
return None
5953

6054

61-
def get_public_ip_by_instance_id(ec2, instance_id):
55+
def get_public_ip_by_instance_id(ec2, instance_id: str) -> Optional[str]:
6256
resp = ec2.describe_instances(InstanceIds=[instance_id])
6357
inst = resp["Reservations"][0]["Instances"][0]
6458
return inst.get("PublicIpAddress")
6559

6660

67-
def list_instance_ids_by_tag(ec2, key, value):
68-
ids = []
61+
def get_private_ip_by_instance_id(ec2, instance_id: str) -> Optional[str]:
62+
resp = ec2.describe_instances(InstanceIds=[instance_id])
63+
inst = resp["Reservations"][0]["Instances"][0]
64+
return inst.get("PrivateIpAddress")
65+
66+
67+
def list_instance_ids_by_tag(ec2, key: str, value: str) -> List[str]:
68+
ids: List[str] = []
6969
paginator = ec2.get_paginator("describe_instances")
7070
for page in paginator.paginate(
7171
Filters=[
@@ -79,10 +79,14 @@ def list_instance_ids_by_tag(ec2, key, value):
7979
return ids
8080

8181

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.
82+
# ----------------------------- SSM helpers ----------------------------- #
8483

85-
Returns dict: {instance_id: {"Status": str, "StdOut": str, "StdErr": str}}
84+
def ssm_send_and_wait(ssm, instance_ids, commands, comment, timeout_sec=900, poll_sec=5) -> Dict[str, Dict[str, str]]:
85+
"""
86+
Send AWS-RunShellScript to one or many instances and wait for completion.
87+
88+
Returns dict keyed by instance_id:
89+
{instance_id: {"Status": str, "StdOut": str, "StdErr": str}}
8690
"""
8791
if isinstance(instance_ids, str):
8892
instance_ids = [instance_ids]
@@ -96,7 +100,7 @@ def ssm_send_and_wait(ssm, instance_ids, commands, comment, timeout_sec=600, pol
96100
cmd_id = resp["Command"]["CommandId"]
97101

98102
deadline = time.time() + timeout_sec
99-
results = {iid: None for iid in instance_ids}
103+
results: Dict[str, Optional[Dict[str, str]]] = {iid: None for iid in instance_ids}
100104

101105
while time.time() < deadline:
102106
done = True
@@ -123,15 +127,17 @@ def ssm_send_and_wait(ssm, instance_ids, commands, comment, timeout_sec=600, pol
123127
break
124128
time.sleep(poll_sec)
125129

126-
# Fill any missing as timeout
130+
# Mark any missing as timeout
127131
for iid in instance_ids:
128132
if results[iid] is None:
129133
results[iid] = {"Status": "TimedOut", "StdOut": "", "StdErr": ""}
130134

131-
return results
135+
return results # type: ignore[return-value]
136+
132137

138+
# ----------------------------- Parsing helpers ----------------------------- #
133139

134-
def parse_redcli_creds_from_text(text):
140+
def parse_redcli_creds_from_text(text: str):
135141
"""
136142
Extract S3_KEY and S3_SECRET from redcli pretty-table-like output.
137143
@@ -146,49 +152,76 @@ def parse_redcli_creds_from_text(text):
146152
return s3_key.strip(), s3_secret.strip()
147153

148154

149-
def main():
155+
# ----------------------------- Main ----------------------------- #
156+
157+
def main() -> int:
150158
ap = argparse.ArgumentParser(description="Infinia S3 API smoke test via SSM")
151159
ap.add_argument("--region", required=True, help="AWS region (e.g. us-east-1)")
152160
ap.add_argument("--admin-password", required=True, help="Infinia realm_admin password")
153161
ap.add_argument("--realm-tag-key", default="Role")
154162
ap.add_argument("--realm-tag-value", default="realm")
155163
ap.add_argument("--client-tag-key", default="Role")
156164
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)
165+
166+
ap.add_argument("--endpoint-port", type=int, default=8111, help="Infinia S3 endpoint port (default: 8111)")
167+
ap.add_argument("--use-private-endpoint", action="store_true",
168+
help="Force using the realm's private IP for the endpoint URL")
169+
ap.add_argument("--endpoint-host", default="",
170+
help="Override endpoint host entirely (e.g., internal NLB DNS). If set, IP detection is skipped.")
171+
ap.add_argument("--no-verify-ssl", action="store_true",
172+
help="Pass --no-verify-ssl to aws CLI when calling the endpoint")
173+
174+
ap.add_argument("--timeout-sec", type=int, default=900, help="Overall SSM per-command timeout (default: 900s)")
175+
161176
args = ap.parse_args()
162177

163178
session = boto3.Session(region_name=args.region)
164179
ec2 = session.client("ec2")
165180
ssm = session.client("ssm")
166181

167-
# 1) Realm
182+
# 1) Realm instance
168183
realm_id = find_one_instance_id_by_tag(ec2, args.realm_tag_key, args.realm_tag_value)
169184
if not realm_id:
170185
eprint(f"❌ No running realm instance found with tag {args.realm_tag_key}={args.realm_tag_value}")
171186
return 2
172187
eprint(f"Realm instance: {realm_id}")
173188

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}")
189+
# Endpoint host resolution
190+
if args.endpoint_host:
191+
target_host = args.endpoint_host
192+
reason = "custom host override"
193+
else:
194+
pub_ip = get_public_ip_by_instance_id(ec2, realm_id)
195+
priv_ip = get_private_ip_by_instance_id(ec2, realm_id)
196+
197+
if args.use_private_endpoint:
198+
if not priv_ip:
199+
eprint("❌ --use-private-endpoint set, but no PrivateIpAddress on realm")
200+
return 2
201+
target_host, reason = priv_ip, "private (forced)"
202+
else:
203+
if pub_ip and pub_ip != "None":
204+
target_host, reason = pub_ip, "public"
205+
elif priv_ip:
206+
target_host, reason = priv_ip, "private (fallback)"
207+
else:
208+
eprint("❌ Neither PublicIpAddress nor PrivateIpAddress found on realm")
209+
return 2
210+
211+
endpoint_url = f"https://{target_host}:{args.endpoint_port}"
212+
eprint(f"Realm endpoint ({reason}): {endpoint_url}")
180213

181214
# 2) Create S3 creds on realm (SSM → redcli)
182215
redcli_cmds = [
183-
'set -euo pipefail',
216+
"set -euo pipefail",
184217
f'redcli user login realm_admin -p "{args.admin_password}"',
185218
'redcli user add s3_test_user -p Adminpassword -r red -t red || true',
186219
'redcli user grant s3_test_user red/red -t red || true',
187220
'OUT="$(redcli s3 access add s3_test_user -t red -r red/red/redobj -e 10y)"',
188221
'echo "===BEGIN-OUT==="',
189222
'echo "$OUT"',
190223
'echo "===END-OUT==="',
191-
# Friendly parse lines for easier regex later
224+
# Emit parse-friendly lines:
192225
'echo "S3_KEY=$(echo "$OUT" | sed -n \'s/.*S3_KEY[^A-Za-z0-9_-]*\\([A-Za-z0-9_-]\\+\\).*/\\1/p\' | head -n1)"',
193226
'echo "S3_SECRET=$(echo "$OUT" | sed -n \'s/.*S3_SECRET[^A-Za-z0-9+\\/=_-]*\\([A-Za-z0-9+\\/=_-]\\+\\).*/\\1/p\' | head -n1)"',
194227
]
@@ -208,11 +241,10 @@ def main():
208241
eprint("STDOUT:\n" + realm_result["StdOut"])
209242
return 4
210243

211-
# Do not print secrets in logs
212244
eprint(f"S3_KEY parsed: {s3_key[:4]}… (masked)")
213245
eprint("S3_SECRET parsed: **** (masked)")
214246

215-
# 3) Find clients
247+
# 3) Client instances
216248
client_ids = list_instance_ids_by_tag(ec2, args.client_tag_key, args.client_tag_value)
217249
if not client_ids:
218250
eprint(f"❌ No running client instances found with tag {args.client_tag_key}={args.client_tag_value}")
@@ -222,38 +254,43 @@ def main():
222254
# 4) S3 test on each client
223255
now = datetime.datetime.utcnow().strftime("%Y%m%d%H%M%S")
224256
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'
226257

227-
create_list_upload_cmds = [
258+
no_verify = "--no-verify-ssl" if args.no_verify_ssl else ""
259+
# All logic happens on the client; avoid jq dependency and keep it pure AWS CLI
260+
client_cmds = [
228261
"set -euo pipefail",
229262
"mkdir -p /home/ubuntu/.aws && chmod 700 /home/ubuntu/.aws",
230-
# write credentials (no echo in logs)
263+
# Write credentials (won't echo contents)
231264
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,
265+
"TS=$(date +%Y%m%dT%H%M%S)",
266+
"FNAME=test-${TS}.txt",
267+
'echo "S3 test file created at $TS" > "/home/ubuntu/$FNAME"',
233268
"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",
269+
# Create bucket (ignore exists)
270+
f'aws {no_verify} --endpoint-url="{endpoint_url}" s3api create-bucket --bucket "{bucket}" || true',
271+
# Upload
272+
f'aws {no_verify} --endpoint-url="{endpoint_url}" s3 cp "/home/ubuntu/$FNAME" "s3://{bucket}/"',
273+
# List and verify presence using --query (prints JSON array or "None")
274+
f'LIST=$(aws {no_verify} --endpoint-url="{endpoint_url}" s3api list-objects-v2 --bucket "{bucket}" --query "Contents[].Key" --output text || true)',
275+
'echo "LIST:$LIST"',
276+
'echo "$LIST" | grep -q "$FNAME" && echo OK',
240277
]
241278

242-
client_results = ssm_send_and_wait(
243-
ssm, client_ids, create_list_upload_cmds, "Infinia S3 smoke test", timeout_sec=args.timeout_sec
279+
fanout = ssm_send_and_wait(
280+
ssm, client_ids, client_cmds, "Infinia S3 smoke test", timeout_sec=args.timeout_sec
244281
)
245282

246-
had_fail = False
247-
for iid, res in client_results.items():
283+
failed = False
284+
for iid, res in fanout.items():
248285
print(f"---- Client {iid} ({res['Status']}) ----")
249-
# Show stdout for visibility but keep credentials masked (we never echo them above)
286+
# StdOut shows "LIST: ..." and "OK" if success; secrets are never printed
250287
print(res["StdOut"])
251288
if res["Status"] != "Success" or "OK" not in res["StdOut"]:
252289
eprint(f"❌ S3 smoke test failed on client {iid}")
253290
eprint(res["StdErr"])
254-
had_fail = True
291+
failed = True
255292

256-
if had_fail:
293+
if failed:
257294
return 6
258295

259296
print("✅ S3 smoke test passed on all clients")

0 commit comments

Comments
 (0)