1010# 2. For each deployment ID:
1111# a. Copies outpostrc hash → {id}:outpost:installation_id (string key)
1212# b. Copies outpost:migration:{name} hashes → {id}:outpost:migration:{name}
13- # c. Copies outpost:migration:{name}:run:* hashes → {id}:outpost:migration:{name}:run:*
1413# 3. Optionally deletes the old shared keys (with --cleanup flag)
1514#
15+ # All writes are batched into a single redis-cli --pipe call for performance.
16+ # This avoids per-command TLS handshake overhead on remote Redis instances.
17+ #
1618# Usage:
1719# # Dry run (default) - shows what would be done
1820# ./migrate.sh
@@ -65,15 +67,21 @@ rcli() {
6567 redis-cli " ${args[@]} " " $@ "
6668}
6769
70+ rcli_pipe () {
71+ local args=(-h " $REDIS_HOST " -p " $REDIS_PORT " --no-auth-warning --pipe)
72+ [[ -n " $REDIS_USER " ]] && args+=(--user " $REDIS_USER " )
73+ [[ -n " $REDIS_PASS " ]] && args+=(--pass " $REDIS_PASS " )
74+ [[ " $REDIS_TLS " == " 1" ]] && args+=(--tls)
75+ redis-cli " ${args[@]} "
76+ }
77+
6878# --- Helpers ---
6979log () { echo " [INFO] $* " ; }
7080warn () { echo " [WARN] $* " >&2 ; }
71- dry () { if $DRY_RUN ; then echo " [DRY] $* " ; else echo " [EXEC] $* " ; fi ; }
7281
7382# --- Step 1: Discover deployment IDs ---
7483log " Discovering deployment IDs..."
7584
76- # Scan for keys matching *:tenant:* and extract unique prefixes
7785DEPLOYMENT_IDS=()
7886cursor=0
7987while true ; do
@@ -82,7 +90,6 @@ while true; do
8290 keys=$( echo " $result " | tail -n +2)
8391
8492 for key in $keys ; do
85- # Extract deployment ID (everything before first ":tenant:")
8693 id=" ${key%%: tenant:* } "
8794 if [[ -n " $id " && " $id " != " $key " ]]; then
8895 DEPLOYMENT_IDS+=(" $id " )
@@ -108,15 +115,15 @@ log "Found ${#DEPLOYMENT_IDS[@]} deployment(s): ${DEPLOYMENT_IDS[*]}"
108115log " "
109116log " Checking shared control plane keys..."
110117
111- # Check outpostrc
112118INSTALLATION_ID=$( rcli HGET outpostrc installation 2> /dev/null || echo " " )
113119if [[ -n " $INSTALLATION_ID " ]]; then
114120 log " outpostrc -> installation = $INSTALLATION_ID "
115121else
116122 log " outpostrc -> (not found, each deployment will generate its own on startup)"
117123fi
118124
119- # Find all outpost:migration:* keys (excluding run history)
125+ # Collect migration keys and their hash data
126+ declare -A MIGRATION_DATA
120127MIGRATION_KEYS=()
121128cursor=0
122129while true ; do
@@ -126,6 +133,7 @@ while true; do
126133
127134 for key in $keys ; do
128135 MIGRATION_KEYS+=(" $key " )
136+ MIGRATION_DATA[" $key " ]=$( rcli HGETALL " $key " 2> /dev/null | tr ' \n' ' ' )
129137 done
130138
131139 if [[ " $cursor " == " 0" ]]; then
@@ -139,96 +147,76 @@ for key in "${MIGRATION_KEYS[@]}"; do
139147 log " $key -> $status "
140148done
141149
142- # Check lock key
143150LOCK_EXISTS=$( rcli EXISTS " .outpost:migration:lock" 2> /dev/null || echo " 0" )
144151if [[ " $LOCK_EXISTS " == " 1" ]]; then
145152 warn " .outpost:migration:lock exists! A migration may be running."
146153fi
147154
148- # --- Step 3: Migrate for each deployment ---
155+ # --- Step 3: Build pipeline commands ---
149156log " "
150- log " === Starting migration ==="
157+ log " === Building migration commands ==="
151158
152- for DEPLOY_ID in " ${DEPLOYMENT_IDS[@]} " ; do
153- log " "
154- log " --- Deployment: $DEPLOY_ID ---"
159+ PIPE_FILE=$( mktemp)
160+ CMD_COUNT=0
155161
156- # 3a. Installation ID
162+ for DEPLOY_ID in " ${DEPLOYMENT_IDS[@]} " ; do
163+ # Installation ID (SET is idempotent)
157164 if [[ -n " $INSTALLATION_ID " ]]; then
158165 target_key=" ${DEPLOY_ID} :outpost:installation_id"
159- existing=$( rcli GET " $target_key " 2> /dev/null || echo " " )
160- if [[ -n " $existing " ]]; then
161- log " $target_key already exists ($existing ), skipping"
162- else
163- dry " SET $target_key $INSTALLATION_ID "
164- if ! $DRY_RUN ; then
165- rcli SET " $target_key " " $INSTALLATION_ID " > /dev/null
166- fi
167- fi
166+ echo " SET ${target_key} ${INSTALLATION_ID} " >> " $PIPE_FILE "
167+ (( CMD_COUNT++ ))
168168 fi
169169
170- # 3b. Migration status keys
170+ # Migration status keys (HSET is idempotent)
171171 for old_key in " ${MIGRATION_KEYS[@]} " ; do
172- # Derive the new key: outpost:migration:X -> {id}:outpost:migration:X
173172 new_key=" ${DEPLOY_ID} :${old_key} "
174-
175- existing=$( rcli EXISTS " $new_key " 2> /dev/null || echo " 0" )
176- if [[ " $existing " == " 1" ]]; then
177- log " $new_key already exists, skipping"
178- continue
179- fi
180-
181- # Copy the hash field by field
182- dry " COPY $old_key -> $new_key "
183- if ! $DRY_RUN ; then
184- # Read all field-value pairs and write them to the new key
185- hset_args=()
186- while IFS= read -r field && IFS= read -r value; do
187- hset_args+=(" $field " " $value " )
188- done < <( rcli HGETALL " $old_key " 2> /dev/null)
189-
190- if [[ ${# hset_args[@]} -gt 0 ]]; then
191- rcli HSET " $new_key " " ${hset_args[@]} " > /dev/null
192- else
193- warn " $old_key has no fields, skipping"
194- fi
195-
196- # Verify the copy
197- if [[ $( rcli EXISTS " $new_key " 2> /dev/null) != " 1" ]]; then
198- warn " FAILED to copy $old_key -> $new_key "
199- fi
173+ hash_data=" ${MIGRATION_DATA[$old_key]} "
174+ if [[ -n " $hash_data " ]]; then
175+ echo " HSET ${new_key} ${hash_data} " >> " $PIPE_FILE "
176+ (( CMD_COUNT++ ))
200177 fi
201178 done
202179done
203180
204- # --- Step 4: Cleanup (optional) ---
205- if $CLEANUP && ! $DRY_RUN ; then
206- log " "
207- log " === Cleaning up old shared keys ==="
208-
181+ # Cleanup commands
182+ if $CLEANUP ; then
209183 if [[ -n " $INSTALLATION_ID " ]]; then
210- log " DEL outpostrc"
211- rcli DEL outpostrc > /dev/null
184+ echo " DEL outpostrc" >> " $PIPE_FILE "
185+ (( CMD_COUNT ++ ))
212186 fi
213-
214187 for key in " ${MIGRATION_KEYS[@]} " ; do
215- log " DEL $key "
216- rcli DEL " $key " > /dev/null
188+ echo " DEL ${ key} " >> " $PIPE_FILE "
189+ (( CMD_COUNT ++ ))
217190 done
218-
219191 if [[ " $LOCK_EXISTS " == " 1" ]]; then
220- log " DEL .outpost:migration:lock"
221- rcli DEL " .outpost:migration:lock " > /dev/null
192+ echo " DEL .outpost:migration:lock" >> " $PIPE_FILE "
193+ (( CMD_COUNT ++ ))
222194 fi
223- elif $CLEANUP && $DRY_RUN ; then
224- log " "
225- log " === Cleanup (dry run) ==="
226- log " Would delete: outpostrc, .outpost:migration:lock, and ${# MIGRATION_KEYS[@]} migration key(s)"
227195fi
228196
229- log " "
197+ log " ${CMD_COUNT} commands for ${# DEPLOYMENT_IDS[@]} deployments"
198+
199+ # --- Step 4: Execute or dry-run ---
230200if $DRY_RUN ; then
201+ log " "
202+ log " === Dry run — commands that would be sent ==="
203+ cat " $PIPE_FILE "
204+ log " "
231205 log " Dry run complete. Re-run with --apply to execute."
232206else
207+ log " "
208+ log " === Executing pipeline ==="
209+ rcli_pipe < " $PIPE_FILE "
210+ log " "
211+
212+ # Verify
213+ migrated=$( rcli --scan --pattern " *:outpost:installation_id" | wc -l | tr -d ' ' )
214+ shared_remaining=$( rcli KEYS " outpostrc" 2> /dev/null | wc -l | tr -d ' ' )
215+ log " Deployments with installation_id: ${migrated} "
216+ if $CLEANUP ; then
217+ log " Shared keys remaining: ${shared_remaining} (should be 0)"
218+ fi
233219 log " Migration complete."
234220fi
221+
222+ rm -f " $PIPE_FILE "
0 commit comments