fix: move DB seeding from bootstrap script to Java ApplicationRunner
The bootstrap script runs before cameleer-saas (Flyway), so tenant tables don't exist yet. Moved DB seeding to BootstrapDataSeeder ApplicationRunner which runs after Flyway migrations complete. Reads bootstrap JSON and creates tenant/environment/license if missing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -359,42 +359,7 @@ if [ -n "$ADMIN_USER_ID" ] && [ "$ADMIN_USER_ID" != "null" ]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# PHASE 7: Seed cameleer_saas database
|
# PHASE 7: Configure cameleer3-server OIDC
|
||||||
# ============================================================
|
|
||||||
|
|
||||||
log "Seeding cameleer_saas database..."
|
|
||||||
pgpass
|
|
||||||
|
|
||||||
# Insert tenant (idempotent via ON CONFLICT)
|
|
||||||
TENANT_UUID=$(psql -h "$PG_HOST" -U "$PG_USER" -d "$PG_DB_SAAS" -t -A -c "
|
|
||||||
INSERT INTO tenants (id, name, slug, tier, status, logto_org_id, created_at, updated_at)
|
|
||||||
VALUES (gen_random_uuid(), '$TENANT_NAME', '$TENANT_SLUG', 'LOW', 'ACTIVE', '$ORG_ID', NOW(), NOW())
|
|
||||||
ON CONFLICT (slug) DO UPDATE SET logto_org_id = EXCLUDED.logto_org_id
|
|
||||||
RETURNING id;
|
|
||||||
")
|
|
||||||
log "Tenant ID: $TENANT_UUID"
|
|
||||||
|
|
||||||
# Insert default environment
|
|
||||||
psql -h "$PG_HOST" -U "$PG_USER" -d "$PG_DB_SAAS" -c "
|
|
||||||
INSERT INTO environments (id, tenant_id, slug, display_name, bootstrap_token, status, created_at, updated_at)
|
|
||||||
VALUES (gen_random_uuid(), '$TENANT_UUID', 'default', 'Default', '$BOOTSTRAP_TOKEN', 'ACTIVE', NOW(), NOW())
|
|
||||||
ON CONFLICT (tenant_id, slug) DO NOTHING;
|
|
||||||
" >/dev/null 2>&1
|
|
||||||
log "Default environment seeded."
|
|
||||||
|
|
||||||
# Insert license
|
|
||||||
psql -h "$PG_HOST" -U "$PG_USER" -d "$PG_DB_SAAS" -c "
|
|
||||||
INSERT INTO licenses (id, tenant_id, tier, features, limits, issued_at, expires_at, token, created_at)
|
|
||||||
SELECT gen_random_uuid(), '$TENANT_UUID', 'LOW',
|
|
||||||
'{\"topology\": true, \"lineage\": false, \"correlation\": false, \"debugger\": false, \"replay\": false}'::jsonb,
|
|
||||||
'{\"max_agents\": 3, \"retention_days\": 7, \"max_environments\": 1}'::jsonb,
|
|
||||||
NOW(), NOW() + INTERVAL '365 days', 'bootstrap-license', NOW()
|
|
||||||
WHERE NOT EXISTS (SELECT 1 FROM licenses WHERE tenant_id = '$TENANT_UUID');
|
|
||||||
" >/dev/null 2>&1
|
|
||||||
log "License seeded."
|
|
||||||
|
|
||||||
# ============================================================
|
|
||||||
# PHASE 8: Configure cameleer3-server OIDC
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|
||||||
SERVER_HEALTHY=$(curl -sf "${SERVER_ENDPOINT}/api/v1/health" 2>/dev/null && echo "yes" || echo "no")
|
SERVER_HEALTHY=$(curl -sf "${SERVER_ENDPOINT}/api/v1/health" 2>/dev/null && echo "yes" || echo "no")
|
||||||
@@ -459,8 +424,9 @@ cat > "$BOOTSTRAP_FILE" <<EOF
|
|||||||
"tradAppId": "$TRAD_ID",
|
"tradAppId": "$TRAD_ID",
|
||||||
"apiResourceIndicator": "$API_RESOURCE_INDICATOR",
|
"apiResourceIndicator": "$API_RESOURCE_INDICATOR",
|
||||||
"organizationId": "$ORG_ID",
|
"organizationId": "$ORG_ID",
|
||||||
|
"tenantName": "$TENANT_NAME",
|
||||||
"tenantSlug": "$TENANT_SLUG",
|
"tenantSlug": "$TENANT_SLUG",
|
||||||
"tenantId": "$TENANT_UUID",
|
"bootstrapToken": "$BOOTSTRAP_TOKEN",
|
||||||
"platformAdminUser": "$SAAS_ADMIN_USER",
|
"platformAdminUser": "$SAAS_ADMIN_USER",
|
||||||
"tenantAdminUser": "$TENANT_ADMIN_USER"
|
"tenantAdminUser": "$TENANT_ADMIN_USER"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,123 @@
|
|||||||
|
package net.siegeln.cameleer.saas.config;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import net.siegeln.cameleer.saas.tenant.TenantEntity;
|
||||||
|
import net.siegeln.cameleer.saas.tenant.TenantRepository;
|
||||||
|
import net.siegeln.cameleer.saas.tenant.TenantStatus;
|
||||||
|
import net.siegeln.cameleer.saas.tenant.Tier;
|
||||||
|
import net.siegeln.cameleer.saas.environment.EnvironmentEntity;
|
||||||
|
import net.siegeln.cameleer.saas.environment.EnvironmentRepository;
|
||||||
|
import net.siegeln.cameleer.saas.environment.EnvironmentStatus;
|
||||||
|
import net.siegeln.cameleer.saas.license.LicenseEntity;
|
||||||
|
import net.siegeln.cameleer.saas.license.LicenseRepository;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.boot.ApplicationArguments;
|
||||||
|
import org.springframework.boot.ApplicationRunner;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.temporal.ChronoUnit;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class BootstrapDataSeeder implements ApplicationRunner {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(BootstrapDataSeeder.class);
|
||||||
|
private static final String BOOTSTRAP_FILE = "/data/bootstrap/logto-bootstrap.json";
|
||||||
|
|
||||||
|
private final TenantRepository tenantRepository;
|
||||||
|
private final EnvironmentRepository environmentRepository;
|
||||||
|
private final LicenseRepository licenseRepository;
|
||||||
|
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
|
||||||
|
public BootstrapDataSeeder(TenantRepository tenantRepository,
|
||||||
|
EnvironmentRepository environmentRepository,
|
||||||
|
LicenseRepository licenseRepository) {
|
||||||
|
this.tenantRepository = tenantRepository;
|
||||||
|
this.environmentRepository = environmentRepository;
|
||||||
|
this.licenseRepository = licenseRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run(ApplicationArguments args) {
|
||||||
|
File file = new File(BOOTSTRAP_FILE);
|
||||||
|
if (!file.exists()) {
|
||||||
|
log.info("No bootstrap file found at {} — skipping data seeding", BOOTSTRAP_FILE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
JsonNode config = objectMapper.readTree(file);
|
||||||
|
String orgId = getField(config, "organizationId");
|
||||||
|
String tenantName = getField(config, "tenantName");
|
||||||
|
String tenantSlug = getField(config, "tenantSlug");
|
||||||
|
String bootstrapToken = getField(config, "bootstrapToken");
|
||||||
|
|
||||||
|
if (orgId == null || tenantSlug == null) {
|
||||||
|
log.info("Bootstrap file missing organizationId or tenantSlug — skipping");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if tenant already exists
|
||||||
|
if (tenantRepository.existsBySlug(tenantSlug)) {
|
||||||
|
log.info("Tenant '{}' already exists — skipping bootstrap seeding", tenantSlug);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("Seeding bootstrap tenant '{}'...", tenantSlug);
|
||||||
|
|
||||||
|
// Create tenant
|
||||||
|
TenantEntity tenant = new TenantEntity();
|
||||||
|
tenant.setName(tenantName != null ? tenantName : "Example Tenant");
|
||||||
|
tenant.setSlug(tenantSlug);
|
||||||
|
tenant.setTier(Tier.LOW);
|
||||||
|
tenant.setStatus(TenantStatus.ACTIVE);
|
||||||
|
tenant.setLogtoOrgId(orgId);
|
||||||
|
tenant = tenantRepository.save(tenant);
|
||||||
|
log.info("Created tenant: {} ({})", tenant.getSlug(), tenant.getId());
|
||||||
|
|
||||||
|
// Create default environment
|
||||||
|
EnvironmentEntity env = new EnvironmentEntity();
|
||||||
|
env.setTenantId(tenant.getId());
|
||||||
|
env.setSlug("default");
|
||||||
|
env.setDisplayName("Default");
|
||||||
|
env.setBootstrapToken(bootstrapToken != null ? bootstrapToken : "default-bootstrap-token");
|
||||||
|
env.setStatus(EnvironmentStatus.ACTIVE);
|
||||||
|
environmentRepository.save(env);
|
||||||
|
log.info("Created default environment for tenant '{}'", tenantSlug);
|
||||||
|
|
||||||
|
// Create license
|
||||||
|
LicenseEntity license = new LicenseEntity();
|
||||||
|
license.setTenantId(tenant.getId());
|
||||||
|
license.setTier("LOW");
|
||||||
|
license.setFeatures(Map.of(
|
||||||
|
"topology", true,
|
||||||
|
"lineage", false,
|
||||||
|
"correlation", false,
|
||||||
|
"debugger", false,
|
||||||
|
"replay", false
|
||||||
|
));
|
||||||
|
license.setLimits(Map.of(
|
||||||
|
"max_agents", 3,
|
||||||
|
"retention_days", 7,
|
||||||
|
"max_environments", 1
|
||||||
|
));
|
||||||
|
license.setIssuedAt(Instant.now());
|
||||||
|
license.setExpiresAt(Instant.now().plus(365, ChronoUnit.DAYS));
|
||||||
|
license.setToken("bootstrap-license");
|
||||||
|
licenseRepository.save(license);
|
||||||
|
log.info("Created license for tenant '{}'", tenantSlug);
|
||||||
|
|
||||||
|
log.info("Bootstrap data seeding complete.");
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to seed bootstrap data: {}", e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getField(JsonNode node, String field) {
|
||||||
|
return node.has(field) ? node.get(field).asText() : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user