Skip to content

Commit 5f56294

Browse files
zreigzfloreks
andauthored
feat: durable deployment-operator caches (#4021)
Co-authored-by: Sebastian Florek <s.florek91@gmail.com>
1 parent 26a54b0 commit 5f56294

26 files changed

Lines changed: 1269 additions & 53 deletions

.vscode/settings.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,6 @@
1414
"editor.formatOnSave": false,
1515
"editor.formatOnPaste": false
1616
},
17-
"editor.formatOnSave": true
17+
"editor.formatOnSave": true,
18+
"git.ignoreLimitWarning": true
1819
}

go/deployment-operator/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ go.work.sum
99

1010
# Binaries for programs and plugins
1111
bin/
12+
/agent
1213
*.exe
1314
*.exe~
1415
*.dll

go/deployment-operator/cmd/agent/args/args.go

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,24 +8,27 @@ import (
88
"strings"
99
"time"
1010

11-
"github.com/pluralsh/console/go/deployment-operator/api/v1alpha1"
12-
"github.com/pluralsh/console/go/polly/containers"
1311
"github.com/spf13/pflag"
1412
"k8s.io/klog/v2"
1513
ctrl "sigs.k8s.io/controller-runtime"
1614
"sigs.k8s.io/controller-runtime/pkg/log/zap"
1715

16+
"github.com/pluralsh/console/go/deployment-operator/api/v1alpha1"
17+
"github.com/pluralsh/console/go/polly/containers"
18+
1819
"github.com/pluralsh/console/go/deployment-operator/internal/helpers"
1920
"github.com/pluralsh/console/go/deployment-operator/pkg/log"
2021
"github.com/pluralsh/console/go/deployment-operator/pkg/streamline/api"
2122
)
2223

2324
const (
24-
EnvDeployToken = "DEPLOY_TOKEN"
25-
EnvDatadogEnabled = "DATADOG_ENABLED"
26-
EnvPyroscopeEnabled = "PYROSCOPE_ENABLED"
27-
EnvProfilerEnabled = "PROFILER_ENABLED"
28-
EnvLocal = "LOCAL"
25+
EnvDeployToken = "DEPLOY_TOKEN"
26+
EnvDatadogEnabled = "DATADOG_ENABLED"
27+
EnvDatadogHost = "DATADOG_HOST"
28+
EnvDatadogEnvironment = "DATADOG_ENV"
29+
EnvPyroscopeEnabled = "PYROSCOPE_ENABLED"
30+
EnvProfilerEnabled = "PROFILER_ENABLED"
31+
EnvLocal = "LOCAL"
2932

3033
defaultProbeAddress = ":9001"
3134
defaultMetricsAddress = ":8000"
@@ -64,6 +67,9 @@ const (
6467
defaultManifestCacheTTL = "3h"
6568
defaultManifestCacheTTLDuration = 3 * time.Hour
6669

70+
defaultCachePersistInterval = "10s"
71+
defaultCachePersistIntervalDuration = 10 * time.Second
72+
6773
defaultComponentShaCacheTTL = "6h"
6874
defaultComponentShaCacheTTLDuration = 6 * time.Hour
6975

@@ -153,14 +159,16 @@ var (
153159
argResourceCacheTTL = flag.String("resource-cache-ttl", defaultResourceCacheTTL, "The time to live of each resource cache entry.")
154160
argManifestCacheTTL = flag.String("manifest-cache-ttl", defaultManifestCacheTTL, "The time to live of service manifests in cache entry.")
155161
argManifestCacheJitter = flag.String("manifest-cache-jitter", defaultManifestCacheJitter, "Deprecated: ignored; manifest cache jitter is fixed at 50% of its TTL.")
162+
argCacheDir = flag.String("cache-dir", "", "Directory used to persist operator caches across restarts. Empty disables persistence.")
163+
argCachePersistInterval = flag.String("cache-persist-interval", defaultCachePersistInterval, "Interval to flush in-memory caches to cache-dir.")
156164
argComponentShaCacheTTL = flag.String("component-sha-cache-ttl", defaultComponentShaCacheTTL, "The time to live of the component sha cache entries.")
157165
argComponentShaCacheJitter = flag.String("component-sha-cache-jitter", defaultComponentShaCacheJitter, "Deprecated: ignored; component SHA cache jitter is fixed at 50% of its TTL.")
158166
argControllerCacheTTL = flag.String("controller-cache-ttl", defaultControllerCacheTTL, "The time to live of console controller cache entries.")
159167
argRestoreNamespace = flag.String("restore-namespace", defaultRestoreNamespace, "The namespace where Velero restores are located.")
160168
argServices = flag.String("services", "", "A comma separated list of service ids to reconcile. Leave empty to reconcile all.")
161169
argPyroscopeAddress = flag.String("pyroscope-address", defaultPyroscopeAddress, "The address of the Pyroscope server.")
162-
argDatadogHost = flag.String("datadog-host", defaultDatadogHost, "The address of the Datadog server.")
163-
argDatadogEnv = flag.String("datadog-env", defaultDatadogEnv, "The environment of the Datadog server.")
170+
argDatadogHost = flag.String("datadog-host", helpers.GetEnv(EnvDatadogHost, defaultDatadogHost), "The address of the Datadog server.")
171+
argDatadogEnv = flag.String("datadog-env", helpers.GetEnv(EnvDatadogEnvironment, defaultDatadogEnv), "The environment of the Datadog server.")
164172
argWorkqueueBaseDelay = flag.String("workqueue-base-delay", defaultWorkqueueBaseDelay, "The base delay for the workqueue.")
165173
argWorkqueueMaxDelay = flag.String("workqueue-max-delay", defaultWorkqueueMaxDelay, "The maximum delay for the workqueue.")
166174
argWorkqueueQPS = flag.Int("workqueue-qps", 10, "The maximum number of items to process per second.")
@@ -377,6 +385,20 @@ func ManifestCacheTTL() time.Duration {
377385
return duration
378386
}
379387

388+
func CacheDir() string {
389+
return *argCacheDir
390+
}
391+
392+
func CachePersistInterval() time.Duration {
393+
duration, err := time.ParseDuration(*argCachePersistInterval)
394+
if err != nil {
395+
klog.ErrorS(err, "Could not parse cache-persist-interval", "value", *argCachePersistInterval, "default", defaultCachePersistIntervalDuration)
396+
return defaultCachePersistIntervalDuration
397+
}
398+
399+
return duration
400+
}
401+
380402
func ComponentShaCacheTTL() time.Duration {
381403
duration, err := time.ParseDuration(*argComponentShaCacheTTL)
382404
if err != nil {

go/deployment-operator/cmd/agent/console.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ func registerConsoleReconcilersOrDie(
6060
discoveryCache discoverycache.Cache,
6161
namespaceCache streamline.NamespaceCache,
6262
svcCache cache.Store[console.ServiceDeploymentForAgent],
63+
cacheDir string,
6364
) {
6465
mgr.AddReconcilerOrDie(service.Identifier, func() (v1.Reconciler, error) {
6566
r, err := service.NewServiceReconciler(consoleClient,
@@ -72,6 +73,7 @@ func registerConsoleReconcilersOrDie(
7273
svcCache,
7374
store,
7475
service.WithManifestTTL(args.ManifestCacheTTL()),
76+
service.WithCacheDir(cacheDir),
7577
service.WithWorkqueueBaseDelay(args.WorkqueueBaseDelay()),
7678
service.WithWorkqueueMaxDelay(args.WorkqueueMaxDelay()),
7779
service.WithWorkqueueQPS(args.WorkqueueQPS()),

go/deployment-operator/cmd/agent/kubernetes.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ func registerKubeReconcilersOrDie(
131131
discoveryCache discoverycache.Cache,
132132
enableKubecostProxy bool,
133133
consoleURL, deployToken string,
134+
userGroupCache cache.UserGroupCache,
134135
) {
135136
rolloutsClient, dynamicClient, kubeClient := initKubeClientsOrDie(config)
136137

@@ -251,6 +252,7 @@ func registerKubeReconcilersOrDie(
251252
Scheme: manager.GetScheme(),
252253
ExtConsoleClient: extConsoleClient,
253254
ConsoleUrl: rawConsoleUrl,
255+
UserGroupCache: userGroupCache,
254256
}).SetupWithManager(manager); err != nil {
255257
setupLog.Error(err, "unable to create controller", "controller", "VirtualCluster")
256258
}
@@ -330,9 +332,10 @@ func registerKubeReconcilersOrDie(
330332
setupLog.Error(err, "unable to create controller", "controller", "AgentRun")
331333
}
332334
if err := (&controller.PluralCAPIClusterController{
333-
Client: manager.GetClient(),
334-
Scheme: manager.GetScheme(),
335-
ConsoleUrl: consoleURL,
335+
Client: manager.GetClient(),
336+
Scheme: manager.GetScheme(),
337+
ConsoleUrl: consoleURL,
338+
UserGroupCache: userGroupCache,
336339
}).SetupWithManager(manager); err != nil {
337340
setupLog.Error(err, "unable to create controller", "controller", "PluralCAPIClusterController")
338341
}

go/deployment-operator/cmd/agent/main.go

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import (
4040
"github.com/pluralsh/console/go/deployment-operator/internal/utils"
4141
"github.com/pluralsh/console/go/deployment-operator/pkg/cache"
4242
discoverycache "github.com/pluralsh/console/go/deployment-operator/pkg/cache/discovery"
43+
"github.com/pluralsh/console/go/deployment-operator/pkg/cache/persist"
4344
"github.com/pluralsh/console/go/deployment-operator/pkg/client"
4445
"github.com/pluralsh/console/go/deployment-operator/pkg/common"
4546
"github.com/pluralsh/console/go/deployment-operator/pkg/ping"
@@ -51,6 +52,7 @@ import (
5152
deploymentsv1alpha1 "github.com/pluralsh/console/go/deployment-operator/api/v1alpha1"
5253
"github.com/pluralsh/console/go/deployment-operator/cmd/agent/args"
5354
consolectrl "github.com/pluralsh/console/go/deployment-operator/pkg/controller"
55+
"github.com/pluralsh/console/go/deployment-operator/pkg/controller/namespaces"
5456
"github.com/pluralsh/console/go/deployment-operator/pkg/controller/service"
5557
)
5658

@@ -161,12 +163,47 @@ func main() {
161163
return extConsoleClient.GetService(id)
162164
})
163165

166+
cacheStore := openCacheStoreOrDie()
167+
defer func() {
168+
if err := cacheStore.Close(); err != nil {
169+
setupLog.Error(err, "unable to release cache dir lock")
170+
}
171+
}()
172+
164173
// Start synchronizer supervisor
165174
supervisor := runSynchronizerSupervisorOrDie(ctx, dynamicClient, dbStore, statusSynchronizer, discoveryCache, namespaceCache, svcCache)
166175
defer supervisor.Stop()
167176

168-
registerConsoleReconcilersOrDie(consoleManager, mapper, clientSet, kubeManager.GetClient(), dynamicClient, dbStore, kubeManager.GetScheme(), extConsoleClient, supervisor, discoveryCache, namespaceCache, svcCache)
169-
registerKubeReconcilersOrDie(ctx, clientSet, kubeManager, consoleManager, config, extConsoleClient, discoveryCache, args.EnableKubecostProxy(), args.ConsoleUrl(), args.DeployToken())
177+
userGroupCache := cache.NewUserGroupCache(extConsoleClient)
178+
179+
registerConsoleReconcilersOrDie(consoleManager, mapper, clientSet, kubeManager.GetClient(), dynamicClient, dbStore, kubeManager.GetScheme(), extConsoleClient, supervisor, discoveryCache, namespaceCache, svcCache, cacheStore.Dir())
180+
registerKubeReconcilersOrDie(ctx, clientSet, kubeManager, consoleManager, config, extConsoleClient, discoveryCache, args.EnableKubecostProxy(), args.ConsoleUrl(), args.DeployToken(), userGroupCache)
181+
182+
svcReconciler := consoleManager.GetReconcilerOrDie(service.Identifier).(*service.ServiceReconciler)
183+
nsReconciler := consoleManager.GetReconcilerOrDie(namespaces.Identifier).(*namespaces.NamespaceReconciler)
184+
saveCaches := func() error {
185+
userIDs, groupIDs := persist.IdentityRecordsFrom(userGroupCache)
186+
return cacheStore.Save(persist.Snapshot{
187+
Manifests: svcReconciler.ManifestCache().Export(),
188+
ComponentSHAs: persist.SHARecordsFrom(cache.ComponentShaCache()),
189+
StatusSHAs: persist.SHARecordsFrom(statusSynchronizer.SHACache()),
190+
UserIDs: userIDs,
191+
GroupIDs: groupIDs,
192+
ManagedNamespaces: persist.PollyRecordsFrom(nsReconciler.NamespaceCache()),
193+
})
194+
}
195+
if snap, err := cacheStore.Load(); err != nil {
196+
setupLog.Error(err, "unable to load durable cache, starting cold")
197+
} else {
198+
setupLog.Info("importing durable cache snapshot")
199+
svcReconciler.ManifestCache().Import(snap.Manifests)
200+
persist.ApplySHARecords(cache.ComponentShaCache(), snap.ComponentSHAs)
201+
persist.ApplySHARecords(statusSynchronizer.SHACache(), snap.StatusSHAs)
202+
persist.ApplyIdentityRecords(userGroupCache, snap.UserIDs, snap.GroupIDs)
203+
persist.ApplyPollyRecords(nsReconciler.NamespaceCache(), snap.ManagedNamespaces)
204+
setupLog.Info("durable cache import finished")
205+
}
206+
cacheStore.StartPeriodic(ctx, args.CachePersistInterval(), saveCaches)
170207

171208
//+kubebuilder:scaffold:builder
172209

@@ -189,6 +226,11 @@ func main() {
189226
// Block the main thread until context cancel.
190227
<-ctx.Done()
191228
setupLog.Info("shutting down")
229+
cacheStore.WaitPeriodic()
230+
setupLog.Info("exporting durable cache snapshot")
231+
if err := saveCaches(); err != nil {
232+
setupLog.Error(err, "unable to persist cache snapshot")
233+
}
192234
}
193235

194236
func loadAgentConfigurationOrDie(ctx context.Context, reader ctrlclient.Reader) {
@@ -338,6 +380,15 @@ func runSynchronizerSupervisorOrDie(ctx context.Context, dynamicClient dynamic.I
338380
return supervisor
339381
}
340382

383+
func openCacheStoreOrDie() *persist.Store {
384+
cacheStore, err := persist.Open(args.CacheDir())
385+
if err != nil {
386+
setupLog.Error(err, "unable to open cache dir")
387+
os.Exit(1)
388+
}
389+
return cacheStore
390+
}
391+
341392
func initDatabaseStoreOrDie(ctx context.Context) store.Store {
342393
dbStore, err := store.NewDatabaseStore(ctx, store.WithStorage(args.StoreStorage()), store.WithFilePath(args.StoreFilePath()))
343394
if err != nil {

go/deployment-operator/go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ require (
6969
github.com/yuin/gopher-lua v1.1.2
7070
gitlab.com/gitlab-org/api/client-go v1.46.0
7171
golang.org/x/oauth2 v0.36.0
72+
golang.org/x/sys v0.46.0
7273
golang.org/x/time v0.15.0
7374
gopkg.in/yaml.v3 v3.0.1
7475
gotest.tools/gotestsum v1.13.0
@@ -342,7 +343,6 @@ require (
342343
golang.org/x/mod v0.37.0 // indirect
343344
golang.org/x/net v0.56.0 // indirect
344345
golang.org/x/sync v0.22.0 // indirect
345-
golang.org/x/sys v0.46.0 // indirect
346346
golang.org/x/term v0.44.0 // indirect
347347
golang.org/x/text v0.39.0 // indirect
348348
golang.org/x/tools v0.47.0 // indirect

go/deployment-operator/internal/controller/pluralcapicluster_controller.go

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,11 @@ const PluralCAPIClusterFinalizer = "deployments.plural.sh/plural-capi-cluster-pr
3232

3333
type PluralCAPIClusterController struct {
3434
k8sClient.Client
35-
Scheme *runtime.Scheme
36-
ConsoleUrl string
35+
Scheme *runtime.Scheme
36+
ConsoleUrl string
37+
UserGroupCache cache.UserGroupCache
3738

38-
userGroupCache cache.UserGroupCache
39-
consoleClient client.Client
39+
consoleClient client.Client
4040
}
4141

4242
func (in *PluralCAPIClusterController) Reconcile(ctx context.Context, req ctrl.Request) (_ reconcile.Result, reterr error) {
@@ -191,7 +191,9 @@ func (in *PluralCAPIClusterController) initConsoleClient(consoleToken string) er
191191
return err
192192
}
193193
in.consoleClient = client.New(fmt.Sprintf("%s/gql", url), consoleToken)
194-
in.userGroupCache = cache.NewUserGroupCache(in.consoleClient)
194+
if in.UserGroupCache == nil {
195+
in.UserGroupCache = cache.NewUserGroupCache(in.consoleClient)
196+
}
195197
}
196198
return nil
197199
}
@@ -238,14 +240,14 @@ func (in *PluralCAPIClusterController) ensureCluster(cluster *v1alpha1.PluralCAP
238240
return nil
239241
}
240242

241-
bindings, req, err := ensureBindings(cluster.Spec.Cluster.Bindings.Read, in.userGroupCache)
243+
bindings, req, err := ensureBindings(cluster.Spec.Cluster.Bindings.Read, in.UserGroupCache)
242244
if err != nil {
243245
return err
244246
}
245247

246248
cluster.Spec.Cluster.Bindings.Read = bindings
247249

248-
bindings, req2, err := ensureBindings(cluster.Spec.Cluster.Bindings.Write, in.userGroupCache)
250+
bindings, req2, err := ensureBindings(cluster.Spec.Cluster.Bindings.Write, in.UserGroupCache)
249251
if err != nil {
250252
return err
251253
}

go/deployment-operator/internal/controller/virtualcluster_controller.go

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,10 @@ type VirtualClusterController struct {
3535
Scheme *runtime.Scheme
3636
ExtConsoleClient client.Client
3737
ConsoleUrl string
38+
UserGroupCache cache.UserGroupCache
3839

39-
userGroupCache cache.UserGroupCache
40-
consoleClient client.Client
41-
myCluster *console.MyCluster_MyCluster_
40+
consoleClient client.Client
41+
myCluster *console.MyCluster_MyCluster_
4242
}
4343

4444
func (in *VirtualClusterController) Reconcile(ctx context.Context, req reconcile.Request) (_ reconcile.Result, reterr error) {
@@ -245,14 +245,14 @@ func (in *VirtualClusterController) ensureCluster(cluster *v1alpha1.VirtualClust
245245
return nil
246246
}
247247

248-
bindings, req, err := ensureBindings(cluster.Spec.Cluster.Bindings.Read, in.userGroupCache)
248+
bindings, req, err := ensureBindings(cluster.Spec.Cluster.Bindings.Read, in.UserGroupCache)
249249
if err != nil {
250250
return err
251251
}
252252

253253
cluster.Spec.Cluster.Bindings.Read = bindings
254254

255-
bindings, req2, err := ensureBindings(cluster.Spec.Cluster.Bindings.Write, in.userGroupCache)
255+
bindings, req2, err := ensureBindings(cluster.Spec.Cluster.Bindings.Write, in.UserGroupCache)
256256
if err != nil {
257257
return err
258258
}
@@ -277,7 +277,9 @@ func (in *VirtualClusterController) initConsoleClient(ctx context.Context, vClus
277277
}
278278

279279
in.consoleClient = client.New(fmt.Sprintf("%s/gql", in.ConsoleUrl), token)
280-
in.userGroupCache = cache.NewUserGroupCache(in.consoleClient)
280+
if in.UserGroupCache == nil {
281+
in.UserGroupCache = cache.NewUserGroupCache(in.consoleClient)
282+
}
281283

282284
return nil
283285
}

0 commit comments

Comments
 (0)