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
20 changes: 20 additions & 0 deletions .github/workflows/pre-commit.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
name: pre-commit
permissions: read-all

on: [pull_request]

jobs:
pre-commit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version-file: go.mod
- name: Install Go tools
run: |
go install golang.org/x/tools/cmd/goimports@v0.44.0
go install mvdan.cc/gofumpt@v0.10.0
go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.12.2
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
- uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ HELM ?= $(LOCALBIN)/helm
## Tool Versions
KUSTOMIZE_VERSION ?= v5.2.1
CONTROLLER_TOOLS_VERSION ?= v0.19.0
GOLANGCI_LINT_VERSION ?= v1.62.2
GOLANGCI_LINT_VERSION ?= v2.12.2
KUBECTL_VERSION ?= v1.28.0
KIND_VERSION ?= v0.20.0
HELM_VERSION ?= v3.13.0
Expand All @@ -262,7 +262,7 @@ $(ENVTEST): $(LOCALBIN)
.PHONY: golangci-lint
golangci-lint: $(GOLANGCI_LINT) ## Download golangci-lint locally if necessary.
$(GOLANGCI_LINT): $(LOCALBIN)
test -s $(LOCALBIN)/golangci-lint || GOBIN=$(LOCALBIN) go install github.com/golangci/golangci-lint/cmd/golangci-lint@$(GOLANGCI_LINT_VERSION)
test -s $(LOCALBIN)/golangci-lint || GOBIN=$(LOCALBIN) go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@$(GOLANGCI_LINT_VERSION)

.PHONY: kubectl
kubectl: $(KUBECTL) ## Download kubectl locally if necessary.
Expand Down
43 changes: 30 additions & 13 deletions internal/controller/database_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"strings"
"time"

batchv1 "k8s.io/api/batch/v1"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
Expand Down Expand Up @@ -45,15 +46,17 @@
// DatabaseReconciler reconciles a Database object
type DatabaseReconciler struct {
client.Client
Scheme *runtime.Scheme
Recorder record.EventRecorder
Scheme *runtime.Scheme
Recorder record.EventRecorder
BackupConfig *BackupConfig // nil when backup not configured

Check failure on line 51 in internal/controller/database_controller.go

View workflow job for this annotation

GitHub Actions / pre-commit

undefined: BackupConfig

Check failure on line 51 in internal/controller/database_controller.go

View workflow job for this annotation

GitHub Actions / pre-commit

undefined: BackupConfig

Check failure on line 51 in internal/controller/database_controller.go

View workflow job for this annotation

GitHub Actions / Integration Tests

undefined: BackupConfig

Check failure on line 51 in internal/controller/database_controller.go

View workflow job for this annotation

GitHub Actions / Test

undefined: BackupConfig

Check failure on line 51 in internal/controller/database_controller.go

View workflow job for this annotation

GitHub Actions / Lint

undefined: BackupConfig

Check failure on line 51 in internal/controller/database_controller.go

View workflow job for this annotation

GitHub Actions / Lint

undefined: BackupConfig

Check failure on line 51 in internal/controller/database_controller.go

View workflow job for this annotation

GitHub Actions / Build and Push

undefined: BackupConfig
}

// +kubebuilder:rbac:groups=database.opzkit.io,resources=databases,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=database.opzkit.io,resources=databases/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=database.opzkit.io,resources=databases/finalizers,verbs=update
// +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch
// +kubebuilder:rbac:groups="",resources=events,verbs=create;patch
// +kubebuilder:rbac:groups=batch,resources=jobs,verbs=get;list;watch;create;delete

func (r *DatabaseReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
logger := log.FromContext(ctx)
Expand Down Expand Up @@ -110,16 +113,17 @@

// Record event for user visibility (only once per error by checking if status changed)
if statusChanged {
if apierrors.IsNotFound(err) {
switch {
case apierrors.IsNotFound(err):
r.Recorder.Event(db, corev1.EventTypeWarning, "ConfigurationError", err.Error())
} else if isAWSPermissionError(err) {
case isAWSPermissionError(err):
r.Recorder.Event(db, corev1.EventTypeWarning, "PermissionError",
"AWS permission denied. Ensure the operator has IAM permissions for Secrets Manager. "+
"Grant secretsmanager:* on the secret ARN, or configure IRSA/instance profile.")
} else if isAWSResourceNotFoundError(err) {
case isAWSResourceNotFoundError(err):
r.Recorder.Event(db, corev1.EventTypeWarning, "ResourceNotFound",
"AWS resource not found. Verify the secret exists in AWS Secrets Manager and the name/region are correct in the Database spec.")
} else {
default:
r.Recorder.Event(db, corev1.EventTypeWarning, "ReconciliationError", err.Error())
}
}
Expand Down Expand Up @@ -294,7 +298,8 @@
"secretName", secretName)

// Decision logic based on resource existence
if dbExists && userExists && secretExists {
switch {
case dbExists && userExists && secretExists:
// All three exist - nothing to do, just verify and update status
logger.Info("Database, user, and secret already exist - skipping creation",
"database", db.Spec.DatabaseName,
Expand Down Expand Up @@ -360,7 +365,7 @@
db.Status.ActualUsername = username
db.Status.ActualSecretName = secretName

} else if (dbExists || userExists) && !secretExists {
case (dbExists || userExists) && !secretExists:
// Database and/or user exist but secret is missing
// Check if this is a region change scenario
regionChanged := db.Status.SecretRegion != "" && db.Status.SecretRegion != region
Expand Down Expand Up @@ -417,7 +422,7 @@
dbExists, userExists, secretExists)
}

} else {
default:
// Create missing resources

// Generate new password for new resources
Expand Down Expand Up @@ -508,7 +513,8 @@
urlScheme = "mysql" // MariaDB uses mysql:// scheme
}

databaseURL := fmt.Sprintf("%s://%s:%s@%s:%d/%s",
databaseURL := fmt.Sprintf(
"%s://%s:%s@%s:%d/%s",
urlScheme,
url.QueryEscape(username),
url.QueryEscape(password),
Expand All @@ -533,11 +539,12 @@
// Determine region: use awsSecretsManager.region if set, otherwise use connectionStringAWSSecretRef.region
region := r.getRegion(db)
var regionSource string
if db.Spec.AWSSecretsManager != nil && db.Spec.AWSSecretsManager.Region != "" {
switch {
case db.Spec.AWSSecretsManager != nil && db.Spec.AWSSecretsManager.Region != "":
regionSource = "spec.awsSecretsManager.region"
} else if db.Spec.ConnectionStringAWSSecretRef != nil && db.Spec.ConnectionStringAWSSecretRef.Region != "" {
case db.Spec.ConnectionStringAWSSecretRef != nil && db.Spec.ConnectionStringAWSSecretRef.Region != "":
regionSource = "spec.connectionStringAWSSecretRef.region"
} else {
default:
regionSource = "AWS SDK default (environment/instance metadata)"
}

Expand Down Expand Up @@ -807,6 +814,15 @@
"retainOnDelete", retainOnDelete)

if !retainOnDelete {
// Check if backup is needed before proceeding with deletion
backupDone, backupResult, backupErr := r.reconcileBackup(ctx, db)

Check failure on line 818 in internal/controller/database_controller.go

View workflow job for this annotation

GitHub Actions / pre-commit

r.reconcileBackup undefined (type *DatabaseReconciler has no field or method reconcileBackup) (typecheck)

Check failure on line 818 in internal/controller/database_controller.go

View workflow job for this annotation

GitHub Actions / pre-commit

r.reconcileBackup undefined (type *DatabaseReconciler has no field or method reconcileBackup)

Check failure on line 818 in internal/controller/database_controller.go

View workflow job for this annotation

GitHub Actions / Integration Tests

r.reconcileBackup undefined (type *DatabaseReconciler has no field or method reconcileBackup)

Check failure on line 818 in internal/controller/database_controller.go

View workflow job for this annotation

GitHub Actions / Test

r.reconcileBackup undefined (type *DatabaseReconciler has no field or method reconcileBackup)

Check failure on line 818 in internal/controller/database_controller.go

View workflow job for this annotation

GitHub Actions / Lint

r.reconcileBackup undefined (type *DatabaseReconciler has no field or method reconcileBackup) (typecheck)

Check failure on line 818 in internal/controller/database_controller.go

View workflow job for this annotation

GitHub Actions / Lint

r.reconcileBackup undefined (type *DatabaseReconciler has no field or method reconcileBackup)) (typecheck)

Check failure on line 818 in internal/controller/database_controller.go

View workflow job for this annotation

GitHub Actions / Build and Push

r.reconcileBackup undefined (type *DatabaseReconciler has no field or method reconcileBackup)
if backupErr != nil {
return ctrl.Result{}, backupErr
}
if !backupDone {
return backupResult, nil
}

logger.Info("Starting cleanup of database resources (retainOnDelete=false)",
"database", db.Spec.DatabaseName,
"username", db.Status.ActualUsername,
Expand Down Expand Up @@ -1215,6 +1231,7 @@
// Configure custom rate limiter with exponential backoff: 15s, 30s, 60s
return ctrl.NewControllerManagedBy(mgr).
For(&databasev1alpha1.Database{}).
Owns(&batchv1.Job{}).
WithOptions(controller.Options{
RateLimiter: workqueue.NewTypedItemExponentialFailureRateLimiter[reconcile.Request](
15*time.Second, // Base delay: 15 seconds
Expand Down
3 changes: 2 additions & 1 deletion internal/database/postgres.go
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,8 @@ func (c *PostgresClient) GrantPrivileges(ctx context.Context, username, dbName s
}

// Create connection string for the target database
targetConnStr := fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=%s",
targetConnStr := fmt.Sprintf(
"postgres://%s:%s@%s:%s/%s?sslmode=%s",
url.QueryEscape(connInfo.Username),
url.QueryEscape(connInfo.Password),
connInfo.Host,
Expand Down
3 changes: 2 additions & 1 deletion test/integration/operator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ var _ = Describe("Database Operator Integration Tests", func() {
}, nil
})

cfg, err := config.LoadDefaultConfig(awsCtx,
cfg, err := config.LoadDefaultConfig(
awsCtx,
config.WithRegion("us-east-1"),
config.WithEndpointResolverWithOptions(customResolver),
config.WithCredentialsProvider(aws.CredentialsProviderFunc(func(ctx context.Context) (aws.Credentials, error) {
Expand Down
Loading