Skip to content

Commit

Permalink
[WIP] Support self-hosted instances
Browse files Browse the repository at this point in the history
  • Loading branch information
bobbyiliev committed Nov 27, 2024
1 parent 3bf6717 commit 29a317d
Show file tree
Hide file tree
Showing 3 changed files with 233 additions and 10 deletions.
69 changes: 69 additions & 0 deletions pkg/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,24 @@ func Provider(version string) *schema.Provider {
Description: "The default region if not specified in the resource",
DefaultFunc: schema.EnvDefaultFunc("MZ_DEFAULT_REGION", "aws/us-east-1"),
},
"host": {
Type: schema.TypeString,
Optional: true,
Description: "The Materialize host. Can also come from the `MZ_HOST` environment variable.",
DefaultFunc: schema.EnvDefaultFunc("MZ_HOST", nil),
},
"port": {
Type: schema.TypeInt,
Optional: true,
Description: "The Materialize port. Can also come from the `MZ_PORT` environment variable.",
DefaultFunc: schema.EnvDefaultFunc("MZ_PORT", 6875),
},
"username": {
Type: schema.TypeString,
Optional: true,
Description: "The Materialize username. Can also come from the `MZ_USERNAME` environment variable.",
DefaultFunc: schema.EnvDefaultFunc("MZ_USERNAME", nil),
},
},
ResourcesMap: map[string]*schema.Resource{
"materialize_app_password": resources.AppPassword(),
Expand Down Expand Up @@ -156,6 +174,56 @@ func Provider(version string) *schema.Provider {
}

func providerConfigure(ctx context.Context, d *schema.ResourceData, version string) (interface{}, diag.Diagnostics) {
// Check for self-hosted configuration
if host := d.Get("host").(string); host != "" {
log.Printf("[DEBUG] Configuring self-hosted provider")
return configureSelfHosted(ctx, d, version)
}
log.Printf("[DEBUG] Configuring SaaS provider")
return configureSaaS(ctx, d, version)
}

func configureSelfHosted(ctx context.Context, d *schema.ResourceData, version string) (interface{}, diag.Diagnostics) {
host := d.Get("host").(string)
port := d.Get("port").(int)
database := d.Get("database").(string)
username := d.Get("username").(string)
password := d.Get("password").(string)
sslmode := d.Get("sslmode").(string)
application_name := fmt.Sprintf("terraform-provider-materialize v%s", version)

// Initialize single DB client for self-hosted
dbClient, diags := clients.NewDBClient(
host,
username,
password,
port,
database,
application_name,
version,
sslmode,
)
if diags.HasError() {
return nil, diags
}

// Create provider meta for self-hosted
dbClients := make(map[clients.Region]*clients.DBClient)
dbClients["self-hosted"] = dbClient

providerMeta := &utils.ProviderMeta{
Mode: utils.ModeSelfHosted,
DB: dbClients,
DefaultRegion: "self-hosted",
RegionsEnabled: map[clients.Region]bool{
"self-hosted": true,
},
}

return providerMeta, nil
}

func configureSaaS(ctx context.Context, d *schema.ResourceData, version string) (interface{}, diag.Diagnostics) {
password := d.Get("password").(string)
database := d.Get("database").(string)
sslmode := d.Get("sslmode").(string)
Expand Down Expand Up @@ -240,6 +308,7 @@ func providerConfigure(ctx context.Context, d *schema.ResourceData, version stri

// Construct and return the provider meta with all clients initialized.
providerMeta := &utils.ProviderMeta{
Mode: utils.ModeSaaS,
DB: dbClients,
Frontegg: fronteggClient,
CloudAPI: cloudAPIClient,
Expand Down
55 changes: 45 additions & 10 deletions pkg/utils/provider_meta.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,26 +9,37 @@ import (
"github.com/jmoiron/sqlx"
)

// ProviderMode represents the mode in which the provider is operating
type ProviderMode string

const (
ModeSaaS ProviderMode = "saas"
ModeSelfHosted ProviderMode = "self_hosted"
)

// ProviderMeta holds essential configuration and client information
// required across various parts of the provider. It acts as a central
// repository of shared data, particularly for database connections, API clients,
// and regional settings.
type ProviderMeta struct {
// Mode represents the mode in which the provider is operating: default to SaaS
Mode ProviderMode

// DB is a map that associates each supported region with its corresponding
// database client. This allows for region-specific database operations.
DB map[clients.Region]*clients.DBClient

// DefaultRegion specifies the default region to be used when no specific
// region is provided in the resources and data sources.
DefaultRegion clients.Region

// Frontegg represents the client used to interact with the Frontegg API,
// which may involve authentication, token management, etc.
Frontegg *clients.FronteggClient

// CloudAPI is the client used for interactions with the cloud API
CloudAPI *clients.CloudAPIClient

// DefaultRegion specifies the default region to be used when no specific
// region is provided in the resources and data sources.
DefaultRegion clients.Region

// RegionsEnabled is a map indicating which regions are currently enabled
// for use. This can be used to quickly check the availability in different regions.
RegionsEnabled map[clients.Region]bool
Expand All @@ -38,15 +49,26 @@ type ProviderMeta struct {
FronteggRoles map[string]string
}

func (p *ProviderMeta) IsSelfHosted() bool {
return p.Mode == ModeSelfHosted
}

func (p *ProviderMeta) IsSaaS() bool {
// Empty string or explicit ModeSaaS both indicate SaaS mode
return p.Mode == "" || p.Mode == ModeSaaS
}

var DefaultRegion string

func GetProviderMeta(meta interface{}) (*ProviderMeta, error) {
providerMeta := meta.(*ProviderMeta)

if err := providerMeta.Frontegg.NeedsTokenRefresh(); err != nil {
err := providerMeta.Frontegg.RefreshToken()
if err != nil {
return nil, fmt.Errorf("failed to refresh token: %v", err)
// Only refresh token for SaaS mode
if providerMeta.Mode == ModeSaaS && providerMeta.Frontegg != nil {
if err := providerMeta.Frontegg.NeedsTokenRefresh(); err != nil {
if err := providerMeta.Frontegg.RefreshToken(); err != nil {
return nil, fmt.Errorf("failed to refresh token: %v", err)
}
}
}

Expand All @@ -59,7 +81,20 @@ func GetDBClientFromMeta(meta interface{}, d *schema.ResourceData) (*sqlx.DB, cl
return nil, "", err
}

// Determine the region to use, if one is not specified, use the default region
if providerMeta.IsSelfHosted() {
region := clients.Region("self-hosted")
if d != nil {
d.Set("region", string(region))
}

dbClient, exists := providerMeta.DB[region]
if !exists {
return nil, region, fmt.Errorf("database client not initialized for self-hosted instance")
}
return dbClient.SQLX(), region, nil
}

// Determine region for SaaS deployments
var region clients.Region
if d != nil && d.Get("region") != "" {
region = clients.Region(d.Get("region").(string))
Expand All @@ -73,7 +108,7 @@ func GetDBClientFromMeta(meta interface{}, d *schema.ResourceData) (*sqlx.DB, cl
d.Set("region", string(region))
}

// Check if the region is enabled using the stored information
// Validate region is enabled (SaaS only)
enabled, exists := providerMeta.RegionsEnabled[region]
if !exists {
var regions []string
Expand Down
119 changes: 119 additions & 0 deletions pkg/utils/provider_meta_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ func TestGetDBClientFromMeta(t *testing.T) {

// Set up the mock ProviderMeta
providerMeta := &ProviderMeta{
Mode: ModeSaaS,
DB: map[clients.Region]*clients.DBClient{
clients.AwsUsEast1: {DB: dbx},
},
Expand Down Expand Up @@ -83,3 +84,121 @@ func TestTransformIdWithRegion(t *testing.T) {
assert.Equal(t, o, c["expected"].(string))
}
}

func TestGetDBClientFromMetaSelfHosted(t *testing.T) {
// Set up the SQL mock database for self-hosted
db, mock, err := sqlmock.New()
require.NoError(t, err)
defer db.Close()

// Wrap the sql.DB with sqlx
dbx := sqlx.NewDb(db, "sqlmock")

// Set up the mock ProviderMeta for self-hosted
providerMeta := &ProviderMeta{
Mode: ModeSelfHosted,
DB: map[clients.Region]*clients.DBClient{
"self-hosted": {DB: dbx},
},
DefaultRegion: "self-hosted",
RegionsEnabled: map[clients.Region]bool{
"self-hosted": true,
},
}

// Test cases to verify different scenarios
tests := []struct {
name string
resourceData *schema.ResourceData
wantRegion clients.Region
wantErr bool
errMsg string
}{
{
name: "self hosted basic",
resourceData: schema.TestResourceDataRaw(t, map[string]*schema.Schema{
"region": {
Type: schema.TypeString,
Optional: true,
},
}, nil),
wantRegion: "self-hosted",
wantErr: false,
},
{
name: "self hosted with region set (should be overridden)",
resourceData: func() *schema.ResourceData {
d := schema.TestResourceDataRaw(t, map[string]*schema.Schema{
"region": {
Type: schema.TypeString,
Optional: true,
},
}, nil)
d.Set("region", "aws/us-east-1")
return d
}(),
wantRegion: "self-hosted",
wantErr: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
dbClient, region, err := GetDBClientFromMeta(providerMeta, tt.resourceData)

if tt.wantErr {
assert.Error(t, err)
assert.Contains(t, err.Error(), tt.errMsg)
return
}

require.NoError(t, err)
assert.NotNil(t, dbClient)
assert.Equal(t, tt.wantRegion, region)

if tt.resourceData != nil {
assert.Equal(t, string(tt.wantRegion), tt.resourceData.Get("region"))
}
})
}

// Check that the mock expectations are met
err = mock.ExpectationsWereMet()
assert.NoError(t, err)
}

func TestProviderMetaModeHelpers(t *testing.T) {
tests := []struct {
name string
mode ProviderMode
wantSaaS bool
wantSelfHosted bool
}{
{
name: "explicit saas mode",
mode: ModeSaaS,
wantSaaS: true,
wantSelfHosted: false,
},
{
name: "empty mode defaults to saas",
mode: "",
wantSaaS: true,
wantSelfHosted: false,
},
{
name: "self hosted mode",
mode: ModeSelfHosted,
wantSaaS: false,
wantSelfHosted: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
pm := &ProviderMeta{Mode: tt.mode}
assert.Equal(t, tt.wantSaaS, pm.IsSaaS())
assert.Equal(t, tt.wantSelfHosted, pm.IsSelfHosted())
})
}
}

0 comments on commit 29a317d

Please sign in to comment.