Skip to content

Commit dede3ae

Browse files
intel352Copilot
andauthored
Tenant support (#2)
* Added tenant functionality and example. Added some tests, all passing except 1 * Started splitting into interfaces. Related changes * Started splitting into interfaces. Related changes * Debugging * Tests passing * Simplifying tests and logic * Simplifying tests and logic * Updating examples * Fixed example startup * Update readme and ci * Update example_tenants/modules.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Fixing linter errors * Update config_provider.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Fixing linter errors --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent eef9940 commit dede3ae

40 files changed

Lines changed: 3184 additions & 1184 deletions

.github/workflows/ci.yml

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ jobs:
6565
args: -c .golangci.github.yml
6666

6767
build-example:
68-
name: Build and run example
68+
name: Build and run example app
6969
runs-on: ubuntu-latest
7070
needs: test
7171
steps:
@@ -103,3 +103,43 @@ jobs:
103103
echo "Example app failed to start"
104104
exit 1
105105
fi
106+
107+
build-example-tenants:
108+
name: Build and run example tenants app
109+
runs-on: ubuntu-latest
110+
needs: test
111+
steps:
112+
- name: Checkout code
113+
uses: actions/checkout@v4
114+
115+
- name: Set up Go
116+
uses: actions/setup-go@v5
117+
with:
118+
go-version: ${{ env.GO_VERSION }}
119+
check-latest: true
120+
cache: true
121+
122+
- name: Get dependencies
123+
run: |
124+
go mod download
125+
go mod verify
126+
127+
- name: Build example
128+
run: cd example_tenants && go build -v .
129+
130+
- name: Test example executable
131+
run: |
132+
cd example_tenants
133+
# Run the example app in the background and give it 5 seconds to start
134+
./$(basename $(pwd)) &
135+
APP_PID=$!
136+
sleep 5
137+
# Check if process is still running (successful start)
138+
if ps -p $APP_PID > /dev/null; then
139+
echo "Example tenants app started successfully"
140+
kill $APP_PID
141+
exit 0
142+
else
143+
echo "Example tenants app failed to start"
144+
exit 1
145+
fi

README.md

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ Modular is a package that provides a structured way to create modular applicatio
1414
- **Service registry**: Register and retrieve application services
1515
- **Configuration management**: Handle configuration for modules and services
1616
- **Dependency injection**: Inject required services into modules
17+
- **Multi-tenancy support**: Build applications that serve multiple tenants with isolated configurations
1718

1819
## Installation
1920

@@ -178,6 +179,122 @@ func (c *AppConfig) Setup() error {
178179
}
179180
```
180181

182+
## Multi-Tenant Support
183+
184+
Modular provides built-in support for multi-tenant applications through:
185+
186+
### Tenant Contexts
187+
188+
```go
189+
// Creating a tenant context
190+
tenantID := modular.TenantID("tenant1")
191+
ctx := modular.NewTenantContext(context.Background(), tenantID)
192+
193+
// Using tenant context with the application
194+
tenantCtx, err := app.WithTenant(tenantID)
195+
if err != nil {
196+
log.Fatal("Failed to create tenant context:", err)
197+
}
198+
199+
// Extract tenant ID from a context
200+
if id, ok := modular.GetTenantIDFromContext(ctx); ok {
201+
fmt.Println("Current tenant:", id)
202+
}
203+
```
204+
205+
### Tenant-Aware Configuration
206+
207+
```go
208+
// Register a tenant service in your module
209+
func (m *MultiTenantModule) ProvidesServices() []modular.ServiceProvider {
210+
return []modular.ServiceProvider{
211+
{
212+
Name: "tenantService",
213+
Description: "Tenant management service",
214+
Instance: modular.NewStandardTenantService(m.logger),
215+
},
216+
{
217+
Name: "tenantConfigLoader",
218+
Description: "Tenant configuration loader",
219+
Instance: modular.DefaultTenantConfigLoader("./configs/tenants"),
220+
},
221+
}
222+
}
223+
224+
// Create tenant-aware configuration
225+
func (m *MultiTenantModule) RegisterConfig(app *modular.Application) {
226+
// Default config
227+
defaultConfig := &MyConfig{
228+
Setting: "default",
229+
}
230+
231+
// Get tenant service (must be provided by another module)
232+
var tenantService modular.TenantService
233+
app.GetService("tenantService", &tenantService)
234+
235+
// Create tenant-aware config provider
236+
tenantAwareConfig := modular.NewTenantAwareConfig(
237+
modular.NewStdConfigProvider(defaultConfig),
238+
tenantService,
239+
"mymodule",
240+
)
241+
242+
app.RegisterConfigSection("mymodule", tenantAwareConfig)
243+
}
244+
245+
// Using tenant-aware configs in your code
246+
func (m *MultiTenantModule) ProcessRequestWithTenant(ctx context.Context) {
247+
// Get config specific to the tenant in the context
248+
config, ok := m.config.(*modular.TenantAwareConfig)
249+
if !ok {
250+
// Handle non-tenant-aware config
251+
return
252+
}
253+
254+
// Get tenant-specific configuration
255+
myConfig := config.GetConfigWithContext(ctx).(*MyConfig)
256+
257+
// Use tenant-specific settings
258+
fmt.Println("Tenant setting:", myConfig.Setting)
259+
}
260+
```
261+
262+
### Tenant-Aware Modules
263+
264+
```go
265+
// Implement the TenantAwareModule interface
266+
type MultiTenantModule struct {
267+
modular.Module
268+
tenantData map[modular.TenantID]*TenantData
269+
}
270+
271+
func (m *MultiTenantModule) OnTenantRegistered(tenantID modular.TenantID) {
272+
// Initialize resources for this tenant
273+
m.tenantData[tenantID] = &TenantData{
274+
initialized: true,
275+
}
276+
}
277+
278+
func (m *MultiTenantModule) OnTenantRemoved(tenantID modular.TenantID) {
279+
// Clean up tenant resources
280+
delete(m.tenantData, tenantID)
281+
}
282+
```
283+
284+
### Loading Tenant Configurations
285+
286+
```go
287+
// Set up a file-based tenant config loader
288+
configLoader := modular.NewFileBasedTenantConfigLoader(modular.TenantConfigParams{
289+
ConfigNameRegex: regexp.MustCompile("^tenant-[\\w-]+\\.(json|yaml)$"),
290+
ConfigDir: "./configs/tenants",
291+
ConfigFeeders: []modular.Feeder{},
292+
})
293+
294+
// Register the loader as a service
295+
app.RegisterService("tenantConfigLoader", configLoader)
296+
```
297+
181298
## Key Interfaces
182299

183300
### Module
@@ -197,6 +314,30 @@ type Module interface {
197314
}
198315
```
199316

317+
### TenantAwareModule
318+
319+
Interface for modules that need to respond to tenant lifecycle events:
320+
321+
```go
322+
type TenantAwareModule interface {
323+
Module
324+
OnTenantRegistered(tenantID TenantID)
325+
OnTenantRemoved(tenantID TenantID)
326+
}
327+
```
328+
329+
### TenantService
330+
331+
Interface for managing tenants:
332+
333+
```go
334+
type TenantService interface {
335+
GetTenantConfig(tenantID TenantID, section string) (ConfigProvider, error)
336+
GetTenants() []TenantID
337+
RegisterTenant(tenantID TenantID, configs map[string]ConfigProvider) error
338+
}
339+
```
340+
200341
### ConfigProvider
201342

202343
Interface for configuration providers:

0 commit comments

Comments
 (0)