mirror of
https://github.com/mulgadc/spinifex.git
synced 2026-10-19 19:01:42 +00:00
2acb37342d
* fix(ec2): stamp subnet AvailabilityZoneId for single-AZ ALB resolution
DescribeSubnets omitted AvailabilityZoneId, so the AWS Load Balancer
Controller's DescribeAvailabilityZones-by-ZoneId lookup resolved no AZ
info ("cannot resolve AZ info for AZs: []") and never built the ALB
model. Stamp the single zone-id on every subnet and share it with
DescribeAvailabilityZones through one SingleZoneID constant so the two
responses can never drift.
* feat(eks): add aws-load-balancer-controller v2.8.1 addon bundle
Baked manifests for the LBC managed addon (CRDs, controller Deployment/
RBAC/webhooks/Service, IngressClass alb, IRSA render markers). Two
wirings make it work against the spinifex gateway:
- --aws-api-endpoints carries sts= alongside elasticloadbalancing/ec2/
acm. The aws-sdk-go v1 credential bootstrap resolves the STS client
from the session endpoint resolver, not AWS_ENDPOINT_URL_STS, so the
web-identity AssumeRoleWithWebIdentity must be pointed at the gateway
here or it falls back to real AWS STS.
- --feature-gates=ALBSingleSubnet=true drops the ALB minimum subnet
count from two to one so an ALB provisions in a single-AZ deployment.
* feat(eks): render IRSA into managed addons + node-minted webhook cert
Extend the addon-sync renderer to inject the projected-token / role-arn /
gateway STS-endpoint + CA block into managed-addon pod specs via opt-in
markers, and substitute cluster-name/region/vpc-id/gateway-endpoint/CA
placeholders. Add cmd/eks-webhook-cert: the eks-node AMI has no openssl,
so this mints (then reuses) a self-signed ECDSA-P256 serving cert for the
admission webhook, cached on disk so the render is byte-stable across
ticks. Wire both into the image build (manifest.conf) plus INSTALL_FILES
for the LBC bundle.
* feat(eks): surface cluster VPC id as EKS_VPC_ID for the LB controller
Thread the control-plane VPC id through K3sServerInput.VpcID into the
first-boot EKS_VPC_ID env so the in-cluster aws-load-balancer-controller
can pass --aws-vpc-id to the gateway elbv2/ec2 handlers.
* fix(eks): point the LB controller at the customer VPC + customer-facing gateway
The aws-load-balancer-controller ran with two control-plane-scoped values
that customer worker pods cannot use:
- --aws-vpc-id was set from the managed control-plane VPC (cpRefs.VpcID),
which the gateway's DescribeVpcs does not return for customer calls, so
backend-SG auto-create failed with InvalidVpcID.NotFound. Source it from
meta.ResourcesVpcConfig.VpcId, the customer cluster VPC where the workers
and their subnets live and where the ALB is provisioned.
- The addon render baked the mgmt gateway endpoint (EKS_GATEWAY_URL /
SystemGatewayURL) into pod specs, unreachable from worker pods. Add a
distinct EKS_ADDON_GATEWAY_URL (the customer-facing GatewayBaseURL, same
host as the OIDC issuer) and substitute it for {{GATEWAY_ENDPOINT}} in
managed addon manifests. EKS_GATEWAY_URL stays mgmt for the CP's own
bootstrap, state-report and token-webhook paths.
* fix(elbv2): resolve forward target group from ForwardConfig
listenerActionFromSDK read only the legacy flat Action.TargetGroupArn, so a
forward action carrying its target group via ForwardConfig.TargetGroups (the
shape the aws-load-balancer-controller emits) left the action target empty and
CreateRule/CreateListener rejected it with MissingParameter. Flatten the first
target group from ForwardConfig when the flat field is absent.
* fix(ec2): mirror CreateTags/DeleteTags into subnet/vpc records
The generic S3 tag store and the per-resource KV records are disjoint:
DescribeSubnets/DescribeVpcs read tags from their KV record, while
CreateTags only wrote the generic store. Subnet tags applied after
create were therefore invisible to tag-filtered describes, breaking
aws-load-balancer-controller subnet auto-discovery (kubernetes.io/role/elb).
Write through CreateTags/DeleteTags to the owning subnet/vpc record so
tag-filtered describes observe them. Resource types the VPC service does
not own are skipped; the generic store stays their record of truth.
* test(daemon): pin synthetic capacity in host-dependent allocate tests
TestCanAllocate_CountEdgeCases/Mixed_instance_types and
TestHandleEC2DescribeInstanceTypes/NoFilterReturnsSupportedRegardlessOfAllocation
called rm.allocate against live-detected host capacity. allocate gates on
readMemAvailableGB (live /proc MemAvailable), so a box already running VMs
fails the allocation spuriously, breaking the preflight gate locally.
Pin hostVCPU/hostMemGB/reserved and nil readMemAvailableGB to a synthetic
host, matching the sibling Capacity_decreases_after_allocation subtest.
* fix(eks): route apiserver->pod traffic via agent tunnel (egress-selector cluster)
In the managed-CP topology the apiserver VM sits in the system CP VPC with
no route to the worker pod network in the unpeered customer VPC. k3s default
egress-selector-mode=agent tunnels only apiserver->kubelet, so kubectl
logs/exec work but apiserver->pod traffic (admission webhooks, aggregated
APIs) is attempted directly and times out -- the aws-load-balancer-controller
webhooks fail with context deadline exceeded, blocking Service/Ingress
creation cluster-wide.
Set egress-selector-mode=cluster so apiserver->pod/service traffic routes
through the agent's outbound tunnel, selected by the node hosting the
destination, without requiring inbound CP->worker reachability.
* feat: bump aws-load-balancer-controller addon bundle to v2.11.0
v2.8.1 predates the ALBSingleSubnet feature gate, so single-AZ spinifex
clusters fail Ingress reconcile (subnet resolver dedups to 1 < required 2)
and never provision an ALB. v2.11.0 honors --feature-gates=ALBSingleSubnet=true.
Replace the baked bundle: new CRDs (ingressclassparams + targetgroupbindings),
controller manifest, and image tag v2.11.0, re-applying the spinifex deltas
(IRSA SA annotation, node-minted webhook serving-cert secret, gateway-CA
secret, gateway aws-api-endpoints, caBundle on every webhook clientConfig,
alb IngressClass). Pick up the new ClusterRole configmaps rule. Drop v2.8.1.
* fix: point LBC at the gateway via AWS_ENDPOINT_URL (sdk-v2)
LBC v2.11 uses aws-sdk-go-v2, which keys --aws-api-endpoints on SDK
service IDs (e.g. 'Elastic Load Balancing v2'), not the legacy lowercase
names the bundle passed. Those overrides were silently ignored, so every
non-STS call (ec2/elbv2/acm) went to real AWS and failed TLS against the
gateway CA -- no ALB could provision.
Inject AWS_ENDPOINT_URL in the shared IRSA env block (sdk-v2 global
base-endpoint override; AWS_ENDPOINT_URL_STS still takes precedence for
STS) and drop the dead --aws-api-endpoints flag from the controller args.
* fix: synthesize taggable ARN for default listener rule
Gateway DescribeRules returned the synthetic IsDefault rule with a null
RuleArn. LBC read the arn-less default rule after creating a listener and
issued DescribeTags with an empty resource ARN, which the gateway rejected
with 400 MissingParameter, aborting reconcile.
Set a deterministic RuleArn on the default rule, resolve it back to its
parent listener in DescribeTags (empty tags) and DescribeRules by-arn.
Also disable shield/waf/wafv2 in the LBC bundle to drop unsupported calls.
* feat: inject ELB subnets into alb IngressClassParams for single-AZ ALB
LBC's tag auto-discovery never threads the ALBSingleSubnet feature gate, so a
single-AZ cluster (subnets sharing one AZ) dedups to 1<2 subnets and aborts
Ingress reconcile. Only the explicit-subnet path honors the gate.
Resolve the cluster's ELB-eligible subnets, dedup to one per AZ, and surface
them as EKS_ELB_SUBNET_IDS. The addon sync renders them into the alb
IngressClassParams .spec.subnets.ids so every Ingress takes the explicit path.
An empty set falls back to auto-discovery, preserving the native UX.
* fix: route LBC service clients to gateway via --aws-api-endpoints, link IngressClassParams
Live verification showed two gaps in the LBC addon bundle:
LBC installs a per-client BaseEndpoint resolver, so the global AWS_ENDPOINT_URL
is ignored for the ec2/elbv2/acm clients (calls hit real AWS, failing TLS).
Redirect them with --aws-api-endpoints keyed on the aws-sdk-go-v2 service IDs
(EC2, "Elastic Load Balancing v2", ACM, STS) rather than the legacy lowercase
names the flag never matched. Drop the misleading AWS_ENDPOINT_URL env; the
web-identity STS client still resolves via AWS_ENDPOINT_URL_STS.
The injected IngressClassParams was orphaned: the alb IngressClass had no
.spec.parameters, so LBC fell back to subnet auto-discovery (which fails on a
single-AZ cluster). Link the IngressClass to the IngressClassParams so the
explicit-subnet path is taken.
* fix: stamp EKS-parity providerID on worker nodes for LBC ip targets
k3s registers workers with providerID k3s://<name>, which the AWS Load
Balancer Controller rejects (providerID invalid for EC2 instances) when
resolving nodes for ip-mode target registration. Resolve the instance-id and
AZ from IMDSv2 at agent boot and set --kubelet-arg=provider-id=aws:///<az>/<id>
so LBC can map the node to its EC2 instance. On any IMDS miss the agent keeps
the default providerID and still starts.
* fix(elbv2): honor SubnetMappings in CreateLoadBalancer
LBC supplies ALB subnets via SubnetMappings, not the plain Subnets list.
createLoadBalancer only read input.Subnets, so ENI allocation was skipped,
VpcId/AZ stayed empty, and the HAProxy data-plane VM never launched — the
ALB went active but could not serve traffic. Add flattenSubnetIDs to read
both sources and reuse it in createLoadBalancer and desiredSubnetSet.
* fix(vm): capture GCInterval before spawning GC goroutine
TestStart_SweepsImmediatelyThenStopsOnCtxCancel mutates the package global
GCInterval and restores it in a defer, racing the Start goroutine's read at
time.NewTicker(GCInterval) under -race. Read GCInterval on the caller's
goroutine into a local before spawning so the global access is ordered with
the test's defer; the goroutine touches only the captured local.
321 lines
14 KiB
Bash
321 lines
14 KiB
Bash
#!/bin/sh
|
|
set -eu
|
|
|
|
# mulga-eks-addon-sync — renders Spinifex managed-addon bundles into the K3s
|
|
# auto-deploy dir and reports per-addon delivery status back to the spinifex
|
|
# control plane. The host-side daemon stages each CreateAddon as a manifest
|
|
# descriptor in KV; the VM cannot read KV directly, so this agent pulls the
|
|
# staged set through the AWS gateway (eks-gateway-fetch, SigV4) and renders the
|
|
# baked bundle for each one. Status flows back via eks-gateway-publish on the
|
|
# "addon" channel, where the reconciler CASes the AddonRecord CREATING→ACTIVE.
|
|
#
|
|
# Pull (host→VM): GET /clusters/{cluster}/internal-addons/{accountId}
|
|
# Report (VM→host): eks.addon.{accountID}.{clusterName}.status
|
|
#
|
|
# Runs server-role only (it owns the single auto-deploy dir); enabled by
|
|
# eks-node-role.sh. Loops every ADDON_SYNC_INTERVAL seconds in service mode,
|
|
# one-shot otherwise.
|
|
|
|
ENVFILE=/etc/spinifex-eks/first-boot.env
|
|
[ -f "${ENVFILE}" ] || { logger -t mulga-eks-addon-sync "${ENVFILE} missing — exiting"; exit 0; }
|
|
# set -a so the sourced creds are exported to the eks-gateway-{fetch,publish}
|
|
# children, which read EKS_ACCOUNT_ID etc. from their environment.
|
|
set -a
|
|
# shellcheck disable=SC1090
|
|
. "${ENVFILE}"
|
|
set +a
|
|
|
|
: "${EKS_GATEWAY_URL:?}"
|
|
: "${EKS_ADDON_GATEWAY_URL:?}"
|
|
: "${EKS_ACCESS_KEY:?}"
|
|
: "${EKS_SECRET_KEY:?}"
|
|
: "${EKS_ACCOUNT_ID:?}"
|
|
: "${EKS_CLUSTER_NAME:?}"
|
|
|
|
KUBECONFIG=${KUBECONFIG:-/etc/rancher/k3s/k3s.yaml}
|
|
export KUBECONFIG
|
|
|
|
# Baked bundles live at BAKED_DIR/<addon>/<version>/*.yaml (air-gapped, shipped
|
|
# in the AMI). Rendered output lands in the K3s auto-deploy dir under a stable
|
|
# per-addon filename so GC can find orphans this agent owns.
|
|
BAKED_DIR=/usr/share/spinifex-eks/addons
|
|
DEPLOY_DIR=/var/lib/rancher/k3s/server/manifests
|
|
RENDER_PREFIX=spinifex-addon-
|
|
# Per-cluster admission-webhook serving certs are minted once and cached here so
|
|
# the rendered manifest is byte-stable across sync ticks (a churning cert would
|
|
# re-apply the addon every tick).
|
|
WEBHOOK_CERT_DIR=/var/lib/spinifex-eks/webhook-certs
|
|
|
|
# log mirrors to syslog (on-VM /var/log) and, best-effort, to the serial
|
|
# console — the host captures ttyS0 to a per-instance log, so this is the only
|
|
# delivery diagnostics channel reachable from the host (the VM has no
|
|
# host-routable address). Keep messages terse; one line per sync outcome.
|
|
log() {
|
|
logger -t mulga-eks-addon-sync "$*"
|
|
echo "[mulga-eks-addon-sync] $*" > /dev/console 2>/dev/null || true
|
|
}
|
|
|
|
# report PHASE ADDON VERSION [MESSAGE] — publish one addon status report.
|
|
report() {
|
|
_phase=$1; _addon=$2; _version=$3; _msg=${4:-}
|
|
printf '{"addon":"%s","version":"%s","phase":"%s","message":"%s","ts":%s}' \
|
|
"${_addon}" "${_version}" "${_phase}" "${_msg}" "$(date +%s)" \
|
|
| eks-gateway-publish -channel addon 2>&1 | logger -t mulga-eks-addon-sync || true
|
|
}
|
|
|
|
# render_addon ADDON VERSION ROLE_ARN — render the baked bundle into the
|
|
# auto-deploy dir, substituting the IRSA role ARN plus the cluster/gateway
|
|
# placeholders an in-cluster AWS-SDK addon (e.g. the LB controller) needs to
|
|
# reach the gateway. Returns non-zero (and reports failed) when the baked bundle
|
|
# for ADDON/VERSION is absent. Placeholders not present in a bundle are no-ops.
|
|
render_addon() {
|
|
_addon=$1; _version=$2; _role=$3
|
|
_src="${BAKED_DIR}/${_addon}/${_version}"
|
|
_dst="${DEPLOY_DIR}/${RENDER_PREFIX}${_addon}.yaml"
|
|
|
|
if [ ! -d "${_src}" ]; then
|
|
log "no baked bundle for ${_addon}/${_version} (looked in ${_src})"
|
|
report failed "${_addon}" "${_version}" "no baked bundle for version ${_version}"
|
|
return 1
|
|
fi
|
|
|
|
# Gateway CA as single-line base64 for a kube Secret .data entry the addon
|
|
# mounts as AWS_CA_BUNDLE. tr -d so busybox base64 (no -w0) stays one line.
|
|
_ca_b64=""
|
|
if [ -n "${EKS_GATEWAY_CA:-}" ] && [ -f "${EKS_GATEWAY_CA}" ]; then
|
|
_ca_b64=$(base64 < "${EKS_GATEWAY_CA}" | tr -d '\n')
|
|
fi
|
|
|
|
# Webhook serving cert (opt-in): a bundle ships webhook.conf with its service
|
|
# SANs when it runs an admission webhook. Mint/reuse a self-signed cert on the
|
|
# node (the AMI has no openssl) and embed it as the Secret + every caBundle.
|
|
_wh_ca=""; _wh_crt=""; _wh_key=""
|
|
if [ -f "${_src}/webhook.conf" ]; then
|
|
WEBHOOK_CN=""; WEBHOOK_DNS=""
|
|
# shellcheck disable=SC1090,SC1091
|
|
. "${_src}/webhook.conf"
|
|
if [ -n "${WEBHOOK_CN}" ] && [ -n "${WEBHOOK_DNS}" ]; then
|
|
_wh_err="${_dst}.whcert.$$"
|
|
if ! _wh_out=$(eks-webhook-cert -dir "${WEBHOOK_CERT_DIR}/${_addon}" \
|
|
-cn "${WEBHOOK_CN}" -dns "${WEBHOOK_DNS}" 2>"${_wh_err}"); then
|
|
log "addon ${_addon}: webhook cert gen failed: $(tr '\n' ' ' < "${_wh_err}")"
|
|
rm -f "${_wh_err}"
|
|
report failed "${_addon}" "${_version}" "webhook cert generation failed"
|
|
return 1
|
|
fi
|
|
rm -f "${_wh_err}"
|
|
_wh_ca=$(printf '%s' "${_wh_out}" | cut -f1)
|
|
_wh_crt=$(printf '%s' "${_wh_out}" | cut -f2)
|
|
_wh_key=$(printf '%s' "${_wh_out}" | cut -f3)
|
|
fi
|
|
fi
|
|
|
|
# Reusable IRSA (web-identity) injection. A bundle opts in by placing the
|
|
# {{IRSA_ENV}} / {{IRSA_VOLUME}} / {{IRSA_VOLUME_MOUNT}} markers on their own
|
|
# line; awk swaps each for the standard block, then the sed pass fills the
|
|
# nested role/region/gateway placeholders. Bundles without the markers are
|
|
# untouched. Blocks assume a Deployment pod spec (env 8sp, volumes 6sp,
|
|
# volumeMounts 8sp). The token (aud sts.amazonaws.com) + role ARN + gateway
|
|
# STS endpoint/CA are what AssumeRoleWithWebIdentity at the gateway needs.
|
|
# AWS_ENDPOINT_URL_STS points the web-identity credential STS client (built
|
|
# from the standard config, before per-service overrides) at the gateway.
|
|
# The LBC service clients (ec2/elbv2/acm) ignore the global AWS_ENDPOINT_URL
|
|
# because LBC installs its own per-client BaseEndpoint resolver; those are
|
|
# redirected via the bundle's --aws-api-endpoints flag, which keys on the
|
|
# SDK-v2 service IDs (EC2, "Elastic Load Balancing v2", ACM), not the legacy
|
|
# lowercase names. AWS_CA_BUNDLE makes every client trust the gateway CA.
|
|
_irsa_env=' - name: AWS_ROLE_ARN
|
|
value: "{{SERVICE_ACCOUNT_ROLE_ARN}}"
|
|
- name: AWS_WEB_IDENTITY_TOKEN_FILE
|
|
value: /var/run/secrets/eks.amazonaws.com/serviceaccount/token
|
|
- name: AWS_STS_REGIONAL_ENDPOINTS
|
|
value: regional
|
|
- name: AWS_REGION
|
|
value: "{{AWS_REGION}}"
|
|
- name: AWS_DEFAULT_REGION
|
|
value: "{{AWS_REGION}}"
|
|
- name: AWS_ENDPOINT_URL_STS
|
|
value: "{{GATEWAY_ENDPOINT}}"
|
|
- name: AWS_CA_BUNDLE
|
|
value: /etc/spinifex/gateway-ca/ca.pem'
|
|
_irsa_vol=' - name: aws-iam-token
|
|
projected:
|
|
defaultMode: 420
|
|
sources:
|
|
- serviceAccountToken:
|
|
audience: sts.amazonaws.com
|
|
expirationSeconds: 86400
|
|
path: token
|
|
- name: spinifex-gateway-ca
|
|
secret:
|
|
defaultMode: 420
|
|
secretName: spinifex-gateway-ca'
|
|
_irsa_mnt=' - name: aws-iam-token
|
|
mountPath: /var/run/secrets/eks.amazonaws.com/serviceaccount
|
|
readOnly: true
|
|
- name: spinifex-gateway-ca
|
|
mountPath: /etc/spinifex/gateway-ca
|
|
readOnly: true'
|
|
|
|
# IngressClassParams spec injection. EKS_ELB_SUBNET_IDS is the cluster's
|
|
# ELB-eligible subnets (CSV, deduped to one per AZ by the daemon). Inject them
|
|
# as explicit .spec.subnets.ids so every Ingress takes LBC's explicit-subnet
|
|
# path, the only one that honors ALBSingleSubnet; a single-AZ cluster otherwise
|
|
# fails reconcile. Empty value drops the marker line, leaving tag auto-discovery.
|
|
_icp_spec=''
|
|
if [ -n "${EKS_ELB_SUBNET_IDS:-}" ]; then
|
|
_icp_spec=' spec:
|
|
subnets:
|
|
ids:'
|
|
_oldifs=$IFS
|
|
IFS=','
|
|
for _sn in ${EKS_ELB_SUBNET_IDS}; do
|
|
[ -n "${_sn}" ] || continue
|
|
_icp_spec="${_icp_spec}
|
|
- ${_sn}"
|
|
done
|
|
IFS=$_oldifs
|
|
fi
|
|
|
|
_tmp="${_dst}.tmp.$$"
|
|
: > "${_tmp}"
|
|
for _f in "${_src}"/*.yaml; do
|
|
[ -e "${_f}" ] || continue
|
|
awk -v env="${_irsa_env}" -v vol="${_irsa_vol}" -v mnt="${_irsa_mnt}" -v icp="${_icp_spec}" '
|
|
index($0, "{{IRSA_ENV}}") { print env; next }
|
|
index($0, "{{IRSA_VOLUME_MOUNT}}") { print mnt; next }
|
|
index($0, "{{IRSA_VOLUME}}") { print vol; next }
|
|
index($0, "{{ELB_INGRESS_PARAMS_SPEC}}") { if (icp != "") print icp; next }
|
|
{ print }
|
|
' "${_f}" \
|
|
| sed -e "s|{{SERVICE_ACCOUNT_ROLE_ARN}}|${_role}|g" \
|
|
-e "s|{{CLUSTER_NAME}}|${EKS_CLUSTER_NAME}|g" \
|
|
-e "s|{{AWS_REGION}}|${EKS_REGION:-}|g" \
|
|
-e "s|{{AWS_VPC_ID}}|${EKS_VPC_ID:-}|g" \
|
|
-e "s|{{GATEWAY_ENDPOINT}}|${EKS_ADDON_GATEWAY_URL}|g" \
|
|
-e "s|{{GATEWAY_CA_PEM_B64}}|${_ca_b64}|g" \
|
|
-e "s|{{WEBHOOK_CA_B64}}|${_wh_ca}|g" \
|
|
-e "s|{{WEBHOOK_TLS_CRT_B64}}|${_wh_crt}|g" \
|
|
-e "s|{{WEBHOOK_TLS_KEY_B64}}|${_wh_key}|g" \
|
|
>> "${_tmp}"
|
|
printf '\n---\n' >> "${_tmp}"
|
|
done
|
|
# Idempotent: only replace the auto-deploy file when the rendered content
|
|
# actually changed. An unconditional mv bumps the file mtime every tick,
|
|
# which K3s' auto-deploy controller treats as a change and re-applies —
|
|
# clearing the Addon CR's .status.gvks each time, so the same-tick readiness
|
|
# check never observes a settled apply and the add-on stays CREATING forever.
|
|
if [ -e "${_dst}" ] && cmp -s "${_tmp}" "${_dst}"; then
|
|
rm -f "${_tmp}"
|
|
return 0
|
|
fi
|
|
mv -f "${_tmp}" "${_dst}"
|
|
return 0
|
|
}
|
|
|
|
# addon_gvks ADDON — echoes the GVKs K3s' auto-deploy controller applied for the
|
|
# rendered manifest, empty until it has applied the objects. A non-empty value
|
|
# means delivery succeeded. Addon-agnostic — confirms delivery, not deep per-pod
|
|
# health (a later hardening item).
|
|
#
|
|
# K3s (v1.32.x) records the applied GVKs in the addon.k3s.cattle.io/gvks
|
|
# *annotation* (e.g. "/v1, Kind=Namespace;/v1, Kind=ConfigMap"); the Addon CR
|
|
# carries no .status subresource. The CR is matched by .spec.source (the
|
|
# absolute path of the rendered file) rather than a guessed CR name, since K3s
|
|
# derives the name from the path with its own transform.
|
|
addon_gvks() {
|
|
_want="${DEPLOY_DIR}/${RENDER_PREFIX}$1.yaml"
|
|
kubectl -n kube-system get addon -o json 2>/dev/null \
|
|
| jq -r --arg src "${_want}" \
|
|
'.items[] | select(.spec.source==$src)
|
|
| .metadata.annotations["addon.k3s.cattle.io/gvks"] // ""' 2>/dev/null \
|
|
| grep -v '^$' | head -1 || true
|
|
}
|
|
|
|
# diag_addon ADDON — one-shot console dump of the live K3s state for a stuck
|
|
# add-on, so a delivery that never reaches ready is diagnosable from the
|
|
# host-captured serial console (the VM is otherwise unreachable). Fires once per
|
|
# process via a sentinel to avoid spamming every tick.
|
|
diag_addon() {
|
|
[ -f /tmp/.addon-diag-"$1" ] && return 0
|
|
: > /tmp/.addon-diag-"$1"
|
|
{
|
|
echo "----- addon-diag ${1} -----"
|
|
echo "[deploy dir]"; ls -l "${DEPLOY_DIR}" 2>&1
|
|
echo "[rendered file head]"; head -40 "${DEPLOY_DIR}/${RENDER_PREFIX}${1}.yaml" 2>&1
|
|
echo "[all addon CRs]"; kubectl get addon -A 2>&1
|
|
echo "[addon CR ${RENDER_PREFIX}${1} -o yaml]"; kubectl -n kube-system get addon "${RENDER_PREFIX}${1}" -o yaml 2>&1
|
|
echo "[namespace ${1}]"; kubectl get ns "${1}" 2>&1
|
|
echo "[configmaps in ${1}]"; kubectl -n "${1}" get cm 2>&1
|
|
echo "----- end addon-diag ${1} -----"
|
|
} > /dev/console 2>&1 || true
|
|
}
|
|
|
|
sync_once() {
|
|
_staged=$(mktemp)
|
|
_ferr=$(mktemp)
|
|
if ! eks-gateway-fetch -resource addons > "${_staged}" 2>"${_ferr}"; then
|
|
log "fetch staged addons failed — keeping current state: $(tr '\n' ' ' < "${_ferr}")"
|
|
rm -f "${_staged}" "${_ferr}"
|
|
return 0
|
|
fi
|
|
rm -f "${_ferr}"
|
|
|
|
# Names currently staged, for the GC pass below.
|
|
_names=$(cut -f1 "${_staged}")
|
|
_count=$(grep -c . "${_staged}" 2>/dev/null || echo 0)
|
|
log "fetch ok: ${_count} staged addon(s)"
|
|
|
|
# Render/refresh each staged addon and report its phase.
|
|
while IFS=$(printf '\t') read -r _addon _version _role _cfg_b64; do
|
|
[ -n "${_addon}" ] || continue
|
|
: "${_cfg_b64:-}" # config values reserved for per-addon templating (follow-up)
|
|
if render_addon "${_addon}" "${_version}" "${_role}"; then
|
|
_gvks=$(addon_gvks "${_addon}")
|
|
if [ -n "${_gvks}" ]; then
|
|
log "addon ${_addon}/${_version}: applied + ready (gvks=${_gvks})"
|
|
report ready "${_addon}" "${_version}"
|
|
else
|
|
log "addon ${_addon}/${_version}: applied, not ready yet (no K3s Addon CR for ${RENDER_PREFIX}${_addon}.yaml with populated .status.gvks)"
|
|
diag_addon "${_addon}"
|
|
report applied "${_addon}" "${_version}"
|
|
fi
|
|
fi
|
|
done < "${_staged}"
|
|
rm -f "${_staged}"
|
|
|
|
# GC: remove rendered manifests this agent owns whose addon is no longer
|
|
# staged (DeleteAddon removed its descriptor). K3s does NOT delete the applied
|
|
# objects when the manifest file disappears, so delete them explicitly.
|
|
for _file in "${DEPLOY_DIR}/${RENDER_PREFIX}"*.yaml; do
|
|
[ -e "${_file}" ] || continue
|
|
_base=$(basename "${_file}")
|
|
_name=${_base#"${RENDER_PREFIX}"}
|
|
_name=${_name%.yaml}
|
|
if ! printf '%s\n' "${_names}" | grep -qx "${_name}"; then
|
|
log "unstaging addon ${_name} (no longer staged)"
|
|
# Copy the manifest aside, drop the original so K3s stops tracking it
|
|
# (and never re-applies), then delete the objects it rendered.
|
|
# --wait=false so namespace finalizers don't stall the sync loop.
|
|
_gc=$(mktemp)
|
|
cp "${_file}" "${_gc}"
|
|
rm -f "${_file}"
|
|
kubectl delete -f "${_gc}" --ignore-not-found --wait=false 2>&1 \
|
|
| logger -t mulga-eks-addon-sync || true
|
|
rm -f "${_gc}"
|
|
fi
|
|
done
|
|
}
|
|
|
|
mkdir -p "${DEPLOY_DIR}"
|
|
|
|
if [ -n "${ADDON_SYNC_INTERVAL:-}" ] && [ "${ADDON_SYNC_INTERVAL}" -gt 0 ] 2>/dev/null; then
|
|
log "starting: cluster=${EKS_CLUSTER_NAME} account=${EKS_ACCOUNT_ID} interval=${ADDON_SYNC_INTERVAL}s gateway=${EKS_GATEWAY_URL}"
|
|
while true; do
|
|
sync_once
|
|
sleep "${ADDON_SYNC_INTERVAL}"
|
|
done
|
|
else
|
|
sync_once
|
|
fi
|