Skip to content

Commit 7f233a8

Browse files
authored
Merge pull request #205 from RS-PYTHON/cicd-improvements
RSPY-816 - CI/CD improvements
2 parents 37a0cda + e6d0f60 commit 7f233a8

File tree

20 files changed

+606
-21
lines changed

20 files changed

+606
-21
lines changed
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
#!/bin/bash
2+
# Copyright 2025 CS Group
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+
# http://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+
set -euo pipefail
17+
18+
# --- Usage ---
19+
# ./configure-cluster.sh "<labels>" "<subdomains>"
20+
# Example :
21+
# ./configure-cluster.sh "node-role.kubernetes.io/infra= node-role.kubernetes.io/rs_env=" "iam kube oauth2-proxy"
22+
23+
LABELS_INPUT="${1:-}"
24+
SUBDOMAINS_INPUT="${2:-}"
25+
26+
if [[ -z "$LABELS_INPUT" || -z "$SUBDOMAINS_INPUT" ]]; then
27+
echo "❌ Usage: $0 \"<labels>\" \"<subdomains>\""
28+
echo "Example: $0 \"node-role.kubernetes.io/infra= node-role.kubernetes.io/rs_env=\" \"iam kube oauth2-proxy\""
29+
exit 1
30+
fi
31+
32+
# --- Convert inputs to arrays ---
33+
read -r -a SUBDOMAINS <<< "$SUBDOMAINS_INPUT"
34+
35+
# --- Label the minikube node ---
36+
echo "🏷️ Applying labels to node 'minikube': $LABELS_INPUT"
37+
kubectl label node minikube "$LABELS_INPUT"
38+
39+
# --- Extract domain and IP
40+
DOMAIN=$(yq e '.platform_domain_name' ./inventory/sample/host_vars/setup/main.yaml)
41+
MINIKUBE_IP=$(minikube ip)
42+
FIXED_IP="192.168.49.240"
43+
44+
echo "🌐 Using domain: $DOMAIN"
45+
echo "💡 Minikube IP: $MINIKUBE_IP"
46+
47+
# --- Ensure runner (GitHub host) can resolve app domains
48+
echo "🧩 Updating /etc/hosts..."
49+
for sd in "${SUBDOMAINS[@]}" ; do
50+
echo "$FIXED_IP $sd.$DOMAIN" | sudo tee -a /etc/hosts
51+
done
52+
53+
# --- Validate /etc/hosts resolution on the runner
54+
echo "🔍 Verifying host DNS resolution..."
55+
for sd in "${SUBDOMAINS[@]}" ; do
56+
if dig +short "$sd.$DOMAIN" > /dev/null; then
57+
echo "$sd.$DOMAIN resolves on host"
58+
else
59+
echo "❌ Host DNS failed for $sd.$DOMAIN"
60+
exit 2
61+
fi
62+
done
63+
echo "✅ Host DNS resolution works"
64+
65+
# --- Create MetalLB configmap to define fixed IP address range
66+
echo "⚙️ Applying MetalLB config..."
67+
kubectl apply -f .github/common/resources/test/metallb-configmap.yaml
68+
69+
# --- Fetch current CoreDNS config
70+
echo "🧠 Patching CoreDNS..."
71+
kubectl -n kube-system get configmap coredns -o yaml > coredns-configmap.yaml
72+
# --- Build hosts entries from SUBDOMAINS
73+
HOSTS_BLOCK=""
74+
for sd in "${SUBDOMAINS[@]}"; do
75+
HOSTS_BLOCK+=" $FIXED_IP ${sd}.${DOMAIN}\n"
76+
done
77+
# --- Inject hosts into coredns config map
78+
awk -v block="$HOSTS_BLOCK" '/192\.168\.49\.1/{printf "%s", block; ok=1} {print} END{exit ok?0:1}' \
79+
coredns-configmap.yaml > coredns-configmap.yaml.patched.yaml || {
80+
echo "❌ awk did not replace anything";
81+
exit 3;
82+
}
83+
# --- Patch CoreDNS and restart
84+
kubectl -n kube-system patch configmap coredns --type merge -p "$(cat coredns-configmap.yaml.patched.yaml)"
85+
kubectl -n kube-system rollout restart deployment coredns
86+
87+
# --- Wait for CoreDNS to be ready
88+
kubectl -n kube-system rollout status deployment coredns --timeout=60s
89+
# --- Create a temporary debug pod to verify DNS from inside cluster
90+
kubectl run dns-test --image=busybox:latest --restart=Never -- sleep 30 &
91+
sleep 1
92+
kubectl wait --for=condition=Ready pod/dns-test --timeout=30s || (echo "❌ dns-test pod not ready" && exit 4)
93+
94+
# --- Verify DNS resolution inside the cluster
95+
echo "🔬 Testing DNS resolution inside the cluster..."
96+
for sd in "${SUBDOMAINS[@]}"; do
97+
echo "🔍 Checking DNS for $sd.$DOMAIN..."
98+
RESOLVED_IP=$(kubectl exec dns-test -- nslookup "$sd.$DOMAIN" 2>/dev/null | awk '/^Address: /{print $2; exit}')
99+
if [[ "$RESOLVED_IP" == "$FIXED_IP" ]]; then
100+
echo "$sd.$DOMAIN resolves correctly to $RESOLVED_IP"
101+
else
102+
echo "❌ DNS resolution failed for $sd.$DOMAIN (expected $FIXED_IP, got ${RESOLVED_IP:-none})"
103+
exit 5
104+
fi
105+
done
106+
echo "✅ DNS resolution works correctly inside the cluster"
107+
# Clean up debug pod
108+
kubectl delete pod dns-test --ignore-not-found=true --grace-period=0 --force
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
# --- Usage check
5+
if [ "$#" -lt 1 ]; then
6+
echo "Usage: $0 <sd1:ns1> [<sd2:ns2> ...]"
7+
exit 1
8+
fi
9+
10+
# --- Directories
11+
RESOURCES_DIR=".github/common/resources/test"
12+
APP_CLUSTER_ISSUER="apps/02-cluster-issuer"
13+
APP_OAUTH2_PROXY="apps/oauth2-proxy"
14+
15+
# --- Generate a local CA
16+
echo "Generating local CA..."
17+
openssl req -x509 -newkey rsa:4096 -days 1 -nodes \
18+
-keyout local-ca.key -out local-ca.crt \
19+
-subj "/CN=local CA/O=rspy/OU=dev" 2> /dev/null
20+
21+
# --- Create local-ca-configmap.yaml
22+
echo "Creating ConfigMap..."
23+
while IFS= read -r line || [ -n "$line" ]; do
24+
printf ' %s\n' "$line" >> "$RESOURCES_DIR/local-ca-configmap.yaml"
25+
done < local-ca.crt
26+
27+
# --- Create local-ca-secret.yaml with inlined base64 data
28+
echo "Creating Secret..."
29+
sed -i \
30+
-e "s!<crt>!$(base64 -w0 local-ca.crt)!g" \
31+
-e "s!<key>!$(base64 -w0 local-ca.key)!g" \
32+
"$RESOURCES_DIR/local-ca-secret.yaml"
33+
34+
# --- Cleanup
35+
rm -f local-ca.crt local-ca.key
36+
37+
# --- Copy CA issuer files
38+
echo "Copying issuer files..."
39+
cp "$RESOURCES_DIR/local-ca-secret.yaml" \
40+
"$RESOURCES_DIR/local-ca-issuer.yaml" \
41+
"$APP_CLUSTER_ISSUER/"
42+
43+
# --- Handle all <sd>:<ns> pairs
44+
CERT_FILES=""
45+
for arg in "$@"; do
46+
sd="${arg%%:*}"
47+
ns="${arg##*:}"
48+
49+
echo "Processing subdomain: $sd (namespace: $ns)"
50+
sed \
51+
-e "s!<sd>!$sd!g" \
52+
-e "s!<ns>!$ns!g" \
53+
"$RESOURCES_DIR/certificate.yaml" \
54+
> "$APP_CLUSTER_ISSUER/certificate-$sd.yaml"
55+
CERT_FILES="${CERT_FILES}\\n- certificate-${sd}.yaml"
56+
done
57+
58+
# --- Update kustomization.yaml
59+
echo "Updating kustomization.yaml..."
60+
sed -i "/- clusterIssuer.yaml/a\- local-ca-secret.yaml\\n- local-ca-issuer.yaml$CERT_FILES" "$APP_CLUSTER_ISSUER/kustomization.yaml"
61+
62+
# --- Trust local CA in oauth2-proxy
63+
echo "Adding local CA trust to oauth2-proxy..."
64+
sed 's!<ns>!iam!g' \
65+
"$RESOURCES_DIR/local-ca-configmap.yaml" \
66+
> "$APP_OAUTH2_PROXY/local-ca-configmap.yaml"
67+
echo -e "resources:\n- local-ca-configmap.yaml\n" | tee -a $APP_OAUTH2_PROXY/kustomization.yaml > /dev/null
68+
cat <<'EOF' | tee -a $APP_OAUTH2_PROXY/values.yaml > /dev/null
69+
extraVolumes:
70+
- name: local-ca
71+
configMap:
72+
name: local-ca-configmap
73+
extraVolumeMounts:
74+
- name: local-ca
75+
mountPath: /etc/ssl/certs/local-ca
76+
readOnly: true
77+
EOF
78+
79+
echo "✅ Done!"
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
#!/usr/bin/env python3
2+
import sys
3+
import json
4+
import re
5+
from urllib.parse import urlparse
6+
from ruamel.yaml import YAML
7+
8+
def remove_ids_and_containerids(obj):
9+
if isinstance(obj, dict):
10+
return {k: remove_ids_and_containerids(v) for k, v in obj.items() if k not in ("id", "containerId")}
11+
elif isinstance(obj, list):
12+
return [remove_ids_and_containerids(el) for el in obj]
13+
else:
14+
return obj
15+
16+
def sort_lists(obj):
17+
if isinstance(obj, dict):
18+
return {k: sort_lists(v) for k, v in obj.items()}
19+
elif isinstance(obj, list):
20+
if all(not isinstance(el, (dict, list)) for el in obj):
21+
return sorted(obj, key=lambda x: str(x))
22+
if all(isinstance(el, dict) for el in obj) and obj:
23+
recursed = [sort_lists(el) for el in obj]
24+
def sort_key(el):
25+
clientId = str(el.get("clientId", ""))
26+
priority = el.get("priority")
27+
if isinstance(priority, str) and priority.isdigit():
28+
priority = int(priority)
29+
elif not isinstance(priority, int):
30+
priority = float("inf")
31+
name = str(el.get("name", ""))
32+
return (clientId, priority, name)
33+
return sorted(recursed, key=sort_key)
34+
return [sort_lists(el) for el in obj]
35+
else:
36+
return obj
37+
38+
def extract_realm_name(obj):
39+
# Try data['realm']['realm'] or data['realm'] as string
40+
if isinstance(obj.get("realm"), dict):
41+
return obj["realm"].get("realm")
42+
elif isinstance(obj.get("realm"), str):
43+
return obj["realm"]
44+
return None
45+
46+
def extract_platform_domain(obj):
47+
# Scan all rootUrl/adminUrl values and pick domain
48+
domains = set()
49+
def scan(o):
50+
if isinstance(o, dict):
51+
for k, v in o.items():
52+
if k in ("rootUrl", "adminUrl") and isinstance(v, str) and v.startswith("https://"):
53+
parsed = urlparse(v)
54+
host = parsed.hostname
55+
if host and "." in host:
56+
# remove subdomain (keep domain)
57+
parts = host.split(".")
58+
domains.add(".".join(parts[1:]))
59+
else:
60+
scan(v)
61+
elif isinstance(o, list):
62+
for el in o:
63+
scan(el)
64+
scan(obj)
65+
return list(domains)[0]
66+
67+
def inject_realm_variables(obj, original_realm, variable="{{ keycloak.realm.name }}"):
68+
if isinstance(obj, dict):
69+
return {k: inject_realm_variables(v, original_realm, variable) for k, v in obj.items()}
70+
elif isinstance(obj, list):
71+
return [inject_realm_variables(el, original_realm, variable) for el in obj]
72+
elif isinstance(obj, str):
73+
s = obj
74+
s = s.replace(f"/realms/{original_realm}/", f"/realms/{variable}/")
75+
s = s.replace(f"/admin/{original_realm}/console/", f"/admin/{variable}/console/")
76+
return s
77+
else:
78+
return obj
79+
80+
def inject_platform_variables(obj, original_domain, variable="{{ platform_domain_name }}"):
81+
if isinstance(obj, dict):
82+
return {k: inject_platform_variables(v, original_domain, variable) for k, v in obj.items()}
83+
elif isinstance(obj, list):
84+
return [inject_platform_variables(el, original_domain, variable) for el in obj]
85+
elif isinstance(obj, str):
86+
s = re.sub(
87+
r"https://([a-zA-Z0-9_-]+)\." + re.escape(original_domain),
88+
r"https://\1." + variable,
89+
obj
90+
)
91+
return s
92+
else:
93+
return obj
94+
95+
def inject_client_secrets(obj):
96+
if isinstance(obj, dict):
97+
new_obj = {}
98+
client_id = obj.get("clientId")
99+
for k, v in obj.items():
100+
if k == "secret" and isinstance(v, str) and v == "**********" and client_id:
101+
new_obj[k] = f"{{{{ {client_id}_oidc_client_secret }}}}"
102+
else:
103+
new_obj[k] = inject_client_secrets(v)
104+
return new_obj
105+
elif isinstance(obj, list):
106+
return [inject_client_secrets(el) for el in obj]
107+
else:
108+
return obj
109+
110+
def inject_smtp_variables(realm_map):
111+
"""
112+
Replace specific SMTP keys with variables in spec.realm.smtpServer.
113+
Only handles the case where smtpServer is a dict under spec.realm.
114+
Other keys are left unchanged.
115+
"""
116+
smtp_server = realm_map.get("smtpServer")
117+
smtp_map = {
118+
"from": "{{ keycloak.smtp.from }}",
119+
"host": "{{ keycloak.smtp.host }}",
120+
"password": "{{ keycloak.smtp.password }}",
121+
"port": "{{ keycloak.smtp.port }}",
122+
"ssl": "{{ keycloak.smtp.ssl }}",
123+
"starttls": "{{ keycloak.smtp.starttls }}",
124+
"user": "{{ keycloak.smtp.user }}"
125+
}
126+
# Replace only the keys specified in smtp_map
127+
for key, var in smtp_map.items():
128+
if key in smtp_server:
129+
smtp_server[key] = var
130+
# Sort keys alphabetically
131+
realm_map["smtpServer"] = dict(sorted(smtp_server.items()))
132+
return realm_map
133+
134+
def main(inpath, outpath):
135+
with open(inpath, "r", encoding="utf-8") as f:
136+
data = json.load(f)
137+
138+
cleaned = remove_ids_and_containerids(data)
139+
sorted_cleaned = sort_lists(cleaned)
140+
141+
realm_map = sorted_cleaned if isinstance(sorted_cleaned, dict) else {"data": sorted_cleaned}
142+
143+
original_realm = extract_realm_name(realm_map)
144+
original_domain = extract_platform_domain(realm_map)
145+
146+
realm_map = inject_realm_variables(realm_map, original_realm)
147+
realm_map = inject_platform_variables(realm_map, original_domain)
148+
realm_map = inject_client_secrets(realm_map)
149+
realm_map = inject_smtp_variables(realm_map)
150+
151+
final = {
152+
"apiVersion": "k8s.keycloak.org/v2alpha1",
153+
"kind": "KeycloakRealmImport",
154+
"metadata": {"name": "rspy", "namespace": "iam", "labels": {"wait-for-deployment": "Done"}},
155+
"spec": {"keycloakCRName": "keycloak", "realm": realm_map},
156+
}
157+
158+
yaml = YAML()
159+
yaml.indent(mapping=2, sequence=4, offset=2)
160+
yaml.width = 130
161+
yaml.preserve_quotes = False
162+
163+
license_header = """# Copyright 2024 CS Group
164+
#
165+
# Licensed under the Apache License, Version 2.0 (the "License");
166+
# you may not use this file except in compliance with the License.
167+
# You may obtain a copy of the License at
168+
#
169+
# http://www.apache.org/licenses/LICENSE-2.0
170+
#
171+
# Unless required by applicable law or agreed to in writing, software
172+
# distributed under the License is distributed on an "AS IS" BASIS,
173+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
174+
# See the License for the specific language governing permissions and
175+
# limitations under the License.
176+
"""
177+
178+
with open(outpath, "w", encoding="utf-8") as f:
179+
f.write(license_header + "\n")
180+
yaml.dump(final, f)
181+
182+
print(f"Transformation completed: {outpath}")
183+
184+
if __name__ == "__main__":
185+
if len(sys.argv) != 3:
186+
print(f"Usage: {sys.argv[0]} <input.json> <output.yaml>", file=sys.stderr)
187+
sys.exit(1)
188+
main(sys.argv[1], sys.argv[2])

0 commit comments

Comments
 (0)