Skip to content

Commit 24f7dad

Browse files
ericfitzclaude
andauthored
feat(terraform): add multi-cloud infrastructure with OCI free tier support (#127)
* feat(terraform): add multi-cloud infrastructure modules for OCI deployment Add Terraform modules for deploying TMI to Oracle Cloud Infrastructure: - Network module: VCN, subnets, gateways, NSGs - Database module: Autonomous Database Free Tier with private endpoint - Secrets module: OCI Vault with secrets and IAM policies - Logging module: Log groups, service connectors, alarms - Compute module: Container instances and load balancer Environment configuration for OCI Free Tier included with sensible defaults. Makefile targets added: - tf-init, tf-plan, tf-apply, tf-destroy - deploy-oci, deploy-oci-plan Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat(logging): add cloud logging support with OCI integration Add CloudLogWriter interface and OCI Logging implementation: - CloudLogWriter: Generic interface for cloud logging providers - CloudLogHandler: slog.Handler that writes to both local and cloud - OCICloudWriter: OCI Logging service implementation - Batched async writes with configurable buffer - Automatic flush on timeout or buffer full - Health tracking and graceful degradation - NoopCloudWriter: For testing or when cloud logging disabled Cloud logging is additive - local file/console logging continues to work independently. If cloud logging fails, only cloud writes are affected; local logging remains uninterrupted. Configuration options: - CloudWriter: Provider instance (nil to disable) - CloudLogLevel: Minimum level for cloud (defaults to local level) - CloudLogBufferSize: Async buffer size (default: 1000) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(terraform/oci): update modules for OCI free tier deployment - Add TMI_DATABASE_URL environment variable for Oracle ADB connection - Fix OCI provider v7.x API changes (ip_addresses format) - Update database module for ECPU model (remove cpu_core_count) - Make private endpoint conditional for free tier (not supported) - Disable bucket versioning for log archive (conflicts with retention) - Add sensitive flag to outputs containing credentials - Comment out container logging (incorrect service name) - Add region variable to database module for wallet PAR URL These changes enable successful deployment of TMI on OCI Always Free tier resources including Oracle Autonomous Database, Container Instances, Load Balancer, and OCI Vault for secrets management. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(oci): improve container startup and debugging for OCI deployment - Add bash and unzip packages to Oracle container image - Fix slogging to respect TMI_LOG_DIR during early initialization - Add JWT secret variable and env config to compute module - Add HTTPS egress rule for ADB Free Tier public endpoint - Increase health check initial delay to 60s - Improve entrypoint script with detailed debugging output - Remove Docker HEALTHCHECK (conflicts with OCI health check) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(oci): fix Oracle wallet path and Redis connection for OCI deployment - Fix entrypoint script sed pattern to properly update sqlnet.ora DIRECTORY path using non-greedy regex [^"]* instead of .* - Change REDIS_URL to TMI_REDIS_URL to match app config expectations - Remove Redis password from URL since distroless Redis container doesn't support password auth (TODO for future fix) The container now successfully connects to Oracle ADB using wallet authentication and to Redis for caching/session management. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(oci): fix SystemSetting migration and Redis auth for Oracle ADB - Remove GORM default tag from SystemSetting.SettingType that caused Oracle migration to fail silently (unquoted 'string' parsed as identifier) - Add Redis password to TMI_REDIS_URL in terraform config for proper authentication with Oracle Linux Redis container - Add --platform linux/amd64 flag to container build script for OCI Container Instances which use AMD64 shapes These fixes resolve ORA-00942 (table not found) errors during server startup and NOAUTH authentication errors when connecting to Redis. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat(oci): add OCI cloud logging with Resource Principal auth - Add cloud logging initialization from environment variables in main.go - Support Resource Principal authentication for OCI Container Instances - Wire OCI Logging service to compute module via oci_log_id variable - Add cloud_log_level configuration for filtering cloud logs - Fix dynamic group matching rule to use 'computecontainerinstance' resource type Cloud logging is now automatically enabled when TMI_CLOUD_LOG_ENABLED=true with TMI_OCI_LOG_ID set. Uses Resource Principal for Container Instances, falls back to Instance Principal for VMs, then to ~/.oci/config for local dev. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(version): reset version to 1.1.0 for feature branch These are fixes to the terraform-multi-cloud feature, not a new feature. Skipping post-commit hook to prevent auto-increment. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(slogging): escape message in JSON fallback to prevent injection CodeQL alert #1221 - unsafe quoting vulnerability in OCI cloud writer. Use json.Marshal to properly escape special characters in the message before embedding in the JSON fallback string. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(slogging): use json.Marshal for entire fallback to satisfy CodeQL Avoid string interpolation entirely by marshaling a map struct. CodeQL go/unsafe-quoting doesn't trust fmt.Sprintf even with pre-marshaled values. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 332702d commit 24f7dad

36 files changed

+3875
-142
lines changed

.gitignore

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@
5151
!.dosu.yml
5252
!api-schema/
5353
!api-schema/**
54+
!terraform/
55+
!terraform/**
5456

5557
# heroku
5658
!.godir
@@ -98,3 +100,10 @@ Wallet_*.zip
98100
config-development-oci.yml
99101
config-test-integration-oci.yml
100102
scripts/oci-env.sh
103+
104+
# Terraform (sensitive state and variable files)
105+
**/*.tfstate
106+
**/*.tfstate.*
107+
**/terraform.tfvars
108+
**/tfplan
109+
**/.terraform/

.version

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
22
"major": 1,
3-
"minor": 0,
4-
"patch": 1
3+
"minor": 1,
4+
"patch": 2
55
}

Dockerfile.server-oracle

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,11 @@ RUN microdnf -y update && \
131131
# Required for Oracle Instant Client
132132
libaio \
133133
# CA certificates for TLS connections
134-
ca-certificates && \
134+
ca-certificates \
135+
# Required for wallet extraction
136+
unzip \
137+
# Required for entrypoint script
138+
bash && \
135139
microdnf clean all && \
136140
rm -rf /var/cache/yum
137141

@@ -155,6 +159,12 @@ RUN mkdir -p /wallet && chown tmi:tmi /wallet
155159
COPY --from=builder /app/tmiserver /tmiserver
156160
RUN chmod +x /tmiserver && chown tmi:tmi /tmiserver
157161

162+
# Create entrypoint script that extracts wallet if needed
163+
# Extract to /tmp/wallet since the mounted /wallet is read-only
164+
# Uses 'env' with exec to ensure environment variables are passed to the process
165+
RUN printf '#!/bin/bash\necho "=== TMI Container Entrypoint ==="\necho "Date: $(date)"\necho "User: $(whoami)"\nWALLET_DIR="/tmp/wallet"\necho ""\necho "=== Checking /wallet mount ==="\nif [ -d "/wallet" ]; then\n ls -la /wallet/ 2>&1\nelse\n echo "ERROR: /wallet directory does not exist!"\nfi\necho ""\nif [ -f "/wallet/wallet.zip" ]; then\n echo "=== Extracting wallet to $WALLET_DIR ==="\n mkdir -p "$WALLET_DIR"\n if unzip -o /wallet/wallet.zip -d "$WALLET_DIR" 2>&1; then\n echo "Wallet extracted successfully"\n # Fix sqlnet.ora to use correct wallet path\n if [ -f "$WALLET_DIR/sqlnet.ora" ]; then\n echo "=== Fixing sqlnet.ora wallet path ==="\n echo "Before:"\n cat "$WALLET_DIR/sqlnet.ora"\n # Update DIRECTORY path in sqlnet.ora to point to extracted wallet\n # Original: DIRECTORY="?/network/admin" -> New: DIRECTORY="/tmp/wallet"\n sed -i "s|DIRECTORY=\\\"[^\\\"]*\\\"|DIRECTORY=\\\"$WALLET_DIR\\\"|g" "$WALLET_DIR/sqlnet.ora"\n echo "After:"\n cat "$WALLET_DIR/sqlnet.ora"\n fi\n echo "=== Wallet contents ==="\n ls -la "$WALLET_DIR"\n else\n echo "ERROR: Failed to extract wallet!"\n fi\nelse\n echo "WARNING: /wallet/wallet.zip not found"\nfi\necho ""\necho "=== Starting TMI Server ==="\nif [ -d "$WALLET_DIR" ] && [ "$(ls -A $WALLET_DIR 2>/dev/null)" ]; then\n echo "Using wallet at $WALLET_DIR"\n exec env TNS_ADMIN="$WALLET_DIR" TMI_ORACLE_WALLET_LOCATION="$WALLET_DIR" /tmiserver "$@"\nelse\n echo "Starting without wallet"\n exec /tmiserver "$@"\nfi\n' > /entrypoint.sh && \
166+
chmod +x /entrypoint.sh && chown tmi:tmi /entrypoint.sh
167+
158168
# Set working directory
159169
WORKDIR /
160170

@@ -171,12 +181,11 @@ ENV GOLANG_PROTOBUF_REGISTRATION_CONFLICT=warn
171181
# Oracle wallet location (mount point)
172182
ENV TNS_ADMIN=/wallet
173183

174-
# Health check using curl (available in oraclelinux:9-slim)
175-
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
176-
CMD curl -f http://localhost:8080/ || exit 1
184+
# Note: Health check is configured in OCI Container Instances, not in Dockerfile
185+
# This avoids conflict between Docker HEALTHCHECK and OCI health check
177186

178187
# Run as non-root user
179188
USER tmi:tmi
180189

181-
# Run the application
182-
ENTRYPOINT ["/tmiserver"]
190+
# Run the application with wallet extraction
191+
ENTRYPOINT ["/entrypoint.sh"]

Makefile

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1096,6 +1096,81 @@ start-containers-environment:
10961096
# Shorthand for all container operations
10971097
build-containers-all: build-containers report-containers
10981098

1099+
# ============================================================================
1100+
# TERRAFORM INFRASTRUCTURE MANAGEMENT
1101+
# ============================================================================
1102+
1103+
.PHONY: tf-init tf-plan tf-apply tf-destroy tf-validate tf-fmt tf-output
1104+
1105+
# Terraform environment selection (default: oci-free-tier)
1106+
TF_ENV ?= oci-free-tier
1107+
TF_DIR := terraform/environments/$(TF_ENV)
1108+
1109+
# Check if Terraform is installed
1110+
tf-check:
1111+
@command -v terraform >/dev/null 2>&1 || { \
1112+
echo -e "$(RED)[ERROR]$(NC) Terraform is not installed."; \
1113+
echo -e "$(BLUE)[INFO]$(NC) Install with: brew install terraform"; \
1114+
exit 1; \
1115+
}
1116+
1117+
# Initialize Terraform
1118+
tf-init: tf-check ## Initialize Terraform for the selected environment (TF_ENV=oci-free-tier)
1119+
$(call log_info,Initializing Terraform in $(TF_DIR)...)
1120+
@cd $(TF_DIR) && terraform init
1121+
$(call log_success,Terraform initialized successfully)
1122+
1123+
# Validate Terraform configuration
1124+
tf-validate: tf-init ## Validate Terraform configuration
1125+
$(call log_info,Validating Terraform configuration...)
1126+
@cd $(TF_DIR) && terraform validate
1127+
$(call log_success,Terraform configuration is valid)
1128+
1129+
# Format Terraform files
1130+
tf-fmt: ## Format Terraform files
1131+
$(call log_info,Formatting Terraform files...)
1132+
@terraform fmt -recursive terraform/
1133+
$(call log_success,Terraform files formatted)
1134+
1135+
# Plan Terraform changes
1136+
tf-plan: tf-init ## Plan Terraform changes (shows what will be created/modified)
1137+
$(call log_info,Planning Terraform changes for $(TF_ENV)...)
1138+
@cd $(TF_DIR) && terraform plan -out=tfplan
1139+
$(call log_success,Terraform plan saved to $(TF_DIR)/tfplan)
1140+
1141+
# Apply Terraform changes
1142+
tf-apply: tf-init ## Apply Terraform changes (creates/modifies infrastructure)
1143+
$(call log_info,Applying Terraform changes for $(TF_ENV)...)
1144+
@cd $(TF_DIR) && terraform apply
1145+
$(call log_success,Terraform apply completed)
1146+
1147+
# Apply Terraform from saved plan
1148+
tf-apply-plan: tf-init ## Apply Terraform from saved plan file
1149+
$(call log_info,Applying Terraform plan for $(TF_ENV)...)
1150+
@cd $(TF_DIR) && terraform apply tfplan
1151+
$(call log_success,Terraform apply completed)
1152+
1153+
# Show Terraform outputs
1154+
tf-output: ## Show Terraform outputs
1155+
$(call log_info,Terraform outputs for $(TF_ENV)...)
1156+
@cd $(TF_DIR) && terraform output
1157+
1158+
# Destroy Terraform infrastructure
1159+
tf-destroy: ## Destroy Terraform infrastructure (DESTRUCTIVE!)
1160+
$(call log_warning,This will destroy all infrastructure in $(TF_ENV)!)
1161+
@cd $(TF_DIR) && terraform destroy
1162+
1163+
# OCI-specific deployment shortcuts
1164+
.PHONY: deploy-oci deploy-oci-plan
1165+
1166+
# Full OCI deployment
1167+
deploy-oci: TF_ENV=oci-free-tier
1168+
deploy-oci: tf-apply ## Deploy TMI to OCI Free Tier
1169+
1170+
# Plan OCI deployment
1171+
deploy-oci-plan: TF_ENV=oci-free-tier
1172+
deploy-oci-plan: tf-plan ## Plan TMI OCI Free Tier deployment
1173+
10991174
# ============================================================================
11001175
# PROMTAIL CONTAINER MANAGEMENT
11011176
# ============================================================================
@@ -1561,6 +1636,18 @@ help:
15611636
@echo " start-containers-environment - Start development with containers"
15621637
@echo " build-containers-all - Run full container build and report"
15631638
@echo ""
1639+
@echo "Terraform Infrastructure Management:"
1640+
@echo " tf-init - Initialize Terraform (TF_ENV=oci-free-tier)"
1641+
@echo " tf-validate - Validate Terraform configuration"
1642+
@echo " tf-fmt - Format all Terraform files"
1643+
@echo " tf-plan - Plan infrastructure changes"
1644+
@echo " tf-apply - Apply infrastructure changes"
1645+
@echo " tf-apply-plan - Apply from saved plan file"
1646+
@echo " tf-output - Show Terraform outputs"
1647+
@echo " tf-destroy - Destroy infrastructure (DESTRUCTIVE!)"
1648+
@echo " deploy-oci - Deploy TMI to OCI Free Tier"
1649+
@echo " deploy-oci-plan - Plan OCI Free Tier deployment"
1650+
@echo ""
15641651
@echo "SBOM Generation (Software Bill of Materials):"
15651652
@echo " generate-sbom - Generate SBOM for Go application (cyclonedx-gomod)"
15661653
@echo " generate-sbom-all - Generate all Go SBOMs (app + modules)"

api/config_handlers.go

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -309,11 +309,11 @@ func (s *Server) UpdateSystemSetting(c *gin.Context, key string) {
309309

310310
// Convert to model
311311
setting := models.SystemSetting{
312-
Key: key,
313-
Value: req.Value,
314-
Type: string(req.Type),
315-
ModifiedAt: time.Now(),
316-
ModifiedBy: modifiedBy,
312+
SettingKey: key,
313+
Value: req.Value,
314+
SettingType: string(req.Type),
315+
ModifiedAt: time.Now(),
316+
ModifiedBy: modifiedBy,
317317
}
318318
if req.Description != nil {
319319
setting.Description = req.Description
@@ -496,9 +496,9 @@ func (s *Server) MigrateSystemSettings(c *gin.Context, params MigrateSystemSetti
496496
// Create or update the setting
497497
description := ms.Description
498498
setting := models.SystemSetting{
499-
Key: ms.Key,
499+
SettingKey: ms.Key,
500500
Value: ms.Value,
501-
Type: ms.Type,
501+
SettingType: ms.Type,
502502
Description: &description,
503503
ModifiedAt: time.Now(),
504504
ModifiedBy: modifiedBy,
@@ -525,9 +525,9 @@ func (s *Server) MigrateSystemSettings(c *gin.Context, params MigrateSystemSetti
525525
// modelToAPISystemSetting converts a models.SystemSetting to an API SystemSetting
526526
func modelToAPISystemSetting(m models.SystemSetting) SystemSetting {
527527
setting := SystemSetting{
528-
Key: m.Key,
528+
Key: m.SettingKey,
529529
Value: m.Value,
530-
Type: SystemSettingType(m.Type),
530+
Type: SystemSettingType(m.SettingType),
531531
ModifiedAt: &m.ModifiedAt,
532532
}
533533
if m.Description != nil {

api/config_handlers_test.go

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ func (m *MockSettingsService) List(ctx context.Context) ([]models.SystemSetting,
6767
}
6868

6969
func (m *MockSettingsService) Set(ctx context.Context, setting *models.SystemSetting) error {
70-
m.settings[setting.Key] = setting
70+
m.settings[setting.SettingKey] = setting
7171
return nil
7272
}
7373

@@ -83,10 +83,10 @@ func (m *MockSettingsService) SeedDefaults(ctx context.Context) error {
8383
// Helper to add a setting to the mock
8484
func (m *MockSettingsService) AddSetting(key, value, settingType string) {
8585
m.settings[key] = &models.SystemSetting{
86-
Key: key,
87-
Value: value,
88-
Type: settingType,
89-
ModifiedAt: time.Now(),
86+
SettingKey: key,
87+
Value: value,
88+
SettingType: settingType,
89+
ModifiedAt: time.Now(),
9090
}
9191
}
9292

@@ -383,9 +383,9 @@ func TestModelToAPISystemSetting(t *testing.T) {
383383
now := time.Now()
384384

385385
model := models.SystemSetting{
386-
Key: "test.key",
386+
SettingKey: "test.key",
387387
Value: "test-value",
388-
Type: "string",
388+
SettingType: "string",
389389
Description: &description,
390390
ModifiedAt: now,
391391
ModifiedBy: &modifiedBy,
@@ -406,10 +406,10 @@ func TestModelToAPISystemSetting_NilOptionalFields(t *testing.T) {
406406
now := time.Now()
407407

408408
model := models.SystemSetting{
409-
Key: "test.key",
410-
Value: "test-value",
411-
Type: "int",
412-
ModifiedAt: now,
409+
SettingKey: "test.key",
410+
Value: "test-value",
411+
SettingType: "int",
412+
ModifiedAt: now,
413413
}
414414

415415
apiSetting := modelToAPISystemSetting(model)

api/models/system_setting.go

Lines changed: 24 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,17 @@ import (
99
// These settings can be modified at runtime without requiring server restart.
1010
// Settings are cached with short TTL for performance.
1111
type SystemSetting struct {
12-
Key string `gorm:"primaryKey;type:varchar(256)" json:"key"`
13-
Value string `gorm:"type:varchar(4000);not null" json:"value"`
14-
Type string `gorm:"type:varchar(50);not null;default:string" json:"type"` // "string", "int", "bool", "json"
12+
// SettingKey is the unique identifier for this setting (e.g., "rate_limit.requests_per_minute")
13+
// Named SettingKey instead of Key to avoid Oracle reserved word conflict
14+
SettingKey string `gorm:"column:setting_key;primaryKey;type:varchar(256)" json:"key"`
15+
Value string `gorm:"type:varchar(4000);not null" json:"value"`
16+
// SettingType stores the value type: "string", "int", "bool", "json"
17+
// Note: default tag removed for Oracle compatibility (unquoted string defaults cause syntax errors)
18+
SettingType string `gorm:"column:setting_type;type:varchar(50);not null" json:"type"`
1519
Description *string `gorm:"type:varchar(1024)" json:"description,omitempty"`
1620
ModifiedAt time.Time `gorm:"not null;autoUpdateTime" json:"modified_at"`
1721
ModifiedBy *string `gorm:"type:varchar(36)" json:"modified_by,omitempty"` // User InternalUUID
18-
19-
// Relationships
20-
Modifier *User `gorm:"foreignKey:ModifiedBy;references:InternalUUID" json:"-"`
22+
// Note: Foreign key relationship to User removed to avoid Oracle migration issues
2123
}
2224

2325
// TableName specifies the table name for SystemSetting
@@ -41,51 +43,51 @@ func DefaultSystemSettings() []SystemSetting {
4143

4244
return []SystemSetting{
4345
{
44-
Key: "rate_limit.requests_per_minute",
46+
SettingKey: "rate_limit.requests_per_minute",
4547
Value: "100",
46-
Type: SystemSettingTypeInt,
48+
SettingType: SystemSettingTypeInt,
4749
Description: defaultDescription("Maximum API requests per minute per user"),
4850
},
4951
{
50-
Key: "rate_limit.requests_per_hour",
52+
SettingKey: "rate_limit.requests_per_hour",
5153
Value: "1000",
52-
Type: SystemSettingTypeInt,
54+
SettingType: SystemSettingTypeInt,
5355
Description: defaultDescription("Maximum API requests per hour per user"),
5456
},
5557
{
56-
Key: "session.timeout_minutes",
58+
SettingKey: "session.timeout_minutes",
5759
Value: "60",
58-
Type: SystemSettingTypeInt,
60+
SettingType: SystemSettingTypeInt,
5961
Description: defaultDescription("JWT token expiration in minutes"),
6062
},
6163
{
62-
Key: "websocket.max_participants",
64+
SettingKey: "websocket.max_participants",
6365
Value: "10",
64-
Type: SystemSettingTypeInt,
66+
SettingType: SystemSettingTypeInt,
6567
Description: defaultDescription("Maximum participants per collaboration session"),
6668
},
6769
{
68-
Key: "features.saml_enabled",
70+
SettingKey: "features.saml_enabled",
6971
Value: "false",
70-
Type: SystemSettingTypeBool,
72+
SettingType: SystemSettingTypeBool,
7173
Description: defaultDescription("Enable SAML authentication"),
7274
},
7375
{
74-
Key: "features.webhooks_enabled",
76+
SettingKey: "features.webhooks_enabled",
7577
Value: "true",
76-
Type: SystemSettingTypeBool,
78+
SettingType: SystemSettingTypeBool,
7779
Description: defaultDescription("Enable webhook subscriptions"),
7880
},
7981
{
80-
Key: "ui.default_theme",
82+
SettingKey: "ui.default_theme",
8183
Value: "auto",
82-
Type: SystemSettingTypeString,
84+
SettingType: SystemSettingTypeString,
8385
Description: defaultDescription("Default UI theme (auto, light, dark)"),
8486
},
8587
{
86-
Key: "upload.max_file_size_mb",
88+
SettingKey: "upload.max_file_size_mb",
8789
Value: "10",
88-
Type: SystemSettingTypeInt,
90+
SettingType: SystemSettingTypeInt,
8991
Description: defaultDescription("Maximum file upload size in megabytes"),
9092
},
9193
}

0 commit comments

Comments
 (0)