-
Notifications
You must be signed in to change notification settings - Fork 17
ROSAENG-62444: feat: replace DynamoDB Streams with SNS/SQS for status notifications #184
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
Changes from all commits
5da29bc
bed56f8
a6be8b9
2b03d7d
cdf2aa6
52c449c
ccbc4c9
7abe27d
42ea423
bfe629c
c756faf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -20,17 +20,16 @@ import ( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "context" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "flag" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "fmt" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "log/slog" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "os" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "strconv" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "strings" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "time" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| awsconfig "github.com/aws/aws-sdk-go-v2/config" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "github.com/aws/aws-sdk-go-v2/service/dynamodb" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "github.com/aws/aws-sdk-go-v2/service/dynamodbstreams" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "github.com/aws/aws-sdk-go-v2/service/sns" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "github.com/aws/aws-sdk-go-v2/service/sqs" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "github.com/aws/aws-sdk-go-v2/service/sts" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| hyperfleetdb "github.com/openshift-online/rosa-hyperfleet-api/hyperfleet-db" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "k8s.io/apimachinery/pkg/runtime" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "k8s.io/apimachinery/pkg/runtime/schema" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ctrl "sigs.k8s.io/controller-runtime" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -41,7 +40,8 @@ import ( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| v1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/hyperfleet-operator/api/v1alpha1" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "github.com/openshift-online/rosa-hyperfleet-api/hyperfleet-operator/internal/controller" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "github.com/openshift-online/rosa-hyperfleet-api/hyperfleet-operator/internal/dynamo" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "github.com/openshift-online/rosa-hyperfleet-api/hyperfleet-operator/internal/dynamo/statusstream" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "github.com/openshift-online/rosa-hyperfleet-api/hyperfleet-operator/internal/dynamo/snspublisher" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "github.com/openshift-online/rosa-hyperfleet-api/hyperfleet-operator/internal/dynamo/statussqsconsumer" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "github.com/openshift-online/rosa-hyperfleet-api/hyperfleet-operator/internal/render" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -53,13 +53,16 @@ func main() { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var awsRegion string | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var baseDomain string | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var maxConcurrentReconciles int | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var sqsStatusQueueURL string | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var sqsStatusQueueURLPrefix string | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metrics endpoint binds to.") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| flag.StringVar(&awsRegion, "aws-region", "", "AWS region for DynamoDB and EKS (required).") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| flag.StringVar(&awsRegion, "aws-region", "", "AWS region for DynamoDB, SNS, SQS, and EKS (required).") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| flag.StringVar(&baseDomain, "base-domain", "", "DNS base domain for hosted clusters (required).") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| flag.IntVar(&maxConcurrentReconciles, "max-concurrent-reconciles", 10, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "Maximum number of concurrent reconciles per controller.") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| flag.StringVar(&sqsStatusQueueURL, "sqs-status-queue-url", "", "Full SQS queue URL for receiving status change notifications from kube-applier. Mutually exclusive with --sqs-status-queue-url-prefix.") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| flag.StringVar(&sqsStatusQueueURLPrefix, "sqs-status-queue-url-prefix", "", "SQS queue URL prefix; the pod ordinal (from hostname) is appended to form the full queue URL. Mutually exclusive with --sqs-status-queue-url.") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| flag.IntVar(&maxConcurrentReconciles, "max-concurrent-reconciles", 10, "Maximum number of concurrent reconciles per controller.") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| opts := zap.Options{Development: true} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| opts.BindFlags(flag.CommandLine) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -75,6 +78,14 @@ func main() { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setupLog.Error(nil, "--base-domain is required") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| os.Exit(1) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if sqsStatusQueueURL != "" && sqsStatusQueueURLPrefix != "" { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setupLog.Error(nil, "--sqs-status-queue-url and --sqs-status-queue-url-prefix are mutually exclusive") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| os.Exit(1) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if sqsStatusQueueURL == "" && sqsStatusQueueURLPrefix == "" { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setupLog.Error(nil, "one of --sqs-status-queue-url or --sqs-status-queue-url-prefix is required") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| os.Exit(1) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| dsn := os.Getenv("POSTGRES_DSN") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if dsn == "" { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -89,6 +100,13 @@ func main() { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| os.Exit(1) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // If a prefix was given, construct the full queue URL by appending the | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // pod ordinal — matching the queue naming convention: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // <prefix><ordinal> e.g. https://sqs…/regional-hyperfleet-operator-2 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if sqsStatusQueueURLPrefix != "" { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sqsStatusQueueURL = fmt.Sprintf("%s%d", sqsStatusQueueURLPrefix, ordinal) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setupLog.Info("shard config", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "replicaCount", replicaCount, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "ordinal", ordinal, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -131,8 +149,22 @@ func main() { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| dynamoDBClient := dynamodb.NewFromConfig(awsCfg) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| dynamoClient := dynamo.NewClient(dynamoDBClient) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| streamsClient := dynamodbstreams.NewFromConfig(awsCfg) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Discover the AWS account ID to construct SNS topic ARNs without an | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // explicit CLI flag. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| stsClient := sts.NewFromConfig(awsCfg) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| identity, err := stsClient.GetCallerIdentity(ctx, &sts.GetCallerIdentityInput{}) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setupLog.Error(err, "Failed to get AWS caller identity for SNS ARN construction") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| os.Exit(1) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| awsAccountID := *identity.Account | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setupLog.Info("Resolved AWS account ID for SNS topic ARNs", "accountID", awsAccountID) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| snsClient := sns.NewFromConfig(awsCfg) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sqsClient := sqs.NewFromConfig(awsCfg) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| publisher := snspublisher.New(snsClient, awsRegion, awsAccountID) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| dynamoClient := dynamo.NewClientWithSNS(dynamoDBClient, publisher) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+152
to
+167
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== locate main.go =="
fd -a 'main.go$' . | sed 's#^\./##'
echo "== git diff stat =="
git diff --stat || true
echo "== inspect relevant main.go sections =="
if [ -f hyperfleet-operator/cmd/manager/main.go ]; then
wc -l hyperfleet-operator/cmd/manager/main.go
cat -n hyperfleet-operator/cmd/manager/main.go | sed -n '1,240p'
fi
echo "== search context usage in manager main =="
rg -n "context|Background|Deadline|WithTimeout|GetCallerIdentity|stsClient|snsClient|sqsClient|dynamodDBClient|NewClientWithSNS" hyperfleet-operator/cmd/manager/main.go || trueRepository: openshift-online/rosa-hyperfleet-api Length of output: 11601 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== inspect relevant main.go sections with line numbers =="
if [ -f hyperfleet-operator/cmd/manager/main.go ]; then
wc -l hyperfleet-operator/cmd/manager/main.go
sed -n '1,240p' hyperfleet-operator/cmd/manager/main.go | nl -ba -v1
fiRepository: openshift-online/rosa-hyperfleet-api Length of output: 316 Add a timeout to the startup STS caller-identity call. At Suggested bounded startup call- identity, err := stsClient.GetCallerIdentity(ctx, &sts.GetCallerIdentityInput{})
+ stsCtx, stsCancel := context.WithTimeout(ctx, 10*time.Second)
+ defer stsCancel()
+ identity, err := stsClient.GetCallerIdentity(stsCtx, &sts.GetCallerIdentityInput{})📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| rcfg := render.RegionalConfig{ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| BaseDomain: baseDomain, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -196,19 +228,24 @@ func main() { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| os.Exit(1) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| streamMgr := statusstream.NewManager( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| dynamoDBClient, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| streamsClient, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| mgr.GetClient(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| []string{dynamo.TableSuffixStatusApplyDesires, dynamo.TableSuffixStatusReadDesires}, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Replace DynamoDB Streams-based statusstream.Manager with a single | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // pre-provisioned SQS queue per operator replica. kube-applier publishes | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // a status notification to SNS after each status write; SNS delivers to | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // all per-replica queues; the operator drains its own queue only. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // EventRouter.Dispatch silently drops document IDs it does not own, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // so no per-MC filtering is required here. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| statusConsumer := statussqsconsumer.New( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sqsClient, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sqsStatusQueueURL, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func(documentID string) { eventRouter.Dispatch(documentID) }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| slog.Default().With("component", "statusstream"), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| watchCtx, watchCancel := context.WithCancel(context.Background()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| defer watchCancel() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| go streamMgr.Run(watchCtx, 5*time.Second) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| go statusConsumer.Run(watchCtx) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setupLog.Info("Starting pgruntime manager") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setupLog.Info("Starting pgruntime manager", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "sqsStatusQueueURL", sqsStatusQueueURL, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+231
to
+248
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
# Locate and inspect the relevant main.go and SQS consumer implementation.
printf '--- main.go excerpt ---\n'
sed -n '200,270p' hyperfleet-operator/cmd/manager/main.go 2>/dev/null || true
printf '\n--- statussqsconsumer definitions/usages ---\n'
fd -a '.*sqs.*|.*status.*' hyperfleet-operator | sed 's#^\./##' | head -100
rg -n "type .*Consumer|func .*Run\\(|func .*handleMessage|Recover|runtime/debug|Add\\(|mgr\\.Add|Run\\(watchCtx\\)" hyperfleet-operator -S
printf '\n--- go.mod controller-runtime version ---\n'
rg -n "controller-runtime|sigs.k8s.io/controller-runtime" -S go.mod go.sum hyperfleet-operator/go.mod 2>/dev/null || trueRepository: openshift-online/rosa-hyperfleet-api Length of output: 265 🏁 Script executed: #!/bin/bash
set -euo pipefail
# Locate and inspect the relevant main.go and SQS consumer implementation.
printf '%s\n' '--- main.go excerpt ---'
sed -n '200,270p' hyperfleet-operator/cmd/manager/main.go 2>/dev/null || true
printf '%s\n' ''
printf '%s\n' '--- statussqsconsumer definitions/usages ---'
fd -a '.*sqs.*|.*status.*' hyperfleet-operator | sed 's#^\./##' | head -100
rg -n "type .*Consumer|func .*Run\\(|func .*handleMessage|Recover|runtime/debug|Add\\(|mgr\\.Add|Run\\(watchCtx\\)" hyperfleet-operator -S || true
printf '%s\n' ''
printf '%s\n' '--- go.mod controller-runtime version ---'
rg -n "controller-runtime|sigs.k8s.io/controller-runtime" -S go.mod go.sum hyperfleet-operator/go.mod 2>/dev/null || trueRepository: openshift-online/rosa-hyperfleet-api Length of output: 4334 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- SQS consumer implementation ---'
sed -n '1,180p' hyperfleet-operator/internal/dynamo/statussqsconsumer/consumer.go
printf '%s\n' ''
printf '%s\n' '--- main.go imports / manager lifecycle section ---'
sed -n '1,120p' hyperfleet-operator/cmd/manager/main.go
sed -n '240,260p' hyperfleet-operator/cmd/manager/main.goRepository: openshift-online/rosa-hyperfleet-api Length of output: 10413 Attach the status SQS consumer to the manager lifecycle and isolate it with recovery.
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if err := mgr.Start(signalCtx); err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setupLog.Error(err, "Failed to run manager") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| os.Exit(1) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -7,6 +7,7 @@ import ( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "errors" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "fmt" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "log/slog" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "strings" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "sync" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "time" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -21,8 +22,8 @@ import ( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var ErrNotFound = errors.New("desire not found") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| TableSuffixApplyDesires = "-applydesires" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| TableSuffixReadDesires = "-readdesires" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| TableSuffixApplyDesires = "-applydesires" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| TableSuffixReadDesires = "-readdesires" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| TableSuffixStatusApplyDesires = "-status-applydesires" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| TableSuffixStatusReadDesires = "-status-readdesires" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| attributeDocumentID = "documentID" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -34,6 +35,16 @@ type dynamoAPI interface { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| DeleteItem(ctx context.Context, params *dynamodb.DeleteItemInput, optFns ...func(*dynamodb.Options)) (*dynamodb.DeleteItemOutput, error) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // SNSPublisher publishes a spec change notification after a desire is written. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Implementations must be safe for concurrent use. A nil SNSPublisher disables | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // notifications (useful for tests and local development). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type SNSPublisher interface { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Publish sends a notification for the given document to the SNS topic for | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // mcName. tableSuffix identifies the table type (e.g. "-applydesires"). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Errors are best-effort: callers should log them but need not propagate. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Publish(ctx context.Context, mcName, documentID, tableSuffix string) error | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // UpsertResult reports whether an upsert changed the item and the updateTime | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // that should be used for staleness tracking. When Changed is false, UpdateTime | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // reflects the existing item's time so callers never need to fabricate one. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -62,22 +73,74 @@ type cacheEntry struct { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type Client struct { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| db dynamoAPI | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| cache sync.Map // table/documentID → cacheEntry | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sns SNSPublisher | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var _ DesireClient = (*Client)(nil) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // NewClient returns a Client with no SNS publisher. Desire writes succeed but | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // no SNS notifications are sent. Use NewClientWithSNS for production. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func NewClient(db dynamoAPI) *Client { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return &Client{db: db} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // NewClientWithSNS returns a Client that publishes a spec change notification | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // to SNS after every desire write where the spec actually changed. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func NewClientWithSNS(db dynamoAPI, publisher SNSPublisher) *Client { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return &Client{db: db, sns: publisher} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // UpsertApplyDesire writes an ApplyDesire spec only when content has changed. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // If the spec changed and an SNSPublisher is configured, it publishes a | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // notification so kube-applier learns about the change without polling Streams. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func (c *Client) UpsertApplyDesire(ctx context.Context, specsPrefix string, desire *ApplyDesire) (UpsertResult, error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return c.upsertDesire(ctx, specsPrefix+TableSuffixApplyDesires, desire.DocumentID, desire.Spec) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| result, err := c.upsertDesire(ctx, specsPrefix+TableSuffixApplyDesires, desire.DocumentID, desire.Spec) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return result, err | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if result.Changed { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| c.publishNotification(ctx, specsPrefix, desire.DocumentID, TableSuffixApplyDesires) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return result, nil | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // UpsertReadDesire writes a ReadDesire spec only when content has changed. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // If the spec changed and an SNSPublisher is configured, it publishes a | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // notification so kube-applier learns about the change without polling Streams. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func (c *Client) UpsertReadDesire(ctx context.Context, specsPrefix string, desire *ReadDesire) (UpsertResult, error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return c.upsertDesire(ctx, specsPrefix+TableSuffixReadDesires, desire.DocumentID, desire.Spec) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| result, err := c.upsertDesire(ctx, specsPrefix+TableSuffixReadDesires, desire.DocumentID, desire.Spec) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return result, err | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if result.Changed { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| c.publishNotification(ctx, specsPrefix, desire.DocumentID, TableSuffixReadDesires) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return result, nil | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // publishNotification sends an SNS notification for a changed desire write. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // It is a no-op when no SNSPublisher is configured. Errors are logged but not | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // propagated — kube-applier's 5-minute safety-net poll covers missed events. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func (c *Client) publishNotification(ctx context.Context, specsPrefix, documentID, tableSuffix string) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if c.sns == nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| mcName := mcNameFromPrefix(specsPrefix) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if err := c.sns.Publish(ctx, mcName, documentID, tableSuffix); err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| slog.Error("Failed to publish desire change notification to SNS", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "mcName", mcName, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "documentID", documentID, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "tableSuffix", tableSuffix, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "error", err, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+121
to
+137
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
printf 'Files matching client.go:\n'
fd -a 'client\.go$' . || true
printf '\nLocate target file and inspect:\n'
if [ -f "hyperfleet-operator/internal/dynamo/client.go" ]; then
wc -l hyperfleet-operator/internal/dynamo/client.go
ast-grep outline hyperfleet-operator/internal/dynamo/client.go || true
printf '\nRelevant lines 1-220:\n'
sed -n '1,220p' hyperfleet-operator/internal/dynamo/client.go | nl -ba
else
printf 'target file not found\n'
fi
printf '\nSearch SNSPublisher/Publish usages:\n'
rg -n "type SNSPublisher|Publish\\(|publishNotification|UpsertApplyDesire|UpsertReadDesire|WithTimeout|Context\\.Func" hyperfleet-operator/internal/dynamo -S || trueRepository: openshift-online/rosa-hyperfleet-api Length of output: 3055 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf 'Relevant client.go lines:\n'
sed -n '1,180p' hyperfleet-operator/internal/dynamo/client.go
printf '\nSearch SNSPublisher/Publish/upsert calls in repository:\n'
rg -n "type SNSPublisher|interface\\s*\\{[^}]*Publish|Publish\\(|publishNotification\\(|UpsertApplyDesire\\(|UpsertReadDesire\\(|NewClientWithSNS|NewClient\\(" hyperfleet-operator -S || true
python3 - <<'PY'
from pathlib import Path
src = Path("hyperfleet-operator/internal/dynamo/client.go").read_text()
for name in ["func (c *Client) publishNotification", "func (c *Client) UpsertApplyDesire", "func (c *Client) UpsertReadDesire", "func (c *Client) upsertDesire"]:
idx = src.find(name)
print(f"\n--- {name} offset {idx} ---")
if idx >= 0:
line = src[:idx].count("\n") + 1
print("start line:", line)
PYRepository: openshift-online/rosa-hyperfleet-api Length of output: 13677 Bound SNS publish latency before returning from a desire upsert.
🕐 Suggested bound on publish latency func (c *Client) publishNotification(ctx context.Context, specsPrefix, documentID, tableSuffix string) {
if c.sns == nil {
return
}
+ ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
+ defer cancel()
mcName := mcNameFromPrefix(specsPrefix)
if err := c.sns.Publish(ctx, mcName, documentID, tableSuffix); err != nil {📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // mcNameFromPrefix strips the "-specs" suffix from a specsPrefix to recover the | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // management cluster name. E.g. "eph-45df5708-mc01-specs" → "eph-45df5708-mc01", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // "mc01-specs" → "mc01". SpecsPrefix is the inverse: SpecsPrefix(mc) = mc+"-specs". | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func mcNameFromPrefix(specsPrefix string) string { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return strings.TrimSuffix(specsPrefix, "-specs") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // GetApplyDesireStatus reads an ApplyDesire from the status table. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
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.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Queue-URL construction silently misroutes if
podOrdinal()can't parse the hostname suffix.This new logic makes
ordinalload-bearing for correct per-replica SQS routing:sqsStatusQueueURL = prefix + ordinal.podOrdinal()(below, unchanged) swallowsstrconv.Atoifailures and returns(0, nil)instead of an error:The existing
if err != nil { os.Exit(1) }check at the call site never fires in that case, so a malformed/non-StatefulSet hostname silently resolves to ordinal0instead of failing fast. Since two replicas defaulting to ordinal0would poll the same SQS queue while another replica's queue goes undrained, this can silently cause missed status notifications for the misrouted replica. Recommend propagating the parse error instead of masking it.🐛 Suggested fix in podOrdinal (outside this diff range)
ordinal, err := strconv.Atoi(last) if err != nil { - return 0, nil + return 0, fmt.Errorf("parse ordinal from hostname %q: %w", hostname, err) } return ordinal, nil🤖 Prompt for AI Agents