Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions components/api-server/plugins/gateways/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ func TestGatewayPostAllowsEmptyReconcilerOwnedIDs(t *testing.T) {
Expect(gatewayOutput.Namespace).To(MatchRegexp(`^openshell-[0-9a-f]{16}$`))
}

func TestGatewayPostRejectsEmptyDatabaseId(t *testing.T) {
func TestGatewayPostAllowsEmptyDatabaseId(t *testing.T) {
h, client := test.RegisterIntegration(t)

account := h.NewRandAccount()
Expand All @@ -114,9 +114,10 @@ func TestGatewayPostRejectsEmptyDatabaseId(t *testing.T) {
DatabaseId: "",
}

_, resp, err := client.DefaultAPI.CreateGateway(ctx).GatewayCreateRequest(gatewayInput).Execute()
Expect(err).To(HaveOccurred())
Expect(resp.StatusCode).To(Equal(http.StatusBadRequest))
gatewayOutput, resp, err := client.DefaultAPI.CreateGateway(ctx).GatewayCreateRequest(gatewayInput).Execute()
Expect(err).NotTo(HaveOccurred())
Expect(resp.StatusCode).To(Equal(http.StatusCreated))
Expect(gatewayOutput.DatabaseId).To(BeEmpty())
}

func TestGatewayPostWithoutRouteRemainsUnrouted(t *testing.T) {
Expand Down
17 changes: 6 additions & 11 deletions components/api-server/plugins/gateways/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,29 +118,24 @@ func (s *sqlGatewayService) Create(ctx context.Context, gateway *Gateway) (*Gate
gateway.FleetId = fleetID
}

if gateway.DatabaseId == "" {
if s.dbFinder == nil {
return nil, errors.Validation("database_id is required")
}
if gateway.DatabaseId == "" && s.dbFinder != nil {
if gateway.FleetId != "" {
dbID, findErr := s.dbFinder.FindSoleInFleet(ctx, gateway.FleetId)
if findErr != nil {
return nil, errors.GeneralError("resolve fleet database: %s", findErr)
}
if dbID == "" {
return nil, errors.Validation("database_id is required: fleet %s has zero or multiple ManagedDatabases", gateway.FleetId)
if dbID != "" {
gateway.DatabaseId = dbID
}
gateway.DatabaseId = dbID
} else {
dbID, fleetID, findErr := s.dbFinder.FindSole(ctx)
if findErr != nil {
return nil, errors.GeneralError("resolve database: %s", findErr)
}
if dbID == "" {
return nil, errors.Validation("database_id is required: zero or multiple ManagedDatabases exist")
if dbID != "" {
gateway.DatabaseId = dbID
gateway.FleetId = fleetID
}
gateway.DatabaseId = dbID
gateway.FleetId = fleetID
}
}

Expand Down
58 changes: 58 additions & 0 deletions components/api-server/plugins/gateways/service_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package gateways

import (
"context"
"testing"

daomocks "github.com/openshift-online/rh-trex-ai/pkg/dao/mocks"
"github.com/openshift-online/rh-trex-ai/pkg/services"
)

type databaseFinderStub struct {
databaseID string
fleetID string
}

func (f databaseFinderStub) FindSoleInFleet(context.Context, string) (string, error) {
return f.databaseID, nil
}

func (f databaseFinderStub) FindSole(context.Context) (string, string, error) {
return f.databaseID, f.fleetID, nil
}

func TestCreateFallsBackWhenNoSoleManagedDatabaseExists(t *testing.T) {
service := NewGatewayService(
nil,
NewMockGatewayDao(),
services.NewEventService(daomocks.NewEventDao()),
databaseFinderStub{},
nil,
)

created, serviceErr := service.Create(context.Background(), &Gateway{Name: "fallback"})
if serviceErr != nil {
t.Fatalf("create gateway without a sole ManagedDatabase: %v", serviceErr)
}
if created.DatabaseId != "" {
t.Fatalf("database_id = %q, want blank fallback assignment", created.DatabaseId)
}
}

func TestCreateStillAutoAssignsSoleManagedDatabase(t *testing.T) {
service := NewGatewayService(
nil,
NewMockGatewayDao(),
services.NewEventService(daomocks.NewEventDao()),
databaseFinderStub{databaseID: "managed-db", fleetID: "fleet-a"},
nil,
)

created, serviceErr := service.Create(context.Background(), &Gateway{Name: "cnpg"})
if serviceErr != nil {
t.Fatalf("create gateway with a sole ManagedDatabase: %v", serviceErr)
}
if created.DatabaseId != "managed-db" || created.FleetId != "fleet-a" {
t.Fatalf("assignment = database:%q fleet:%q", created.DatabaseId, created.FleetId)
}
}
4 changes: 4 additions & 0 deletions components/control-plane/internal/gateway/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,10 @@ type ReconcileOpts struct {
CNPG CNPGConfig
ControlPlaneNamespace string
Images ImageDefaults
// SelfManagedDB provisions the legacy per-gateway PostgreSQL Deployment. It
// is used only for gateways without a database_id; assigned gateways continue
// through the CNPG path.
SelfManagedDB bool
// SkipNetworkPolicies disables creation of the per-tenant gateway
// NetworkPolicies. On distributions where the shared Gateway data plane
// runs out-of-cluster (e.g. cloud-provider-kind's Envoy container in local
Expand Down
32 changes: 32 additions & 0 deletions components/control-plane/internal/gateway/manifests.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,38 @@ func ApplyManifestToNamespace(manifest *unstructured.Unstructured, namespace str
return result, nil
}

// ApplySelfManagedDatabaseOverrides renders the legacy per-gateway PostgreSQL
// manifest used only when a Gateway has no ManagedDatabase assignment.
func ApplySelfManagedDatabaseOverrides(obj *unstructured.Unstructured, images ImageDefaults) error {
if images == nil {
images = StaticImageDefaults{}
}

jsonBytes, err := obj.MarshalJSON()
if err != nil {
return fmt.Errorf("marshal for self-managed database overrides: %w", err)
}
manifestJSON := string(jsonBytes)

dbImage := images.DefaultDatabaseImage()
userKey, passKey, dbKey := selfManagedPostgresEnvKeys(dbImage)
dataPath := selfManagedPostgresDataPath(dbImage)

// DB_IMAGE_PLACEHOLDER must be replaced before the generic
// IMAGE_PLACEHOLDER substitution because it contains that shorter token.
manifestJSON = strings.ReplaceAll(manifestJSON, "DB_IMAGE_PLACEHOLDER", dbImage)
manifestJSON = strings.ReplaceAll(manifestJSON, "DB_STORAGE_PLACEHOLDER", "5Gi")
manifestJSON = strings.ReplaceAll(manifestJSON, "DB_USER_KEY_PLACEHOLDER", userKey)
manifestJSON = strings.ReplaceAll(manifestJSON, "DB_PASS_KEY_PLACEHOLDER", passKey)
manifestJSON = strings.ReplaceAll(manifestJSON, "DB_NAME_KEY_PLACEHOLDER", dbKey)
manifestJSON = strings.ReplaceAll(manifestJSON, "DB_DATA_PATH_PLACEHOLDER", dataPath)

if err := obj.UnmarshalJSON([]byte(manifestJSON)); err != nil {
return fmt.Errorf("unmarshal after self-managed database overrides: %w", err)
}
return nil
}

func ApplyConfigOverrides(obj *unstructured.Unstructured, config GatewayConfig, tenantNamespace ...string) error {
kind := obj.GetKind()

Expand Down
57 changes: 57 additions & 0 deletions components/control-plane/internal/gateway/manifests_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,63 @@ import (
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)

func TestSelfManagedDatabaseManifestRenders(t *testing.T) {
t.Setenv("HYPERSHELL_DATABASE_IMAGE", "postgres:18")

manifests, err := LoadGatewayManifests("../../manifests/gateway")
if err != nil {
t.Fatalf("load gateway manifests: %v", err)
}
resources, ok := manifests["database.yaml"]
if !ok {
t.Fatal("database.yaml was not loaded")
}
if len(resources) != 4 {
t.Fatalf("database.yaml resources = %d, want 4", len(resources))
}

seen := make(map[string]bool)
for _, manifest := range resources {
raw := manifest.DeepCopy()
if err := ApplySelfManagedDatabaseOverrides(raw, StaticImageDefaults{}); err != nil {
t.Fatalf("apply database overrides to %s: %v", raw.GetKind(), err)
}
obj, err := ApplyManifestToNamespace(raw, "openshell-test", GatewayConfig{}, StaticImageDefaults{})
if err != nil {
t.Fatalf("render %s: %v", raw.GetKind(), err)
}
data, err := obj.MarshalJSON()
if err != nil {
t.Fatalf("marshal rendered %s: %v", obj.GetKind(), err)
}
rendered := string(data)
if strings.Contains(rendered, "PLACEHOLDER") {
t.Errorf("rendered %s still contains a placeholder: %s", obj.GetKind(), rendered)
}
if obj.GetNamespace() != "openshell-test" {
t.Errorf("rendered %s namespace = %q, want openshell-test", obj.GetKind(), obj.GetNamespace())
}
seen[obj.GetKind()] = true

if obj.GetKind() == "Deployment" {
for _, want := range []string{`"image":"postgres:18"`, `"name":"POSTGRES_USER"`, `"name":"POSTGRES_PASSWORD"`, `"name":"POSTGRES_DB"`} {
if !strings.Contains(rendered, want) {
t.Errorf("rendered database Deployment missing %s", want)
}
}
}
if obj.GetKind() == "PersistentVolumeClaim" && !strings.Contains(rendered, `"storage":"5Gi"`) {
t.Error("rendered database PVC does not request 5Gi")
}
}

for _, kind := range []string{"PersistentVolumeClaim", "Deployment", "Service", "NetworkPolicy"} {
if !seen[kind] {
t.Errorf("database.yaml missing %s", kind)
}
}
}

func TestApplyCredentialDriverToml_KubernetesSecrets(t *testing.T) {
lines := []string{
"[openshell.gateway]",
Expand Down
Loading
Loading