Skip to content
Merged
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
10 changes: 10 additions & 0 deletions .github/workflows/docker-publish-x402.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,14 @@ on:
- 'v*'
paths:
- 'internal/x402/**'
- 'internal/serviceoffercontroller/**'
- 'internal/monetizeapi/**'
- 'cmd/x402-verifier/**'
- 'cmd/x402-buyer/**'
- 'cmd/serviceoffer-controller/**'
- 'Dockerfile.x402-verifier'
- 'Dockerfile.x402-buyer'
- 'Dockerfile.serviceoffer-controller'
- 'go.mod'
- 'go.sum'
- '.github/workflows/docker-publish-x402.yml'
Expand Down Expand Up @@ -46,6 +50,10 @@ jobs:
image: obolnetwork/x402-buyer
dockerfile: Dockerfile.x402-buyer
description: x402 buy-side payment sidecar for Obol Stack
- component: serviceoffer-controller
image: obolnetwork/serviceoffer-controller
dockerfile: Dockerfile.serviceoffer-controller
description: ServiceOffer reconciler for Obol Stack monetization

steps:
- name: Checkout
Expand Down Expand Up @@ -111,6 +119,8 @@ jobs:
image: obolnetwork/x402-verifier
- component: x402-buyer
image: obolnetwork/x402-buyer
- component: serviceoffer-controller
image: obolnetwork/serviceoffer-controller

steps:
- name: Run Trivy vulnerability scanner
Expand Down
89 changes: 3 additions & 86 deletions cmd/serviceoffer-controller/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,19 @@ package main
import (
"context"
"flag"
"fmt"
"log"
"os"
"os/signal"
"syscall"
"time"

"github.com/ObolNetwork/obol-stack/internal/serviceoffercontroller"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/tools/leaderelection"
"k8s.io/client-go/tools/leaderelection/resourcelock"
)

func main() {
kubeconfig := flag.String("kubeconfig", "", "Path to kubeconfig for out-of-cluster runs")
workers := flag.Int("workers", 4, "Number of reconcile workers")
leaderElection := flag.Bool("leader-election", true, "Enable lease-based leader election")
leaderElectionNamespace := flag.String("leader-election-namespace", getenvDefault("POD_NAMESPACE", "x402"), "Namespace for the leader election Lease")
leaderElectionName := flag.String("leader-election-name", "serviceoffer-controller", "Name of the leader election Lease")
workers := flag.Int("workers", 1, "Number of reconcile workers")
flag.Parse()

cfg, err := loadConfig(*kubeconfig)
Expand All @@ -40,18 +31,8 @@ func main() {
ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer cancel()

run := func(runCtx context.Context) error {
return controller.Run(runCtx, *workers)
}

var errRun error
if *leaderElection {
errRun = runWithLeaderElection(ctx, cfg, *leaderElectionNamespace, *leaderElectionName, run)
} else {
errRun = run(ctx)
}
if errRun != nil {
log.Fatalf("run controller: %v", errRun)
if err := controller.Run(ctx, *workers); err != nil {
log.Fatalf("run controller: %v", err)
}
}

Expand All @@ -64,67 +45,3 @@ func loadConfig(kubeconfig string) (*rest.Config, error) {
}
return rest.InClusterConfig()
}

func runWithLeaderElection(ctx context.Context, cfg *rest.Config, namespace, name string, run func(context.Context) error) error {
clientset, err := kubernetes.NewForConfig(cfg)
if err != nil {
return fmt.Errorf("create kubernetes clientset: %w", err)
}

hostname, err := os.Hostname()
if err != nil {
return fmt.Errorf("resolve hostname: %w", err)
}

lock := &resourcelock.LeaseLock{
LeaseMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
},
Client: clientset.CoordinationV1(),
LockConfig: resourcelock.ResourceLockConfig{
Identity: fmt.Sprintf("%s-%d", hostname, time.Now().UnixNano()),
},
}

errCh := make(chan error, 1)
go leaderelection.RunOrDie(ctx, leaderelection.LeaderElectionConfig{
Lock: lock,
ReleaseOnCancel: true,
LeaseDuration: 15 * time.Second,
RenewDeadline: 10 * time.Second,
RetryPeriod: 2 * time.Second,
Callbacks: leaderelection.LeaderCallbacks{
OnStartedLeading: func(runCtx context.Context) {
if err := run(runCtx); err != nil {
select {
case errCh <- err:
default:
}
}
},
OnStoppedLeading: func() {
if ctx.Err() == nil {
select {
case errCh <- fmt.Errorf("leader election lost"):
default:
}
}
},
},
})

select {
case err := <-errCh:
return err
case <-ctx.Done():
return nil
}
}

func getenvDefault(key, fallback string) string {
if value := os.Getenv(key); value != "" {
return value
}
return fallback
}
5 changes: 2 additions & 3 deletions internal/embed/infrastructure/base/templates/x402.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ metadata:
labels:
app: serviceoffer-controller
spec:
replicas: 1
replicas: 1 # Do not scale — multiple replicas race on ERC-8004 on-chain registration
selector:
matchLabels:
app: serviceoffer-controller
Expand All @@ -238,8 +238,7 @@ spec:
valueFrom:
fieldRef:
fieldPath: metadata.namespace
args:
- --workers=4
args: []
resources:
requests:
cpu: 25m
Expand Down
Loading