-
Notifications
You must be signed in to change notification settings - Fork 2
feat: provision per-host Postgres user for RAG service instances #299
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
tsivaprasad
wants to merge
2
commits into
PLAT-488-rag-service-api-design-validation
Choose a base branch
from
PLAT-489-rag-service-service-user-provisioning
base: PLAT-488-rag-service-api-design-validation
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
199 changes: 199 additions & 0 deletions
199
server/internal/orchestrator/swarm/rag_service_user_role.go
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,199 @@ | ||
| package swarm | ||
|
|
||
| import ( | ||
| "context" | ||
| "fmt" | ||
|
|
||
| "github.com/rs/zerolog" | ||
| "github.com/samber/do" | ||
|
|
||
| "github.com/pgEdge/control-plane/server/internal/database" | ||
| "github.com/pgEdge/control-plane/server/internal/postgres" | ||
| "github.com/pgEdge/control-plane/server/internal/resource" | ||
| "github.com/pgEdge/control-plane/server/internal/utils" | ||
| ) | ||
|
|
||
| var _ resource.Resource = (*RAGServiceUserRole)(nil) | ||
|
|
||
| const ResourceTypeRAGServiceUserRole resource.Type = "swarm.rag_service_user_role" | ||
|
|
||
| func RAGServiceUserRoleIdentifier(serviceID string) resource.Identifier { | ||
| return resource.Identifier{ | ||
| ID: serviceID, | ||
| Type: ResourceTypeRAGServiceUserRole, | ||
| } | ||
| } | ||
|
|
||
| // RAGServiceUserRole manages the Postgres role for a RAG service. | ||
| // The role is created on the primary of the co-located Postgres instance | ||
| // and granted the pgedge_application_read_only built-in role. | ||
| // Spock replicates the role to every other node because we connect via r.DatabaseName. | ||
| type RAGServiceUserRole struct { | ||
| ServiceID string `json:"service_id"` | ||
| DatabaseID string `json:"database_id"` | ||
| DatabaseName string `json:"database_name"` | ||
| NodeName string `json:"node_name"` // Database node name for PrimaryExecutor routing | ||
| Username string `json:"username"` | ||
| Password string `json:"password"` // Generated on Create, persisted in state | ||
| } | ||
|
|
||
| func (r *RAGServiceUserRole) ResourceVersion() string { | ||
| return "1" | ||
| } | ||
|
|
||
| func (r *RAGServiceUserRole) DiffIgnore() []string { | ||
| return []string{ | ||
| "/node_name", | ||
| "/username", | ||
| "/password", | ||
| } | ||
| } | ||
|
|
||
| func (r *RAGServiceUserRole) Identifier() resource.Identifier { | ||
| return RAGServiceUserRoleIdentifier(r.ServiceID) | ||
| } | ||
|
|
||
| func (r *RAGServiceUserRole) Executor() resource.Executor { | ||
| return resource.PrimaryExecutor(r.NodeName) | ||
| } | ||
|
|
||
| func (r *RAGServiceUserRole) Dependencies() []resource.Identifier { | ||
| return nil | ||
| } | ||
|
|
||
| func (r *RAGServiceUserRole) TypeDependencies() []resource.Type { | ||
| return nil | ||
| } | ||
|
|
||
| func (r *RAGServiceUserRole) Refresh(ctx context.Context, rc *resource.Context) error { | ||
| if r.Username == "" || r.Password == "" { | ||
| return resource.ErrNotFound | ||
| } | ||
|
|
||
| logger, err := do.Invoke[zerolog.Logger](rc.Injector) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| logger = logger.With(). | ||
| Str("service_id", r.ServiceID). | ||
| Str("database_id", r.DatabaseID). | ||
| Logger() | ||
|
|
||
| primary, err := database.GetPrimaryInstance(ctx, rc, r.NodeName) | ||
| if err != nil { | ||
| return fmt.Errorf("failed to get primary instance: %w", err) | ||
| } | ||
| conn, err := primary.Connection(ctx, rc, r.DatabaseName) | ||
| if err != nil { | ||
| return fmt.Errorf("failed to connect to database: %w", err) | ||
| } | ||
| defer conn.Close(ctx) | ||
|
|
||
| needsCreate, err := postgres.UserRoleNeedsCreate(r.Username).Scalar(ctx, conn) | ||
| if err != nil { | ||
| logger.Warn().Err(err).Msg("pg_roles query failed") | ||
| return fmt.Errorf("pg_roles query failed: %w", err) | ||
| } | ||
| if needsCreate { | ||
| return resource.ErrNotFound | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
| func (r *RAGServiceUserRole) Create(ctx context.Context, rc *resource.Context) error { | ||
| logger, err := do.Invoke[zerolog.Logger](rc.Injector) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| logger = logger.With(). | ||
| Str("service_id", r.ServiceID). | ||
| Str("database_id", r.DatabaseID). | ||
| Logger() | ||
| logger.Info().Msg("creating RAG service user role") | ||
|
|
||
| r.Username = database.GenerateServiceUsername(r.ServiceID) | ||
| if r.Password == "" { | ||
| password, err := utils.RandomString(32) | ||
| if err != nil { | ||
| return fmt.Errorf("failed to generate password: %w", err) | ||
| } | ||
| r.Password = password | ||
| } | ||
|
|
||
| if err := r.createRole(ctx, rc); err != nil { | ||
| return fmt.Errorf("failed to create RAG service user role: %w", err) | ||
| } | ||
|
|
||
| logger.Info().Str("username", r.Username).Msg("RAG service user role created successfully") | ||
| return nil | ||
| } | ||
|
|
||
| func (r *RAGServiceUserRole) createRole(ctx context.Context, rc *resource.Context) error { | ||
| primary, err := database.GetPrimaryInstance(ctx, rc, r.NodeName) | ||
| if err != nil { | ||
| return fmt.Errorf("failed to get primary instance: %w", err) | ||
| } | ||
| conn, err := primary.Connection(ctx, rc, r.DatabaseName) | ||
| if err != nil { | ||
| return fmt.Errorf("failed to connect to database: %w", err) | ||
| } | ||
| defer conn.Close(ctx) | ||
|
|
||
| statements, err := postgres.CreateUserRole(postgres.UserRoleOptions{ | ||
| Name: r.Username, | ||
| Password: r.Password, | ||
| DBName: r.DatabaseName, | ||
| DBOwner: false, | ||
| Attributes: []string{"LOGIN"}, | ||
| Roles: []string{"pgedge_application_read_only"}, | ||
| }) | ||
| if err != nil { | ||
| return fmt.Errorf("failed to generate create user role statements: %w", err) | ||
| } | ||
|
|
||
| if err := statements.Exec(ctx, conn); err != nil { | ||
| return fmt.Errorf("failed to create RAG service user: %w", err) | ||
| } | ||
|
|
||
| return nil | ||
| } | ||
|
|
||
| func (r *RAGServiceUserRole) Update(ctx context.Context, rc *resource.Context) error { | ||
| return nil | ||
| } | ||
|
|
||
| func (r *RAGServiceUserRole) Delete(ctx context.Context, rc *resource.Context) error { | ||
| logger, err := do.Invoke[zerolog.Logger](rc.Injector) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| logger = logger.With(). | ||
| Str("service_id", r.ServiceID). | ||
| Str("database_id", r.DatabaseID). | ||
| Str("username", r.Username). | ||
| Logger() | ||
| logger.Info().Msg("deleting RAG service user from database") | ||
|
|
||
| primary, err := database.GetPrimaryInstance(ctx, rc, r.NodeName) | ||
| if err != nil { | ||
| // During deletion the database may already be gone or unreachable. | ||
| logger.Warn().Err(err).Msg("failed to get primary instance, skipping RAG user deletion") | ||
| return nil | ||
| } | ||
| conn, err := primary.Connection(ctx, rc, r.DatabaseName) | ||
| if err != nil { | ||
| // During deletion the database may already be gone or unreachable. | ||
| logger.Warn().Err(err).Msg("failed to connect to database, skipping RAG user deletion") | ||
| return nil | ||
| } | ||
| defer conn.Close(ctx) | ||
|
|
||
| _, err = conn.Exec(ctx, fmt.Sprintf("DROP ROLE IF EXISTS %s", sanitizeIdentifier(r.Username))) | ||
| if err != nil { | ||
| logger.Warn().Err(err).Msg("failed to drop RAG user role, continuing anyway") | ||
| return nil | ||
| } | ||
|
|
||
| logger.Info().Msg("RAG service user deleted successfully") | ||
| return nil | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ryan has fixed a few bugs and added some features to
ServiceUserRole, and it now supports a configurable read-only or read-write user. Could you please look and see if it's feasible to reuse that same resource for RAG rather than adding this new one?If there is a good reason to add this new resource, could you please port over the bug fixes that he's made? In particular, that resource now uses the
NodeNameas its identifier, it depends on the node resource, and it connects to the"postgres"database. There could be others, so please take a look at the differences between this resource and that one.