Skip to content

Commit ab606aa

Browse files
committed
feat: Use separate superpositions for serving RC and dashboard
1 parent 41d1b1c commit ab606aa

8 files changed

Lines changed: 250 additions & 43 deletions

File tree

airborne_server/.env.example

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
# Superposition configuration
22
SUPERPOSITION_URL=http://localhost:8080
3+
SUPERPOSITION_RC_URL=http://localhost:8080
34
SUPERPOSITION_ORG_ID=get-org-id-from-superposition
5+
ENABLE_AUTHENTICATED_SUPERPOSITION=false
6+
SUPERPOSITION_TOKEN=
7+
SUPERPOSITION_USER_TOKEN=
8+
SUPERPOSITION_ORG_TOKEN=
9+
SUPERPOSITION_RC_USER_TOKEN=
10+
SUPERPOSITION_RC_ORG_TOKEN=
411

512
# Keycloak settings
613
KEYCLOAK_URL=http://localhost:8180

airborne_server/README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,12 @@ The server relies on a set of environment variables for its configuration. These
296296
- `KEYCLOAK_REALM`: Keycloak realm name.
297297
- `KEYCLOAK_PUBLIC_KEY`: Public key for validating JWTs issued by Keycloak.
298298
- `SUPERPOSITION_URL`: URL of the Superposition service.
299+
- `SUPERPOSITION_RC_URL`: URL of the Superposition service used by RC (`/release`) endpoints. Defaults to `SUPERPOSITION_URL` when unset.
300+
- `ENABLE_AUTHENTICATED_SUPERPOSITION`: Enables cookie-based auth for Superposition SDK requests.
301+
- `SUPERPOSITION_USER_TOKEN`: User token for Superposition auth cookie (`user=...`).
302+
- `SUPERPOSITION_ORG_TOKEN`: Org token for Superposition auth cookie (`org_<SUPERPOSITION_ORG_ID>=...`).
303+
- `SUPERPOSITION_RC_USER_TOKEN`: RC-specific user token for auth cookie; defaults to `SUPERPOSITION_USER_TOKEN` when unset.
304+
- `SUPERPOSITION_RC_ORG_TOKEN`: RC-specific org token for auth cookie; defaults to `SUPERPOSITION_ORG_TOKEN` when unset.
299305
- `SUPERPOSITION_ORG_ID`: The organization ID within Superposition used by the server.
300306
- `AWS_BUCKET`: Name of the S3 bucket for storing package assets.
301307
- `PUBLIC_ENDPOINT`: The public-facing URL for accessing assets stored in S3.

airborne_server/scripts/encrypt-envs.sh

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,87 @@ shell_quote() {
6666
printf "'%s'" "$(printf '%s' "$value" | sed "s/'/'\\\\''/g")"
6767
}
6868

69+
strip_shell_quotes() {
70+
local value="$1"
71+
72+
if [[ ${#value} -ge 2 ]]; then
73+
if [[ "${value#\'}" != "$value" ]] && [[ "${value%\'}" != "$value" ]]; then
74+
value="${value#\'}"
75+
value="${value%\'}"
76+
value="${value//\'\\\'\'/\'}"
77+
elif [[ "${value#\"}" != "$value" ]] && [[ "${value%\"}" != "$value" ]]; then
78+
value="${value#\"}"
79+
value="${value%\"}"
80+
fi
81+
fi
82+
83+
printf '%s' "$value"
84+
}
85+
86+
read_env_raw() {
87+
local key="$1"
88+
local value
89+
value=$(grep "^${key}=" "$ENV_FILE" 2>/dev/null | cut -d'=' -f2- | head -1)
90+
strip_shell_quotes "$value"
91+
}
92+
93+
is_value_empty() {
94+
local value="$1"
95+
case "$value" in
96+
""|"''"|'""')
97+
return 0
98+
;;
99+
*)
100+
return 1
101+
;;
102+
esac
103+
}
104+
105+
upsert_env_raw() {
106+
local key="$1"
107+
local raw_value="$2"
108+
local tmp_file
109+
110+
tmp_file=$(mktemp "${TMPDIR:-/tmp}/airborne-env.XXXXXX")
111+
112+
if [[ -f "$ENV_FILE" ]]; then
113+
awk -v key="$key" -v value="$raw_value" '
114+
BEGIN { updated = 0 }
115+
index($0, key "=") == 1 { print key "=" value; updated = 1; next }
116+
{ print }
117+
END { if (!updated) print key "=" value }
118+
' "$ENV_FILE" > "$tmp_file"
119+
else
120+
printf "%s=%s\n" "$key" "$raw_value" > "$tmp_file"
121+
fi
122+
123+
mv "$tmp_file" "$ENV_FILE"
124+
}
125+
126+
sync_superposition_rc_env_defaults() {
127+
local superposition_url superposition_rc_url
128+
local superposition_user_token superposition_rc_user_token
129+
local superposition_org_token superposition_rc_org_token
130+
131+
superposition_url=$(read_env_raw "SUPERPOSITION_URL")
132+
superposition_rc_url=$(read_env_raw "SUPERPOSITION_RC_URL")
133+
if is_value_empty "$superposition_rc_url"; then
134+
upsert_env_raw "SUPERPOSITION_RC_URL" "$superposition_url"
135+
fi
136+
137+
superposition_user_token=$(read_env_raw "SUPERPOSITION_USER_TOKEN")
138+
superposition_rc_user_token=$(read_env_raw "SUPERPOSITION_RC_USER_TOKEN")
139+
if is_value_empty "$superposition_rc_user_token"; then
140+
upsert_env_raw "SUPERPOSITION_RC_USER_TOKEN" "$superposition_user_token"
141+
fi
142+
143+
superposition_org_token=$(read_env_raw "SUPERPOSITION_ORG_TOKEN")
144+
superposition_rc_org_token=$(read_env_raw "SUPERPOSITION_RC_ORG_TOKEN")
145+
if is_value_empty "$superposition_rc_org_token"; then
146+
upsert_env_raw "SUPERPOSITION_RC_ORG_TOKEN" "$superposition_org_token"
147+
fi
148+
}
149+
69150
# Function to encrypt a value using AES-GCM
70151
encrypt_value() {
71152
local value="$1"
@@ -105,6 +186,8 @@ SECRETS=(
105186
"SUPERPOSITION_TOKEN"
106187
"SUPERPOSITION_USER_TOKEN"
107188
"SUPERPOSITION_ORG_TOKEN"
189+
"SUPERPOSITION_RC_USER_TOKEN"
190+
"SUPERPOSITION_RC_ORG_TOKEN"
108191
"GOOGLE_SERVICE_ACCOUNT_KEY"
109192
)
110193

@@ -123,6 +206,9 @@ if [[ ! -f "$ENV_FILE" ]]; then
123206
cp "$ENV_EXAMPLE" "$ENV_FILE"
124207
fi
125208

209+
# Keep local RC Superposition vars aligned with existing Superposition vars when unset.
210+
sync_superposition_rc_env_defaults
211+
126212
if [[ "$PLAINTEXT_MODE" == true ]]; then
127213
echo -e "${GREEN}✅ Plaintext .env file ready at: $ENV_FILE${NC}"
128214
echo ""

airborne_server/scripts/init-localstack.sh

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,56 @@ upsert_env_var() {
133133
mv "$tmp_file" "$file"
134134
}
135135

136+
read_env_value() {
137+
local key="$1"
138+
local value=""
139+
140+
if [ -f ".env" ]; then
141+
value=$(grep "^${key}=" ".env" 2>/dev/null | cut -d'=' -f2- | head -1)
142+
value=$(strip_shell_quotes "$value")
143+
fi
144+
145+
echo "$value"
146+
}
147+
148+
is_value_empty() {
149+
local value="$1"
150+
case "$value" in
151+
""|"''"|'""')
152+
return 0
153+
;;
154+
*)
155+
return 1
156+
;;
157+
esac
158+
}
159+
160+
sync_superposition_rc_env_defaults() {
161+
local superposition_url superposition_rc_url
162+
local superposition_user_token superposition_rc_user_token
163+
local superposition_org_token superposition_rc_org_token
164+
165+
superposition_url=$(read_env_value "SUPERPOSITION_URL")
166+
superposition_rc_url=$(read_env_value "SUPERPOSITION_RC_URL")
167+
if is_value_empty "$superposition_rc_url"; then
168+
upsert_env_var ".env" "SUPERPOSITION_RC_URL" "$superposition_url"
169+
fi
170+
171+
superposition_user_token=$(read_env_value "SUPERPOSITION_USER_TOKEN")
172+
superposition_rc_user_token=$(read_env_value "SUPERPOSITION_RC_USER_TOKEN")
173+
if is_value_empty "$superposition_rc_user_token"; then
174+
upsert_env_var ".env" "SUPERPOSITION_RC_USER_TOKEN" "$superposition_user_token"
175+
fi
176+
177+
superposition_org_token=$(read_env_value "SUPERPOSITION_ORG_TOKEN")
178+
superposition_rc_org_token=$(read_env_value "SUPERPOSITION_RC_ORG_TOKEN")
179+
if is_value_empty "$superposition_rc_org_token"; then
180+
upsert_env_var ".env" "SUPERPOSITION_RC_ORG_TOKEN" "$superposition_org_token"
181+
fi
182+
}
183+
184+
sync_superposition_rc_env_defaults
185+
136186
echo "${YELLOW}☁️ AWS Endpoint:${NC} ${GREEN}${AWS_ENDPOINT_URL}${NC}"
137187
echo "${YELLOW}🪣 S3 Bucket:${NC} ${GREEN}${AWS_BUCKET}${NC}"
138188
echo "${YELLOW}🌍 Region:${NC} ${GREEN}${AWS_REGION}${NC}"
@@ -162,7 +212,16 @@ aws --endpoint-url=${AWS_ENDPOINT_URL} s3 mb s3://$AWS_BUCKET >/dev/null 2>&1 ||
162212
echo "${GREEN}✅ S3 bucket ready: $AWS_BUCKET${NC}"
163213

164214
# Variables that need encryption/processing
165-
SENSITIVE_VARS=("DB_PASSWORD" "DB_MIGRATION_PASSWORD" "KEYCLOAK_SECRET")
215+
SENSITIVE_VARS=(
216+
"DB_PASSWORD"
217+
"DB_MIGRATION_PASSWORD"
218+
"KEYCLOAK_SECRET"
219+
"SUPERPOSITION_TOKEN"
220+
"SUPERPOSITION_USER_TOKEN"
221+
"SUPERPOSITION_ORG_TOKEN"
222+
"SUPERPOSITION_RC_USER_TOKEN"
223+
"SUPERPOSITION_RC_ORG_TOKEN"
224+
)
166225

167226
# Get values from .env.example or .env.generated
168227
get_value() {

airborne_server/src/config.rs

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,13 @@ pub struct AppConfig {
5252

5353
// Superposition settings
5454
pub superposition_url: String,
55+
pub superposition_rc_url: String,
5556
pub superposition_org_id: String,
5657
pub superposition_token: Option<String>,
5758
pub superposition_user_token: Option<String>,
5859
pub superposition_org_token: Option<String>,
60+
pub superposition_rc_user_token: Option<String>,
61+
pub superposition_rc_org_token: Option<String>,
5962
pub enable_authenticated_superposition: bool,
6063

6164
// Feature flags
@@ -135,6 +138,17 @@ impl AppConfig {
135138
let get_optional =
136139
|name: &str| -> Option<String> { env::var(name).ok().filter(|v| !v.is_empty()) };
137140

141+
let superposition_url = get_env("SUPERPOSITION_URL", None)?;
142+
let superposition_rc_url =
143+
get_optional("SUPERPOSITION_RC_URL").unwrap_or_else(|| superposition_url.clone());
144+
let superposition_token = get_optional_secret("SUPERPOSITION_TOKEN")?;
145+
let superposition_user_token = get_optional_secret("SUPERPOSITION_USER_TOKEN")?;
146+
let superposition_org_token = get_optional_secret("SUPERPOSITION_ORG_TOKEN")?;
147+
let superposition_rc_user_token = get_optional_secret("SUPERPOSITION_RC_USER_TOKEN")?
148+
.or_else(|| superposition_user_token.clone());
149+
let superposition_rc_org_token = get_optional_secret("SUPERPOSITION_RC_ORG_TOKEN")?
150+
.or_else(|| superposition_org_token.clone());
151+
138152
Ok(AppConfig {
139153
// Server settings
140154
port: parse_env("PORT", 8081),
@@ -168,11 +182,14 @@ impl AppConfig {
168182
keycloak_public_key: get_env("KEYCLOAK_PUBLIC_KEY", None)?,
169183

170184
// Superposition settings
171-
superposition_url: get_env("SUPERPOSITION_URL", None)?,
185+
superposition_url,
186+
superposition_rc_url,
172187
superposition_org_id: get_env("SUPERPOSITION_ORG_ID", None)?,
173-
superposition_token: get_optional_secret("SUPERPOSITION_TOKEN")?,
174-
superposition_user_token: get_optional_secret("SUPERPOSITION_USER_TOKEN")?,
175-
superposition_org_token: get_optional_secret("SUPERPOSITION_ORG_TOKEN")?,
188+
superposition_token,
189+
superposition_user_token,
190+
superposition_org_token,
191+
superposition_rc_user_token,
192+
superposition_rc_org_token,
176193
enable_authenticated_superposition: parse_env(
177194
"ENABLE_AUTHENTICATED_SUPERPOSITION",
178195
false,

airborne_server/src/main.rs

Lines changed: 62 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,8 @@ async fn main() -> std::io::Result<()> {
145145
let secret = app_config.keycloak_secret.clone();
146146
let superposition_token = app_config.superposition_token.clone().unwrap_or_default();
147147

148-
let cac_url = app_config.superposition_url.clone();
148+
let dashboard_superposition_url = app_config.superposition_url.clone();
149+
let rc_superposition_url = app_config.superposition_rc_url.clone();
149150
let superposition_org_id_env = app_config.superposition_org_id.clone();
150151

151152
let env = types::Environment {
@@ -202,44 +203,73 @@ async fn main() -> std::io::Result<()> {
202203
hub = Some(Sheets::new(client, gcp_auth));
203204
}
204205

205-
let superposition_client = if app_config.enable_authenticated_superposition {
206-
let superposition_user_token = app_config.superposition_user_token.clone().expect(
207-
"SUPERPOSITION_USER_TOKEN must be set when ENABLE_AUTHENTICATED_SUPERPOSITION=true",
208-
);
209-
let superposition_org_token = app_config.superposition_org_token.clone().expect(
210-
"SUPERPOSITION_ORG_TOKEN must be set when ENABLE_AUTHENTICATED_SUPERPOSITION=true",
211-
);
212-
213-
// Inject Auth cookie for Superposition SDK calls
214-
let cookie_interceptor = CookieIntercept::new(format!(
215-
"user={}; org_{}={}",
216-
superposition_user_token, superposition_org_id_env, superposition_org_token,
217-
));
218-
219-
superposition_sdk::Client::from_conf(
220-
SrsConfig::builder()
221-
.endpoint_url(cac_url.clone())
222-
.behavior_version_latest()
223-
.bearer_token(superposition_token.into())
224-
.interceptor(cookie_interceptor)
225-
.build(),
226-
)
227-
} else {
228-
superposition_sdk::Client::from_conf(
229-
SrsConfig::builder()
230-
.endpoint_url(cac_url.clone())
231-
.behavior_version_latest()
232-
.bearer_token(superposition_token.into())
233-
.build(),
234-
)
235-
};
206+
let create_superposition_client =
207+
|endpoint_url: String,
208+
user_token: Option<String>,
209+
org_token: Option<String>,
210+
user_token_env_hint: &str,
211+
org_token_env_hint: &str| {
212+
if app_config.enable_authenticated_superposition {
213+
let superposition_user_token = user_token.unwrap_or_else(|| {
214+
panic!(
215+
"{} must be set when ENABLE_AUTHENTICATED_SUPERPOSITION=true",
216+
user_token_env_hint
217+
)
218+
});
219+
let superposition_org_token = org_token.unwrap_or_else(|| {
220+
panic!(
221+
"{} must be set when ENABLE_AUTHENTICATED_SUPERPOSITION=true",
222+
org_token_env_hint
223+
)
224+
});
225+
226+
// Inject Auth cookie for Superposition SDK calls
227+
let cookie_interceptor = CookieIntercept::new(format!(
228+
"user={}; org_{}={}",
229+
superposition_user_token, superposition_org_id_env, superposition_org_token,
230+
));
231+
232+
superposition_sdk::Client::from_conf(
233+
SrsConfig::builder()
234+
.endpoint_url(endpoint_url)
235+
.behavior_version_latest()
236+
.bearer_token(superposition_token.clone().into())
237+
.interceptor(cookie_interceptor)
238+
.build(),
239+
)
240+
} else {
241+
superposition_sdk::Client::from_conf(
242+
SrsConfig::builder()
243+
.endpoint_url(endpoint_url)
244+
.behavior_version_latest()
245+
.bearer_token(superposition_token.clone().into())
246+
.build(),
247+
)
248+
}
249+
};
250+
251+
let superposition_client = create_superposition_client(
252+
dashboard_superposition_url,
253+
app_config.superposition_user_token.clone(),
254+
app_config.superposition_org_token.clone(),
255+
"SUPERPOSITION_USER_TOKEN",
256+
"SUPERPOSITION_ORG_TOKEN",
257+
);
258+
let rc_superposition_client = create_superposition_client(
259+
rc_superposition_url,
260+
app_config.superposition_rc_user_token.clone(),
261+
app_config.superposition_rc_org_token.clone(),
262+
"SUPERPOSITION_RC_USER_TOKEN or SUPERPOSITION_USER_TOKEN",
263+
"SUPERPOSITION_RC_ORG_TOKEN or SUPERPOSITION_ORG_TOKEN",
264+
);
236265

237266
let app_state = Arc::new(types::AppState {
238267
env: env.clone(),
239268
db_pool: pool,
240269
s3_client: aws_s3_client,
241270
cf_client: aws_cloudfront_client,
242271
superposition_client,
272+
rc_superposition_client,
243273
sheets_hub: hub,
244274
});
245275

0 commit comments

Comments
 (0)